diff --git a/.coveragerc b/.coveragerc index ac3d44b96..7baf22129 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,5 @@ [run] +source = sapl omit = sapl/wsgi.py manage.py diff --git a/.gitignore b/.gitignore index 3bf1301ec..429873229 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# Nodejs +node_modules/ + # Distribution / packaging .Python env/ diff --git a/Dockerfile b/Dockerfile index 42ed97c71..fbbd8575d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,7 +41,7 @@ RUN python3 manage.py bower_install -- --allow-root --no-input && \ python3 manage.py compilescss RUN python3 manage.py collectstatic --noinput --clear - + # Remove .env(fake) e sapl.db da imagem RUN rm -rf /var/interlegis/sapl/sapl/.env && \ rm -rf /var/interlegis/sapl/sapl.db diff --git a/README.rst b/README.rst index 3cc73b28b..ba8923e19 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Instalação do Ambiente de Desenvolvimento Instruções para Importação da base mysql 2.5 ============================================ - `Importação da Base do SAPL 2.5 para SAPL 3.1 `_ + `Importação da Base do SAPL 2.5 para SAPL 3.1 `_ Instruções para Deploy diff --git a/check_migrations.sh b/check_migrations.sh index ff545a3c5..2933bdbcd 100755 --- a/check_migrations.sh +++ b/check_migrations.sh @@ -21,6 +21,7 @@ if [ $MIGRATIONS -eq 0 ]; then echo echo -e "${RED}ALGUMAS ALTERAÇÕES EXIGEM MIGRAÇÃO.${NC}" echo -e "${RED}RODE 'python manage.py makemigrations' ANTES DE SUBMETER SEU CÓDIGO...${NC}" + echo -e "${RED}lembre de adicionar os arquivos criados ao git com 'git add .' ou semelhante.${NC}" echo exit 1 fi \ No newline at end of file diff --git a/check_qa.sh b/check_qa.sh index 607bb52bd..47f2b56d3 100755 --- a/check_qa.sh +++ b/check_qa.sh @@ -1,17 +1,17 @@ #!/bin/bash -# Check if there's some debug breakpoint in codebase +# Verifica se um breakpoint foi esquecido no código me=`basename "$0"` -stmts=`grep --exclude=$me -r -l "ipdb.set_trace()" * | wc -l` -if [ $stmts != '0' ] +busca=`grep --color=auto --exclude=$me --exclude=ipython_log.py* -r -l "pdb.set_trace()" .` + +if [ ! -z "$busca" ] then - echo "==================================================================" - echo "ERROR: ipdb.set_trace() call in codebase! Remove, please." - grep --exclude=$me -r -n "ipdb.set_trace()" * - echo "==================================================================" + echo "============================================================================" + echo "ERROR: pdb.set_trace() encontrado nos seguintes arquivos. Remova, por favor." + echo "$busca" + echo "============================================================================" fi -# QA checks: run this before every commit -./manage.py check -flake8 --exclude='ipython_log.py*,migrations,templates' . -isort --recursive --check-only --skip='migrations' --skip='templates' --skip='ipython_log.py' . +# ./manage.py check +# flake8 --exclude='ipython_log.py*,migrations,templates' . +# isort --recursive --check-only --skip='migrations' --skip='templates' --skip='ipython_log.py' . diff --git a/docker-compose.yml b/docker-compose.yml index 7a13998d9..f7602b1f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ sapldb: image: postgres + restart: always environment: POSTGRES_PASSWORD: sapl POSTGRES_USER: sapl @@ -8,9 +9,19 @@ sapldb: volumes: - sapldb_data:/var/lib/postgresql/data/ ports: - - "5532:5432" + - "5432:5432" sapl: - image: interlegis/sapl:3.1.20-BETA + image: interlegis/sapl:3.1.39-BETA + restart: always + environment: + ADMIN_PASSWORD: interlegis + ADMIN_EMAIL: email@dominio.net + DEBUG: 'False' + USE_TLS: 'False' + EMAIL_PORT: 587 + EMAIL_HOST: smtp.dominio.net + EMAIL_HOST_USER: usuariosmtp + EMAIL_HOST_PASSWORD: senhasmtp volumes: - sapl_data:/var/interlegis/sapl/data - sapl_media:/var/interlegis/sapl/media diff --git a/docs/credits.txt b/docs/credits.txt new file mode 100644 index 000000000..413f2bcad --- /dev/null +++ b/docs/credits.txt @@ -0,0 +1,34 @@ +Créditos do SAPL - até versão 2.5 +================================= + +Obrigado aos colaboradores: + +- Adriano Gomes +- Angelo Marcondes Neto +- Claudio Morale +- Daniel C. Azevedo +- Davi Lima de Medeiros +- Edson Ma +- Fernando Ciciliati Júnior +- Gustavo Lepri +- Halison Casimiro +- Helder Vieira +- Jean Rodrigo Ferri +- João Lima +- José Borges +- Leandro Roberto +- Leonardo Caballero +- Luciano Di Fázio +- Luis Fernando Pires Machado +- Marcio Mazza +- Marcos Fragomeni +- Maria Cristina André de Mello +- Marta Maria Pincowsca Cardoso Maia +- Paulo Fernandes de Souza Júnior +- Petronio Barbosa Carvalho +- Ricardo Esperandio +- Rodrigo Barbosa Luz +- Sesóstris Vieira +- Sérgio Damiati +- Wilton Souza Alencar +- Wu Man Qi diff --git a/docs/deploy.rst b/docs/deploy.rst index 101b7bb43..7e5e1cc5e 100644 --- a/docs/deploy.rst +++ b/docs/deploy.rst @@ -30,7 +30,7 @@ Com o ambiente em produção, os arquivos estáticos devem ser servidos pelo web para que os arquivos SASS/SCSS sejam compilados em arquivos .css em ambiente de produção, e em seguida rode:: - ./manage.py collectstatic --no-input + ./manage.py collectstatic --no-input --clear para coletar todos os arquivos estáticos do projeto e guarda-los no diretório definido em `STATIC_ROOT`, que será também o diretório no qual o `NGINX` irá referenciar para a aplicação. diff --git a/docs/importacao_25_31.rst b/docs/importacao_25_31.rst deleted file mode 100644 index 44819091b..000000000 --- a/docs/importacao_25_31.rst +++ /dev/null @@ -1,111 +0,0 @@ -Instruções para Importação da base mysql 2.5 -============================================ - - -Para entrar no ambiente virtual:: - - workon sapl - - - -Instalar Dependências:: - - pip3 install -r requirements/migration-requirements.txt - -Criar um arquivo sapl/legacy/.env com o seguinte conteúdo (parametros de acesso ao banco 2.5):: - - DATABASE_URL = mysql://[usuario do mysql]:[senha do myuysql]@[host]:[porta]/[banco] - - -o conteúdo do arquivo será semelhante a isso:: - - DATABASE_URL = mysql://sapl:sapl@localhost:3306/interlegis - - -Posteriormente rodar a seguinte sequencia de comandos estando no ambiente virtual:: - - ./manage.py shell --settings=sapl.legacy_migration_settings - - %run sapl/legacy/migration.py - - migrate() - - -Migração de documentos do sapl 2.5 ----------------------------------- - -No sapl 2.5 todos os documentos ficavam armazenados no ZODB (o banco do Zope). -No sapl 3.1 eles ficam no sistema de arquivos convencional e portanto precisam: - -1. ser exportados para o sistema de arquivos -2. ser vinculados ao novo banco importado para o sapl 3.1 - - -Exportar os documentos para o sistema de arquivos -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Siga os seguintes passos: - -1. Instale o `Products.FSDump` no sapl 2.5. - - Para isso basta colocar a pasta `Products/FSDump` do projeto https://github.com/zopefoundation/Products.FSDump na pasta `Products` da instalação do sapl 2.5 e reiniciar o Zope. - - A pasta a ser instalada é a seguinte: - https://github.com/zopefoundation/Products.FSDump/tree/master/Products/FSDump - -2. Na ZMI, na pasta `sapl_documentos`, adicione um objeto do tipo `Dumper`: - - - Em `Filesystem path` escolha uma pasta do sistema de arquivos local para onde os arquivos serão copiados - - Desmarque a opção `Use .metadata file` - - Clique no botão `Add` - -3. Use o objeto `Dumper` criado para exportar os arquivos: - - - Clique no objeto `Dumper` criado para ver suas opções - - Confira seus parametros e clique em `Change and Dump` - - Aguarde a exportação dos arquivos e verifique que foram copiados para a pasta indicada - - -Vincular os documentos ao novo banco do sapl 3.1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Primeiramente migre o banco do sapl 2.5 para o sapl 3.1 - -2. Copie a pasta exportada `sapl_documentos` dentro da pasta `media` da instalação do sapl 3.1 - -3. De forma semelhante ao realizado na migração do banco, dentro no mesmo ambiente virtual, rode os seguintes comandos:: - - ./manage.py shell --settings=sapl.legacy_migration_settings - - %run sapl/legacy/migracao_documentos.py - - migrar_documentos() - - -Para indexar os arquivos para pesquisa textual -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -1. workon sapl -2. ./manage.py rebuild_index - - -Dependendo da quantidade de arquivos a serem indexados, pode ser listado o seguinte erro 'Too many open files' - -Isto está ligado a quantidade máxima de aquivos que podem ser abertos ao mesmo tempo pelo sistema operacional - -Para aumentar este limite:: - - sudo nano /etc/security/limits.conf - * soft nofile 9000 - * hard nofile 65000 - - - sudo nano /etc/pam.d/common-session - session required pam_limits.so - -Após reiniciar, verificar se foram carregados os novos parâmetros com o comando:: - ulimit -a - -deve ser apresentado o seguinte:: - open files (-n) 9000 - - diff --git a/docs/instalacao31.rst b/docs/instalacao31.rst index a7f9e3060..83bd34d60 100644 --- a/docs/instalacao31.rst +++ b/docs/instalacao31.rst @@ -145,7 +145,7 @@ Criação da `SECRET_KEY MAX_IMAGE_UPLOAD_SIZE: - raise ValidationError("Imagem muito grande. ( > 2mb )") + raise ValidationError("Imagem muito grande. ( > 2MB )") return logotipo @@ -642,18 +630,25 @@ class LoginForm(AuthenticationForm): class ConfiguracoesAppForm(ModelForm): + mostrar_brasao_painel = forms.BooleanField( + help_text=_('Sugerimos fortemente que faça o upload de imagens com ' + 'o fundo transparente.'), + label=_('Mostrar brasão da Casa no painel?'), + required=False) + class Meta: model = AppConfig fields = ['documentos_administrativos', 'sequencia_numeracao', - 'painel_aberto', + # 'painel_aberto', # TODO: a ser implementado na versão 3.2 'texto_articulado_proposicao', 'texto_articulado_materia', 'texto_articulado_norma', 'proposicao_incorporacao_obrigatoria', 'cronometro_discurso', 'cronometro_aparte', - 'cronometro_ordem'] + 'cronometro_ordem', + 'mostrar_brasao_painel'] def __init__(self, *args, **kwargs): super(ConfiguracoesAppForm, self).__init__(*args, **kwargs) @@ -661,6 +656,20 @@ class ConfiguracoesAppForm(ModelForm): self.fields['cronometro_aparte'].widget.attrs['class'] = 'cronometro' self.fields['cronometro_ordem'].widget.attrs['class'] = 'cronometro' + def clean_mostrar_brasao_painel(self): + mostrar_brasao_painel = self.cleaned_data.get( + 'mostrar_brasao_painel', False) + casa = CasaLegislativa.objects.first() + + if not casa: + raise ValidationError("Não há casa legislativa relacionada") + + if (not bool(casa.logotipo) and mostrar_brasao_painel): + raise ValidationError("Não há logitipo configurado para esta " + "Casa legislativa.") + + return mostrar_brasao_painel + class RecuperarSenhaForm(PasswordResetForm): @@ -671,7 +680,7 @@ class RecuperarSenhaForm(PasswordResetForm): self.helper.layout = Layout( Fieldset(_('Insira o e-mail cadastrado com a sua conta'), row1, - form_actions(save_label='Enviar')) + form_actions(label='Enviar')) ) super(RecuperarSenhaForm, self).__init__(*args, **kwargs) @@ -702,4 +711,4 @@ class NovaSenhaForm(SetPasswordForm): self.helper = FormHelper() self.helper.layout = Layout( row1, - form_actions(save_label='Enviar')) + form_actions(label='Enviar')) diff --git a/sapl/base/migrations/0009_appconfig_mostrar_brasao_painel.py b/sapl/base/migrations/0009_appconfig_mostrar_brasao_painel.py new file mode 100644 index 000000000..9fb469da7 --- /dev/null +++ b/sapl/base/migrations/0009_appconfig_mostrar_brasao_painel.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-10-16 20:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0008_auto_20170814_1409'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='mostrar_brasao_painel', + field=models.BooleanField(default=False, verbose_name='Mostrar brasão da Casa no painel?'), + ), + ] diff --git a/sapl/base/migrations/0010_remove_appconfig_painel_aberto.py b/sapl/base/migrations/0010_remove_appconfig_painel_aberto.py new file mode 100644 index 000000000..cfe5f5c43 --- /dev/null +++ b/sapl/base/migrations/0010_remove_appconfig_painel_aberto.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-10-18 16:50 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0009_appconfig_mostrar_brasao_painel'), + ] + + operations = [ + migrations.RemoveField( + model_name='appconfig', + name='painel_aberto', + ), + ] diff --git a/sapl/base/migrations/0011_auto_20171121_0958.py b/sapl/base/migrations/0011_auto_20171121_0958.py new file mode 100644 index 000000000..6178bd950 --- /dev/null +++ b/sapl/base/migrations/0011_auto_20171121_0958.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-11-21 11:58 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0010_remove_appconfig_painel_aberto'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appconfig', + options={'ordering': ('-id',), 'permissions': (('menu_sistemas', 'Renderizar Menu Sistemas'), ('view_tabelas_auxiliares', 'Visualizar Tabelas Auxiliares')), 'verbose_name': 'Configurações da Aplicação', 'verbose_name_plural': 'Configurações da Aplicação'}, + ), + ] diff --git a/sapl/base/migrations/0012_auto_20171205_0917.py b/sapl/base/migrations/0012_auto_20171205_0917.py new file mode 100644 index 000000000..4080818fa --- /dev/null +++ b/sapl/base/migrations/0012_auto_20171205_0917.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-12-05 11:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0011_auto_20171121_0958'), + ] + + operations = [ + migrations.AlterField( + model_name='tipoautor', + name='descricao', + field=models.CharField(help_text='Obs: Não crie tipos de autores semelhante aos tipos fixos. ', max_length=50, verbose_name='Descrição'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index c75091341..15e14e39c 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,10 +1,18 @@ -import reversion from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models +from django.db.models.signals import post_migrate +from django.db.utils import DEFAULT_DB_ALIAS from django.utils.translation import ugettext_lazy as _ +import reversion + +from sapl.utils import ( + UF, + YES_NO_CHOICES, + get_settings_auth_user_model, + models_with_gr_for_model +) -from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), ('R', _('Restritivo'))) @@ -122,9 +130,10 @@ class AppConfig(models.Model): verbose_name=_('Sequência de numeração'), choices=SEQUENCIA_NUMERACAO, default='A') - painel_aberto = models.BooleanField( - verbose_name=_('Painel aberto para usuário anônimo'), - choices=YES_NO_CHOICES, default=False) + # TODO: a ser implementado na versão 3.2 + # painel_aberto = models.BooleanField( + # verbose_name=_('Painel aberto para usuário anônimo'), + # choices=YES_NO_CHOICES, default=False) texto_articulado_proposicao = models.BooleanField( verbose_name=_('Usar Textos Articulados para Proposições'), @@ -157,6 +166,10 @@ class AppConfig(models.Model): blank=True, null=True) + mostrar_brasao_painel = models.BooleanField( + default=False, + verbose_name=_('Mostrar brasão da Casa no painel?')) + class Meta: verbose_name = _('Configurações da Aplicação') verbose_name_plural = _('Configurações da Aplicação') @@ -164,13 +177,15 @@ class AppConfig(models.Model): ('menu_sistemas', _('Renderizar Menu Sistemas')), ('view_tabelas_auxiliares', _('Visualizar Tabelas Auxiliares')), ) + ordering = ('-id',) @classmethod def attr(cls, attr): config = AppConfig.objects.first() if not config: - return '' + config = AppConfig() + config.save() return getattr(config, attr) @@ -181,7 +196,10 @@ class AppConfig(models.Model): @reversion.register() class TipoAutor(models.Model): - descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) + descricao = models.CharField( + max_length=50, verbose_name=_('Descrição'), + help_text=_('Obs: Não crie tipos de autores ' + 'semelhante aos tipos fixos. ')) content_type = models.OneToOneField( ContentType, @@ -242,3 +260,40 @@ class Autor(models.Model): return str(self.partido) else: """ + + +def cria_models_tipo_autor(app_config, verbosity=2, interactive=True, + using=DEFAULT_DB_ALIAS, **kwargs): + + models = models_with_gr_for_model(Autor) + + print("\n\033[93m\033[1m{}\033[0m".format( + _('Atualizando registros TipoAutor do SAPL:'))) + for model in models: + content_type = ContentType.objects.get_for_model(model) + tipo_autor = TipoAutor.objects.filter( + content_type=content_type.id).exists() + + if tipo_autor: + msg1 = "Carga de {} não efetuada.".format( + TipoAutor._meta.verbose_name) + msg2 = " Já Existe um {} {} relacionado...".format( + TipoAutor._meta.verbose_name, + model._meta.verbose_name) + msg = " {}{}".format(msg1, msg2) + else: + novo_autor = TipoAutor() + novo_autor.content_type_id = content_type.id + novo_autor.descricao = model._meta.verbose_name + novo_autor.save() + msg1 = "Carga de {} efetuada.".format( + TipoAutor._meta.verbose_name) + msg2 = " {} {} criado...".format( + TipoAutor._meta.verbose_name, content_type.model) + msg = " {}{}".format(msg1, msg2) + print(msg) + # Disconecta função para evitar a chamada repetidas vezes. + post_migrate.disconnect(receiver=cria_models_tipo_autor) + + +post_migrate.connect(receiver=cria_models_tipo_autor) diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index cee126dd2..9c11f304f 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -3,33 +3,36 @@ import os.path import re import string -import textract +from django.db.models import Q, F, Value +from django.db.models.fields import TextField +from django.db.models.fields.files import FieldFile +from django.db.models.functions import Concat from django.template import loader -from haystack import indexes +from haystack.constants import Indexable +from haystack.fields import CharField +from haystack.indexes import SearchIndex +from haystack.utils import get_model_ct_tuple from textract.exceptions import ExtensionNotSupported +import textract +from sapl.compilacao.models import TextoArticulado, Dispositivo,\ + STATUS_TA_PUBLIC, STATUS_TA_IMMUTABLE_PUBLIC from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa from sapl.norma.models import NormaJuridica from sapl.settings import BASE_DIR, SOLR_URL -logger = logging.getLogger(BASE_DIR.name) +logger = logging.getLogger(BASE_DIR.name) -class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - filename = 'arquivo' - model = DocumentoAcessorio - template_name = 'materia/documentoacessorio_text.txt' +class TextExtractField(CharField): - def get_model(self): - return self.model - - def index_queryset(self, using=None): - return self.get_model().objects.all() + def __init__(self, **kwargs): + super().__init__(**kwargs) + assert self.model_attr - def get_updated_field(self): - return 'data_ultima_atualizacao' + if not isinstance(self.model_attr, (list, tuple)): + self.model_attr = (self.model_attr, ) def solr_extraction(self, arquivo): extracted_data = self._get_backend(None).extract_file_contents( @@ -59,71 +62,122 @@ class DocumentoAcessorioIndex(indexes.SearchIndex, indexes.Indexable): print(msg) logger.error(msg) - def prepare(self, obj): - if not self.filename or not self.model or not self.template_name: - raise Exception - - data = super(DocumentoAcessorioIndex, self).prepare(obj) - - arquivo = getattr(obj, self.filename) - - if arquivo: - if not os.path.exists(arquivo.path): - return self.prepared_data - - if not os.path.splitext(arquivo.path)[1][:1]: - return self.prepared_data - - # Em ambiente de produção utiliza-se o SOLR - if SOLR_URL: - try: - extracted_data = self.solr_extraction(arquivo) - except Exception: - self.print_error(arquivo) - return self.prepared_data - - # Em ambiente de DEV utiliza-se o Whoosh - # Como ele não possui extração, faz-se uso do textract - else: - try: - extracted_data = self.whoosh_extraction(arquivo) - except ExtensionNotSupported as e: - print(str(e)) - logger.error(str(e)) - return self.prepared_data - except Exception: - self.print_error(arquivo) - return self.prepared_data - - # Now we'll finally perform the template processing to render the - # text field with *all* of our metadata visible for templating: - t = loader.select_template(( - 'search/indexes/' + self.template_name, )) - data['text'] = t.render({'object': obj, - 'extracted': extracted_data}) - - return data - - return self.prepared_data - + def file_extractor(self, arquivo): + if not os.path.exists(arquivo.path) or \ + not os.path.splitext(arquivo.path)[1][:1]: + return '' + + # Em ambiente de produção utiliza-se o SOLR + if SOLR_URL: + try: + return self.solr_extraction(arquivo) + except Exception: + self.print_error(arquivo) + + # Em ambiente de DEV utiliza-se o Whoosh + # Como ele não possui extração, faz-se uso do textract + else: + try: + return self.whoosh_extraction(arquivo) + except ExtensionNotSupported as e: + print(str(e)) + logger.error(str(e)) + except Exception: + self.print_error(arquivo) + return '' + + def ta_extractor(self, value): + r = [] + for ta in value.filter(privacidade__in=[ + STATUS_TA_PUBLIC, + STATUS_TA_IMMUTABLE_PUBLIC]): + dispositivos = Dispositivo.objects.filter( + Q(ta=ta) | Q(ta_publicado=ta) + ).order_by( + 'ordem' + ).annotate( + rotulo_texto=Concat( + F('rotulo'), Value(' '), F('texto'), + output_field=TextField(), + ) + ).values_list( + 'rotulo_texto', flat=True) + r += list(filter(lambda x: x.strip(), dispositivos)) + return ' '.join(r) + + def string_extractor(self, value): + return value + + def extract_data(self, obj): + + data = '' + + for attr, func in self.model_attr: + if not hasattr(obj, attr) or not hasattr(self, func): + raise Exception + + value = getattr(obj, attr) + if not value: + continue + data += getattr(self, func)(value) + + return data + + def prepare_template(self, obj): + app_label, model_name = get_model_ct_tuple(obj) + template_names = ['search/indexes/%s/%s_%s.txt' % + (app_label, model_name, self.instance_name)] + + t = loader.select_template(template_names) + + return t.render({'object': obj, + 'extracted': self.extract_data(obj)}) + + +class DocumentoAcessorioIndex(SearchIndex, Indexable): + model = DocumentoAcessorio + text = TextExtractField( + document=True, use_template=True, + model_attr=( + ('arquivo', 'file_extractor'), + ('ementa', 'string_extractor'), + ('indexacao', 'string_extractor'), + ) + ) -class MateriaLegislativaIndex(DocumentoAcessorioIndex): - text = indexes.CharField(document=True, use_template=True) + def get_model(self): + return self.model - filename = 'texto_original' - model = MateriaLegislativa - template_name = 'materia/materialegislativa_text.txt' + def index_queryset(self, using=None): + return self.get_model().objects.all() def get_updated_field(self): return 'data_ultima_atualizacao' class NormaJuridicaIndex(DocumentoAcessorioIndex): - text = indexes.CharField(document=True, use_template=True) - - filename = 'texto_integral' model = NormaJuridica - template_name = 'norma/normajuridica_text.txt' + text = TextExtractField( + document=True, use_template=True, + model_attr=( + ('texto_integral', 'file_extractor'), + ('texto_articulado', 'ta_extractor'), + ('ementa', 'string_extractor'), + ('indexacao', 'string_extractor'), + ('observacao', 'string_extractor'), + ) + ) - def get_updated_field(self): - return 'data_ultima_atualizacao' + +class MateriaLegislativaIndex(DocumentoAcessorioIndex): + model = MateriaLegislativa + text = TextExtractField( + document=True, use_template=True, + model_attr=( + ('texto_original', 'file_extractor'), + ('texto_articulado', 'ta_extractor'), + ('ementa', 'string_extractor'), + ('indexacao', 'string_extractor'), + ('observacao', 'string_extractor'), + ) + ) diff --git a/sapl/base/templatetags/base_tags.py b/sapl/base/templatetags/base_tags.py new file mode 100644 index 000000000..22f0aa4b1 --- /dev/null +++ b/sapl/base/templatetags/base_tags.py @@ -0,0 +1,9 @@ + +from django import template + +register = template.Library() + + +@register.filter +def tipoautor_contenttype_list(tipo): + return 'sapl.'+tipo.content_type.app_label+':'+tipo.content_type.model+'_list' diff --git a/sapl/base/tests/test_form.py b/sapl/base/tests/test_form.py index 72e5a69e7..35f1c82bf 100644 --- a/sapl/base/tests/test_form.py +++ b/sapl/base/tests/test_form.py @@ -1,3 +1,4 @@ +import pytest from django.utils.translation import ugettext_lazy as _ from sapl.base.forms import CasaLegislativaForm @@ -18,3 +19,23 @@ def test_valida_campos_obrigatorios_casa_legislativa_form(): assert errors['uf'] == [_('Este campo é obrigatório.')] assert len(errors) == 6 + + +@pytest.mark.django_db(transaction=False) +def test_casa_legislativa_form_invalido(): + form = CasaLegislativaForm(data={'codigo': 'codigo', + 'nome': 'nome', + 'sigla': 'sg', + 'endereco': 'endereco', + 'cep': '7000000', + 'municipio': 'municipio', + 'uf': 'uf', + 'telefone': '33333333', + 'fax': '33333333', + 'logotipo': 'image', + 'endereco_web': 'web', + 'email': 'email', + 'informacao_geral': 'informacao_geral' + }) + + assert not form.is_valid() diff --git a/sapl/base/tests/test_view_base.py b/sapl/base/tests/test_view_base.py new file mode 100644 index 000000000..fc61ac160 --- /dev/null +++ b/sapl/base/tests/test_view_base.py @@ -0,0 +1,35 @@ +import pytest +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + + +@pytest.mark.django_db(transaction=False) +def test_incluir_casa_legislativa_errors(admin_client): + + response = admin_client.post(reverse('sapl.base:casalegislativa_create'), + {'salvar': 'salvar'}, + follow=True) + + assert (response.context_data['form'].errors['nome'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['sigla'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['endereco'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['cep'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['municipio'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['uf'] == + [_('Este campo é obrigatório.')]) + + +@pytest.mark.django_db(transaction=False) +def test_incluir_tipo_autor_errors(admin_client): + + response = admin_client.post(reverse('sapl.base:tipoautor_create'), + {'salvar': 'salvar'}, + follow=True) + + assert (response.context_data['form'].errors['descricao'] == + [_('Este campo é obrigatório.')]) diff --git a/sapl/base/tests/teststub_urls.py b/sapl/base/tests/teststub_urls.py index 4ad6c6d9e..9796768f2 100644 --- a/sapl/base/tests/teststub_urls.py +++ b/sapl/base/tests/teststub_urls.py @@ -3,7 +3,9 @@ from django.views.generic.base import TemplateView from sapl.urls import urlpatterns as original_patterns -urlpatterns = original_patterns + patterns('', url(r'^zzzz$', - TemplateView.as_view( - template_name='index.html'), - name='zzzz')) +ptrn = patterns('', + url(r'^zzzz$', + TemplateView.as_view( + template_name='index.html'), name='zzzz')) + +urlpatterns = original_patterns + ptrn diff --git a/sapl/base/urls.py b/sapl/base/urls.py index ed55f1767..d45532d0a 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -11,7 +11,7 @@ from sapl.settings import EMAIL_SEND_USER from .apps import AppConfig from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm -from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, +from .views import (AppConfigCrud, CasaLegislativaCrud, HelpTopicView, RelatorioAtasView, RelatorioHistoricoTramitacaoView, RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView, @@ -54,12 +54,10 @@ urlpatterns = [ url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())), url(r'^sistema/autor/', include(AutorCrud.get_urls())), - url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda.html')), url(r'^sistema/ajuda/(?P\w+)$', - HelpView.as_view(), name='help_topic'), - url(r'^sistema/ajuda/', - TemplateView.as_view(template_name='ajuda/index.html'), - name='help_base'), + HelpTopicView.as_view(), name='help_topic'), + url(r'^sistema/ajuda/$', TemplateView.as_view(template_name='ajuda.html'), + name='help'), url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()), name="casa_legislativa"), url(r'^sistema/app-config/', include(AppConfigCrud.get_urls())), diff --git a/sapl/base/views.py b/sapl/base/views.py index cae2052c9..6b666da0d 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,17 +1,17 @@ - from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Group from django.contrib.auth.tokens import default_token_generator -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.mail import send_mail from django.core.urlresolvers import reverse from django.db.models import Count -from django.http import HttpResponseRedirect +from django.http import Http404, HttpResponseRedirect +from django.template import TemplateDoesNotExist +from django.template.loader import get_template from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, string_concat from django.views.generic.base import TemplateView from django_filters.views import FilterView from haystack.views import SearchView @@ -23,7 +23,8 @@ from sapl.materia.models import (Autoria, MateriaLegislativa, TipoMateriaLegislativa) from sapl.sessao.models import (PresencaOrdemDia, SessaoPlenaria, SessaoPlenariaPresenca) -from sapl.utils import parlamentares_ativos, sapl_logger +from sapl.utils import (parlamentares_ativos, sapl_logger, + show_results_filter_set) from .forms import (CasaLegislativaForm, ConfiguracoesAppForm, RelatorioAtasFilterSet, @@ -53,16 +54,52 @@ class ConfirmarEmailView(TemplateView): class TipoAutorCrud(CrudAux): model = TipoAutor - help_path = 'tipo-autor' + help_topic = 'tipo-autor' class BaseMixin(CrudAux.BaseMixin): - list_field_names = ['descricao', 'content_type'] + list_field_names = ['descricao'] form_class = TipoAutorForm + @property + def verbose_name(self): + vn = super().verbose_name + vn = string_concat(vn, ' ', _('Externo ao SAPL')) + return vn + + class ListView(CrudAux.ListView): + def get_queryset(self): + qs = CrudAux.ListView.get_queryset(self) + qs = qs.filter(content_type__isnull=True) + return qs + + def get_context_data(self, **kwargs): + context = CrudAux.ListView.get_context_data(self, **kwargs) + + context['tipos_sapl'] = TipoAutor.objects.filter( + content_type__isnull=False) + + return context + + class TipoAutorMixin: + def dispatch(self, request, *args, **kwargs): + object = self.get_object() + if object.content_type: + raise PermissionDenied() + return super().get(request, *args, **kwargs) + + class UpdateView(TipoAutorMixin, CrudAux.UpdateView): + pass + + class DetailView(TipoAutorMixin, CrudAux.DetailView): + pass + + class DeleteView(TipoAutorMixin, CrudAux.DeleteView): + pass + class AutorCrud(CrudAux): model = Autor - help_path = 'autor' + help_topic = 'autor' class BaseMixin(CrudAux.BaseMixin): list_field_names = ['tipo', 'nome', 'user'] @@ -202,6 +239,9 @@ class RelatorioAtasView(FilterView): context['object_list'] = context['object_list'].exclude(upload_ata='') qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + return context @@ -220,80 +260,83 @@ class RelatorioPresencaSessaoView(FilterView): return context # ===================================================================== - if 'salvar' in self.request.GET: - where = context['object_list'].query.where - _range = where.children[0].rhs - - sufixo = 'sessao_plenaria__data_inicio__range' - param0 = {'%s' % sufixo: _range} - - # Parlamentares com Mandato no intervalo de tempo (Ativos) - parlamentares_qs = parlamentares_ativos( - _range[0], _range[1]).order_by('nome_parlamentar') - parlamentares_id = parlamentares_qs.values_list( - 'id', flat=True) - - # Presenças de cada Parlamentar em Sessões - presenca_sessao = SessaoPlenariaPresenca.objects.filter( - parlamentar_id__in=parlamentares_id, - sessao_plenaria__data_inicio__range=_range).values_list( - 'parlamentar_id').annotate( - sessao_count=Count('id')) - - # Presenças de cada Ordem do Dia - presenca_ordem = PresencaOrdemDia.objects.filter( - parlamentar_id__in=parlamentares_id, - sessao_plenaria__data_inicio__range=_range).values_list( - 'parlamentar_id').annotate( - sessao_count=Count('id')) - - total_ordemdia = PresencaOrdemDia.objects.filter( - **param0).distinct('sessao_plenaria__id').order_by( - 'sessao_plenaria__id').count() - - total_sessao = context['object_list'].count() - - # Completa o dicionario as informacoes parlamentar/sessao/ordem - parlamentares_presencas = [] - for i, p in enumerate(parlamentares_qs): - parlamentares_presencas.append({ - 'parlamentar': p, - 'sessao_porc': 0, - 'ordemdia_porc': 0 - }) - try: - sessao_count = presenca_sessao.get(parlamentar_id=p.id)[1] - except ObjectDoesNotExist: - sessao_count = 0 - try: - ordemdia_count = presenca_ordem.get(parlamentar_id=p.id)[1] - except ObjectDoesNotExist: - ordemdia_count = 0 - - parlamentares_presencas[i].update({ - 'sessao_count': sessao_count, - 'ordemdia_count': ordemdia_count - }) - - if total_sessao != 0: - parlamentares_presencas[i].update( - {'sessao_porc': round( - sessao_count * 100 / total_sessao, 2)}) - if total_ordemdia != 0: - parlamentares_presencas[i].update( - {'ordemdia_porc': round( - ordemdia_count * 100 / total_ordemdia, 2)}) - - context['date_range'] = _range - context['total_ordemdia'] = total_ordemdia - context['total_sessao'] = context['object_list'].count() - context['parlamentares'] = parlamentares_presencas - context['periodo'] = ( - self.request.GET['data_inicio_0'] + - ' - ' + self.request.GET['data_inicio_1']) + # if 'salvar' not in self.request.GET: + where = context['object_list'].query.where + _range = where.children[0].rhs + + sufixo = 'sessao_plenaria__data_inicio__range' + param0 = {'%s' % sufixo: _range} + + # Parlamentares com Mandato no intervalo de tempo (Ativos) + parlamentares_qs = parlamentares_ativos( + _range[0], _range[1]).order_by('nome_parlamentar') + parlamentares_id = parlamentares_qs.values_list( + 'id', flat=True) + + # Presenças de cada Parlamentar em Sessões + presenca_sessao = SessaoPlenariaPresenca.objects.filter( + parlamentar_id__in=parlamentares_id, + sessao_plenaria__data_inicio__range=_range).values_list( + 'parlamentar_id').annotate( + sessao_count=Count('id')) + + # Presenças de cada Ordem do Dia + presenca_ordem = PresencaOrdemDia.objects.filter( + parlamentar_id__in=parlamentares_id, + sessao_plenaria__data_inicio__range=_range).values_list( + 'parlamentar_id').annotate( + sessao_count=Count('id')) + + total_ordemdia = PresencaOrdemDia.objects.filter( + **param0).distinct('sessao_plenaria__id').order_by( + 'sessao_plenaria__id').count() + + total_sessao = context['object_list'].count() + + # Completa o dicionario as informacoes parlamentar/sessao/ordem + parlamentares_presencas = [] + for i, p in enumerate(parlamentares_qs): + parlamentares_presencas.append({ + 'parlamentar': p, + 'sessao_porc': 0, + 'ordemdia_porc': 0 + }) + try: + sessao_count = presenca_sessao.get(parlamentar_id=p.id)[1] + except ObjectDoesNotExist: + sessao_count = 0 + try: + ordemdia_count = presenca_ordem.get(parlamentar_id=p.id)[1] + except ObjectDoesNotExist: + ordemdia_count = 0 + + parlamentares_presencas[i].update({ + 'sessao_count': sessao_count, + 'ordemdia_count': ordemdia_count + }) + + if total_sessao != 0: + parlamentares_presencas[i].update( + {'sessao_porc': round( + sessao_count * 100 / total_sessao, 2)}) + if total_ordemdia != 0: + parlamentares_presencas[i].update( + {'ordemdia_porc': round( + ordemdia_count * 100 / total_ordemdia, 2)}) + + context['date_range'] = _range + context['total_ordemdia'] = total_ordemdia + context['total_sessao'] = context['object_list'].count() + context['parlamentares'] = parlamentares_presencas + context['periodo'] = ( + self.request.GET['data_inicio_0'] + + ' - ' + self.request.GET['data_inicio_1']) # ===================================================================== qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + return context @@ -308,6 +351,9 @@ class RelatorioHistoricoTramitacaoView(FilterView): context['title'] = _('Histórico de Tramitações') qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + return context @@ -337,6 +383,8 @@ class RelatorioMateriasTramitacaoView(FilterView): qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + context['show_results'] = show_results_filter_set(qr) + return context @@ -409,6 +457,8 @@ class RelatorioMateriasPorAnoAutorTipoView(FilterView): qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + context['show_results'] = show_results_filter_set(qr) + if 'ano' in self.request.GET and self.request.GET['ano']: ano = int(self.request.GET['ano']) context['relatorio'] = self.get_materias_autor_ano(ano) @@ -447,6 +497,8 @@ class RelatorioMateriasPorAutorView(FilterView): qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + context['show_results'] = show_results_filter_set(qr) + return context @@ -477,11 +529,15 @@ class CasaLegislativaCrud(CrudAux): kwargs={'pk': self.kwargs['pk']})) -class HelpView(PermissionRequiredMixin, TemplateView): - # XXX treat non existing template as a 404!!!! +class HelpTopicView(TemplateView): def get_template_names(self): - return ['ajuda/%s.html' % self.kwargs['topic']] + topico = self.kwargs['topic'] + try: + get_template('ajuda/%s.html' % topico) + except TemplateDoesNotExist: + raise Http404() + return ['ajuda/%s.html' % topico] class AppConfigCrud(CrudAux): @@ -501,15 +557,18 @@ class AppConfigCrud(CrudAux): class CreateView(CrudAux.CreateView): def get(self, request, *args, **kwargs): - app_config = AppConfig.objects.last() - if app_config: - return HttpResponseRedirect( - reverse('sapl.base:appconfig_update', - kwargs={'pk': app_config.pk})) - else: - self.object = None - return super(CrudAux.CreateView, self).get( - request, *args, **kwargs) + app_config = AppConfig.objects.first() + + if not app_config: + app_config = AppConfig() + app_config.save() + + return HttpResponseRedirect( + reverse('sapl.base:appconfig_update', + kwargs={'pk': app_config.pk})) + + def post(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) class ListView(CrudAux.ListView): diff --git a/sapl/comissoes/migrations/0003_auto_20171204_1658.py b/sapl/comissoes/migrations/0003_auto_20171204_1658.py new file mode 100644 index 000000000..340249cfa --- /dev/null +++ b/sapl/comissoes/migrations/0003_auto_20171204_1658.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-12-04 18:58 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('comissoes', '0002_auto_20170809_1236'), + ] + + operations = [ + migrations.AlterModelOptions( + name='comissao', + options={'ordering': ['nome'], 'verbose_name': 'Comissão', 'verbose_name_plural': 'Comissões'}, + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 0b3c664ee..510071d4f 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -95,6 +95,7 @@ class Comissao(models.Model): class Meta: verbose_name = _('Comissão') verbose_name_plural = _('Comissões') + ordering = ['nome'] def __str__(self): return self.sigla + ' - ' + self.nome diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index 713c8f03e..0c315f0d9 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -55,9 +55,6 @@ class ComposicaoCrud(MasterDetailCrud): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) -# context['composicao_pk'] = context['composicao_list'].last( -# ).pk if self.take_composicao_pk( -# ) == 0 else self.take_composicao_pk() composicao_pk = self.take_composicao_pk() @@ -72,13 +69,13 @@ class ComposicaoCrud(MasterDetailCrud): context['participacao_set'] = Participacao.objects.filter( composicao__pk=context['composicao_pk'] - ).order_by('parlamentar') + ).order_by('id') return context class ComissaoCrud(Crud): model = Comissao - help_path = 'modulo_comissoes' + help_topic = 'modulo_comissoes' public = [RP_LIST, RP_DETAIL, ] class BaseMixin(Crud.BaseMixin): diff --git a/sapl/compilacao/migrations/0004_auto_20171031_1327.py b/sapl/compilacao/migrations/0004_auto_20171031_1327.py new file mode 100644 index 000000000..f5820c6a8 --- /dev/null +++ b/sapl/compilacao/migrations/0004_auto_20171031_1327.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-10-31 15:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compilacao', '0003_auto_20170825_1136'), + ] + + operations = [ + migrations.AlterField( + model_name='tipotextoarticulado', + name='participacao_social', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Participação Social'), + ), + migrations.AlterField( + model_name='tipotextoarticulado', + name='publicacao_func', + field=models.BooleanField(choices=[(True, 'Sim'), (False, 'Não')], default=False, verbose_name='Histórico de Publicação'), + ), + ] diff --git a/sapl/compilacao/models.py b/sapl/compilacao/models.py index 2d3d80116..e4be0762b 100644 --- a/sapl/compilacao/models.py +++ b/sapl/compilacao/models.py @@ -1,6 +1,4 @@ -from datetime import datetime -import reversion from django.contrib import messages from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -10,9 +8,11 @@ from django.db.models.aggregates import Max from django.db.models.deletion import PROTECT from django.http.response import Http404 from django.template import defaultfilters +from django.utils import timezone from django.utils.decorators import classonlymethod from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +import reversion from sapl.compilacao.utils import (get_integrations_view_names, int_to_letter, int_to_roman) @@ -115,13 +115,13 @@ class TipoTextoArticulado(models.Model): on_delete=models.SET_NULL, verbose_name=_('Modelo Integrado')) participacao_social = models.BooleanField( - blank=False, + blank=False, default=False, choices=YES_NO_CHOICES, verbose_name=_('Participação Social')) publicacao_func = models.BooleanField( choices=YES_NO_CHOICES, - blank=False, + blank=False, default=False, verbose_name=_('Histórico de Publicação')) perfis = models.ManyToManyField( @@ -357,9 +357,9 @@ class TextoArticulado(TimestampedMixin): if not ta.data: ta.data = getattr(obj, map_fields['data'] if map_fields['data'] else 'xxx', - datetime.now()) + timezone.now()) if not ta.data: - ta.data = datetime.now() + ta.data = timezone.now() ta.ementa = getattr( obj, map_fields['ementa'] @@ -370,15 +370,16 @@ class TextoArticulado(TimestampedMixin): obj, map_fields['observacao'] if map_fields['observacao'] else 'xxx', '') + now = timezone.now() ta.numero = getattr( obj, map_fields['numero'] if map_fields['numero'] else 'xxx', int('%s%s%s' % ( - int(datetime.now().year), - int(datetime.now().month), - int(datetime.now().day)))) + int(now.year), + int(now.month), + int(now.day)))) ta.ano = getattr(obj, map_fields['ano'] - if map_fields['ano'] else 'xxx', datetime.now().year) + if map_fields['ano'] else 'xxx', now.year) ta.save() return ta diff --git a/sapl/compilacao/tests/test_tipo_texto_articulado_form.py b/sapl/compilacao/tests/test_tipo_texto_articulado_form.py index e121d771f..d9db055e3 100644 --- a/sapl/compilacao/tests/test_tipo_texto_articulado_form.py +++ b/sapl/compilacao/tests/test_tipo_texto_articulado_form.py @@ -3,8 +3,7 @@ from django.utils.translation import ugettext as _ from model_mommy import mommy from sapl.compilacao import forms -from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, - TipoNota) +from sapl.compilacao.models import PerfilEstruturalTextoArticulado, TipoNota from sapl.compilacao.views import choice_models_in_extenal_views diff --git a/sapl/compilacao/views.py b/sapl/compilacao/views.py index 4dfad8e99..878c21b06 100644 --- a/sapl/compilacao/views.py +++ b/sapl/compilacao/views.py @@ -1,7 +1,7 @@ -import logging -import sys from collections import OrderedDict from datetime import timedelta +import logging +import sys from braces.views import FormMessagesMixin from django import forms @@ -19,8 +19,8 @@ from django.http.response import (HttpResponse, HttpResponseRedirect, from django.shortcuts import get_object_or_404, redirect from django.utils.dateparse import parse_date from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import (CreateView, DeleteView, FormView, @@ -50,6 +50,7 @@ from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, from sapl.crud.base import Crud, CrudListView, make_pagination from sapl.settings import BASE_DIR + TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota') TipoVideCrud = Crud.build(TipoVide, 'tipo_vide') TipoPublicacaoCrud = Crud.build(TipoPublicacao, 'tipo_publicacao') @@ -829,7 +830,7 @@ class PublicacaoListView(PublicacaoMixin, ListView): @property def title(self): - return _('%s de %s' % ( + return _('%s (%s)' % ( self.model._meta.verbose_name_plural, self.ta)) @@ -845,6 +846,8 @@ class PublicacaoListView(PublicacaoMixin, ListView): def get_context_data(self, **kwargs): context = super(PublicacaoListView, self).get_context_data(**kwargs) + context['title'] = self.title + context['object'] = self.ta context['NO_ENTRIES_MSG'] = CrudListView.no_entries_msg return context @@ -876,7 +879,17 @@ class PublicacaoCreateView(PublicacaoMixin, FormMessagesMixin, CreateView): class PublicacaoDetailView(PublicacaoMixin, DetailView): model = Publicacao - permission_required = 'compilacao.detail_publicacao' + permission_required = [] + + @property + def list_url(self): + return reverse_lazy('sapl.compilacao:ta_pub_list', + kwargs={ + 'ta_id': self.kwargs['ta_id']}) + + @property + def verbose_name_plural(self): + return self.model._meta.verbose_name_plural class PublicacaoUpdateView(PublicacaoMixin, UpdateView): @@ -1157,10 +1170,14 @@ class TextEditView(CompMixin, TemplateView): self.object.save() messages.success(request, _( 'Texto Articulado desbloqueado com sucesso.')) + + if self.object.content_object: + self.object.content_object.save() + else: if 'lock' in request.GET: - # TODO - implementar logging de ação de usuário + # TODO - implementar logging de ação de usuário notificacoes = self.get_notificacoes( object_list=self.object.dispositivos_set.all(), type_notificacoes=['danger', ]) @@ -1183,6 +1200,9 @@ class TextEditView(CompMixin, TemplateView): messages.success(request, _( 'Texto Articulado bloqueado com sucesso.')) + if self.object.content_object: + self.object.content_object.save() + return redirect(to=reverse_lazy( 'sapl.compilacao:ta_text', kwargs={ 'ta_id': self.object.id})) diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index eec259190..0b8541a58 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,13 +1,13 @@ from math import ceil -import rtyaml from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit from django import template -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy from django.utils import formats from django.utils.translation import ugettext as _ +import rtyaml def heads_and_tails(list_of_lists): @@ -34,9 +34,19 @@ def to_fieldsets(fields): yield field -def form_actions(more=[], save_label=_('Salvar')): +def form_actions(more=[], + label=_('Salvar'), name='salvar', css_class='pull-right', disabled=True): + + if disabled: + doubleclick = 'this.form.submit();this.disabled=true;' + else: + doubleclick = 'return true;' + return FormActions( - Submit('salvar', save_label, css_class='pull-right'), *more) + Submit(name, label, css_class=css_class, + # para impedir resubmissão do form + onclick=doubleclick), + *more) class SaplFormLayout(Layout): @@ -46,7 +56,7 @@ class SaplFormLayout(Layout): buttons = actions if not buttons: - buttons = form_actions(save_label=save_label, more=[ + buttons = form_actions(label=save_label, more=[ HTML('%s' % cancel_label) if cancel_label else None]) @@ -202,7 +212,16 @@ class CrispyLayoutFormMixin: def get_column(self, fieldname, span): obj = self.get_object() - verbose_name, text = get_field_display(obj, fieldname) + + func = None + if '|' in fieldname: + fieldname, func = tuple(fieldname.split('|')) + + if func: + verbose_name, text = getattr(self, func)(obj, fieldname) + else: + verbose_name, text = get_field_display(obj, fieldname) + return { 'id': fieldname, 'span': span, @@ -210,6 +229,52 @@ class CrispyLayoutFormMixin: 'text': text, } + def fk_urlize_for_detail(self, obj, fieldname): + + field = obj._meta.get_field(fieldname) + value = getattr(obj, fieldname) + + display = '{}'.format( + reverse( + '%s:%s_detail' % ( + value._meta.app_config.name, value._meta.model_name), + args=(value.id,)), + value) + + return field.verbose_name, display + + def m2m_urlize_for_detail(self, obj, fieldname): + + manager, fieldname = tuple(fieldname.split('__')) + + manager = getattr(obj, manager) + + verbose_name = manager.model._meta.verbose_name + display = '' + for item in manager.all(): + obj_m2m = getattr(item, fieldname) + + if obj == obj_m2m: + continue + + verbose_name = item._meta.get_field(fieldname).verbose_name + + display += '
  • {}
  • '.format( + reverse( + '%s:%s_detail' % ( + obj_m2m._meta.app_config.name, obj_m2m._meta.model_name), + args=(obj_m2m.id,)), + obj_m2m) + + display += '' + + if display: + display = '
      %s
    ' % display + else: + verbose_name = '' + + return verbose_name, display + @property def layout_display(self): diff --git a/sapl/crud/base.py b/sapl/crud/base.py index 05c2b03a9..d53125249 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -81,7 +81,7 @@ def make_pagination(index, num_pages): """ variáveis do crud: - help_path + help_topic container_field container_field_set is_m2m @@ -865,7 +865,7 @@ class Crud: DetailView = CrudDetailView UpdateView = CrudUpdateView DeleteView = CrudDeleteView - help_path = '' + help_topic = '' class PublicMixin: permission_required = [] @@ -877,7 +877,7 @@ class Crud: if view: class CrudViewWithBase(cls.BaseMixin, view): model = cls.model - help_path = cls.help_path + help_topic = cls.help_topic crud = cls CrudViewWithBase.__name__ = view.__name__ return CrudViewWithBase @@ -909,13 +909,13 @@ class Crud: for regex, view, suffix in cruds] @classonlymethod - def build(cls, _model, _help_path, _model_set=None, list_field_names=[]): + def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]): def create_class(_list_field_names): class ModelCrud(cls): model = _model model_set = _model_set - help_path = _help_path + help_topic = _help_topic list_field_names = _list_field_names return ModelCrud @@ -938,16 +938,6 @@ class CrudAux(Crud): class BaseMixin(Crud.BaseMixin): subnav_template_name = None - def __init__(self, **kwargs): - super().__init__(**kwargs) - """ - Mantem as permissões individuais geradas pelo Crud através do - Modelo e adiciona a obrigatoriedade de permissão para view - tabelas auxiliares. - """ - self.permission_required = self.permission_required + \ - self.crud.permission_required - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) """Força o template filter subnav em base/templatetags/menus.py @@ -960,10 +950,10 @@ class CrudAux(Crud): return context @classonlymethod - def build(cls, _model, _help_path, _model_set=None, list_field_names=[]): + def build(cls, _model, _help_topic, _model_set=None, list_field_names=[]): ModelCrud = Crud.build( - _model, _help_path, _model_set, list_field_names) + _model, _help_topic, _model_set, list_field_names) class ModelCrudAux(CrudAux, ModelCrud): pass @@ -1415,10 +1405,10 @@ class MasterDetailCrud(Crud): return '' @classonlymethod - def build(cls, model, parent_field, help_path, + def build(cls, model, parent_field, help_topic, _model_set=None, list_field_names=[]): crud = super(MasterDetailCrud, cls).build( - model, help_path, _model_set=_model_set, + model, help_topic, _model_set=_model_set, list_field_names=list_field_names) crud.parent_field = parent_field return crud diff --git a/sapl/crud/tests/stub_app/views.py b/sapl/crud/tests/stub_app/views.py index 81c6b834d..11d4e3d9f 100644 --- a/sapl/crud/tests/stub_app/views.py +++ b/sapl/crud/tests/stub_app/views.py @@ -6,7 +6,7 @@ from .models import City, Country class CountryCrud(Crud): model = Country - help_path = 'help_path', + help_topic = 'help_topic', class ListView(CrudListView): paginate_by = 10 @@ -14,4 +14,4 @@ class CountryCrud(Crud): class CityCrud(MasterDetailCrud): model = City - help_path = 'help_path', + help_topic = 'help_topic', diff --git a/sapl/decorators.py b/sapl/decorators.py index 6bf61d81b..fac94344a 100644 --- a/sapl/decorators.py +++ b/sapl/decorators.py @@ -1,6 +1,8 @@ -from datetime import date from functools import wraps +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + def vigencia_atual(decorated_method): """ @@ -13,26 +15,27 @@ def vigencia_atual(decorated_method): """ @wraps(decorated_method) def display_atual(self): - try: - string_displayed = decorated_method(self) - except TypeError: - string_displayed = "" + string_displayed = decorated_method(self) if hasattr(self, 'data_inicio') and hasattr(self, 'data_fim'): - today = date.today() + today = timezone.now().today().date() e_atual = self.data_inicio <= today <= self.data_fim string_displayed = "{} {}".format( string_displayed, "(Atual)" if e_atual else "") else: - print('{} {}'.format( - "Instance does not have the attributes [{}, {}].".format( - 'data_inicio', - 'data_fim' - ), - "Decorator @{} has been disabled.".format( - vigencia_atual.__name__() - ) - ) + instancia_sem_atributo = "{} [{}, {}].".format( + 'Instância não possui os atributos', + 'data_inicio', + 'data_fim') + + mensagem_decorator = "Decorator @{} foi desabilitado.".format( + vigencia_atual.__name__() + ) + print(_('{} {}'.format( + _(instancia_sem_atributo), + _(mensagem_decorator) + ) + ) ) return string_displayed diff --git a/sapl/hashers.py b/sapl/hashers.py new file mode 100644 index 000000000..a514f8f19 --- /dev/null +++ b/sapl/hashers.py @@ -0,0 +1,54 @@ +import base64 +import hashlib + +from django.contrib.auth.hashers import PBKDF2PasswordHasher, make_password +from django.utils.encoding import force_bytes + + +def to_base64(source): + return base64.b64encode(source).decode('utf-8') + + +class ZopeSHA1PasswordHasher(PBKDF2PasswordHasher): + """ + The SHA1 password hashing algorithm used by Zope. + Zope uses `password + salt`, Django has `salt + password`. + Pre encode with SHA1 in this order and PBKDF2 afterwards. + + based on https://www.fourdigits.nl/blog/converting-plone-data-to-django/ + """ + + algorithm = "zope_sha1_pbkdf2" + + def encode(self, password, salt, iterations=None): + assert password is not None + assert salt + password = force_bytes(password) + decoded_salt = base64.b64decode(salt) + + # this is what is stored in zope + hashed = hashlib.sha1(password + decoded_salt).digest() + decoded_salt + hashed = to_base64(hashed) + + # encode again with the standard method + return super().encode(hashed, salt, iterations) + + +def get_salt_from_zope_sha1(data): + intermediate = base64.b64decode(data) + salt = intermediate[20:].strip() + return to_base64(salt) + + +ZOPE_SHA1_PREFIX = '{SSHA}' + + +def zope_encoded_password_to_django(encoded): + if encoded.startswith(ZOPE_SHA1_PREFIX): + data = encoded[len(ZOPE_SHA1_PREFIX):] + salt = get_salt_from_zope_sha1(data) + hasher = ZopeSHA1PasswordHasher() + return super(ZopeSHA1PasswordHasher, hasher).encode(data, salt) + else: + # assume it's a plain password and use the default hashing + return make_password(encoded) diff --git a/sapl/legacy/management/commands/migracao_25_31.py b/sapl/legacy/management/commands/migracao_25_31.py index bddb26a8c..c0e2fa352 100644 --- a/sapl/legacy/management/commands/migracao_25_31.py +++ b/sapl/legacy/management/commands/migracao_25_31.py @@ -1,3 +1,4 @@ +from django.core import management from django.core.management.base import BaseCommand from sapl.legacy import migration @@ -17,4 +18,5 @@ class Command(BaseCommand): ) def handle(self, *args, **options): + management.call_command('migrate') migration.migrate(interativo=not options['force']) diff --git a/sapl/legacy/management/commands/recria_constraints.py b/sapl/legacy/management/commands/recria_constraints.py index 9e999e5f6..d1d8d606d 100644 --- a/sapl/legacy/management/commands/recria_constraints.py +++ b/sapl/legacy/management/commands/recria_constraints.py @@ -1,7 +1,5 @@ from django.core.management.base import BaseCommand -from sapl.legacy.migration import recria_constraints - class Command(BaseCommand): @@ -9,4 +7,4 @@ class Command(BaseCommand): 'migração de dados') def handle(self, *args, **options): - recria_constraints() + pass diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 2e514942b..1d2d38dcc 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -5,6 +5,7 @@ import re import magic from sapl.base.models import CasaLegislativa +from sapl.legacy.migration import warn from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, Proposicao) from sapl.norma.models import NormaJuridica @@ -21,6 +22,7 @@ EXTENSOES = { 'application/vnd.oasis.opendocument.text': '.odt', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa 'application/xml': '.xml', + 'text/xml': '.xml', 'application/zip': '.zip', 'image/jpeg': '.jpeg', 'image/png': '.png', @@ -90,11 +92,11 @@ DOCS = { ], } -DOCS = {tipo: [(campo, - os.path.join('sapl_documentos', origem), - os.path.join('sapl', destino)) - for campo, origem, destino in campos] - for tipo, campos in DOCS.items()} +DOCS = {model: [(campo, + os.path.join('sapl_documentos', origem), + os.path.join('sapl', destino)) + for campo, origem, destino in campos] + for model, campos in DOCS.items()} def em_media(caminho): @@ -125,10 +127,14 @@ def migrar_docs_logo(): # a pasta props_sapl deve conter apenas o origem e metadatas! # Edit: Aparentemente há diretório que contém properties ao invés de # metadata. O assert foi modificado para essa situação. - assert set(os.listdir(em_media(props_sapl))) < { + sobrando = set(os.listdir(em_media(props_sapl))) - { 'logo_casa.gif', '.metadata', 'logo_casa.gif.metadata', '.properties', 'logo_casa.gif.properties', '.objects'} + if sobrando: + warn('Os seguintes arquivos da pasta props_sapl foram ignorados: ' + + ', '.join(sobrando)) + mover_documento(origem, destino) casa = get_casa_legislativa() casa.logotipo = destino @@ -152,9 +158,9 @@ def get_extensao(caminho): )) from e -def migrar_docs_por_ids(tipo): - for campo, base_origem, base_destino in DOCS[tipo]: - print('#### Migrando {} de {} ####'.format(campo, tipo.__name__)) +def migrar_docs_por_ids(model): + for campo, base_origem, base_destino in DOCS[model]: + print('#### Migrando {} de {} ####'.format(campo, model.__name__)) dir_origem, nome_origem = os.path.split(em_media(base_origem)) pat = re.compile('^{}$'.format(nome_origem.format('(\d+)'))) @@ -168,11 +174,15 @@ def migrar_docs_por_ids(tipo): match = pat.match(arq) if match: # associa documento ao objeto + origem = os.path.join(dir_origem, match.group(0)) + id = match.group(1) try: - origem = os.path.join(dir_origem, match.group(0)) - id = match.group(1) - obj = tipo.objects.get(pk=id) - + obj = model.objects.get(pk=id) + except model.DoesNotExist: + msg = ' {} (pk={}) não encontrado para documento em [{}]' + print(msg.format( + model.__name__, id, origem)) + else: extensao = get_extensao(origem) if hasattr(obj, "ano"): destino = base_destino.format(id, extensao, obj.ano) @@ -185,19 +195,19 @@ def migrar_docs_por_ids(tipo): setattr(obj, campo, destino) obj.save() - except tipo.DoesNotExist: - msg = ' {} (pk={}) não encontrado para documento em [{}]' - print(msg.format( - tipo.__name__, id, destino)) def migrar_documentos(): # aqui supomos que uma pasta chamada sapl_documentos está em MEDIA_ROOT # com o conteúdo da pasta de mesmo nome do zope - # Os arquivos da pasta serão movidos para a nova estrutura e a pasta será + # Os arquivos da pasta serão MOVIDOS para a nova estrutura e a pasta será # apagada + # + # Isto significa que para rodar novamente esta função é preciso + # restaurar a pasta sapl_documentos ao estado inicial + migrar_docs_logo() - for tipo in [ + for model in [ Parlamentar, MateriaLegislativa, DocumentoAcessorio, @@ -207,7 +217,7 @@ def migrar_documentos(): DocumentoAdministrativo, DocumentoAcessorioAdministrativo, ]: - migrar_docs_por_ids(tipo) + migrar_docs_por_ids(model) sobrando = [os.path.join(dir, file) for (dir, _, files) in os.walk(em_media('sapl_documentos')) diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py new file mode 100644 index 000000000..88fb77343 --- /dev/null +++ b/sapl/legacy/migracao_usuarios.py @@ -0,0 +1,79 @@ +from django.contrib.auth.models import Group, User + +from sapl.settings import MEDIA_ROOT + +PERFIL_LEGADO_PARA_NOVO = {legado: Group.objects.get(name=novo) + for legado, novo in [ + ('Autor', 'Autor'), + ('Operador', 'Operador Geral'), + ('Operador Comissao', 'Operador de Comissões'), + ('Operador Materia', 'Operador de Matéria'), + ('Operador Modulo Administrativo', 'Operador Administrativo'), + ('Operador Norma', 'Operador de Norma Jurídica'), + ('Operador Parlamentar', 'Parlamentar'), + ('Operador Protocolo', 'Operador de Protocolo Administrativo'), + ('Operador Sessao Plenaria', 'Operador de Sessão Plenária'), + ('Parlamentar', 'Votante'), +] +} + +ADMINISTRADORES = {'Administrador', 'Manager'} + +IGNORADOS = { + # sem significado fora do zope + 'Alterar Senha', 'Authenticated', 'Owner', + + # obsoletos (vide docs a seguir) + 'Operador Mesa Diretora', + 'Operador Ordem Dia', + 'Operador Tabela Auxiliar', + 'Operador Lexml', +} + + +def migra_usuarios(): + """ + Lê o arquivo media/USERS e importa os usuários nele listados, + com senhas e perfis. + Os usuários são criados se necessário e seus perfis ajustados. + + Os seguintes perfis no legado não correspondem a nenhum no código atual + e estão sendo **ignorados**: + + * Operador Mesa Diretora + Apenas **8 usuários**, em todas as bases, têm esse perfil + e não têm nem "Operador" nem "Operador Sessao Plenaria" + + * Operador Ordem Dia + Apenas **16 usuários**, em todas as bases, têm esse perfil + e não têm nem "Operador" nem "Operador Sessao Plenaria" + + * Operador Tabela Auxiliar + A edição das tabelas auxiliares deve ser feita por um administrador + + * Operador Lexml + Também podemos assumir que essa é uma tarefa de um administrador + """ + + ARQUIVO_USUARIOS = MEDIA_ROOT.child('USERS') + with open(ARQUIVO_USUARIOS, 'r') as f: + usuarios = eval(f.read()) + usuarios = [ + (nome, + # troca senha "inicial" por uma inutilizável + senha if senha != 'inicial' else None, + # filtra perfis ignorados + {p for p in perfis if p not in IGNORADOS}) + for nome, senha, perfis in usuarios] + + for nome, senha, perfis in usuarios: + usuario = User.objects.get_or_create(username=nome)[0] + for perfil in perfis: + if perfil in ADMINISTRADORES: + # Manager + usuario.is_staff = True + usuario.save() + else: + usuario.groups.add(PERFIL_LEGADO_PARA_NOVO[perfil]) + # apaga arquivo (importante pois contém senhas) + ARQUIVO_USUARIOS.remove() diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index 7b31f00f9..f61f564ff 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -1,5 +1,6 @@ import re from datetime import date +from functools import lru_cache, partial from subprocess import PIPE, call import pkg_resources @@ -11,14 +12,15 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from django.db import OperationalError, ProgrammingError, connections, models -from django.db.models import CharField, Count, Max, ProtectedError, TextField +from django.db import connections, transaction +from django.db.models import Count, Max from django.db.models.base import ModelBase -from model_mommy import mommy -from model_mommy.mommy import foreign_key_required, make -from sapl.base.models import Argumento, Autor, Constraint, ProblemaMigracao +from sapl.base.models import AppConfig as AppConf +from sapl.base.models import (Autor, CasaLegislativa, ProblemaMigracao, + TipoAutor) from sapl.comissoes.models import Comissao, Composicao, Participacao +from sapl.legacy.models import TipoNumeracaoProtocolo from sapl.materia.models import (AcompanhamentoMateria, Proposicao, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, @@ -26,7 +28,7 @@ from sapl.materia.models import (AcompanhamentoMateria, Proposicao, from sapl.norma.models import (AssuntoNorma, NormaJuridica, NormaRelacionada, TipoVinculoNormaJuridica) from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar, - TipoAfastamento) + Partido, TipoAfastamento) from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo) from sapl.sessao.models import ExpedienteMateria, OrdemDia, RegistroVotacao @@ -112,80 +114,29 @@ def warn(msg): print('CUIDADO! ' + msg) -def erro(msg): - print('ERRO: ' + msg) +class ForeignKeyFaltando(ObjectDoesNotExist): + 'Uma FK aponta para um registro inexistente' + pass -def get_fk_related(field, value, label=None): - if value is None and field.null is False: - value = 0 - if value is not None: - try: - value = field.related_model.objects.get(id=value) - except ObjectDoesNotExist: - msg = 'FK [%s] não encontrada para valor %s ' \ - '(em %s %s)' % ( - field.name, value, - field.model.__name__, label or '---') - if value == 0: - if not field.null: - fields_dict = get_fields_dict(field.related_model) - # Cria stub ao final da tabela para evitar erros - pk = get_last_value(field.related_model) - with reversion.create_revision(): - reversion.set_comment('Stub criado pela migração') - value = mommy.make( - field.related_model, **fields_dict, - pk=(pk + 1 or 1)) - descricao = 'stub criado para campos não nuláveis!' - save_relation(value, [field.name], msg, descricao, - eh_stub=True) - warn(msg + ' => ' + descricao) - else: - value = None - else: - if field.model._meta.label == 'sessao.RegistroVotacao' and \ - field.name == 'ordem': - return value - # Caso TipoProposicao não exista, um objeto será criado então - # com content_type=13 (ProblemaMigracao) - if field.related_model.__name__ == 'TipoProposicao': - tipo = TipoProposicao.objects.filter(descricao='Erro') - if not tipo: - with reversion.create_revision(): - reversion.set_comment( - 'TipoProposicao "Erro" criado') - ct = ContentType.objects.get(pk=13) - value = TipoProposicao.objects.create( - id=value, descricao='Erro', content_type=ct) - ultimo_valor = get_last_value(type(value)) - alter_sequence(type(value), ultimo_valor + 1) - else: - value = tipo[0] - else: - with reversion.create_revision(): - reversion.set_comment('Stub criado pela migração') - value = make_stub(field.related_model, value) - descricao = 'stub criado para entrada orfã!' - warn(msg + ' => ' + descricao) - save_relation(value, [field.name], msg, descricao, - eh_stub=True) - else: - assert value - return value - +@lru_cache() +def _get_all_ids_from_model(model): + # esta função para uso apenas em get_fk_related + return set(model.objects.values_list('id', flat=True)) -def get_field(model, fieldname): - return model._meta.get_field(fieldname) +def get_fk_related(field, value, label=None): + if value is None and field.null: + return None -def exec_sql_file(path, db='default'): - cursor = connections[db].cursor() - for line in open(path): - try: - cursor.execute(line) - except (OperationalError, ProgrammingError) as e: - print("Args: '%s'" % (str(e.args))) + # if field.related_model.objects.filter(id=value).exists(): + if value in _get_all_ids_from_model(field.related_model): + return value + else: + msg = 'FK [%s] não encontrada para o valor %s (em %s %s)' % ( + field.name, value, field.model.__name__, label or '---') + warn(msg) + raise ForeignKeyFaltando(msg) def exec_sql(sql, db='default'): @@ -193,6 +144,123 @@ def exec_sql(sql, db='default'): cursor.execute(sql) return cursor +# UNIFORMIZAÇÃO DO BANCO ANTES DA MIGRAÇÃO ############################### + +SQL_NAO_TEM_TABELA = ''' + SELECT count(*) + FROM information_schema.columns + WHERE table_schema=database() + AND TABLE_NAME="{}" +''' +SQL_NAO_TEM_COLUNA = SQL_NAO_TEM_TABELA + ' AND COLUMN_NAME="{}"' + +exec_legado = partial(exec_sql, db='legacy') + + +def existe_tabela_no_legado(tabela): + sql = SQL_NAO_TEM_TABELA.format(tabela) + return exec_legado(sql).fetchone()[0] + + +def existe_coluna_no_legado(tabela, coluna): + sql = SQL_NAO_TEM_COLUNA.format(tabela, coluna) + return exec_legado(sql).fetchone()[0] > 0 + + +def garante_coluna_no_legado(tabela, spec_coluna): + coluna = spec_coluna.split()[0] + if not existe_coluna_no_legado(tabela, coluna): + exec_legado('ALTER TABLE {} ADD COLUMN {}'.format(tabela, spec_coluna)) + assert existe_coluna_no_legado(tabela, coluna) + + +def garante_tabela_no_legado(create_table): + tabela = create_table.strip().splitlines()[0].split()[2] + if not existe_tabela_no_legado(tabela): + exec_legado(create_table) + assert existe_tabela_no_legado(tabela) + + +def uniformiza_banco(): + exec_legado(''' + SELECT replace(@@sql_mode,"STRICT_TRANS_TABLES,","ALLOW_INVALID_DATES"); + ''') + + # ajusta data zero em proposicao + # isso é necessário para poder alterar a tabela a seguir + exec_legado(''' + UPDATE proposicao SET dat_envio = "1800-01-01" WHERE dat_envio = 0; + alter table proposicao modify dat_envio datetime; + UPDATE proposicao SET dat_envio = NULL where dat_envio = "1800-01-01"; + ''') + + garante_coluna_no_legado('proposicao', + 'num_proposicao int(11) NULL') + + garante_coluna_no_legado('tipo_materia_legislativa', + 'ind_num_automatica BOOLEAN NULL DEFAULT FALSE') + + garante_coluna_no_legado('tipo_materia_legislativa', + 'quorum_minimo_votacao int(11) NULL') + + # Cria campos cod_presenca_sessao (sendo a nova PK da tabela) + # e dat_sessao em sessao_plenaria_presenca + if not existe_coluna_no_legado('sessao_plenaria_presenca', + 'cod_presenca_sessao'): + exec_legado(''' + ALTER TABLE sessao_plenaria_presenca + DROP PRIMARY KEY, + ADD cod_presenca_sessao INT auto_increment PRIMARY KEY FIRST; + ''') + assert existe_coluna_no_legado('sessao_plenaria_presenca', + 'cod_presenca_sessao') + + garante_coluna_no_legado('sessao_plenaria_presenca', + 'dat_sessao DATE NULL') + + garante_tabela_no_legado(''' + CREATE TABLE lexml_registro_publicador ( + cod_publicador INT auto_increment NOT NULL, + id_publicador INT, nom_publicador varchar(255), + adm_email varchar(50), + sigla varchar(255), + nom_responsavel varchar(255), + tipo varchar(50), + id_responsavel INT, PRIMARY KEY (cod_publicador)); + ''') + + garante_tabela_no_legado(''' + CREATE TABLE lexml_registro_provedor ( + cod_provedor INT auto_increment NOT NULL, + id_provedor INT, nom_provedor varchar(255), + sgl_provedor varchar(15), + adm_email varchar(50), + nom_responsavel varchar(255), + tipo varchar(50), + id_responsavel INT, xml_provedor longtext, + PRIMARY KEY (cod_provedor)); + ''') + + garante_tabela_no_legado(''' + CREATE TABLE tipo_situacao_militar ( + tip_situacao_militar INT auto_increment NOT NULL, + des_tipo_situacao varchar(50), + ind_excluido INT, PRIMARY KEY (tip_situacao_militar)); + ''') + + update_specs = ''' +vinculo_norma_juridica| ind_excluido = '' | trim(ind_excluido) = '0' +unidade_tramitacao | cod_parlamentar = NULL | cod_parlamentar = 0 +parlamentar | cod_nivel_instrucao = NULL | cod_nivel_instrucao = 0 +parlamentar | tip_situacao_militar = NULL | tip_situacao_militar = 0 +mandato | tip_afastamento = NULL | tip_afastamento = 0 +relatoria | tip_fim_relatoria = NULL | tip_fim_relatoria = 0 + '''.strip().splitlines() + + for spec in update_specs: + spec = spec.split('|') + exec_legado('UPDATE {} SET {} WHERE {}'.format(*spec)) + def iter_sql_records(sql, db): class Record: @@ -204,150 +272,6 @@ def iter_sql_records(sql, db): record.__dict__.update(zip(fieldnames, row)) yield record -# Todos os models têm no máximo uma constraint unique together -# Isso é necessário para que o método delete_constraints funcione corretamente -assert all(len(model._meta.unique_together) <= 1 - for app in appconfs - for model in app.models.values()) - - -def delete_constraints(model): - # pega nome da unique constraint dado o nome da tabela - table = model._meta.db_table - cursor = exec_sql("SELECT conname FROM pg_constraint WHERE conrelid = " - "(SELECT oid FROM pg_class WHERE relname LIKE " - "'%s') and contype = 'u';" % (table)) - result = () - result = cursor.fetchall() - # se existir um resultado, unique constraint será deletado - for r in result: - if r[0].endswith('key'): - words_list = r[0].split('_') - constraint = Constraint.objects.create( - nome_tabela=table, nome_constraint=r[0], - nome_model=model.__name__, tipo_constraint='one_to_one') - for w in words_list: - Argumento.objects.create(constraint=constraint, argumento=w) - else: - if model._meta.unique_together: - args_list = model._meta.unique_together[0] - constraint = Constraint.objects.create( - nome_tabela=table, nome_constraint=r[0], - nome_model=model.__name__, - tipo_constraint='unique_together') - for a in args_list: - Argumento.objects.create(constraint=constraint, - argumento=a) - warn('Excluindo unique constraint de nome %s' % r[0]) - exec_sql("ALTER TABLE %s DROP CONSTRAINT %s;" % - (table, r[0])) - - -def problema_duplicatas(model, lista_duplicatas, argumentos): - for obj in lista_duplicatas: - pks = [] - string_pks = "" - problema = "%s de PK %s não é único." % (model.__name__, obj.pk) - args_dict = {k: obj.__dict__[k] - for k in set(argumentos) & set(obj.__dict__.keys())} - for dup in model.objects.filter(**args_dict): - pks.append(dup.pk) - string_pks = "(" + ", ".join(map(str, pks)) + ")" - descricao = "As entradas de PK %s são idênticas, mas " \ - "apenas uma deve existir" % string_pks - with reversion.create_revision(): - warn(problema + ' => ' + descricao) - save_relation(obj=obj, problema=problema, - descricao=descricao, eh_stub=False, critico=True) - reversion.set_comment('%s não é único.' % model.__name__) - - -def recria_constraints(): - constraints = Constraint.objects.all() - for con in constraints: - if con.tipo_constraint == 'one_to_one': - nome_tabela = con.nome_tabela - nome_constraint = con.nome_constraint - args = [a.argumento for a in con.argumento_set.all()] - args_string = '' - args_string = "(" + "_".join(map(str, args[2:-1])) + ")" - model = ContentType.objects.filter( - model=con.nome_model.lower())[0].model_class() - try: - exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % - (nome_tabela, nome_constraint, args_string)) - except ProgrammingError: - info('A constraint %s já foi recriada!' % nome_constraint) - if con.tipo_constraint == 'unique_together': - nome_tabela = con.nome_tabela - nome_constraint = con.nome_constraint - # Pegando explicitamente o primeiro valor do filter, - # pois pode ser que haja mais de uma ocorrência - model = ContentType.objects.filter( - model=con.nome_model.lower())[0].model_class() - args = [a.argumento for a in con.argumento_set.all()] - for i in range(len(args)): - if isinstance(model._meta.get_field(args[i]), - models.ForeignKey): - args[i] = args[i] + '_id' - args_string = '' - args_string += "(" + ', '.join(map(str, args)) + ")" - - distintos = model.objects.distinct(*args) - todos = model.objects.all() - if hasattr(model, "content_type"): - distintos = distintos.exclude(content_type_id=None, - object_id=None) - todos = todos.exclude(content_type_id=None, object_id=None) - - lista_duplicatas = list(set(todos).difference(set(distintos))) - if lista_duplicatas: - problema_duplicatas(model, lista_duplicatas, args) - else: - try: - exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % - (nome_tabela, nome_constraint, args_string)) - except ProgrammingError: - info('A constraint %s já foi recriada!' % nome_constraint) - except Exception as err: - problema = re.findall('\(.*?\)', err.args[0]) - erro('A constraint [%s] da tabela [%s] não pode ser" \ - recriada' % (nome_constraint, nome_tabela)) - erro('Os dados %s = %s estão duplicados. ' - 'Arrume antes de recriar as constraints!' % - (problema[0], problema[1])) - - -def obj_desnecessario(obj): - relacoes = [ - f for f in obj._meta.get_fields() - if (f.one_to_many or f.one_to_one) and f.auto_created] - sem_referencia = not any(rr.related_model.objects.filter( - **{rr.field.name: obj}).exists() for rr in relacoes) - if type(obj).__name__ == 'Parlamentar' and sem_referencia and \ - obj.autor.all(): - sem_referencia = False - return sem_referencia - - -def get_last_value(model): - last_value = model.objects.all().aggregate(Max('pk')) - return last_value['pk__max'] if last_value['pk__max'] else 0 - - -def alter_sequence(model, id): - sequence_name = '%s_id_seq' % model._meta.db_table - exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % ( - sequence_name, id)) - - -def save_with_id(new, id): - last_value = get_last_value(type(new)) - alter_sequence(type(new), id) - new.save() - alter_sequence(type(new), last_value + 1) - assert new.id == id, 'New id is different from provided!' - def save_relation(obj, nome_campo='', problema='', descricao='', eh_stub=False, critico=False): @@ -357,24 +281,6 @@ def save_relation(obj, nome_campo='', problema='', descricao='', link.save() -def make_stub(model, id): - fields_dict = get_fields_dict(model) - new = mommy.prepare(model, **fields_dict, pk=id) - save_with_id(new, id) - - return new - - -def get_fields_dict(model): - all_fields = model._meta.get_fields() - fields_dict = {} - fields_dict = {f.name: '????????????'[:f.max_length] - for f in all_fields - if isinstance(f, (CharField, TextField)) and - not f.choices and not f.blank} - return fields_dict - - def fill_vinculo_norma_juridica(): lista = [('A', 'Altera o(a)', 'Alterado(a) pelo(a)'), @@ -408,6 +314,27 @@ def fill_vinculo_norma_juridica(): TipoVinculoNormaJuridica.objects.bulk_create(lista_objs) +def fill_dados_basicos(): + # Ajusta sequencia numérica e cria base.AppConfig + letra = 'A' + try: + tipo = TipoNumeracaoProtocolo.objects.latest('dat_inicial_protocolo') + if 'POR ANO' in tipo.des_numeracao_protocolo: + letra = 'A' + elif 'POR LEGISLATURA' in tipo.des_numeracao_protocolo: + letra = 'L' + elif 'CONSECUTIVO' in tipo.des_numeracao_protocolo: + letra = 'U' + except Exception as e: + pass + appconf = AppConf(sequencia_numeracao=letra) + appconf.save() + + # Cria instância de CasaLegislativa + casa = CasaLegislativa() + casa.save() + + # Uma anomalia no sapl 2.5 causa a duplicação de registros de votação. # Essa duplicação deve ser eliminada para que não haja erro no sapl 3.1 def excluir_registrovotacao_duplicados(): @@ -436,6 +363,35 @@ def excluir_registrovotacao_duplicados(): assert 0 +def delete_old(legacy_model, cols_values): + # ajuste necessário por conta de cósigos html em txt_expediente + if legacy_model.__name__ == 'ExpedienteSessaoPlenaria': + cols_values.pop('txt_expediente') + + def eq_clause(col, value): + if value is None: + return '{} IS NULL'.format(col) + else: + return '{}="{}"'.format(col, value) + + delete_sql = 'delete from {} where {}'.format( + legacy_model._meta.db_table, + ' and '.join([eq_clause(col, value) + for col, value in cols_values.items()])) + exec_sql(delete_sql, 'legacy') + + +def get_last_pk(model): + last_value = model.objects.all().aggregate(Max('pk')) + return last_value['pk__max'] or 0 + + +def reinicia_sequence(model, id): + sequence_name = '%s_id_seq' % model._meta.db_table + exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % ( + sequence_name, id)) + + class DataMigrator: def __init__(self): @@ -449,52 +405,46 @@ class DataMigrator: for field in new._meta.fields: old_field_name = renames.get(field.name) field_type = field.get_internal_type() - msg = ("O valor do campo %s (%s) da model %s era inválido" % - (field.name, field_type, field.model.__name__)) if old_field_name: old_value = getattr(old, old_field_name) - if isinstance(field, models.ForeignKey): - old_type = type(old) # not necessarily a model - if hasattr(old_type, '_meta') and \ - old_type._meta.pk.name != 'id': + + if field_type == 'ForeignKey': + # not necessarily a model + if hasattr(old, '_meta') and old._meta.pk.name != 'id': label = old.pk else: label = '-- SEM PK --' + fk_field_name = '{}_id'.format(field.name) value = get_fk_related(field, old_value, label) + setattr(new, fk_field_name, value) else: value = getattr(old, old_field_name) - if field_type == 'DateField' and \ - not field.null and value is None: - descricao = 'A data 1111-11-11 foi colocada no lugar' - problema = 'O valor da data era nulo ou inválido' - warn(msg + - ' => ' + descricao) - value = date(1111, 11, 11) - self.data_mudada['obj'] = new - self.data_mudada['descricao'] = descricao - self.data_mudada['problema'] = problema - self.data_mudada.setdefault('nome_campo', []).\ - append(field.name) - if field_type == 'CharField' or field_type == 'TextField': - if value is None or value == 'None': + # TODO rever esse DateField após as mudança para datas com + # timezone + if field_type == 'DateField' and \ + not field.null and value is None: + # TODO REVER ISSO + descricao = 'A data 1111-11-11 foi colocada no lugar' + problema = 'O valor da data era nulo ou inválido' + warn("O valor do campo %s (%s) do model %s " + "era inválido => %s" % ( + field.name, field_type, + field.model.__name__, descricao)) + value = date(1111, 11, 11) + self.data_mudada['obj'] = new + self.data_mudada['descricao'] = descricao + self.data_mudada['problema'] = problema + self.data_mudada.setdefault('nome_campo', []).\ + append(field.name) + if (field_type in ['CharField', 'TextField'] + and value in [None, 'None']): value = '' - setattr(new, field.name, value) - elif field.model.__name__ == 'TipoAutor' and \ - field.name == 'content_type': - - model = normalize(new.descricao.lower()).replace(' ', '') - content_types = field.related_model.objects.filter( - model=model).exclude(app_label='legacy') - assert len(content_types) <= 1 - - value = content_types[0] if content_types else None - setattr(new, field.name, value) + setattr(new, field.name, value) def migrate(self, obj=appconfs, interativo=True): # warning: model/app migration order is of utmost importance - exec_sql_file(PROJECT_DIR.child( - 'sapl', 'legacy', 'scripts', 'fix_tables.sql'), 'legacy') - self.to_delete = [] + + uniformiza_banco() # excluindo database antigo. if interativo: @@ -513,32 +463,13 @@ class DataMigrator: '--database=default', '--no-input'], stdout=PIPE) fill_vinculo_norma_juridica() + fill_dados_basicos() info('Começando migração: %s...' % obj) self._do_migrate(obj) - # Itera várias vezes na lista excluindo o que for possível - info('Deletando models com ind_excluido...') - while self.delete_ind_excluido(): - pass - # Salva o que não pôde ser excluido da lista no problema da migração - for obj in self.to_delete: - msg = 'A entrada de PK %s da model %s não pode ser ' \ - 'excluida' % (obj.pk, obj._meta.model_name) - descricao = 'Um ou mais objetos protegidos' - warn(msg + ' => ' + descricao) - save_relation(obj=obj, problema=msg, - descricao=descricao, eh_stub=False) - info('Excluindo possíveis duplicações em RegistroVotacao...') excluir_registrovotacao_duplicados() - info('Deletando stubs desnecessários...') - while self.delete_stubs(): - pass - - info('Recriando constraints...') - recria_constraints() - def _do_migrate(self, obj): if isinstance(obj, AppConfig): models_to_migrate = (model for model in obj.models.values() @@ -566,80 +497,70 @@ class DataMigrator: legacy_model = legacy_app.get_model(legacy_model_name) legacy_pk_name = legacy_model._meta.pk.name - delete_constraints(model) - # setup migration strategy for tables with or without a pk if legacy_pk_name == 'id': + deve_ajustar_sequence_ao_final = False # There is no pk in the legacy table + def save(new, old): with reversion.create_revision(): new.save() reversion.set_comment('Objeto criado pela migração') + + # apaga registro do legado + delete_old(legacy_model, old.__dict__) + old_records = iter_sql_records( 'select * from ' + legacy_model._meta.db_table, 'legacy') else: + deve_ajustar_sequence_ao_final = True + def save(new, old): with reversion.create_revision(): - save_with_id(new, getattr(old, legacy_pk_name)) + # salva new com id de old + new.id = getattr(old, legacy_pk_name) + new.save() reversion.set_comment('Objeto criado pela migração') + # apaga registro do legado + delete_old(legacy_model, {legacy_pk_name: new.id}) + old_records = legacy_model.objects.all().order_by(legacy_pk_name) ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model) ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model) # convert old records to new ones - for old in old_records: - new = model() - self.populate_renamed_fields(new, old) - if ajuste_antes_salvar: - ajuste_antes_salvar(new, old) - save(new, old) - if ajuste_depois_salvar: - ajuste_depois_salvar(new, old) - if self.data_mudada: - with reversion.create_revision(): - save_relation(**self.data_mudada) - self.data_mudada.clear() - reversion.set_comment('Ajuste de data pela migração') - if getattr(old, 'ind_excluido', False): - self.to_delete.append(new) - - # necessário para ajustar sequence da tabela para o ultimo valor de id - ultimo_valor = get_last_value(model) - alter_sequence(model, ultimo_valor + 1) - - def delete_ind_excluido(self): - excluidos = 0 - for obj in self.to_delete: - if obj_desnecessario(obj): + with transaction.atomic(): + for old in old_records: + if getattr(old, 'ind_excluido', False): + # não migramos registros marcados como excluídos + continue + new = model() try: - obj.delete() - except ProtectedError: - pass + self.populate_renamed_fields(new, old) + if ajuste_antes_salvar: + ajuste_antes_salvar(new, old) + except ForeignKeyFaltando: + # tentamos preencher uma FK e o ojeto relacionado + # não existe + # então este é um objeo órfão: simplesmente ignoramos + continue else: - self.to_delete.remove(obj) - excluidos += 1 - - return excluidos - - def delete_stubs(self): - excluidos = 0 - for obj in ProblemaMigracao.objects.all(): - if obj.content_object and obj.eh_stub: - original = obj.content_type.get_all_objects_for_this_type( - id=obj.object_id) - if obj_desnecessario(original[0]): - qtd_exclusoes, *_ = original.delete() - assert qtd_exclusoes == 1 - qtd_exclusoes, *_ = obj.delete() - assert qtd_exclusoes == 1 - excluidos = excluidos + 1 - elif not obj.content_object and not obj.eh_stub: - qtd_exclusoes, *_ = obj.delete() - assert qtd_exclusoes == 1 - excluidos = excluidos + 1 - return excluidos + save(new, old) + if ajuste_depois_salvar: + ajuste_depois_salvar(new, old) + + if self.data_mudada: + with reversion.create_revision(): + save_relation(**self.data_mudada) + self.data_mudada.clear() + reversion.set_comment( + 'Ajuste de data pela migração') + # reinicia sequence + if deve_ajustar_sequence_ao_final: + last_pk = get_last_pk(model) + reinicia_sequence(model, last_pk + 1) def migrate(obj=appconfs, interativo=True): @@ -654,28 +575,23 @@ def adjust_acompanhamentomateria(new, old): def adjust_documentoadministrativo(new, old): - if new.numero_protocolo: - try: - protocolo = Protocolo.objects.get(numero=new.numero_protocolo, - ano=new.ano) - new.protocolo = protocolo - except Exception: - try: - protocolo = Protocolo.objects.get(numero=new.numero_protocolo, - ano=new.ano + 1) - new.protocolo = protocolo - except Exception: - protocolo = mommy.make(Protocolo, numero=new.numero_protocolo, - ano=new.ano) - with reversion.create_revision(): - problema = 'Protocolo Vinculado [numero_protocolo=%s, '\ - 'ano=%s] não existe' % (new.numero_protocolo, - new.ano) - descricao = 'O protocolo inexistente foi criado' - warn(problema + ' => ' + descricao) - save_relation(obj=protocolo, problema=problema, - descricao=descricao, eh_stub=True) - reversion.set_comment('Protocolo não existia.') + if old.num_protocolo: + protocolo = Protocolo.objects.filter( + numero=old.num_protocolo, ano=new.ano) + if not protocolo: + protocolo = Protocolo.objects.filter( + numero=old.num_protocolo, ano=new.ano + 1) + print('PROTOCOLO ENCONTRADO APENAS PARA O ANO SEGUINTE!!!!! ' + 'DocumentoAdministrativo: {}, numero_protocolo: {}, ' + 'ano doc adm: {}'.format( + old.cod_documento, old.num_protocolo, new.ano)) + if not protocolo: + raise ForeignKeyFaltando( + 'Protocolo {} faltando ' + '(referenciado no documento administrativo {}'.format( + old.num_protocolo, old.cod_documento)) + assert len(protocolo) == 1 + new.protocolo = protocolo[0] def adjust_mandato(new, old): @@ -685,6 +601,9 @@ def adjust_mandato(new, old): legislatura = Legislatura.objects.latest('data_fim') new.data_fim_mandato = legislatura.data_fim new.data_expedicao_diploma = legislatura.data_inicio + if not new.data_inicio_mandato: + new.data_inicio_mandato = new.legislatura.data_inicio + new.data_fim_mandato = new.legislatura.data_fim def adjust_ordemdia_antes_salvar(new, old): @@ -707,7 +626,6 @@ def adjust_ordemdia_depois_salvar(new, old): save_relation(obj=new, problema=problema, descricao=descricao, eh_stub=False) reversion.set_comment('OrdemDia sem número da ordem.') - pass def adjust_parlamentar(new, old): @@ -723,7 +641,7 @@ def adjust_parlamentar(new, old): def adjust_participacao(new, old): composicao = Composicao() - composicao.comissao, composicao.periodo = [ + composicao.comissao_id, composicao.periodo_id = [ get_fk_related(Composicao._meta.get_field(name), value) for name, value in (('comissao', old.cod_comissao), ('periodo', old.cod_periodo_comp))] @@ -766,10 +684,8 @@ def adjust_normarelacionada(new, old): def adjust_protocolo_antes_salvar(new, old): - data_ajuste = date(2014, 11, 13) - - if old.num_protocolo is None and data_ajuste >= old.dat_protocolo: - new.numero = old.pk + if old.num_protocolo is None: + new.numero = old.cod_protocolo def adjust_protocolo_depois_salvar(new, old): @@ -815,11 +731,18 @@ def adjust_tipoafastamento(new, old): def adjust_tipoproposicao(new, old): if old.ind_mat_ou_doc == 'M': - new.tipo_conteudo_related = TipoMateriaLegislativa.objects.get( + tipo_materia = TipoMateriaLegislativa.objects.filter( pk=old.tip_mat_ou_doc) + if tipo_materia: + new.tipo_conteudo_related = tipo_materia[0] + else: + raise ForeignKeyFaltando elif old.ind_mat_ou_doc == 'D': - new.tipo_conteudo_related = TipoDocumento.objects.get( - pk=old.tip_mat_ou_doc) + tipo_documento = TipoDocumento.objects.filter(pk=old.tip_mat_ou_doc) + if tipo_documento: + new.tipo_conteudo_related = tipo_documento[0] + else: + raise ForeignKeyFaltando def adjust_statustramitacao(new, old): @@ -840,6 +763,14 @@ def adjust_tramitacao(new, old): new.turno = 'U' +def adjust_tipo_autor(new, old): + model_apontado = normalize(new.descricao.lower()).replace(' ', '') + content_types = ContentType.objects.filter( + model=model_apontado).exclude(app_label='legacy') + assert len(content_types) <= 1 + new.content_type = content_types[0] if content_types else None + + def adjust_normajuridica_antes_salvar(new, old): # Ajusta choice de esfera_federacao # O 'S' vem de 'Selecionar'. Na versão antiga do SAPL, quando uma opção do @@ -852,43 +783,55 @@ def adjust_normajuridica_antes_salvar(new, old): def adjust_normajuridica_depois_salvar(new, old): # Ajusta relação M2M - lista_pks_assunto = old.cod_assunto.split(',') - # list(filter(..)) usado para retirar strings vazias da lista - for pk_assunto in list(filter(None, lista_pks_assunto)): - new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto)) + if not old.cod_assunto: # it can be null or empty + return + # lista de pks separadas por vírgulas (ignorando strings vazias) + lista_pks_assunto = [int(pk) for pk in old.cod_assunto.split(',') if pk] -def adjust_autor(new, old): - if old.cod_parlamentar: + for pk_assunto in lista_pks_assunto: try: - new.autor_related = Parlamentar.objects.get(pk=old.cod_parlamentar) - except Exception: - with reversion.create_revision(): - msg = 'Um parlamentar relacionado de PK [%s] não existia' \ - % old.cod_parlamentar - reversion.set_comment('Stub criado pela migração') - value = make_stub(Parlamentar, old.cod_parlamentar) - descricao = 'stub criado para entrada orfã!' - warn(msg + ' => ' + descricao) - save_relation(value, [], msg, descricao, - eh_stub=True) - new.autor_related = value - new.nome = new.autor_related.nome_parlamentar - - elif old.cod_comissao: - new.autor_related = Comissao.objects.get(pk=old.cod_comissao) - new.nome = new.autor_related.nome + new.assuntos.add(AssuntoNorma.objects.get(pk=pk_assunto)) + except ObjectDoesNotExist: + pass # ignora assuntos inexistentes + + +def vincula_autor(new, old, model_relacionado, campo_relacionado, campo_nome): + pk_rel = getattr(old, campo_relacionado) + if pk_rel: + try: + new.autor_related = model_relacionado.objects.get(pk=pk_rel) + except ObjectDoesNotExist: + # ignoramos o autor órfão + raise ForeignKeyFaltando('{} inexiste para autor'.format( + model_relacionado._meta.verbose_name)) + else: + new.nome = getattr(new.autor_related, campo_nome) + return True + + +def adjust_autor(new, old): + for args in [ + # essa ordem é importante + (Parlamentar, 'cod_parlamentar', 'nome_parlamentar'), + (Comissao, 'cod_comissao', 'nome'), + (Partido, 'cod_partido', 'nome')]: + if vincula_autor(new, old, *args): + break if old.col_username: - if not get_user_model().objects.filter( - username=old.col_username).exists(): - user = get_user_model()(username=old.col_username) - user.set_password(12345) + user_model = get_user_model() + if not user_model.objects.filter(username=old.col_username).exists(): + # cria um novo ususaŕio para o autor + user = user_model(username=old.col_username) + # gera uma senha inutilizável, que precisará ser trocada + user.set_password(None) with reversion.create_revision(): user.save() - reversion.set_comment('Objeto criado pela migração') - + reversion.set_comment( + 'Usuário criado pela migração para o autor {}'.format( + old.cod_autor)) grupo_autor = Group.objects.get(name="Autor") user.groups.add(grupo_autor) @@ -905,6 +848,7 @@ def adjust_comissao(new, old): AJUSTE_ANTES_SALVAR = { Autor: adjust_autor, + TipoAutor: adjust_tipo_autor, AcompanhamentoMateria: adjust_acompanhamentomateria, Comissao: adjust_comissao, DocumentoAdministrativo: adjust_documentoadministrativo, @@ -935,31 +879,13 @@ AJUSTE_DEPOIS_SALVAR = { # CHECKS #################################################################### -def get_ind_excluido(obj): - legacy_model = legacy_app.get_model(type(obj).__name__) - return getattr(legacy_model.objects.get( - **{legacy_model._meta.pk.name: obj.id}), 'ind_excluido', False) +def get_ind_excluido(new): + legacy_model = legacy_app.get_model(type(new).__name__) + old = legacy_model.objects.get(**{legacy_model._meta.pk.name: new.id}) + return getattr(old, 'ind_excluido', False) def check_app_no_ind_excluido(app): for model in app.models.values(): - assert not any(get_ind_excluido(obj) for obj in model.objects.all()) + assert not any(get_ind_excluido(new) for new in model.objects.all()) print('OK!') - -# MOMMY MAKE WITH LOG ###################################################### - - -def make_with_log(model, _quantity=None, make_m2m=False, **attrs): - last_value = get_last_value(model) - alter_sequence(model, last_value + 1) - fields_dict = get_fields_dict(model) - stub = make(model, _quantity, make_m2m, **fields_dict) - problema = 'Um stub foi necessário durante a criação de um outro stub' - descricao = 'Essa entrada é necessária para um dos stubs criados' - ' anteriormente' - warn(problema) - save_relation(obj=stub, problema=problema, - descricao=descricao, eh_stub=True) - return stub - -make_with_log.required = foreign_key_required diff --git a/sapl/legacy/models.py b/sapl/legacy/models.py index 343f22feb..4c459c9dd 100644 --- a/sapl/legacy/models.py +++ b/sapl/legacy/models.py @@ -968,6 +968,19 @@ class TipoNormaJuridica(models.Model): db_table = 'tipo_norma_juridica' +class TipoNumeracaoProtocolo(models.Model): + seq_tip_num_protocolo = models.AutoField(primary_key=True) + tip_numeracao_protocolo = models.IntegerField() + des_numeracao_protocolo = models.CharField(max_length=50) + dat_inicial_protocolo = models.DateTimeField() + vlr_inicial_protocolo = models.IntegerField() + ind_excluido = models.IntegerField() + + class Meta: + managed = False + db_table = 'tipo_numeracao_protocolo' + + class TipoProposicao(models.Model): tip_proposicao = models.AutoField(primary_key=True) des_tipo_proposicao = models.CharField(max_length=50) diff --git a/sapl/legacy/scripts/exporta_zope/.gitignore b/sapl/legacy/scripts/exporta_zope/.gitignore new file mode 100644 index 000000000..11c2f1d6b --- /dev/null +++ b/sapl/legacy/scripts/exporta_zope/.gitignore @@ -0,0 +1,3 @@ +Data*.fs* +sapl_documentos +XSLT diff --git a/sapl/legacy/scripts/exporta_zope/exporta_zope.py b/sapl/legacy/scripts/exporta_zope/exporta_zope.py new file mode 100755 index 000000000..d1dcc4736 --- /dev/null +++ b/sapl/legacy/scripts/exporta_zope/exporta_zope.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# IMPORTANTE: +# Esse script precisa rodar em python 2 +# e depende apenas do descrito no arquivo requiments.txt + +import os.path +import sys +from collections import defaultdict +from functools import partial +from os.path import splitext + +import yaml +import ZODB.DB +import ZODB.FileStorage +from ZODB.broken import Broken + +EXTENSOES = { + 'application/msword': '.doc', + 'application/pdf': '.pdf', + 'application/vnd.oasis.opendocument.text': '.odt', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa + 'application/xml': '.xml', + 'text/xml': '.xml', + 'application/zip': '.zip', + 'image/jpeg': '.jpeg', + 'image/png': '.png', + 'image/gif': '.gif', + 'text/html': '.html', + 'text/rtf': '.rtf', + 'text/x-python': '.py', + 'text/plain': '.txt', + 'SDE-Document': 'xml', + + # TODO rever... + 'text/richtext': '.rtf', + + # sem extensao + 'application/octet-stream': '', # binario + 'inode/x-empty': '', # vazio + 'text/x-unknown-content-type': '', +} + + +def br(obj): + if isinstance(obj, Broken): + return obj.__Broken_state__ + else: + return obj + + +extensoes_desconhecidas = defaultdict(list) + + +def dump_file(doc, path): + id = doc['__name__'] + name, extension = splitext(id) + content_type = doc['content_type'] + extension = extension or EXTENSOES.get(content_type, 'ZZZZ') + + fullname = os.path.join(path, name + extension) + print(fullname) + + if extension == 'ZZZZ': + extensoes_desconhecidas[content_type].append(fullname) + + # A partir daqui usamos dict.pop('...') nos __Broken_state__ + # para contornar um "vazamento" de memória que ocorre + # ao percorrer a árvore de objetos + # + # Imaginamos que, internamente, o ZODB está guardando referências + # para os objetos Broken criados e não conseguimos identificar como. + # + # Essa medida descarta quase todos os dados retornados + # e só funciona na primeira passagem + + pdata = br(doc.pop('data')) + if isinstance(pdata, str): + # Retrocedemos se pdata ja eh uma str (necessario em Images) + doc['data'] = pdata + pdata = doc + + with open(fullname, 'w') as arq: + while pdata: + arq.write(pdata.pop('data')) + pdata = br(pdata.pop('next', None)) + + return id + + +def enumerate_by_key_list(folder, key_list, type_key): + for entry in folder.get(key_list, []): + id, meta_type = entry['id'], entry[type_key] + obj = br(folder.get(id, None)) + yield id, obj, meta_type + + +enumerate_folder = partial(enumerate_by_key_list, + key_list='_objects', type_key='meta_type') + +enumerate_properties = partial(enumerate_by_key_list, + key_list='_properties', type_key='type') + + +def enumerate_btree(folder): + contagem_esperada = folder['_count'].value + tree = folder['_tree'] + for contagem_real, (id, obj) in enumerate(tree.iteritems(), start=1): + obj, meta_type = br(obj), type(obj).__name__ + yield id, obj, meta_type + # verificação de consistência + assert contagem_esperada == contagem_real + + +nao_identificados = defaultdict(list) + + +def dump_folder(folder, path='', enum=enumerate_folder): + name = folder['id'] + path = os.path.join(path, name) + if not os.path.exists(path): + os.makedirs(path) + for id, obj, meta_type in enum(folder): + dump = DUMP_FUNCTIONS.get(meta_type, '?') + if dump == '?': + nao_identificados[meta_type].append(path + '/' + id) + elif dump: + id_interno = dump(obj, path) + assert id == id_interno + return name + + +def decode_iso8859(obj): + return obj.decode('iso8859-1') if isinstance(obj, str) else obj + + +def read_sde(element): + + def read_properties(): + for id, obj, meta_type in enumerate_properties(element): + yield id, decode_iso8859(obj) + + def read_children(): + for id, obj, meta_type in enumerate_folder(element): + assert meta_type in ['SDE-Document-Element', + 'SDE-Template-Element', + 'SDE-Template-Link', + 'SDE-Template-Attribute', + 'Script (Python)', + ] + if meta_type != 'Script (Python)': + # ignoramos os scrips python de eventos dos templates + yield id, read_sde(obj) + + data = dict(read_properties()) + children = list(read_children()) + if children: + data['children'] = children + return data + + +def save_as_yaml(path, name, obj): + fullname = os.path.join(path, name) + with open(fullname, 'w') as arquivo: + yaml.safe_dump(obj, arquivo) + print(fullname) + return fullname + + +def dump_sde(strdoc, path, tipo): + id = strdoc['id'] + sde = read_sde(strdoc) + save_as_yaml(path, '{}.{}.yaml'.format(id, tipo), sde) + return id + + +DUMP_FUNCTIONS = { + 'File': dump_file, + 'Image': dump_file, + 'Folder': partial(dump_folder, enum=enumerate_folder), + 'BTreeFolder2': partial(dump_folder, enum=enumerate_btree), + 'SDE-Document': partial(dump_sde, tipo='sde.document'), + 'SDE-Template': partial(dump_sde, tipo='sde.template'), + + # explicitamente ignorados + 'ZCatalog': None, + 'Dumper': None, +} + + +def get_app(data_fs_path): + storage = ZODB.FileStorage.FileStorage(data_fs_path) + db = ZODB.DB(storage) + connection = db.open() + root = connection.root() + app = br(root['Application']) + + def close_db(): + db.close() + + return app, close_db + + +def find_sapl(app): + for obj in app['_objects']: + id, meta_type = obj['id'], obj['meta_type'] + if id.startswith('cm_') and meta_type == 'Folder': + cm_zzz = br(app[id]) + sapl = br(cm_zzz.get('sapl', None)) + if sapl and 'sapl_documentos' in sapl and 'acl_users' in sapl: + return sapl + + +def dump_propriedades(docs, path): + props_sapl = br(docs['props_sapl']) + ids = [p['id'] for p in props_sapl['_properties']] + props = {id: props_sapl[id] for id in ids} + props = {id: p.decode('iso-8859-1') if isinstance(p, str) else p + for id, p in props.items()} + save_as_yaml(path, 'sapl_documentos/propriedades.yaml', props) + + +def dump_usuarios(sapl, path): + users = br(br(sapl['acl_users'])['data']) + users = {k: br(v) for k, v in users['data'].items()} + save_as_yaml(path, 'usuarios.yaml', users) + + +def dump_sapl(data_fs_path, destino='../../../../media'): + app, close_db = get_app(data_fs_path) + try: + sapl = find_sapl(app) + # extrai folhas XSLT + dump_folder(br(sapl['XSLT']), destino) + # extrai usuários com suas senhas e perfis + dump_usuarios(sapl, destino) + + # extrai documentos + docs = br(sapl['sapl_documentos']) + nao_identificados.clear() + dump_folder(docs, destino) + dump_propriedades(docs, destino) + if nao_identificados: + print('#' * 80) + print('#' * 80) + print(u'FORAM ENCONTRADOS ARQUIVOS DE FORMATO NÃO IDENTIFICADO!!!') + print(u'REFAÇA A EXPORTAÇÃO\n') + print(nao_identificados) + print('#' * 80) + print('#' * 80) + finally: + close_db() + + +if __name__ == "__main__": + if len(sys.argv) == 2: + data_fs_path = sys.argv[1] + dump_sapl(data_fs_path) + else: + print('Uso: python exporta_zope ') diff --git a/sapl/legacy/scripts/exporta_zope/requirements.txt b/sapl/legacy/scripts/exporta_zope/requirements.txt new file mode 100644 index 000000000..4794267ae --- /dev/null +++ b/sapl/legacy/scripts/exporta_zope/requirements.txt @@ -0,0 +1,3 @@ +# ZODB version 3.7.4 +PyYAML==3.12 +ZODB==5.3.0 diff --git a/sapl/legacy/scripts/fix_tables.sql b/sapl/legacy/scripts/fix_tables.sql deleted file mode 100644 index 1f8cdb63b..000000000 --- a/sapl/legacy/scripts/fix_tables.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Apaga as restrições somente para essa sessão -SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES,','ALLOW_INVALID_DATES'); --- Exclui procedures caso já existam -DROP PROCEDURE IF EXISTS verifica_campos_proposicao; -DROP PROCEDURE IF EXISTS verifica_campos_tipo_materia_legislativa; -DROP PROCEDURE IF EXISTS verifica_campos_sessao_plenaria_presenca; -DROP PROCEDURE IF EXISTS cria_lexml_registro_provedor_e_publicador; -DROP PROCEDURE IF EXISTS cria_tipo_situacao_militar; -DROP PROCEDURE IF EXISTS muda_vinculo_norma_juridica_ind_excluido; --- Procedure para criar campo num_proposicao em proposicao -CREATE PROCEDURE verifica_campos_proposicao() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='proposicao' AND column_name='num_proposicao') THEN UPDATE proposicao SET dat_envio = '1800-01-01' WHERE CAST(dat_envio AS CHAR(20)) = '0000-00-00 00:00:00'; ALTER TABLE proposicao ADD COLUMN num_proposicao INT(11) NULL after txt_justif_devolucao; END IF; END; --- Procedure para criar campo iind_num_automatica em tipo_materia_legislativa -CREATE PROCEDURE verifica_campos_tipo_materia_legislativa() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_materia_legislativa' AND column_name='ind_num_automatica') THEN ALTER TABLE tipo_materia_legislativa ADD COLUMN ind_num_automatica BOOLEAN NULL DEFAULT FALSE after des_tipo_materia; END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_materia_legislativa' AND column_name='quorum_minimo_votacao') THEN ALTER TABLE tipo_materia_legislativa ADD COLUMN quorum_minimo_votacao INT(11) NULL after ind_num_automatica; END IF; END; --- Procedure para criar campos cod_presenca_sessao (sendo a nova PK da tabela) e dat_sessao em sessao_plenaria_presenca -CREATE PROCEDURE verifica_campos_sessao_plenaria_presenca() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='sessao_plenaria_presenca' AND column_name='cod_presenca_sessao') THEN ALTER TABLE sessao_plenaria_presenca DROP PRIMARY KEY, ADD cod_presenca_sessao INT AUTO_INCREMENT PRIMARY KEY FIRST; END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='sessao_plenaria_presenca' AND column_name='dat_sessao') THEN ALTER TABLE sessao_plenaria_presenca ADD COLUMN dat_sessao DATE NULL after cod_parlamentar; END IF; END; --- Procedure para criar tabela lexml_registro_provedor e lexml_registro_publicador -CREATE PROCEDURE cria_lexml_registro_provedor_e_publicador() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='lexml_registro_publicador') THEN CREATE TABLE lexml_registro_publicador (cod_publicador INT AUTO_INCREMENT NOT NULL, id_publicador INT, nom_publicador VARCHAR(255), adm_email VARCHAR(50), sigla VARCHAR(255), nom_responsavel VARCHAR(255), tipo VARCHAR(50), id_responsavel INT, PRIMARY KEY (cod_publicador)); END IF; IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='lexml_registro_provedor') THEN CREATE TABLE lexml_registro_provedor (cod_provedor INT AUTO_INCREMENT NOT NULL, id_provedor INT, nom_provedor VARCHAR(255), sgl_provedor VARCHAR(15), adm_email VARCHAR(50), nom_responsavel VARCHAR(255), tipo VARCHAR(50), id_responsavel INT, xml_provedor LONGTEXT, PRIMARY KEY (cod_provedor)); END IF; END; --- Procedure para criar tabela tipo_situacao_militar -CREATE PROCEDURE cria_tipo_situacao_militar() BEGIN IF NOT EXISTS (SELECT * FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tipo_situacao_militar') THEN CREATE TABLE tipo_situacao_militar (tip_situacao_militar INT AUTO_INCREMENT NOT NULL, des_tipo_situacao VARCHAR(50), ind_excluido INT, PRIMARY KEY (tip_situacao_militar)); END IF; END; --- Procedure para mudar valor do campo ind_excluido da tabela vinculo_norma_juridica de 0 para string vazia '' -CREATE PROCEDURE muda_vinculo_norma_juridica_ind_excluido() BEGIN UPDATE vinculo_norma_juridica SET ind_excluido = '' WHERE trim(ind_excluido) = '0'; END; --- Executa as procedures criadas acima -CALL verifica_campos_proposicao; -CALL verifica_campos_tipo_materia_legislativa; -CALL verifica_campos_sessao_plenaria_presenca; -CALL cria_lexml_registro_provedor_e_publicador; -CALL cria_tipo_situacao_militar; -CALL muda_vinculo_norma_juridica_ind_excluido; diff --git a/sapl/legacy/scripts/migra_dbs.sh b/sapl/legacy/scripts/migra_dbs.sh index c7529e015..25a78e01c 100755 --- a/sapl/legacy/scripts/migra_dbs.sh +++ b/sapl/legacy/scripts/migra_dbs.sh @@ -2,4 +2,9 @@ # rodar esse script na raiz do projeto -parallel --verbose ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u root -padmin -e 'show databases;' | grep '^sapl_') +if [ $# -ge 1 ]; then + parallel -eta --verbose -j+0 ./sapl/legacy/scripts/migra_um_db.sh :::: <(mysql -u $1 -p$2 -e 'show databases;' | grep '^sapl_') ::: $1 ::: $2 +else + echo "USO:" + echo " $0 [senha mysql]" +fi; \ No newline at end of file diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index f55dfb53a..85e171792 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -1,31 +1,48 @@ #!/bin/bash # rodar esse script na raiz do projeto - -DIR=~/logs_migracao -mkdir -p $DIR - -LOG="$DIR/$1.migracao.log" -rm -f $LOG - -echo "########################################" | tee -a $LOG -echo "MIGRANDO BANCO $1" | tee -a $LOG -echo "########################################" | tee -a $LOG -echo >> $LOG - - -echo "--- DJANGO MIGRATE ---" | tee -a $LOG -echo >> $LOG -DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings -echo >> $LOG - -echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG -echo >> $LOG -DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG -echo >> $LOG - - -echo "--- RECRIANDO CONSTRAINTS ---" | tee -a $LOG -echo >> $LOG -DATABASE_NAME=$1 ./manage.py recria_constraints --settings sapl.legacy_migration_settings |& tee -a $LOG -echo >> $LOG +if [ $# -ge 2 ]; then + + # proteje pasta com dumps de alterações acidentais + chmod -R -w ~/sapl_dumps + + DATE=$(date +%Y-%m-%d) + DIR=~/${DATE}_logs_migracao + mkdir -p $DIR + + LOG="$DIR/$1.migracao.log" + rm -f $LOG + + echo "########################################" | tee -a $LOG + echo "MIGRANDO BANCO $1" | tee -a $LOG + echo "########################################" | tee -a $LOG + echo >> $LOG + + if [ $3 ]; then + # se há senha do mysql + mysql -u $2 -p "$3" -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" + mysql -u $2 -p "$3" < ~/sapl_dumps/$1.sql + else + # se não há senha do mysql + mysql -u $2 -N -s -e "DROP DATABASE IF EXISTS $1; CREATE DATABASE $1;" + mysql -u $2 < ~/sapl_dumps/$1.sql + fi; + echo "O banco legado foi restaurado" |& tee -a $LOG + echo >> $LOG + + echo "--- DJANGO MIGRATE ---" | tee -a $LOG + echo >> $LOG + DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings + echo >> $LOG + + # XXX Na primeira execução desse comando aparece o erro de "Coammands out of sync" + # A solução mais rápida foi executar duas vezes seguidas pra poder migrar. + DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings + echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG + echo >> $LOG + DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG + echo >> $LOG +else + echo "USO:" + echo " $0 [senha mysql]" +fi; diff --git a/sapl/legacy/scripts/recria_dbs_postgres.sh b/sapl/legacy/scripts/recria_dbs_postgres.sh index 6fbf18794..450a306b4 100755 --- a/sapl/legacy/scripts/recria_dbs_postgres.sh +++ b/sapl/legacy/scripts/recria_dbs_postgres.sh @@ -1,5 +1,11 @@ +#!/bin/bash + # (Re)cria todos os bancos postgres para migração # cria um banco postgres (de mesmo nome) para cada banco mysql cujo nome começa com "sapl_" -mysql -u root -padmin -e 'show databases;' | grep '^sapl_' | xargs -I{} ./recria_um_db_postgres.sh {} - +if [ $# -eq 2 ]; then + parallel --verbose -j+0 ./recria_um_db_postgres.sh :::: <(mysql -u $1 -p$2 -e 'show databases;' | grep '^sapl_' | grep -v '_copy$') +else + echo "USO:" + echo " $0 [usuário mysql] [senha mysql]" +fi; \ No newline at end of file diff --git a/sapl/legacy/scripts/recria_um_db_postgres.sh b/sapl/legacy/scripts/recria_um_db_postgres.sh index fd56a95ee..3ff66e8f3 100755 --- a/sapl/legacy/scripts/recria_um_db_postgres.sh +++ b/sapl/legacy/scripts/recria_um_db_postgres.sh @@ -1,5 +1,6 @@ # (Re)cria um db postgres # uso: recria_um_db_postgres +echo "Database $1" sudo -u postgres psql -c "drop DATABASE if exists $1" sudo -u postgres psql -c "CREATE DATABASE $1 WITH OWNER = sapl ENCODING = 'UTF8' TABLESPACE = pg_default LC_COLLATE = 'pt_BR.UTF-8' LC_CTYPE = 'pt_BR.UTF-8' CONNECTION LIMIT = -1 TEMPLATE template0;" diff --git a/sapl/legacy/scripts/street_sweeper.py b/sapl/legacy/scripts/street_sweeper.py deleted file mode 100644 index d01fd3b52..000000000 --- a/sapl/legacy/scripts/street_sweeper.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/python - -# requisito: pip install PyMySQL - -import pymysql.cursors - -HOST = 'localhost' -USER = 'root' -PASSWORD = '' -DB = '' - - -SELECT_EXCLUIDOS = "SELECT %s FROM %s WHERE ind_excluido = 1 ORDER BY %s" - -REGISTROS_INCONSISTENTES = "DELETE FROM %s WHERE %s " -"in (%s) AND ind_excluido = 0 " - -EXCLUI_REGISTRO = "DELETE FROM %s WHERE ind_excluido=1" - -NORMA_DEP = "DELETE FROM vinculo_norma_juridica WHERE cod_norma_referente in (%s) OR \ - cod_norma_referida in (%s) AND ind_excluido = 0 " - -mapa = {} # mapa com tabela principal -> tabelas dependentes - -mapa['tipo_autor'] = ['autor'] -mapa['materia_legislativa'] = ['acomp_materia', 'autoria', 'despacho_inicial', - 'documento_acessorio', 'expediente_materia', - 'legislacao_citada', 'materia_assunto', - 'numeracao', 'ordem_dia', 'parecer', - 'proposicao', 'registro_votacao', - 'relatoria', 'tramitacao'] -mapa['norma_juridica'] = ['vinculo_norma_juridica'] -mapa['comissao'] = ['composicao_comissao'] -mapa['sessao_legislativa'] = ['composicao_mesa'] -mapa['tipo_expediente'] = ['expediente_sessao_plenaria'] - -""" -mapa['autor'] = ['tipo_autor', 'partido', 'comissao', 'parlamentar'] -mapa['parlamentar'] = ['autor', 'autoria', 'composicao_comissao', - 'composicao_mesa', 'dependente', 'filiacao', - 'mandato', 'mesa_sessao_plenaria', 'oradores', - 'oradores_expediente', 'ordem_dia_presenca', - 'registro_votacao_parlamentar', 'relatoria', - 'sessao_plenaria_presenca', 'unidade_tramitacao'] -""" - - -def get_ids_excluidos(cursor, query): - """ - recupera as PKs de registros com ind_excluido = 1 da tabela principal - """ - cursor.execute(query) - excluidos = cursor.fetchall() - # flat tuple of tuples with map transformation into string - excluidos = [str(val) for sublist in excluidos for val in sublist] - return excluidos - - -def remove_tabelas(cursor, tabela_principal, pk, query_dependentes=None): - - QUERY = SELECT_EXCLUIDOS % (pk, tabela_principal, pk) - ids_excluidos = get_ids_excluidos(cursor, QUERY) - print("\nRegistros da tabela '%s' com ind_excluido = 1: %s" % - (tabela_principal.upper(), len(ids_excluidos))) - - """ - Remove registros de tabelas que dependem da tabela principal, - e que se encontram com ind_excluido = 0 (nao excluidas), se - tais registros existirem. - """ - if ids_excluidos: - print("Dependencias inconsistentes") - for tabela in mapa[tabela_principal]: - - QUERY_DEP = REGISTROS_INCONSISTENTES % ( - tabela, pk, ','.join(ids_excluidos)) - - # Trata caso especifico de norma_juridica - if query_dependentes: - QUERY_DEP = query_dependentes % (','.join(ids_excluidos), - ','.join(ids_excluidos)) - - print(tabela.upper(), cursor.execute(QUERY_DEP)) - - """ - Remove todos os registros com ind_excluido = 1 das tabelas - dependentes e da tabela principal, nesta ordem. - """ - print("\n\nRegistros com ind_excluido = 1") - for tabela in mapa[tabela_principal] + [tabela_principal]: - QUERY = EXCLUI_REGISTRO % tabela - print(tabela.upper(), cursor.execute(QUERY)) - - -def remove_excluidas(cursor): - cursor.execute("SHOW_TABLES") - for row in cursor.fetchall(): - print(row) - - -def remove_proposicao_invalida(cursor): - return cursor.execute( - "DELETE FROM proposicao WHERE cod_mat_ou_doc is null") - - -def remove_materia_assunto_invalida(cursor): - return cursor.execute( - "DELETE FROM materia_assunto WHERE cod_assunto = 0") - - -def shotgun_remove(cursor): - for tabela in get_ids_excluidos(cursor, "SHOW TABLES"): - try: - cursor.execute("DELETE FROM %s WHERE ind_excluido = 1" % tabela) - except: - pass - - -if __name__ == '__main__': - connection = pymysql.connect(host=HOST, - user=USER, - password=PASSWORD, - db=DB) - cursor = connection.cursor() - # TIPO AUTOR - remove_tabelas(cursor, 'tipo_autor', 'tip_autor') - # MATERIA LEGISLATIVA - remove_tabelas(cursor, 'materia_legislativa', 'cod_materia') - # NORMA JURIDICA - remove_tabelas(cursor, 'norma_juridica', 'cod_norma', NORMA_DEP) - # COMISSAO - remove_tabelas(cursor, 'comissao', 'cod_comissao') - # SESSAO LEGISLATIVA - remove_tabelas(cursor, 'sessao_legislativa', 'cod_sessao_leg') - # EXPEDIENTE SESSAO - remove_tabelas(cursor, 'tipo_expediente', 'cod_expediente') - # AUTOR - remove_tabelas(cursor, 'autor', 'cod_autor') - # PARLAMENTAR - remove_tabelas(cursor, 'parlamentar', 'cod_parlamentar') - - # PROPOSICAO - remove_proposicao_invalida(cursor) - - # MATERIA_ASSUNTO - remove_materia_assunto_invalida(cursor) - - # shotgun_remove(cursor) - - cursor.close() diff --git a/sapl/legacy_migration_settings.py b/sapl/legacy_migration_settings.py index b9d2cefc2..c8b1fe3a7 100644 --- a/sapl/legacy_migration_settings.py +++ b/sapl/legacy_migration_settings.py @@ -12,14 +12,20 @@ INSTALLED_APPS += ( 'sapl.legacy', # legacy reversed model definitions ) -DATABASES['legacy'] = config('DATABASE_URL', cast=db_url,) +DATABASES['legacy'] = config('DATABASE_URL_FONTE', cast=db_url,) +DATABASES['default'] = config( + 'DATABASE_URL_DESTINO', + cast=lambda v: v if isinstance(v, dict) else db_url(v), + default=DATABASES['default']) # Sobrescreve o nome dos bancos caso a variável de ambiente seja definida # Útil para migração em lote de vários bancos DATABASE_NAME_OVERRIDE = os.environ.get('DATABASE_NAME') if DATABASE_NAME_OVERRIDE: - for db in DATABASES.values(): - db['NAME'] = DATABASE_NAME_OVERRIDE + DATABASES['legacy']['NAME'] = DATABASE_NAME_OVERRIDE + # não altera o nome se o destino é um banco em memória + if not DATABASES['default']['NAME'] == ':memory:': + DATABASES['default']['NAME'] = DATABASE_NAME_OVERRIDE DATABASE_ROUTERS = ['sapl.legacy.router.LegacyRouter', ] @@ -28,3 +34,6 @@ DEBUG = True MOMMY_CUSTOM_FIELDS_GEN = { 'django.db.models.ForeignKey': 'sapl.legacy.migration.make_with_log' } + +# delisga indexação fulltext em tempo real +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor' diff --git a/sapl/materia/apps.py b/sapl/materia/apps.py index 3ac053d1b..883109d31 100644 --- a/sapl/materia/apps.py +++ b/sapl/materia/apps.py @@ -6,3 +6,6 @@ class AppConfig(apps.AppConfig): name = 'sapl.materia' label = 'materia' verbose_name = _('Matéria') + + def ready(self): + from . import receivers \ No newline at end of file diff --git a/sapl/materia/email_utils.py b/sapl/materia/email_utils.py index 3cf53423d..3dc6b220d 100644 --- a/sapl/materia/email_utils.py +++ b/sapl/materia/email_utils.py @@ -1,8 +1,9 @@ -from datetime import datetime +from datetime import datetime as dt from django.core.mail import EmailMultiAlternatives, get_connection, send_mail from django.core.urlresolvers import reverse from django.template import Context, loader +from django.utils import timezone from sapl.base.models import CasaLegislativa from sapl.settings import EMAIL_SEND_USER @@ -150,7 +151,8 @@ def criar_email_tramitacao(base_url, casa_legislativa, materia, status, templates = load_email_templates(['email/tramitacao.txt', 'email/tramitacao.html'], {"casa_legislativa": casa_nome, - "data_registro": datetime.now().strftime( + "data_registro": dt.strftime( + timezone.now(), "%d/%m/%Y"), "cod_materia": materia.id, "logotipo": casa_legislativa.logotipo, diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index e2ff7f535..bb4b64706 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -1,8 +1,6 @@ import os -from datetime import date, datetime -import django_filters from crispy_forms.bootstrap import Alert, FormActions, InlineRadios from crispy_forms.helper import FormHelper from crispy_forms.layout import (HTML, Button, Column, Div, Field, Fieldset, @@ -18,12 +16,13 @@ from django.forms import ModelChoiceField, ModelForm, widgets from django.forms.forms import Form from django.forms.models import ModelMultipleChoiceField from django.forms.widgets import CheckboxSelectMultiple, HiddenInput, Select +from django.utils import timezone from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ +import django_filters -import sapl from sapl.base.models import Autor, TipoAutor from sapl.comissoes.models import Comissao from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_PUBLIC, @@ -42,6 +41,7 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, MateriaPesquisaOrderingFilter, RangeWidgetOverride, autor_label, autor_modal, models_with_gr_for_model, qs_override_django_filter) +import sapl from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial, DocumentoAcessorio, Numeracao, Proposicao, Relatoria, @@ -54,8 +54,8 @@ def ANO_CHOICES(): def em_tramitacao(): return [('', 'Tanto Faz'), - (1, 'Sim'), - (0, 'Não')] + (True, 'Sim'), + (False, 'Não')] class AdicionarVariasAutoriasFilterSet(django_filters.FilterSet): @@ -73,7 +73,7 @@ class AdicionarVariasAutoriasFilterSet(django_filters.FilterSet): self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Filtrar Autores'), - row1, form_actions(save_label='Filtrar')) + row1, form_actions(label='Filtrar')) ) @@ -86,7 +86,7 @@ class ReceberProposicaoForm(Form): self.helper.layout = Layout( Fieldset( _('Incorporar Proposição'), row1, - form_actions(save_label='Buscar Proposição') + form_actions(label='Buscar Proposição') ) ) super(ReceberProposicaoForm, self).__init__(*args, **kwargs) @@ -113,7 +113,7 @@ class MateriaSimplificadaForm(ModelForm): Fieldset( _('Formulário Simplificado'), row1, row2, row3, row4, row5, - form_actions(save_label='Salvar') + form_actions(label='Salvar') ) ) super(MateriaSimplificadaForm, self).__init__(*args, **kwargs) @@ -151,7 +151,7 @@ class AcompanhamentoMateriaForm(ModelForm): row1 = to_row([('email', 10)]) row1.append( - Column(form_actions(save_label='Cadastrar'), css_class='col-md-2') + Column(form_actions(label='Cadastrar'), css_class='col-md-2') ) self.helper = FormHelper() @@ -215,24 +215,26 @@ class TramitacaoForm(ModelForm): def __init__(self, *args, **kwargs): super(TramitacaoForm, self).__init__(*args, **kwargs) - self.fields['data_tramitacao'].initial = datetime.now() + self.fields['data_tramitacao'].initial = timezone.now().date() def clean(self): - super(TramitacaoForm, self).clean() + cleaned_data = super(TramitacaoForm, self).clean() - if 'data_encaminhamento' in self.data: - data_enc_form = self.cleaned_data['data_encaminhamento'] - if 'data_fim_prazo' in self.data: - data_prazo_form = self.cleaned_data['data_fim_prazo'] - if 'data_tramitacao' in self.data: - data_tram_form = self.cleaned_data['data_tramitacao'] + if 'data_encaminhamento' in cleaned_data: + data_enc_form = cleaned_data['data_encaminhamento'] + if 'data_fim_prazo' in cleaned_data: + data_prazo_form = cleaned_data['data_fim_prazo'] + if 'data_tramitacao' in cleaned_data: + data_tram_form = cleaned_data['data_tramitacao'] if self.errors: return self.errors ultima_tramitacao = Tramitacao.objects.filter( materia_id=self.instance.materia_id).exclude( - id=self.instance.id).last() + id=self.instance.id).order_by( + '-data_tramitacao', + '-id').first() if not self.instance.data_tramitacao: @@ -243,7 +245,7 @@ class TramitacaoForm(ModelForm): 'destino da última adicionada!') raise ValidationError(msg) - if self.cleaned_data['data_tramitacao'] > datetime.now().date(): + if cleaned_data['data_tramitacao'] > timezone.now().date(): msg = _( 'A data de tramitação deve ser ' + 'menor ou igual a data de hoje!') @@ -267,7 +269,7 @@ class TramitacaoForm(ModelForm): 'maior que a data de tramitação!') raise ValidationError(msg) - return self.cleaned_data + return cleaned_data class TramitacaoUpdateForm(TramitacaoForm): @@ -296,13 +298,26 @@ class TramitacaoUpdateForm(TramitacaoForm): } def clean(self): - super(TramitacaoUpdateForm, self).clean() + ultima_tramitacao = Tramitacao.objects.filter( + materia_id=self.instance.materia_id).order_by( + '-data_tramitacao', + '-id').first() + + # Se a Tramitação que está sendo editada não for a mais recente, + # ela não pode ter seu destino alterado. + if ultima_tramitacao != self.instance: + if self.cleaned_data['unidade_tramitacao_destino'] != \ + self.instance.unidade_tramitacao_destino: + raise ValidationError( + 'Você não pode mudar a Unidade de Destino desta ' + 'tramitação, pois irá conflitar com a Unidade ' + 'Local da tramitação seguinte') - local = self.instance.unidade_tramitacao_local - data_tram = self.instance.data_tramitacao + self.cleaned_data['data_tramitacao'] = \ + self.instance.data_tramitacao + self.cleaned_data['unidade_tramitacao_local'] = \ + self.instance.unidade_tramitacao_local - self.cleaned_data['data_tramitacao'] = data_tram - self.cleaned_data['unidade_tramitacao_local'] = local return super(TramitacaoUpdateForm, self).clean() @@ -587,7 +602,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet): HTML(autor_label), HTML(autor_modal), row4, row5, row6, row7, row8, row9, row10, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @property @@ -674,7 +689,7 @@ class AutoriaForm(ModelForm): self.helper = FormHelper() self.helper.layout = Layout( Fieldset(_('Autoria'), - row1, 'data_relativa', form_actions(save_label='Salvar'))) + row1, 'data_relativa', form_actions(label='Salvar'))) if not kwargs['instance']: self.fields['autor'].choices = [] @@ -732,7 +747,7 @@ class AutoriaMultiCreateForm(Form): self.helper.layout = Layout( Fieldset( _('Autorias'), row1, row2, 'data_relativa', 'autores', - form_actions(save_label='Incluir Autores Selecionados'))) + form_actions(label='Incluir Autores Selecionados'))) self.fields['autor'].choices = [] @@ -777,7 +792,7 @@ class AcessorioEmLoteFilterSet(django_filters.FilterSet): self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Documentos Acessórios em Lote'), - row1, row2, form_actions(save_label='Pesquisar'))) + row1, row2, form_actions(label='Pesquisar'))) class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet): @@ -809,7 +824,7 @@ class PrimeiraTramitacaoEmLoteFilterSet(django_filters.FilterSet): self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Primeira Tramitação'), - row1, row2, form_actions(save_label='Pesquisar'))) + row1, row2, form_actions(label='Pesquisar'))) class TramitacaoEmLoteFilterSet(django_filters.FilterSet): @@ -850,7 +865,7 @@ class TramitacaoEmLoteFilterSet(django_filters.FilterSet): self.form.helper.form_method = 'GET' self.form.helper.layout = Layout( Fieldset(_('Tramitação em Lote'), - row1, row2, form_actions(save_label='Pesquisar'))) + row1, row2, form_actions(label='Pesquisar'))) class TipoProposicaoForm(ModelForm): @@ -924,6 +939,12 @@ class TipoProposicaoForm(ModelForm): _('O Registro definido (%s) não está na base de %s.' ) % (cd['tipo_conteudo_related'], content_type)) + """ + A unicidade de tipo proposição para tipo de conteudo + foi desabilitada pois existem casos em quem é o procedimento da + instituição convergir vários tipos de proposição + para um tipo de matéria. + unique_value = self._meta.model.objects.filter( content_type=content_type, object_id=cd['tipo_conteudo_related']) @@ -938,7 +959,7 @@ class TipoProposicaoForm(ModelForm): 'que foi defindo como (%s) para (%s)' ) % (unique_value, content_type, - unique_value.tipo_conteudo_related)) + unique_value.tipo_conteudo_related))""" return super().clean() @@ -1149,10 +1170,10 @@ class ProposicaoForm(forms.ModelForm): return super().save(commit) - inst.ano = datetime.now().year + inst.ano = timezone.now().year numero__max = Proposicao.objects.filter( autor=inst.autor, - ano=datetime.now().year).aggregate(Max('numero_proposicao')) + ano=timezone.now().year).aggregate(Max('numero_proposicao')) numero__max = numero__max['numero_proposicao__max'] inst.numero_proposicao = ( numero__max + 1) if numero__max else 1 @@ -1162,6 +1183,84 @@ class ProposicaoForm(forms.ModelForm): return inst +class DevolverProposicaoForm(ProposicaoForm): + + justificativa_devolucao = forms.CharField( + required=False, widget=widgets.Textarea(attrs={'rows': 5})) + + class Meta: + model = Proposicao + fields = [ + 'justificativa_devolucao', + ] + + def __init__(self, *args, **kwargs): + + # esta chamada isola o __init__ de ProposicaoForm + super(ProposicaoForm, self).__init__(*args, **kwargs) + fields = [] + + fields.append( + Fieldset( + _('Registro de Devolução'), + to_column(('justificativa_devolucao', 12)), + to_column( + (form_actions(label=_('Devolver'), + name='devolver', + css_class='btn-danger pull-right'), 12) + ) + ) + ) + + self.helper = FormHelper() + self.helper.layout = Layout(*fields) + + def clean(self): + super(DevolverProposicaoForm, self).clean() + + numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao') + + if not numeracao: + raise ValidationError("A sequência de numeração (por ano ou geral)" + " não foi configurada para a aplicação em " + "tabelas auxiliares") + + cd = ProposicaoForm.clean(self) + + cd = self.cleaned_data + + if 'justificativa_devolucao' not in cd or\ + not cd['justificativa_devolucao']: + # TODO Implementar notificação ao autor por email + raise ValidationError( + _('Adicione uma Justificativa para devolução.')) + return cd + + @transaction.atomic + def save(self, commit=False): + # TODO Implementar workflow entre protocolo e autores + cd = self.cleaned_data + + self.instance.data_devolucao = timezone.now() + self.instance.data_recebimento = None + self.instance.data_envio = None + self.instance.save() + + if self.instance.texto_articulado.exists(): + ta = self.instance.texto_articulado.first() + ta.privacidade = STATUS_TA_PRIVATE + ta.editing_locked = False + ta.save() + + self.instance.results = { + 'messages': { + 'success': [_('Devolução efetuada com sucesso.'), ] + }, + 'url': reverse('sapl.materia:receber-proposicao') + } + return self.instance + + class ConfirmarProposicaoForm(ProposicaoForm): tipo_readonly = forms.CharField( @@ -1174,9 +1273,6 @@ class ConfirmarProposicaoForm(ProposicaoForm): required=False, widget=widgets.TextInput( attrs={'readonly': 'readonly'})) - justificativa_devolucao = forms.CharField( - required=False, widget=widgets.Textarea(attrs={'rows': 5})) - regime_tramitacao = forms.ModelChoiceField( required=False, queryset=RegimeTramitacao.objects.all()) @@ -1195,7 +1291,6 @@ class ConfirmarProposicaoForm(ProposicaoForm): fields = [ 'data_envio', 'descricao', - 'justificativa_devolucao', 'gerar_protocolo', 'numero_de_paginas' ] @@ -1282,20 +1377,16 @@ class ConfirmarProposicaoForm(ProposicaoForm): if self.proposicao_incorporacao_obrigatoria != 'N': itens_incorporacao.append(to_column(('numero_de_paginas', 4))) - itens_incorporacao.append(to_column((FormActions(Submit( - 'incorporar', _('Incorporar'), css_class='pull-right')), 12))) + itens_incorporacao.append( + to_column( + (form_actions(label=_('Incorporar'), + name='incorporar'), 12) + ) + ) fields.append( Fieldset(_('Registro de Incorporação'), *itens_incorporacao)) - fields.append( - Fieldset( - _('Registro de Devolução'), - to_column(('justificativa_devolucao', 12)), - to_column((FormActions(Submit( - 'devolver', _('Devolver'), - css_class='btn-danger pull-right')), 12)) - )) self.helper = FormHelper() self.helper.layout = Layout(*fields) @@ -1325,34 +1416,23 @@ class ConfirmarProposicaoForm(ProposicaoForm): raise ValidationError("A sequência de numeração (por ano ou geral)" " não foi configurada para a aplicação em " "tabelas auxiliares") - if 'incorporar' in self.data: - cd = ProposicaoForm.clean(self) - if self.instance.tipo.content_type.model_class() ==\ - TipoMateriaLegislativa: - if 'regime_tramitacao' not in cd or\ - not cd['regime_tramitacao']: - raise ValidationError( - _('Regime de Tramitação deve ser informado.')) - - elif self.instance.tipo.content_type.model_class( - ) == TipoDocumento and not cd['materia_de_vinculo']: + cd = ProposicaoForm.clean(self) + if self.instance.tipo.content_type.model_class() ==\ + TipoMateriaLegislativa: + if 'regime_tramitacao' not in cd or\ + not cd['regime_tramitacao']: raise ValidationError( - _('Documentos não podem ser incorporados sem definir ' - 'para qual Matéria Legislativa ele se destina.')) + _('Regime de Tramitação deve ser informado.')) - elif 'devolver' in self.data: - cd = self.cleaned_data + elif self.instance.tipo.content_type.model_class( + ) == TipoDocumento and not cd['materia_de_vinculo']: - if 'justificativa_devolucao' not in cd or\ - not cd['justificativa_devolucao']: - # TODO Implementar notificação ao autor por email - raise ValidationError( - _('Adicione uma Justificativa para devolução.')) - else: raise ValidationError( - _('Dados de Confirmação invalidos.')) + _('Documentos não podem ser incorporados sem definir ' + 'para qual Matéria Legislativa ele se destina.')) + return cd @transaction.atomic @@ -1360,37 +1440,16 @@ class ConfirmarProposicaoForm(ProposicaoForm): # TODO Implementar workflow entre protocolo e autores cd = self.cleaned_data - if 'devolver' in self.data: - self.instance.data_devolucao = datetime.now() - self.instance.data_recebimento = None - self.instance.data_envio = None - self.instance.save() - - if self.instance.texto_articulado.exists(): - ta = self.instance.texto_articulado.first() - ta.privacidade = STATUS_TA_PRIVATE - ta.editing_locked = False - ta.save() - - self.instance.results = { - 'messages': { - 'success': [_('Devolução efetuada com sucesso.'), ] - }, - 'url': reverse('sapl.materia:receber-proposicao') - } - return self.instance + self.instance.justificativa_devolucao = '' + self.instance.data_devolucao = None + self.instance.data_recebimento = timezone.now() + self.instance.materia_de_vinculo = cd['materia_de_vinculo'] - elif 'incorporar' in self.data: - self.instance.justificativa_devolucao = '' - self.instance.data_devolucao = None - self.instance.data_recebimento = datetime.now() - self.instance.materia_de_vinculo = cd['materia_de_vinculo'] - - if self.instance.texto_articulado.exists(): - ta = self.instance.texto_articulado.first() - ta.privacidade = STATUS_TA_IMMUTABLE_PUBLIC - ta.editing_locked = True - ta.save() + if self.instance.texto_articulado.exists(): + ta = self.instance.texto_articulado.first() + ta.privacidade = STATUS_TA_IMMUTABLE_PUBLIC + ta.editing_locked = True + ta.save() self.instance.save() @@ -1420,7 +1479,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): ) == TipoMateriaLegislativa: numero__max = MateriaLegislativa.objects.filter( tipo=proposicao.tipo.tipo_conteudo_related, - ano=datetime.now().year).aggregate(Max('numero')) + ano=timezone.now().year).aggregate(Max('numero')) numero__max = numero__max['numero__max'] # dados básicos @@ -1428,8 +1487,8 @@ class ConfirmarProposicaoForm(ProposicaoForm): materia.numero = (numero__max + 1) if numero__max else 1 materia.tipo = proposicao.tipo.tipo_conteudo_related materia.ementa = proposicao.descricao - materia.ano = datetime.now().year - materia.data_apresentacao = datetime.now() + materia.ano = timezone.now().year + materia.data_apresentacao = timezone.now() materia.em_tramitacao = True materia.regime_tramitacao = cd['regime_tramitacao'] @@ -1468,7 +1527,7 @@ class ConfirmarProposicaoForm(ProposicaoForm): anexada = Anexada() anexada.materia_principal = proposicao.materia_de_vinculo anexada.materia_anexada = materia - anexada.data_anexacao = datetime.now() + anexada.data_anexacao = timezone.now() anexada.save() self.instance.results['messages']['success'].append(_( @@ -1540,18 +1599,18 @@ class ConfirmarProposicaoForm(ProposicaoForm): numeracao = sapl.base.models.AppConfig.attr('sequencia_numeracao') if numeracao == 'A': nm = Protocolo.objects.filter( - ano=date.today().year).aggregate(Max('numero')) + ano=timezone.now().year).aggregate(Max('numero')) elif numeracao == 'U': nm = Protocolo.objects.all().aggregate(Max('numero')) protocolo = Protocolo() protocolo.numero = (nm['numero__max'] + 1) if nm['numero__max'] else 1 - protocolo.ano = date.today().year - protocolo.data = date.today() - protocolo.hora = datetime.now().time() + protocolo.ano = timezone.now().year + protocolo.data = timezone.now() + protocolo.hora = timezone.now().time() # TODO transformar campo timestamp em auto_now_add - protocolo.timestamp = datetime.now() + protocolo.timestamp = timezone.now() protocolo.tipo_protocolo = '1' protocolo.interessado = str(proposicao.autor) @@ -1641,7 +1700,7 @@ class EtiquetaPesquisaForm(forms.Form): Fieldset( ('Formulário de Etiqueta'), row1, row2, - form_actions(save_label='Pesquisar') + form_actions(label='Pesquisar') ) ) @@ -1653,7 +1712,7 @@ class EtiquetaPesquisaForm(forms.Form): # Então verifica se o usuário preencheu o Incial e mas não # preencheu o Final, ou vice-versa if (not cleaned_data['data_inicial'] or - not cleaned_data['data_final']): + not cleaned_data['data_final']): raise ValidationError(_( 'Caso pesquise por data, os campos de Data Incial e ' + 'Data Final devem ser preenchidos obrigatoriamente')) @@ -1665,17 +1724,84 @@ class EtiquetaPesquisaForm(forms.Form): # O mesmo processo anterior é feito com o processo if (cleaned_data['processo_inicial'] or - cleaned_data['processo_final']): + cleaned_data['processo_final']): if (not cleaned_data['processo_inicial'] or - not cleaned_data['processo_final']): + not cleaned_data['processo_final']): raise ValidationError(_( 'Caso pesquise por número de processo, os campos de ' + 'Processo Inicial e Processo Final ' + 'devem ser preenchidos obrigatoriamente')) elif (cleaned_data['processo_final'] < cleaned_data['processo_inicial']): - raise ValidationError(_( - 'O processo final não pode ser menor que o inicial')) + raise ValidationError(_( + 'O processo final não pode ser menor que o inicial')) + + return cleaned_data + + +class FichaPesquisaForm(forms.Form): + tipo_materia = forms.ModelChoiceField( + label=TipoMateriaLegislativa._meta.verbose_name, + queryset=TipoMateriaLegislativa.objects.all(), + empty_label='Selecione') + + data_inicial = forms.DateField( + label='Data Inicial', + widget=forms.DateInput(format='%d/%m/%Y') + ) + + data_final = forms.DateField( + label='Data Final', + widget=forms.DateInput(format='%d/%m/%Y') + ) + + def __init__(self, *args, **kwargs): + super(FichaPesquisaForm, self).__init__(*args, **kwargs) + + row1 = to_row( + [('tipo_materia', 6), + ('data_inicial', 3), + ('data_final', 3)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + ('Formulário de Ficha'), + row1, + form_actions(label='Pesquisar') + ) + ) + + def clean(self): + cleaned_data = super(FichaPesquisaForm, self).clean() + + if not self.is_valid(): + return cleaned_data + + if cleaned_data['data_final'] < cleaned_data['data_inicial']: + raise ValidationError(_( + 'A Data Final não pode ser menor que a Data Inicial')) return cleaned_data + +class FichaSelecionaForm(forms.Form): + materia = forms.ModelChoiceField( + widget=forms.RadioSelect, + queryset=MateriaLegislativa.objects.all(), + label='') + + def __init__(self, *args, **kwargs): + super(FichaSelecionaForm, self).__init__(*args, **kwargs) + + row1 = to_row( + [('materia', 12)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + ('Selecione a ficha que deseja imprimir'), + row1, + form_actions(label='Gerar Impresso') + ) + ) diff --git a/sapl/materia/migrations/0012_auto_20170815_1238.py b/sapl/materia/migrations/0012_auto_20170815_1238.py new file mode 100644 index 000000000..5f62c1670 --- /dev/null +++ b/sapl/materia/migrations/0012_auto_20170815_1238.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2017-08-15 12:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0011_auto_20170808_1034'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='tipo', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='materia.TipoProposicao', verbose_name='Tipo'), + ), + migrations.AlterField( + model_name='tramitacao', + name='status', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='materia.StatusTramitacao', verbose_name='Status'), + ), + ] diff --git a/sapl/materia/migrations/0013_auto_20170816_1136.py b/sapl/materia/migrations/0013_auto_20170816_1136.py new file mode 100644 index 000000000..e6804e8e1 --- /dev/null +++ b/sapl/materia/migrations/0013_auto_20170816_1136.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2017-08-16 11:36 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0012_auto_20170815_1238'), + ] + + operations = [ + migrations.AlterField( + model_name='tramitacao', + name='unidade_tramitacao_destino', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tramitacoes_destino', to='materia.UnidadeTramitacao', verbose_name='Unidade Destino'), + ), + ] diff --git a/sapl/materia/migrations/0014_auto_20170905_0818.py b/sapl/materia/migrations/0014_auto_20170905_0818.py new file mode 100644 index 000000000..afe266c58 --- /dev/null +++ b/sapl/materia/migrations/0014_auto_20170905_0818.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2017-09-05 08:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0013_adiciona_status_tramitacao'), + ] + + operations = [ + migrations.AlterModelOptions( + name='unidadetramitacao', + options={'ordering': ['orgao', 'comissao', 'parlamentar'], 'verbose_name': 'Unidade de Tramitação', 'verbose_name_plural': 'Unidades de Tramitação'}, + ), + ] diff --git a/sapl/materia/migrations/0015_auto_20170908_1024.py b/sapl/materia/migrations/0015_auto_20170908_1024.py new file mode 100644 index 000000000..3332b6504 --- /dev/null +++ b/sapl/materia/migrations/0015_auto_20170908_1024.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-08 10:24 +from __future__ import unicode_literals + +from django.db import migrations + +from sapl.materia.models import TipoProposicao + + +class AlterUniqueTogetherFixConstraintInexistente( + migrations.AlterUniqueTogether): + + def database_forwards(self, + app_label, schema_editor, from_state, to_state): + constraint_names = schema_editor._constraint_names( + TipoProposicao, ['content_type_id', 'object_id'], unique=True) + if constraint_names: + # por alguma razão a constraint não existe em alguns bancos + # se ela existir continua a exetução normal + super(AlterUniqueTogetherFixConstraintInexistente, + self).database_forwards( + app_label, schema_editor, from_state, to_state + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0014_auto_20170905_0818'), + ] + + operations = [ + AlterUniqueTogetherFixConstraintInexistente( + name='tipoproposicao', + unique_together=set([]), + ), + ] diff --git a/sapl/materia/migrations/0016_merge.py b/sapl/materia/migrations/0016_merge.py new file mode 100644 index 000000000..472cefbd4 --- /dev/null +++ b/sapl/materia/migrations/0016_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-08 11:57 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0015_auto_20170908_1024'), + ('materia', '0013_auto_20170816_1136'), + ] + + operations = [ + ] diff --git a/sapl/materia/migrations/0017_auto_20170918_1257.py b/sapl/materia/migrations/0017_auto_20170918_1257.py new file mode 100644 index 000000000..d09c5daaa --- /dev/null +++ b/sapl/materia/migrations/0017_auto_20170918_1257.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-18 12:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0016_merge'), + ] + + operations = [ + migrations.AlterField( + model_name='proposicao', + name='data_envio', + field=models.DateTimeField(null=True, verbose_name='Data de Envio'), + ), + ] diff --git a/sapl/materia/migrations/0018_auto_20171113_1339.py b/sapl/materia/migrations/0018_auto_20171113_1339.py new file mode 100644 index 000000000..a84b1c6d8 --- /dev/null +++ b/sapl/materia/migrations/0018_auto_20171113_1339.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-11-13 15:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0017_auto_20170918_1257'), + ] + + operations = [ + migrations.AlterField( + model_name='anexada', + name='materia_anexada', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_anexada_set', to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='anexada', + name='materia_principal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_principal_set', to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='autoria', + name='autor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Autor', verbose_name='Autor'), + ), + migrations.AlterField( + model_name='autoria', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria Legislativa'), + ), + migrations.AlterField( + model_name='despachoinicial', + name='comissao', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comissoes.Comissao'), + ), + migrations.AlterField( + model_name='despachoinicial', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='documentoacessorio', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='materiaassunto', + name='assunto', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.AssuntoMateria', verbose_name='Assunto'), + ), + migrations.AlterField( + model_name='materiaassunto', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa', verbose_name='Matéria'), + ), + migrations.AlterField( + model_name='numeracao', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='parecer', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='parecer', + name='relatoria', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.Relatoria'), + ), + migrations.AlterField( + model_name='proposicao', + name='materia_de_vinculo', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='proposicao_set', to='materia.MateriaLegislativa', verbose_name='Matéria anexadora'), + ), + migrations.AlterField( + model_name='relatoria', + name='comissao', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comissoes.Comissao', verbose_name='Comissão'), + ), + migrations.AlterField( + model_name='relatoria', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='relatoria', + name='parlamentar', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='parlamentares.Parlamentar', verbose_name='Parlamentar'), + ), + migrations.AlterField( + model_name='tramitacao', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + ] diff --git a/sapl/materia/migrations/0019_auto_20171127_1500.py b/sapl/materia/migrations/0019_auto_20171127_1500.py new file mode 100644 index 000000000..2bd2c02a9 --- /dev/null +++ b/sapl/materia/migrations/0019_auto_20171127_1500.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-11-27 17:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0018_auto_20171113_1339'), + ] + + operations = [ + migrations.AlterField( + model_name='anexada', + name='materia_anexada', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_anexada_set', to='materia.MateriaLegislativa', verbose_name='Matéria Anexada'), + ), + migrations.AlterField( + model_name='anexada', + name='materia_principal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='materia_principal_set', to='materia.MateriaLegislativa', verbose_name='Matéria Principal'), + ), + ] diff --git a/sapl/materia/migrations/0020_auto_20171204_1658.py b/sapl/materia/migrations/0020_auto_20171204_1658.py new file mode 100644 index 000000000..dce162b47 --- /dev/null +++ b/sapl/materia/migrations/0020_auto_20171204_1658.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-12-04 18:58 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0019_auto_20171127_1500'), + ] + + operations = [ + migrations.AlterModelOptions( + name='orgao', + options={'ordering': ['nome'], 'verbose_name': 'Órgão', 'verbose_name_plural': 'Órgãos'}, + ), + migrations.AlterModelOptions( + name='tipodocumento', + options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Documento', 'verbose_name_plural': 'Tipos de Documento'}, + ), + migrations.AlterModelOptions( + name='unidadetramitacao', + options={'verbose_name': 'Unidade de Tramitação', 'verbose_name_plural': 'Unidades de Tramitação'}, + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 9c95e1c44..0aade9df3 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1,4 +1,3 @@ -from datetime import datetime import reversion from django.contrib.auth.models import Group @@ -6,7 +5,8 @@ from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import models -from django.utils import formats +from django.db.models.functions import Concat +from django.utils import formats, timezone from django.utils.translation import ugettext_lazy as _ from model_utils import Choices @@ -19,8 +19,8 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path) -EM_TRAMITACAO = [(1, 'Sim'), - (0, 'Não')] +EM_TRAMITACAO = [(True, 'Sim'), + (False, 'Não')] def grupo_autor(): @@ -66,7 +66,6 @@ class TipoProposicao(models.Model): class Meta: verbose_name = _('Tipo de Proposição') verbose_name_plural = _('Tipos de Proposições') - unique_together = (('content_type', 'object_id'), ) def __str__(self): return self.descricao @@ -289,9 +288,9 @@ class MateriaLegislativa(models.Model): class Autoria(models.Model): autor = models.ForeignKey(Autor, verbose_name=_('Autor'), - on_delete=models.PROTECT) + on_delete=models.CASCADE) materia = models.ForeignKey( - MateriaLegislativa, on_delete=models.PROTECT, + MateriaLegislativa, on_delete=models.CASCADE, verbose_name=_('Matéria Legislativa')) primeiro_autor = models.BooleanField(verbose_name=_('Primeiro Autor'), choices=YES_NO_CHOICES, @@ -304,14 +303,14 @@ class Autoria(models.Model): ordering = ('-primeiro_autor', 'autor__nome') def __str__(self): - return _('%(autor)s - %(materia)s') % { + return _('Autoria: %(autor)s - %(materia)s') % { 'autor': self.autor, 'materia': self.materia} @reversion.register() class AcompanhamentoMateria(models.Model): usuario = models.CharField(max_length=50) - materia = models.ForeignKey(MateriaLegislativa) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) email = models.EmailField( max_length=100, verbose_name=_('E-mail')) data_cadastro = models.DateField(auto_now_add=True) @@ -332,10 +331,12 @@ class AcompanhamentoMateria(models.Model): class Anexada(models.Model): materia_principal = models.ForeignKey( MateriaLegislativa, related_name='materia_principal_set', - on_delete=models.PROTECT) + on_delete=models.CASCADE, + verbose_name=_('Matéria Principal')) materia_anexada = models.ForeignKey( MateriaLegislativa, related_name='materia_anexada_set', - on_delete=models.PROTECT) + on_delete=models.CASCADE, + verbose_name=_('Matéria Anexada')) data_anexacao = models.DateField(verbose_name=_('Data Anexação')) data_desanexacao = models.DateField( blank=True, null=True, verbose_name=_('Data Desanexação')) @@ -374,8 +375,8 @@ class DespachoInicial(models.Model): # TODO M2M? # TODO Despachos não são necessáriamente comissoes, podem ser outros # órgãos, ex: procuradorias - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) - comissao = models.ForeignKey(Comissao, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) + comissao = models.ForeignKey(Comissao, on_delete=models.CASCADE) class Meta: verbose_name = _('Despacho Inicial') @@ -402,6 +403,7 @@ class TipoDocumento(models.Model): class Meta: verbose_name = _('Tipo de Documento') verbose_name_plural = _('Tipos de Documento') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -409,7 +411,7 @@ class TipoDocumento(models.Model): @reversion.register() class DocumentoAcessorio(models.Model): - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) tipo = models.ForeignKey(TipoDocumento, on_delete=models.PROTECT, verbose_name=_('Tipo')) @@ -479,11 +481,11 @@ class MateriaAssunto(models.Model): # TODO M2M ?? assunto = models.ForeignKey( AssuntoMateria, - on_delete=models.PROTECT, + on_delete=models.CASCADE, verbose_name=_('Assunto')) materia = models.ForeignKey( MateriaLegislativa, - on_delete=models.PROTECT, + on_delete=models.CASCADE, verbose_name=_('Matéria')) class Meta: @@ -497,7 +499,7 @@ class MateriaAssunto(models.Model): @reversion.register() class Numeracao(models.Model): - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) tipo_materia = models.ForeignKey( TipoMateriaLegislativa, on_delete=models.PROTECT, @@ -545,6 +547,7 @@ class Orgao(models.Model): class Meta: verbose_name = _('Órgão') verbose_name_plural = _('Órgãos') + ordering = ['nome'] def __str__(self): return _( @@ -566,9 +569,9 @@ class TipoFimRelatoria(models.Model): @reversion.register() class Relatoria(models.Model): - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) parlamentar = models.ForeignKey(Parlamentar, - on_delete=models.PROTECT, + on_delete=models.CASCADE, verbose_name=_('Parlamentar')) tipo_fim_relatoria = models.ForeignKey( TipoFimRelatoria, @@ -578,7 +581,7 @@ class Relatoria(models.Model): verbose_name=_('Motivo Fim Relatoria')) comissao = models.ForeignKey( Comissao, blank=True, null=True, - on_delete=models.PROTECT, verbose_name=_('Comissão')) + on_delete=models.CASCADE, verbose_name=_('Comissão')) data_designacao_relator = models.DateField( verbose_name=_('Data Designação')) data_destituicao_relator = models.DateField( @@ -597,8 +600,8 @@ class Relatoria(models.Model): @reversion.register() class Parecer(models.Model): - relatoria = models.ForeignKey(Relatoria, on_delete=models.PROTECT) - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + relatoria = models.ForeignKey(Relatoria, on_delete=models.CASCADE) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) tipo_conclusao = models.CharField(max_length=3, blank=True) tipo_apresentacao = models.CharField( max_length=1, choices=TIPO_APRESENTACAO_CHOICES) @@ -621,11 +624,13 @@ class Proposicao(models.Model): blank=True, on_delete=models.PROTECT) tipo = models.ForeignKey(TipoProposicao, on_delete=models.PROTECT, + blank=False, + null=True, verbose_name=_('Tipo')) # XXX data_envio was not null, but actual data said otherwise!!! data_envio = models.DateTimeField( - blank=True, null=True, verbose_name=_('Data de Envio')) + blank=False, null=True, verbose_name=_('Data de Envio')) data_recebimento = models.DateTimeField( blank=True, null=True, verbose_name=_('Data de Recebimento')) data_devolucao = models.DateTimeField( @@ -680,7 +685,7 @@ class Proposicao(models.Model): # retire o comentário quando resolver materia_de_vinculo = models.ForeignKey( MateriaLegislativa, blank=True, null=True, - on_delete=models.PROTECT, + on_delete=models.CASCADE, verbose_name=_('Matéria anexadora'), related_name=_('proposicao_set')) @@ -710,7 +715,7 @@ class Proposicao(models.Model): def title_type(self): return '%s nº _____ %s' % ( self.tipo, formats.date_format( - self.data_envio if self.data_envio else datetime.now(), + self.data_envio if self.data_envio else timezone.now(), "\d\e d \d\e F \d\e Y")) class Meta: @@ -788,6 +793,19 @@ class StatusTramitacao(models.Model): 'descricao': self.descricao} +class UnidadeTramitacaoManager(models.Manager): + """ + Esta classe permite ordenar alfabeticamente a unidade de tramitacao + através da concatenação de 3 fields + """ + def get_queryset(self): + return super(UnidadeTramitacaoManager, self).get_queryset().annotate( + nome_composto=Concat('orgao__nome', + 'comissao__sigla', + 'parlamentar__nome_parlamentar') + ).order_by('nome_composto') + + @reversion.register() class UnidadeTramitacao(models.Model): comissao = models.ForeignKey( @@ -800,6 +818,8 @@ class UnidadeTramitacao(models.Model): Parlamentar, blank=True, null=True, on_delete=models.PROTECT, verbose_name=_('Parlamentar')) + objects = UnidadeTramitacaoManager() + class Meta: verbose_name = _('Unidade de Tramitação') verbose_name_plural = _('Unidades de Tramitação') @@ -844,8 +864,12 @@ class Tramitacao(models.Model): ) status = models.ForeignKey(StatusTramitacao, on_delete=models.PROTECT, + # TODO PÓS MIGRACAO INICIAL (vide #1381) + # não nulo quando todas as + # bases tiverem sido corrigidas + null=True, verbose_name=_('Status')) - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) data_tramitacao = models.DateField(verbose_name=_('Data Tramitação')) unidade_tramitacao_local = models.ForeignKey( UnidadeTramitacao, @@ -856,6 +880,10 @@ class Tramitacao(models.Model): blank=True, null=True, verbose_name=_('Data Encaminhamento')) unidade_tramitacao_destino = models.ForeignKey( UnidadeTramitacao, + # TODO PÓS MIGRACAO INICIAL (vide #1381) + # não nulo quando todas as + # bases tiverem sido corrigidas + null=True, related_name='tramitacoes_destino', on_delete=models.PROTECT, verbose_name=_('Unidade Destino')) diff --git a/sapl/materia/receivers.py b/sapl/materia/receivers.py index 31f353fe0..945c6636e 100644 --- a/sapl/materia/receivers.py +++ b/sapl/materia/receivers.py @@ -1,5 +1,7 @@ +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from sapl.materia.models import Tramitacao from sapl.materia.signals import tramitacao_signal from sapl.utils import get_base_url @@ -17,3 +19,11 @@ def handle_tramitacao_signal(sender, **kwargs): materia, tramitacao.status, tramitacao.unidade_tramitacao_destino) + + +@receiver(post_delete, sender=Tramitacao) +def status_tramitacao_materia(sender, instance, **kwargs): + if instance.status.indicador == 'F': + materia = instance.materia + materia.em_tramitacao = True + materia.save() diff --git a/sapl/materia/tests/test_materia.py b/sapl/materia/tests/test_materia.py index 071d51d4c..a538cfb87 100644 --- a/sapl/materia/tests/test_materia.py +++ b/sapl/materia/tests/test_materia.py @@ -1,9 +1,9 @@ -import pytest from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.files.uploadedfile import SimpleUploadedFile from django.core.urlresolvers import reverse from model_mommy import mommy +import pytest from sapl.base.models import Autor, TipoAutor from sapl.comissoes.models import Comissao, TipoComissao @@ -18,6 +18,7 @@ from sapl.norma.models import (LegislacaoCitada, NormaJuridica, from sapl.utils import models_with_gr_for_model +@pytest.mark.django_db(transaction=False) def make_unidade_tramitacao(descricao): # Cria uma comissão para ser a unidade de tramitação tipo_comissao = mommy.make(TipoComissao) diff --git a/sapl/materia/tests/test_materia_form.py b/sapl/materia/tests/test_materia_form.py new file mode 100644 index 000000000..41ad04837 --- /dev/null +++ b/sapl/materia/tests/test_materia_form.py @@ -0,0 +1,68 @@ +import pytest +from django.utils.translation import ugettext as _ +from model_mommy import mommy + +from sapl.materia import forms +from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa + + +@pytest.mark.django_db(transaction=False) +def test_valida_campos_obrigatorios_ficha_pesquisa_form(): + form = forms.FichaPesquisaForm(data={}) + + assert not form.is_valid() + + errors = form.errors + + assert errors['tipo_materia'] == [_('Este campo é obrigatório.')] + assert errors['data_inicial'] == [_('Este campo é obrigatório.')] + assert errors['data_final'] == [_('Este campo é obrigatório.')] + + assert len(errors) == 3 + + +@pytest.mark.django_db(transaction=False) +def test_ficha_pesquisa_form_datas_invalidas(): + tipo = mommy.make(TipoMateriaLegislativa) + + form = forms.FichaPesquisaForm(data={'tipo_materia': str(tipo.pk), + 'data_inicial': '10/11/2017', + 'data_final': '09/11/2017' + }) + assert not form.is_valid() + assert form.errors['__all__'] == [_('A Data Final não pode ser menor que ' + 'a Data Inicial')] + + +@pytest.mark.django_db(transaction=False) +def test_ficha_pesquisa_form_invalido(): + tipo = mommy.make(TipoMateriaLegislativa) + + form = forms.FichaPesquisaForm(data={'tipo_materia': str(tipo.pk), + 'data_inicial': '10/11/2017', + 'data_final': '09/11/2017' + }) + + assert not form.is_valid() + + +@pytest.mark.django_db(transaction=False) +def test_valida_campos_obrigatorios_ficha_seleciona_form(): + form = forms.FichaSelecionaForm(data={}) + + assert not form.is_valid() + + errors = form.errors + + assert errors['materia'] == [_('Este campo é obrigatório.')] + + assert len(errors) == 1 + + +@pytest.mark.django_db(transaction=False) +def test_ficha_seleciona_form_valido(): + materia = mommy.make(MateriaLegislativa) + + form = forms.FichaSelecionaForm(data={'materia': str(materia.pk)}) + + assert form.is_valid() diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index e803f18e0..611bdf080 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -8,7 +8,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, CriarProtocoloMateriaView, DespachoInicialCrud, DocumentoAcessorioCrud, DocumentoAcessorioEmLoteView, - ImpressosView, EtiquetaPesquisaView, + EtiquetaPesquisaView, FichaPesquisaView, + FichaSelecionaView, ImpressosView, LegislacaoCitadaCrud, MateriaAssuntoCrud, MateriaLegislativaCrud, MateriaLegislativaPesquisaView, MateriaTaView, @@ -24,6 +25,8 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, TramitacaoEmLoteView, UnidadeTramitacaoCrud, proposicao_texto, recuperar_materia) +from sapl.norma.views import NormaPesquisaSimplesView + from .apps import AppConfig app_name = AppConfig.name @@ -35,6 +38,15 @@ urlpatterns_impressos = [ url(r'^materia/impressos/etiqueta-pesquisa/$', EtiquetaPesquisaView.as_view(), name='impressos_etiqueta'), + url(r'^materia/impressos/ficha-pesquisa/$', + FichaPesquisaView.as_view(), + name='impressos_ficha_pesquisa'), + url(r'^materia/impressos/ficha-seleciona/$', + FichaSelecionaView.as_view(), + name='impressos_ficha_seleciona'), + url(r'^materia/impressos/norma-pesquisa/$', + NormaPesquisaSimplesView.as_view(), + name='impressos_norma_pesquisa'), ] urlpatterns_materia = [ diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 2d11ecf67..df26a31ca 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -12,15 +12,15 @@ from django.core.urlresolvers import reverse from django.http import HttpResponse, JsonResponse from django.http.response import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect -from django.template import Context, loader, RequestContext -from django.utils import formats +from django.template import RequestContext, loader +from django.utils import formats, timezone from django.utils.translation import ugettext_lazy as _ from django.views.generic import CreateView, ListView, TemplateView, UpdateView from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django_filters.views import FilterView +import weasyprint -import sapl from sapl.base.models import Autor, CasaLegislativa from sapl.comissoes.models import Comissao, Participacao from sapl.compilacao.models import (STATUS_TA_IMMUTABLE_RESTRICT, @@ -35,18 +35,20 @@ from sapl.materia.forms import (AnexadaForm, AutoriaForm, AutoriaMultiCreateForm, ConfirmarProposicaoForm, LegislacaoCitadaForm, ProposicaoForm, TipoProposicaoForm, - TramitacaoForm, TramitacaoUpdateForm) + TramitacaoForm, TramitacaoUpdateForm, + DevolverProposicaoForm) from sapl.norma.models import LegislacaoCitada from sapl.protocoloadm.models import Protocolo from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, autor_modal, gerar_hash_arquivo, get_base_url, - montar_row_autor) + montar_row_autor, show_results_filter_set, get_mime_type_from_file_extension) +import sapl from .email_utils import do_envia_email_confirmacao from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, AdicionarVariasAutoriasFilterSet, DespachoInicialForm, DocumentoAcessorioForm, EtiquetaPesquisaForm, - MateriaAssuntoForm, + FichaPesquisaForm, FichaSelecionaForm, MateriaAssuntoForm, MateriaLegislativaFilterSet, MateriaSimplificadaForm, PrimeiraTramitacaoEmLoteFilterSet, ReceberProposicaoForm, RelatoriaForm, TramitacaoEmLoteFilterSet, @@ -61,7 +63,6 @@ from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, TipoProposicao, Tramitacao, UnidadeTramitacao) from .signals import tramitacao_signal -import weasyprint AssuntoMateriaCrud = Crud.build(AssuntoMateria, 'assunto_materia') @@ -81,9 +82,9 @@ TipoFimRelatoriaCrud = CrudAux.build( def autores_ja_adicionados(materia_pk): - autorias = Autoria.objects.filter(materia_id=materia_pk) - pks = [a.autor.pk for a in autorias] - return pks + autorias = Autoria.objects.filter(materia_id=materia_pk).values_list( + 'autor_id', flat=True) + return autorias def proposicao_texto(request, pk): @@ -96,12 +97,7 @@ def proposicao_texto(request, pk): arquivo = proposicao.texto_original - ext = arquivo.name.split('.')[-1] - mime = '' - if ext == 'odt': - mime = 'application/vnd.oasis.opendocument.text' - else: - mime = "application/%s" % (ext,) + mime = get_mime_type_from_file_extension(arquivo.name) with open(arquivo.path, 'rb') as f: data = f.read() @@ -138,6 +134,9 @@ class AdicionarVariasAutorias(PermissionRequiredForAppCrudMixin, FilterView): context['title'] = _('Pesquisar Autores') qr = self.request.GET.copy() context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + context['pk_materia'] = self.kwargs['pk'] return context @@ -173,14 +172,14 @@ class CriarProtocoloMateriaView(CreateView): except ObjectDoesNotExist: raise Http404() - materias_ano = MateriaLegislativa.objects.filter( - ano=protocolo.ano, - tipo=protocolo.tipo_materia).order_by('-numero') - - if materias_ano: - numero = materias_ano.first().numero + 1 - else: - numero = 1 + numero = 1 + try: + materias_ano = MateriaLegislativa.objects.filter( + ano=protocolo.ano, + tipo=protocolo.tipo_materia).latest('numero') + numero = materias_ano.numero + 1 + except ObjectDoesNotExist: + pass # numero ficou com o valor padrão 1 acima context['form'].fields['tipo'].initial = protocolo.tipo_materia context['form'].fields['numero'].initial = numero @@ -282,7 +281,7 @@ def recuperar_materia(request): ano = request.GET.get('ano', '') param = {'tipo': tipo} - param['data_apresentacao__year'] = ano if ano else datetime.now().year + param['data_apresentacao__year'] = ano if ano else timezone.now().year materia = MateriaLegislativa.objects.filter(**param).order_by( 'tipo', 'ano', 'numero').values_list('numero', 'ano').last() @@ -291,7 +290,7 @@ def recuperar_materia(request): 'ano': materia[1]}) else: response = JsonResponse( - {'numero': 1, 'ano': ano if ano else datetime.now().year}) + {'numero': 1, 'ano': ano if ano else timezone.now().year}) return response @@ -321,14 +320,14 @@ def criar_materia_proposicao(proposicao): tipo_materia = TipoMateriaLegislativa.objects.get( descricao=proposicao.tipo.descricao) numero = MateriaLegislativa.objects.filter( - ano=datetime.now().year).order_by('numero').last().numero + 1 + ano=timezone.now().year).order_by('numero').last().numero + 1 regime = RegimeTramitacao.objects.get(descricao='Normal') return MateriaLegislativa.objects.create( tipo=tipo_materia, - ano=datetime.now().year, + ano=timezone.now().year, numero=numero, - data_apresentacao=datetime.now(), + data_apresentacao=timezone.now(), regime_tramitacao=regime, em_tramitacao=True, ementa=proposicao.descricao, @@ -473,7 +472,7 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): app_label = sapl.protocoloadm.apps.AppConfig.label template_name = "materia/confirmar_proposicao.html" model = Proposicao - form_class = ConfirmarProposicaoForm + form_class = ConfirmarProposicaoForm, DevolverProposicaoForm def get_success_url(self): msgs = self.object.results['messages'] @@ -486,7 +485,8 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): def get_object(self, queryset=None): try: - """Não deve haver acesso na rotina de confirmação a proposições: + """ + Não deve haver acesso na rotina de confirmação a proposições: já recebidas -> data_recebimento != None não enviadas -> data_envio == None """ @@ -494,11 +494,9 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): data_envio__isnull=False, data_recebimento__isnull=True) self.object = None - # FIXME implementar hash para texto eletrônico if proposicao.texto_articulado.exists(): ta = proposicao.texto_articulado.first() - # FIXME hash para textos articulados hasher = 'P' + ta.hash() + '/' + str(proposicao.id) else: hasher = gerar_hash_arquivo( @@ -520,10 +518,25 @@ class ConfirmarProposicao(PermissionRequiredForAppCrudMixin, UpdateView): context['subnav_template_name'] = '' return context + def get_form(self, form_class=None): + if form_class is None: + form_class = self.get_form_class() + + if self.request.POST: + if 'justificativa_devolucao' in self.request.POST: + return form_class[1](**self.get_form_kwargs()) + else: + return form_class[0](**self.get_form_kwargs()) + else: + forms = [] + for form in form_class: + forms.append(form(**self.get_form_kwargs())) + return forms + class UnidadeTramitacaoCrud(CrudAux): model = UnidadeTramitacao - help_path = 'unidade_tramitacao' + help_topic = 'unidade_tramitacao' class BaseMixin(Crud.BaseMixin): list_field_names = ['comissao', 'orgao', 'parlamentar'] @@ -549,7 +562,7 @@ class UnidadeTramitacaoCrud(CrudAux): class ProposicaoCrud(Crud): model = Proposicao - help_path = '' + help_topic = 'proposicao' container_field = 'autor__user' class BaseMixin(Crud.BaseMixin): @@ -614,7 +627,7 @@ class ProposicaoCrud(Crud): 'Texto associado.') else: p.data_devolucao = None - p.data_envio = datetime.now() + p.data_envio = timezone.now() p.save() if p.texto_articulado.exists(): @@ -836,7 +849,7 @@ class ReciboProposicaoView(TemplateView): class RelatoriaCrud(MasterDetailCrud): model = Relatoria parent_field = 'materia' - help_path = '' + help_topic = 'tramitacao_relatoria' public = [RP_LIST, RP_DETAIL] class CreateView(MasterDetailCrud.CreateView): @@ -898,11 +911,9 @@ class RelatoriaCrud(MasterDetailCrud): participacao = Participacao.objects.filter( composicao=composicao) - parlamentares = [] - for p in participacao: - if p.titular: - parlamentares.append( - [p.parlamentar.id, p.parlamentar.nome_parlamentar]) + parlamentares = [[p.parlamentar.id, p.parlamentar.nome_parlamentar] for + p in participacao if p.titular] + context['form'].fields['parlamentar'].choices = parlamentares return context @@ -911,7 +922,7 @@ class RelatoriaCrud(MasterDetailCrud): class TramitacaoCrud(MasterDetailCrud): model = Tramitacao parent_field = 'materia' - help_path = '' + help_topic = 'tramitacao_relatoria' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -930,27 +941,30 @@ class TramitacaoCrud(MasterDetailCrud): def get_initial(self): local = MateriaLegislativa.objects.get( pk=self.kwargs['pk']).tramitacao_set.order_by( - '-data_tramitacao').first() + '-data_tramitacao', + '-id').first() if local: self.initial['unidade_tramitacao_local' ] = local.unidade_tramitacao_destino.pk else: self.initial['unidade_tramitacao_local'] = '' - self.initial['data_tramitacao'] = datetime.now() + self.initial['data_tramitacao'] = timezone.now().date() return self.initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - primeira_tramitacao = not(Tramitacao.objects.filter( - materia_id=int(kwargs['root_pk'])).exists()) + ultima_tramitacao = Tramitacao.objects.filter( + materia_id=self.kwargs['pk']).order_by( + '-data_tramitacao', + '-id').first() - # Se não for a primeira tramitação daquela matéria, o campo - # não pode ser modificado - if not primeira_tramitacao: + if ultima_tramitacao: context['form'].fields[ - 'unidade_tramitacao_local'].widget.attrs['disabled'] = True + 'unidade_tramitacao_local'].choices = [ + (ultima_tramitacao.unidade_tramitacao_destino.pk, + ultima_tramitacao.unidade_tramitacao_destino)] return context def form_valid(self, form): @@ -1009,7 +1023,8 @@ class TramitacaoCrud(MasterDetailCrud): def get_queryset(self): qs = super(MasterDetailCrud.ListView, self).get_queryset() kwargs = {self.crud.parent_field: self.kwargs['pk']} - return qs.filter(**kwargs).order_by('-data_tramitacao', '-id') + return qs.filter(**kwargs).order_by('-data_tramitacao', + '-id') class DeleteView(MasterDetailCrud.DeleteView): @@ -1019,7 +1034,11 @@ class TramitacaoCrud(MasterDetailCrud): url = reverse('sapl.materia:tramitacao_list', kwargs={'pk': tramitacao.materia.id}) - if tramitacao.pk != materia.tramitacao_set.last().pk: + ultima_tramitacao = materia.tramitacao_set.order_by( + '-data_tramitacao', + '-id').first() + + if tramitacao.pk != ultima_tramitacao.pk: msg = _('Somente a última tramitação pode ser deletada!') messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(url) @@ -1050,7 +1069,7 @@ def montar_helper_documento_acessorio(self): class DocumentoAcessorioCrud(MasterDetailCrud): model = DocumentoAcessorio parent_field = 'materia' - help_path = '' + help_topic = 'despacho_autoria' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -1063,7 +1082,7 @@ class DocumentoAcessorioCrud(MasterDetailCrud): super(MasterDetailCrud.CreateView, self).__init__(**kwargs) def get_initial(self): - self.initial['data'] = datetime.now().date() + self.initial['data'] = timezone.now().date() return self.initial @@ -1086,7 +1105,7 @@ class DocumentoAcessorioCrud(MasterDetailCrud): class AutoriaCrud(MasterDetailCrud): model = Autoria parent_field = 'materia' - help_path = '' + help_topic = 'despacho_autoria' public = [RP_LIST, RP_DETAIL] list_field_names = ['autor', 'autor__tipo__descricao', 'primeiro_autor'] @@ -1161,7 +1180,7 @@ class AutoriaMultiCreateView(PermissionRequiredForAppCrudMixin, FormView): class DespachoInicialCrud(MasterDetailCrud): model = DespachoInicial parent_field = 'materia' - help_path = '' + help_topic = 'despacho_autoria' public = [RP_LIST, RP_DETAIL] class CreateView(MasterDetailCrud.CreateView): @@ -1174,7 +1193,7 @@ class DespachoInicialCrud(MasterDetailCrud): class LegislacaoCitadaCrud(MasterDetailCrud): model = LegislacaoCitada parent_field = 'materia' - help_path = '' + help_topic = 'legislacao_cita_matanexada' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -1185,55 +1204,6 @@ class LegislacaoCitadaCrud(MasterDetailCrud): return reverse('%s:%s' % (namespace, self.url_name(suffix)), args=args) - def has_permission(self): - perms = self.get_permission_required() - # Torna a view pública se não possuir conteudo - # no atributo permission_required - return self.request.user.has_module_perms('materia')\ - if len(perms) else True - - def permission(self, rad): - return '%s%s%s' % ('norma' if rad.endswith('_') else '', - rad, - self.model_name if rad.endswith('_') else '') - - @property - def detail_create_url(self): - obj = self.crud if hasattr(self, 'crud') else self - if self.request.user.has_module_perms('materia'): - parent_field = obj.parent_field.split('__')[0] - parent_object = getattr(self.object, parent_field) - - root_pk = parent_object.pk - - return self.resolve_url(ACTION_CREATE, args=(root_pk,)) - return '' - - @property - def list_url(self): - return self.resolve_url(ACTION_LIST, args=(self.kwargs['pk'],))\ - if self.request.user.has_module_perms('materia') else '' - - @property - def create_url(self): - return self.resolve_url(ACTION_CREATE, args=(self.kwargs['pk'],))\ - if self.request.user.has_module_perms('materia') else '' - - @property - def detail_url(self): - return self.resolve_url(ACTION_DETAIL, args=(self.object.id,))\ - if self.request.user.has_module_perms('materia') else '' - - @property - def update_url(self): - return self.resolve_url(ACTION_UPDATE, args=(self.kwargs['pk'],))\ - if self.request.user.has_module_perms('materia') else '' - - @property - def delete_url(self): - return self.resolve_url(ACTION_DELETE, args=(self.object.id,))\ - if self.request.user.has_module_perms('materia') else '' - class CreateView(MasterDetailCrud.CreateView): form_class = LegislacaoCitadaForm @@ -1259,14 +1229,14 @@ class LegislacaoCitadaCrud(MasterDetailCrud): class NumeracaoCrud(MasterDetailCrud): model = Numeracao parent_field = 'materia' - help_path = '' + help_topic = 'numeracao_docsacess' public = [RP_LIST, RP_DETAIL] class AnexadaCrud(MasterDetailCrud): model = Anexada parent_field = 'materia_principal' - help_path = '' + help_topic = 'materia_anexada' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -1294,7 +1264,7 @@ class AnexadaCrud(MasterDetailCrud): class MateriaAssuntoCrud(MasterDetailCrud): model = MateriaAssunto parent_field = 'materia' - help_path = '' + help_topic = '' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -1318,7 +1288,7 @@ class MateriaAssuntoCrud(MasterDetailCrud): class MateriaLegislativaCrud(Crud): model = MateriaLegislativa - help_path = 'materia_legislativa' + help_topic = 'materia_legislativa' public = [RP_LIST, RP_DETAIL] class BaseMixin(Crud.BaseMixin): @@ -1507,6 +1477,8 @@ class MateriaLegislativaPesquisaView(FilterView): context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + context['show_results'] = show_results_filter_set(qr) + return context @@ -1608,6 +1580,9 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): context['object_list'] = context['object_list'].order_by( 'ano', 'numero') context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + return context def post(self, request, *args, **kwargs): @@ -1620,13 +1595,16 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): tipo = TipoDocumento.objects.get(descricao=request.POST['tipo']) + tz = timezone.get_current_timezone() + for materia_id in marcadas: doc = DocumentoAcessorio() doc.materia_id = materia_id doc.tipo = tipo doc.arquivo = request.FILES['arquivo'] doc.nome = request.POST['nome'] - doc.data = datetime.strptime(request.POST['data'], "%d/%m/%Y") + doc.data = tz.localize(datetime.strptime( + request.POST['data'], "%d/%m/%Y")) doc.autor = request.POST['autor'] doc.ementa = request.POST['ementa'] doc.save() @@ -1675,25 +1653,30 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): 'ano', 'numero') context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + + context['show_results'] = show_results_filter_set(qr) + return context def post(self, request, *args, **kwargs): marcadas = request.POST.getlist('materia_id') + tz = timezone.get_current_timezone() + if len(marcadas) == 0: msg = _('Nenhuma máteria foi selecionada.') messages.add_message(request, messages.ERROR, msg) return self.get(request, self.kwargs) if request.POST['data_encaminhamento']: - data_encaminhamento = datetime.strptime( - request.POST['data_encaminhamento'], "%d/%m/%Y") + data_encaminhamento = tz.localize(datetime.strptime( + request.POST['data_encaminhamento'], "%d/%m/%Y")) else: data_encaminhamento = None if request.POST['data_fim_prazo']: - data_fim_prazo = datetime.strptime( - request.POST['data_fim_prazo'], "%d/%m/%Y") + data_fim_prazo = tz.localize(datetime.strptime( + request.POST['data_fim_prazo'], "%d/%m/%Y")) else: data_fim_prazo = None @@ -1704,8 +1687,8 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): for materia_id in marcadas: t = Tramitacao( materia_id=materia_id, - data_tramitacao=datetime.strptime( - request.POST['data_tramitacao'], "%d/%m/%Y"), + data_tramitacao=tz.localize(datetime.strptime( + request.POST['data_tramitacao'], "%d/%m/%Y")), data_encaminhamento=data_encaminhamento, data_fim_prazo=data_fim_prazo, unidade_tramitacao_local_id=request.POST[ @@ -1718,6 +1701,12 @@ class PrimeiraTramitacaoEmLoteView(PermissionRequiredMixin, FilterView): texto=request.POST['texto'] ) t.save() + + if request.POST['turno'] == 'F': + for materia in MateriaLegislativa.objects.filter(id__in=marcadas): + materia.em_tramitacao = False + materia.save() + msg = _('Tramitação completa.') messages.add_message(request, messages.SUCCESS, msg) return self.get(request, self.kwargs) @@ -1751,8 +1740,9 @@ class ImpressosView(PermissionRequiredMixin, TemplateView): template_name = 'materia/impressos/impressos.html' permission_required = ('materia.can_access_impressos', ) -def gerar_pdf_impressos(request, context): - template = loader.get_template('materia/impressos/pdf.html') + +def gerar_pdf_impressos(request, context, template_name): + template = loader.get_template(template_name) html = template.render(RequestContext(request, context)) response = HttpResponse(content_type="application/pdf") weasyprint.HTML( @@ -1796,6 +1786,82 @@ class EtiquetaPesquisaView(PermissionRequiredMixin, FormView): context['materias'] = materias - return gerar_pdf_impressos(self.request, context) + return gerar_pdf_impressos(self.request, context, + 'materia/impressos/etiqueta_pdf.html') + + +class FichaPesquisaView(PermissionRequiredMixin, FormView): + form_class = FichaPesquisaForm + template_name = 'materia/impressos/ficha.html' + permission_required = ('materia.can_access_impressos', ) + + def form_valid(self, form): + tipo_materia = form.data['tipo_materia'] + data_inicial = form.data['data_inicial'] + data_final = form.data['data_final'] + + url = reverse('sapl.materia:impressos_ficha_seleciona') + url = url + '?tipo=%s&data_inicial=%s&data_final=%s' % ( + tipo_materia, data_inicial, data_final) + + return HttpResponseRedirect(url) + + +class FichaSelecionaView(PermissionRequiredMixin, FormView): + form_class = FichaSelecionaForm + template_name = 'materia/impressos/ficha_seleciona.html' + permission_required = ('materia.can_access_impressos', ) + + def get_context_data(self, **kwargs): + if ('tipo' not in self.request.GET or + 'data_inicial' not in self.request.GET or + 'data_final' not in self.request.GET): + return HttpResponseRedirect(reverse( + 'sapl.materia:impressos_ficha_pesquisa')) + + context = super(FichaSelecionaView, self).get_context_data( + **kwargs) + + tipo = self.request.GET['tipo'] + data_inicial = datetime.strptime( + self.request.GET['data_inicial'], "%d/%m/%Y").date() + data_final = datetime.strptime( + self.request.GET['data_final'], "%d/%m/%Y").date() + + materia_list = MateriaLegislativa.objects.filter( + tipo=tipo, + data_apresentacao__range=(data_inicial, data_final)) + context['quantidade'] = len(materia_list) + materia_list = materia_list[:20] + + context['form'].fields['materia'].choices = [ + (m.id, str(m)) for m in materia_list] + + if context['quantidade'] > 20: + messages.info(self.request, _('Sua pesquisa retornou mais do que ' + '20 impressos. Por questões de ' + 'performance, foram retornados ' + 'apenas os 20 primeiros. Caso ' + 'queira outros, tente fazer uma ' + 'pesquisa mais específica')) + + return context + + def form_valid(self, form): + context = {} + + try: + materia = MateriaLegislativa.objects.get( + id=form.data['materia']) + except ObjectDoesNotExist: + mensagem = _('Esta Máteria não existe!') + self.messages.add_message(self.request, messages.INFO, mensagem) + + return self.render_to_response(context) + context['materia'] = materia + context['despachos'] = materia.despachoinicial_set.all().values_list( + 'comissao__nome', flat=True) + return gerar_pdf_impressos(self.request, context, + 'materia/impressos/ficha_pdf.html') diff --git a/sapl/norma/fixtures/pre_popula_tipo_vinculo_norma.json b/sapl/norma/fixtures/pre_popula_tipo_vinculo_norma.json new file mode 100644 index 000000000..50c6ad647 --- /dev/null +++ b/sapl/norma/fixtures/pre_popula_tipo_vinculo_norma.json @@ -0,0 +1,119 @@ +[ + { + "fields": { + "descricao_ativa": "Altera o(a)", + "descricao_passiva": "Alterado(a) pelo(a)", + "sigla": "A" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "1" + }, + { + "fields": { + "descricao_ativa": "Revoga integralmente o(a)", + "descricao_passiva": "Revogado(a) integralmente pelo(a)", + "sigla": "R" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "2" + }, + { + "fields": { + "descricao_ativa": "Revoga parcialmente o(a)", + "descricao_passiva": "Revogado(a) parcialmente pelo(a)", + "sigla": "P" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "3" + }, + { + "fields": { + "descricao_ativa": "Revoga integralmente por consolida\u00e7\u00e3o", + "descricao_passiva": "Revogado(a) integralmente por consolida\u00e7\u00e3o", + "sigla": "T" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "4" + }, + { + "fields": { + "descricao_ativa": "Norma correlata", + "descricao_passiva": "Norma correlata", + "sigla": "C" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "5" + }, + { + "fields": { + "descricao_ativa": "Ressalva o(a)", + "descricao_passiva": "Ressalvada pelo(a)", + "sigla": "S" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "6" + }, + { + "fields": { + "descricao_ativa": "Reedita o(a)", + "descricao_passiva": "Reeditada pelo(a)", + "sigla": "E" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "7" + }, + { + "fields": { + "descricao_ativa": "Reedita com altera\u00e7\u00e3o o(a)", + "descricao_passiva": "Reeditada com altera\u00e7\u00e3o pelo(a)", + "sigla": "I" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "8" + }, + { + "fields": { + "descricao_ativa": "Regulamenta o(a)", + "descricao_passiva": "Regulamentada pelo(a)", + "sigla": "G" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "9" + }, + { + "fields": { + "descricao_ativa": "Suspende parcialmente o(a)", + "descricao_passiva": "Suspenso(a) parcialmente pelo(a)", + "sigla": "K" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "10" + }, + { + "fields": { + "descricao_ativa": "Suspende integralmente o(a)", + "descricao_passiva": "Suspenso(a) integralmente pelo(a)", + "sigla": "L" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "11" + }, + { + "fields": { + "descricao_ativa": "Julga integralmente inconstitucional", + "descricao_passiva": "Julgada integralmente inconstitucional", + "sigla": "N" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "12" + }, + { + "fields": { + "descricao_ativa": "Julga parcialmente inconstitucional", + "descricao_passiva": "Julgada parcialmente inconstitucional", + "sigla": "O" + }, + "model": "norma.TipoVinculoNormaJuridica", + "pk": "13" + } +] \ No newline at end of file diff --git a/sapl/norma/forms.py b/sapl/norma/forms.py index 5149ac926..e72b41f26 100644 --- a/sapl/norma/forms.py +++ b/sapl/norma/forms.py @@ -1,4 +1,3 @@ -from datetime import datetime import django_filters from crispy_forms.helper import FormHelper @@ -7,6 +6,7 @@ from django import forms from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db import models from django.forms import ModelForm, widgets +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from sapl.crispy_layout_mixin import form_actions, to_row @@ -72,7 +72,7 @@ class NormaFilterSet(django_filters.FilterSet): self.form.helper.layout = Layout( Fieldset(_('Pesquisa de Norma'), row1, row2, row3, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @@ -131,11 +131,13 @@ class NormaJuridicaForm(ModelForm): tipo_id=cleaned_data['tipo_materia'], numero=cleaned_data['numero_materia'], ano=cleaned_data['ano_materia']) + except ObjectDoesNotExist: raise forms.ValidationError( - _("Matéria %s/%s é inexistente." % ( + _("Matéria Legislativa %s/%s (%s) é inexistente." % ( self.cleaned_data['numero_materia'], - self.cleaned_data['ano_materia']))) + self.cleaned_data['ano_materia'], + cleaned_data['tipo_materia'].descricao))) else: cleaned_data['materia'] = materia @@ -154,7 +156,7 @@ class NormaJuridicaForm(ModelForm): def save(self, commit=False): norma = self.instance - norma.timestamp = datetime.now() + norma.timestamp = timezone.now() norma.materia = self.cleaned_data['materia'] norma = super(NormaJuridicaForm, self).save(commit=True) return norma @@ -206,3 +208,61 @@ class NormaRelacionadaForm(ModelForm): relacionada.norma_relacionada = self.cleaned_data['norma_relacionada'] relacionada.save() return relacionada + +class NormaPesquisaSimplesForm(forms.Form): + tipo_norma = forms.ModelChoiceField( + label=TipoNormaJuridica._meta.verbose_name, + queryset=TipoNormaJuridica.objects.all(), + required=False, + empty_label='Selecione') + + data_inicial = forms.DateField( + label='Data Inicial', + required=False, + widget=forms.DateInput(format='%d/%m/%Y') + ) + + data_final = forms.DateField( + label='Data Final', + required=False, + widget=forms.DateInput(format='%d/%m/%Y') + ) + + def __init__(self, *args, **kwargs): + super(NormaPesquisaSimplesForm, self).__init__(*args, **kwargs) + + row1 = to_row( + [('tipo_norma', 6), + ('data_inicial', 3), + ('data_final', 3)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + ('Índice de Normas'), + row1, + form_actions(label='Pesquisar') + ) + ) + + def clean(self): + super(NormaPesquisaSimplesForm, self).clean() + cleaned_data = self.cleaned_data + + data_inicial = cleaned_data['data_inicial'] + data_final = cleaned_data['data_final'] + + if (data_inicial and data_final and + data_inicial > data_final): + raise ValidationError(_( + 'A Data Final não pode ser menor que a Data Inicial')) + else: + condicao1 = data_inicial and not data_final + condicao2 = not data_inicial and data_final + if condicao1 or condicao2: + raise ValidationError(_('Caso pesquise por data, os campos de Data Inicial e ' + + 'Data Final devem ser preenchidos obrigatoriamente')) + + + return cleaned_data + diff --git a/sapl/norma/migrations/0008_normajuridica_popula_tipo_vinculo_norma.py b/sapl/norma/migrations/0008_normajuridica_popula_tipo_vinculo_norma.py new file mode 100644 index 000000000..3bfb90132 --- /dev/null +++ b/sapl/norma/migrations/0008_normajuridica_popula_tipo_vinculo_norma.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +from django.core.management import call_command +from django.db import migrations + + +def gera_tipo_vinculo(apps, schema_editor): + TipoVinculoNormaJuridica = apps.get_model("norma", "TipoVinculoNormaJuridica") + + db_alias = schema_editor.connection.alias + tipo_vinculos = TipoVinculoNormaJuridica.objects.all().exists() + + if tipo_vinculos: + # Caso haja algum TipoVinculoNormaJuridica cadastrado na base de dados, + # a migração não deve ser carregada para evitar duplicações de dados. + print("Carga de {} não efetuada. Já Existem {} cadastrados...".format( + TipoVinculoNormaJuridica._meta.verbose_name, + TipoVinculoNormaJuridica._meta.verbose_name_plural + ) + ) + else: + fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) + # pega tipo_vinculo_norma_juridica listados em fixtures/pre_popula_tipo_vinculo_norma.json + fixture_filename = 'pre_popula_tipo_vinculo_norma.json' + fixture_file = os.path.join(fixture_dir, fixture_filename) + call_command('loaddata', fixture_file) + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0007_auto_20170904_1708'), + ] + + operations = [ + migrations.RunPython(gera_tipo_vinculo), + ] \ No newline at end of file diff --git a/sapl/norma/migrations/0009_auto_20171113_1339.py b/sapl/norma/migrations/0009_auto_20171113_1339.py new file mode 100644 index 000000000..672d87c94 --- /dev/null +++ b/sapl/norma/migrations/0009_auto_20171113_1339.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-11-13 15:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('norma', '0008_normajuridica_popula_tipo_vinculo_norma'), + ] + + operations = [ + migrations.AlterField( + model_name='legislacaocitada', + name='materia', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='materia.MateriaLegislativa'), + ), + migrations.AlterField( + model_name='legislacaocitada', + name='norma', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='norma.NormaJuridica'), + ), + ] diff --git a/sapl/norma/models.py b/sapl/norma/models.py index d00be8ce7..0836ed506 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -174,8 +174,8 @@ class NormaJuridica(models.Model): @reversion.register() class LegislacaoCitada(models.Model): - materia = models.ForeignKey(MateriaLegislativa, on_delete=models.PROTECT) - norma = models.ForeignKey(NormaJuridica, on_delete=models.PROTECT) + materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE) + norma = models.ForeignKey(NormaJuridica, on_delete=models.CASCADE) disposicoes = models.CharField( max_length=15, blank=True, verbose_name=_('Disposição')) parte = models.CharField( diff --git a/sapl/norma/tests/test_norma.py b/sapl/norma/tests/test_norma.py index fa8214e58..1857f0966 100644 --- a/sapl/norma/tests/test_norma.py +++ b/sapl/norma/tests/test_norma.py @@ -1,13 +1,11 @@ import pytest - from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ - from model_mommy import mommy from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.norma.forms import NormaJuridicaForm, NormaRelacionadaForm -from sapl.norma.models import (NormaJuridica, TipoNormaJuridica) +from sapl.norma.models import NormaJuridica, TipoNormaJuridica @pytest.mark.django_db(transaction=False) @@ -78,7 +76,7 @@ def test_norma_form_invalida(): def test_norma_juridica_materia_inexistente(): tipo = mommy.make(TipoNormaJuridica) - tipo_materia = mommy.make(TipoMateriaLegislativa) + tipo_materia = mommy.make(TipoMateriaLegislativa, descricao='VETO') form = NormaJuridicaForm(data={'tipo': str(tipo.pk), 'numero': '1', @@ -93,17 +91,17 @@ def test_norma_juridica_materia_inexistente(): assert not form.is_valid() - assert form.errors['__all__'] == [_("Matéria 2/2017 é inexistente.")] + assert form.errors['__all__'] == [_("Matéria Legislativa 2/2017 (VETO) é inexistente.")] @pytest.mark.django_db(transaction=False) def test_norma_juridica_materia_existente(): tipo = mommy.make(TipoNormaJuridica) tipo_materia = mommy.make(TipoMateriaLegislativa) - materia = mommy.make(MateriaLegislativa, - numero=2, - ano=2017, - tipo=tipo_materia) + mommy.make(MateriaLegislativa, + numero=2, + ano=2017, + tipo=tipo_materia) form = NormaJuridicaForm(data={'tipo': str(tipo.pk), 'numero': '1', diff --git a/sapl/norma/views.py b/sapl/norma/views.py index 28835a2a0..7ff408983 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -1,17 +1,26 @@ -from datetime import datetime + from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.http import JsonResponse +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import RedirectView from django_filters.views import FilterView +from django.contrib.auth.mixins import PermissionRequiredMixin + +from django.http import HttpResponse, JsonResponse +from django.views.generic.edit import FormView +from django.views.generic import CreateView, ListView, TemplateView, UpdateView +from django.template import RequestContext, loader +import weasyprint from sapl.base.models import AppConfig from sapl.compilacao.views import IntegracaoTaView from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, MasterDetailCrud, make_pagination) +from sapl.utils import show_results_filter_set -from .forms import NormaFilterSet, NormaJuridicaForm, NormaRelacionadaForm +from .forms import NormaFilterSet, NormaJuridicaForm, NormaRelacionadaForm, NormaPesquisaSimplesForm from .models import (AssuntoNorma, NormaJuridica, NormaRelacionada, TipoNormaJuridica, TipoVinculoNormaJuridica) @@ -31,7 +40,7 @@ TipoVinculoNormaJuridicaCrud = CrudAux.build( class NormaRelacionadaCrud(MasterDetailCrud): model = NormaRelacionada parent_field = 'norma_principal' - help_path = '' + help_topic = 'norma_juridica' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -62,6 +71,13 @@ class NormaPesquisaView(FilterView): filterset_class = NormaFilterSet paginate_by = 10 + def get_queryset(self): + qs = super().get_queryset() + + qs.select_related('tipo', 'materia') + + return qs + def get_context_data(self, **kwargs): context = super(NormaPesquisaView, self).get_context_data(**kwargs) @@ -80,6 +96,8 @@ class NormaPesquisaView(FilterView): context['filter_url'] = ('&' + qr.urlencode()) if len(qr) > 0 else '' + context['show_results'] = show_results_filter_set(qr) + return context @@ -112,7 +130,7 @@ class NormaTaView(IntegracaoTaView): class NormaCrud(Crud): model = NormaJuridica - help_path = 'norma_juridica' + help_topic = 'norma_juridica' public = [RP_LIST, RP_DETAIL] class BaseMixin(Crud.BaseMixin): @@ -146,7 +164,6 @@ class NormaCrud(Crud): def layout_key(self): return 'NormaJuridicaCreate' - class ListView(Crud.ListView, RedirectView): def get_redirect_url(self, *args, **kwargs): @@ -194,7 +211,7 @@ def recuperar_numero_norma(request): ano = request.GET.get('ano', '') param = {'tipo': tipo} - param['ano'] = ano if ano else datetime.now().year + param['ano'] = ano if ano else timezone.now().year norma = NormaJuridica.objects.filter(**param).order_by( 'tipo', 'ano', 'numero').values_list('numero', 'ano').last() @@ -206,3 +223,48 @@ def recuperar_numero_norma(request): {'numero': 1, 'ano': ano}) return response + + +class ImpressosView(PermissionRequiredMixin, TemplateView): + template_name = 'materia/impressos/impressos.html' + permission_required = ('materia.can_access_impressos', ) + + +def gerar_pdf_impressos(request, context, template_name): + template = loader.get_template(template_name) + html = template.render(RequestContext(request, context)) + response = HttpResponse(content_type="application/pdf") + weasyprint.HTML( + string=html, + base_url=request.build_absolute_uri()).write_pdf( + response) + + return response + +class NormaPesquisaSimplesView(PermissionRequiredMixin, FormView): + form_class = NormaPesquisaSimplesForm + template_name = 'materia/impressos/norma.html' + permission_required = ('materia.can_access_impressos', ) + + + def form_valid(self, form): + normas = NormaJuridica.objects.all().order_by( + '-numero') + template_norma = 'materia/impressos/normas_pdf.html' + + if form.cleaned_data['tipo_norma']: + normas = normas.filter(tipo=form.cleaned_data['tipo_norma']) + + if form.cleaned_data['data_inicial']: + normas = normas.filter( + data__gte=form.cleaned_data['data_inicial'], + data__lte=form.cleaned_data['data_final']) + + qtd_resultados = len(normas) + if qtd_resultados > 2000: + normas = normas[:2000] + + context = {'quantidade': qtd_resultados, + 'normas': normas} + + return gerar_pdf_impressos(self.request, context, template_norma) diff --git a/sapl/painel/urls.py b/sapl/painel/urls.py index 66ef92ac1..0795d0a35 100644 --- a/sapl/painel/urls.py +++ b/sapl/painel/urls.py @@ -3,7 +3,7 @@ from django.conf.urls import url from .apps import AppConfig from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view, painel_parlamentar_view, painel_view, painel_votacao_view, - votante_view) + switch_painel, verifica_painel, votante_view) app_name = AppConfig.name @@ -14,7 +14,11 @@ urlpatterns = [ url(r'^painel/mensagem$', painel_mensagem_view, name="painel_mensagem"), url(r'^painel/parlamentar$', painel_parlamentar_view, name='painel_parlamentar'), + url(r'^painel/switch-painel$', switch_painel, + name="switch_painel"), url(r'^painel/votacao$', painel_votacao_view, name='painel_votacao'), + url(r'^painel/verifica-painel$', verifica_painel, + name="verifica_painel"), url(r'^painel/cronometro$', cronometro_painel, name='cronometro_painel'), # url(r'^painel/cronometro$', include(CronometroPainelCrud.get_urls())), diff --git a/sapl/painel/views.py b/sapl/painel/views.py index 3183c61db..a1faee91c 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -1,4 +1,4 @@ - +import json from django.contrib import messages from django.contrib.auth.decorators import user_passes_test @@ -8,15 +8,19 @@ from django.db.models import Q from django.http import HttpResponse, JsonResponse from django.http.response import Http404, HttpResponseRedirect from django.shortcuts import render +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from sapl.base.models import AppConfig as ConfiguracoesAplicacao +from sapl.base.models import CasaLegislativa from sapl.crud.base import Crud from sapl.painel.apps import AppConfig -from sapl.parlamentares.models import Filiacao, Votante -from sapl.sessao.models import (ExpedienteMateria, OrdemDia, PresencaOrdemDia, +from sapl.parlamentares.models import Legislatura, Parlamentar, Votante +from sapl.sessao.models import (ExpedienteMateria, OrdemDia, OradorExpediente, + PresencaOrdemDia, RegistroVotacao, SessaoPlenaria, SessaoPlenariaPresenca, VotoParlamentar) -from sapl.utils import get_client_ip +from sapl.utils import filiacao_data, get_client_ip, sort_lista_chave from .models import Cronometro @@ -224,6 +228,28 @@ def painel_view(request, pk): return render(request, 'painel/index.html', context) +@user_passes_test(check_permission) +def switch_painel(request): + sessao = SessaoPlenaria.objects.get(id=request.POST['pk_sessao']) + switch = json.loads(request.POST['aberto']) + + if switch: + sessao.painel_aberto = True + else: + sessao.painel_aberto = False + + sessao.save() + return JsonResponse({}) + + +@user_passes_test(check_permission) +def verifica_painel(request): + sessao = SessaoPlenaria.objects.get(id=request.GET['pk_sessao']) + status = sessao.painel_aberto + resposta = JsonResponse(dict(status=status)) + return resposta + + @user_passes_test(check_permission) def painel_mensagem_view(request): return render(request, 'painel/mensagem.html') @@ -252,8 +278,6 @@ def get_cronometro_status(request, name): cronometro = '' return cronometro -# ##############################ORDEM DO DIA################################## - def get_materia_aberta(pk): return OrdemDia.objects.filter( @@ -261,254 +285,155 @@ def get_materia_aberta(pk): def get_presentes(pk, response, materia): - filiacao = Filiacao.objects.filter( - data_desfiliacao__isnull=True, parlamentar__ativo=True) - parlamentar_partido = {} - for f in filiacao: - parlamentar_partido[ - f.parlamentar.nome_parlamentar] = f.partido.sigla - - sessao_plenaria_presenca = SessaoPlenariaPresenca.objects.filter( - sessao_plenaria_id=pk) - presentes_sessao_plenaria = [ - p.parlamentar.nome_parlamentar for p in sessao_plenaria_presenca] - num_presentes_sessao_plen = len(presentes_sessao_plenaria) - - presenca_ordem_dia = PresencaOrdemDia.objects.filter( - sessao_plenaria_id=pk) - presentes_ordem_dia = [] - for p in presenca_ordem_dia: - nome_parlamentar = p.parlamentar.nome_parlamentar - - try: - parlamentar_partido[nome_parlamentar] - except KeyError: - presentes_ordem_dia.append( - {'id': p.id, - 'nome': nome_parlamentar, - 'partido': str(_('Sem Registro')), - }) - else: - presentes_ordem_dia.append( - {'id': p.id, - 'nome': nome_parlamentar, - 'partido': parlamentar_partido[nome_parlamentar], - }) - num_presentes_ordem_dia = len(presentes_ordem_dia) - - if materia.tipo_votacao == 1: - tipo_votacao = str(_('Simbólica')) - response = get_votos(response, materia) - elif materia.tipo_votacao == 2: - tipo_votacao = 'Nominal' - response = get_votos_nominal(response, materia) - elif materia.tipo_votacao == 3: - tipo_votacao = 'Secreta' - response = get_votos(response, materia) - - response.update({ - 'presentes_ordem_dia': presentes_ordem_dia, - 'num_presentes_ordem_dia': num_presentes_ordem_dia, - 'presentes_sessao_plenaria': presentes_sessao_plenaria, - 'num_presentes_sessao_plenaria': num_presentes_sessao_plen, - 'status_painel': 'ABERTO', - 'msg_painel': str(_('Votação aberta!')), - 'tipo_resultado': materia.resultado, - 'tipo_votacao': tipo_votacao, - 'observacao_materia': materia.observacao, - 'materia_legislativa_texto': str(materia.materia)}) - - return response + if type(materia) == OrdemDia: + presentes = PresencaOrdemDia.objects.filter( + sessao_plenaria_id=pk) + else: + presentes = SessaoPlenariaPresenca.objects.filter( + sessao_plenaria_id=pk) + sessao = SessaoPlenaria.objects.get(id=pk) + num_presentes = len(presentes) + data_sessao = sessao.data_inicio + oradores = OradorExpediente.objects.filter( + sessao_plenaria_id=pk).order_by('numero_ordem') + + oradores_list = [] + for o in oradores: + + oradores_list.append( + { + 'nome': o.parlamentar.nome_parlamentar, + 'numero': o.numero_ordem + }) + + presentes_list = [] + for p in presentes: + now_year = timezone.now().year + # Recupera a legislatura vigente + legislatura = Legislatura.objects.get(data_inicio__year__lte = now_year, + data_fim__year__gte = now_year) + # Recupera os mandatos daquele parlamentar + mandatos = p.parlamentar.mandato_set.filter(legislatura=legislatura) + + if p.parlamentar.ativo and mandatos: + filiacao = filiacao_data(p.parlamentar, data_sessao, data_sessao) + if not filiacao: + partido = 'Sem Registro' + else: + partido = filiacao -# ########################EXPEDIENTE############################################ + presentes_list.append( + {'id': p.id, + 'parlamentar_id': p.parlamentar.id, + 'nome': p.parlamentar.nome_parlamentar, + 'partido': partido, + 'voto': '' + }) + elif not p.parlamentar.ativo or not mandatos: + num_presentes += -1 -def get_materia_expediente_aberta(pk): - return ExpedienteMateria.objects.filter( - sessao_plenaria_id=pk, votacao_aberta=True).last() + if materia: + if materia.tipo_votacao == 1: + tipo_votacao = 'Simbólica' + elif materia.tipo_votacao == 2: + tipo_votacao = 'Nominal' + elif materia.tipo_votacao == 3: + tipo_votacao = 'Secreta' + response.update({ + 'tipo_resultado': materia.resultado, + 'observacao_materia': materia.observacao, + 'tipo_votacao': tipo_votacao, + 'materia_legislativa_texto': str(materia.materia), + 'oradores': oradores_list + }) -def get_presentes_expediente(pk, response, materia): - filiacao = Filiacao.objects.filter( - data_desfiliacao__isnull=True, parlamentar__ativo=True) - parlamentar_partido = {} - for f in filiacao: - parlamentar_partido[ - f.parlamentar.nome_parlamentar] = f.partido.sigla - - sessao_plenaria_presenca = SessaoPlenariaPresenca.objects.filter( - sessao_plenaria_id=pk) - presentes_sessao_plenaria = [ - p.parlamentar.nome_parlamentar for p in sessao_plenaria_presenca] - num_presentes_sessao_plen = len(presentes_sessao_plenaria) - - presenca_expediente = SessaoPlenariaPresenca.objects.filter( - sessao_plenaria_id=pk) - presentes_expediente = [] - for p in presenca_expediente: - nome_parlamentar = p.parlamentar.nome_parlamentar - - try: - parlamentar_partido[nome_parlamentar] - except KeyError: - presentes_expediente.append( - {'id': p.id, - 'nome': nome_parlamentar, - 'partido': str(_('Sem Registro')), - }) - else: - presentes_expediente.append( - {'id': p.id, - 'nome': nome_parlamentar, - 'partido': parlamentar_partido[nome_parlamentar], - }) - num_presentes_expediente = len(presentes_expediente) - - if materia.tipo_votacao == 1: - tipo_votacao = 'Simbólica' - response = get_votos(response, materia) - elif materia.tipo_votacao == 2: - tipo_votacao = 'Nominal' - response = get_votos_nominal(response, materia) - elif materia.tipo_votacao == 3: - tipo_votacao = 'Secreta' - response = get_votos(response, materia) + presentes_list = sort_lista_chave(presentes_list, 'nome') response.update({ - 'presentes_expediente': presentes_expediente, - 'num_presentes_expediente': num_presentes_expediente, - 'presentes_sessao_plenaria': presentes_sessao_plenaria, - 'num_presentes_sessao_plenaria': num_presentes_sessao_plen, - 'status_painel': str(_('ABERTO')), + 'presentes': presentes_list, + 'num_presentes': num_presentes, 'msg_painel': str(_('Votação aberta!')), - 'tipo_resultado': tipo_votacao, - 'observacao_materia': materia.observacao, - 'materia_legislativa_texto': str(materia.materia)}) + }) return response -# ##########################GENERAL FUNCTIONS############################# +def get_materia_expediente_aberta(pk): + return ExpedienteMateria.objects.filter( + sessao_plenaria_id=pk, votacao_aberta=True).last() + def response_nenhuma_materia(response): response.update({ - 'status_painel': 'FECHADO', 'msg_painel': str(_('Nenhuma matéria disponivel para votação.'))}) return JsonResponse(response) def get_votos(response, materia): - if materia.tipo_votacao == 1: - tipo_votacao = 'Simbólica' - elif materia.tipo_votacao == 2: - tipo_votacao = 'Nominal' - elif materia.tipo_votacao == 3: - tipo_votacao = 'Secreta' - if type(materia) == OrdemDia: registro = RegistroVotacao.objects.filter( ordem=materia, materia=materia.materia).last() - else: + tipo = 'ordem' + elif type(materia) == ExpedienteMateria: registro = RegistroVotacao.objects.filter( expediente=materia, materia=materia.materia).last() + tipo = 'expediente' - if registro: - total = (registro.numero_votos_sim + - registro.numero_votos_nao + - registro.numero_abstencoes) - response.update({ - 'numero_votos_sim': registro.numero_votos_sim, - 'numero_votos_nao': registro.numero_votos_nao, - 'numero_abstencoes': registro.numero_abstencoes, - 'total_votos': total, - 'tipo_votacao': tipo_votacao, - 'tipo_resultado': registro.tipo_resultado_votacao.nome, - 'natureza_resultado': registro.tipo_resultado_votacao.natureza, - }) - else: + if not registro: response.update({ 'numero_votos_sim': 0, 'numero_votos_nao': 0, 'numero_abstencoes': 0, + 'registro': None, 'total_votos': 0, - 'tipo_votacao': tipo_votacao, 'tipo_resultado': 'Ainda não foi votada.', - 'natureza_resultado': None, }) - return response + if materia.tipo_votacao == 2: + if tipo == 'ordem': + votos_parlamentares = VotoParlamentar.objects.filter( + ordem_id=materia.id).order_by( + 'parlamentar__nome_parlamentar') + else: + votos_parlamentares = VotoParlamentar.objects.filter( + expediente_id=materia.id).order_by( + 'parlamentar__nome_parlamentar') -def get_votos_nominal(response, materia): - votos = [] - if materia.tipo_votacao == 1: - tipo_votacao = 'Simbólica' - elif materia.tipo_votacao == 2: - tipo_votacao = 'Nominal' - elif materia.tipo_votacao == 3: - tipo_votacao = 'Secreta' + for i, p in enumerate(response['presentes']): + try: + if votos_parlamentares.get(parlamentar_id=p['parlamentar_id']).voto: + response['presentes'][i]['voto'] = 'Voto Informado' + except ObjectDoesNotExist: + response['presentes'][i]['voto'] = '' - if type(materia) == OrdemDia: - registro = RegistroVotacao.objects.filter( - ordem=materia, materia=materia.materia).last() else: - registro = RegistroVotacao.objects.filter( - expediente=materia, materia=materia.materia).last() - - if not registro: - response.update({ - 'numero_votos_sim': 0, - 'numero_votos_nao': 0, - 'numero_abstencoes': 0, - 'total_votos': 0, - 'tipo_votacao': tipo_votacao, - 'tipo_resultado': 'Não foi votado ainda', - 'natureza_resultado': None, - 'votos': None - }) - - else: - votos_parlamentares = VotoParlamentar.objects.filter( - votacao_id=registro.id).order_by('parlamentar__nome_parlamentar') - - filiacao = Filiacao.objects.filter( - data_desfiliacao__isnull=True, parlamentar__ativo=True) - parlamentar_partido = {} - for f in filiacao: - parlamentar_partido[ - f.parlamentar.nome_parlamentar] = f.partido.sigla - - for v in votos_parlamentares: - try: - parlamentar_partido[v.parlamentar.nome_parlamentar] - except KeyError: - votos.append({ - 'parlamentar': v.parlamentar.nome_parlamentar, - 'voto': str(v.voto), - 'partido': str(_('Sem Registro')) - }) - else: - votos.append({ - 'parlamentar': v.parlamentar.nome_parlamentar, - 'voto': str(v.voto), - 'partido': parlamentar_partido[ - v.parlamentar.nome_parlamentar] - }) - total = (registro.numero_votos_sim + registro.numero_votos_nao + registro.numero_abstencoes) + if materia.tipo_votacao == 2: + votos_parlamentares = VotoParlamentar.objects.filter( + votacao_id=registro.id).order_by( + 'parlamentar__nome_parlamentar') + + for i, p in enumerate(response['presentes']): + try: + response['presentes'][i]['voto'] = votos_parlamentares.get( + parlamentar_id=p['parlamentar_id']).voto + except ObjectDoesNotExist: + response['presentes'][i]['voto'] = None + response.update({ 'numero_votos_sim': registro.numero_votos_sim, 'numero_votos_nao': registro.numero_votos_nao, 'numero_abstencoes': registro.numero_abstencoes, + 'registro': True, 'total_votos': total, - 'tipo_votacao': tipo_votacao, 'tipo_resultado': registro.tipo_resultado_votacao.nome, - 'natureza_resultado': registro.tipo_resultado_votacao.natureza, - 'votos': votos }) return response @@ -517,79 +442,71 @@ def get_votos_nominal(response, materia): @user_passes_test(check_permission) def get_dados_painel(request, pk): sessao = SessaoPlenaria.objects.get(id=pk) - cronometro_discurso = get_cronometro_status(request, 'discurso') - cronometro_aparte = get_cronometro_status(request, 'aparte') - cronometro_ordem = get_cronometro_status(request, 'ordem') + + casa = CasaLegislativa.objects.first() + + app_config = ConfiguracoesAplicacao.objects.first() + + brasao = None + if casa and app_config and (bool(casa.logotipo)): + brasao = casa.logotipo.url \ + if app_config.mostrar_brasao_painel else None response = { 'sessao_plenaria': str(sessao), 'sessao_plenaria_data': sessao.data_inicio.strftime('%d/%m/%Y'), 'sessao_plenaria_hora_inicio': sessao.hora_inicio, - "cronometro_aparte": cronometro_aparte, - "cronometro_discurso": cronometro_discurso, - "cronometro_ordem": cronometro_ordem, + 'cronometro_aparte': get_cronometro_status(request, 'aparte'), + 'cronometro_discurso': get_cronometro_status(request, 'discurso'), + 'cronometro_ordem': get_cronometro_status(request, 'ordem'), + 'status_painel': sessao.painel_aberto, + 'brasao': brasao } ordem_dia = get_materia_aberta(pk) expediente = get_materia_expediente_aberta(pk) + # Caso tenha alguma matéria com votação aberta, ela é mostrada no painel + # com prioridade para Ordem do Dia. if ordem_dia: - return JsonResponse(get_presentes(pk, response, ordem_dia)) + return JsonResponse(get_votos( + get_presentes(pk, response, ordem_dia), + ordem_dia)) elif expediente: - return JsonResponse(get_presentes_expediente(pk, response, expediente)) + return JsonResponse(get_votos( + get_presentes(pk, response, expediente), + expediente)) - # Ultimo voto em ordem e ultimo voto em expediente + # Caso não tenha nenhuma aberta, + # a matéria a ser mostrada no Painel deve ser a última votada last_ordem_voto = RegistroVotacao.objects.filter( ordem__sessao_plenaria=sessao).last() last_expediente_voto = RegistroVotacao.objects.filter( expediente__sessao_plenaria=sessao).last() - # Ultimas materias votadas if last_ordem_voto: ultima_ordem_votada = last_ordem_voto.ordem if last_expediente_voto: ultimo_expediente_votado = last_expediente_voto.expediente - # Caso não tenha nenhuma votação aberta if last_ordem_voto or last_expediente_voto: - # Se alguma ordem E algum expediente já tiver sido votado... if last_ordem_voto and last_expediente_voto: - # Verifica se o último resultado é um uma ordem do dia - if last_ordem_voto.pk >= last_expediente_voto.pk: - if ultima_ordem_votada.tipo_votacao in [1, 3]: - return JsonResponse( - get_votos(get_presentes( - pk, response, ultima_ordem_votada), - ultima_ordem_votada)) - elif ultima_ordem_votada.tipo_votacao == 2: - return JsonResponse( - get_votos_nominal(get_presentes( - pk, response, ultima_ordem_votada), - ultima_ordem_votada)) - # Caso não seja, verifica se é um expediente - else: - if ultimo_expediente_votado.tipo_votacao in [1, 3]: - return JsonResponse( - get_votos(get_presentes_expediente( - pk, response, ultimo_expediente_votado), - ultimo_expediente_votado)) - elif ultimo_expediente_votado.tipo_votacao == 2: - return JsonResponse( - get_votos_nominal(get_presentes_expediente( - pk, response, - ultimo_expediente_votado), - ultimo_expediente_votado)) + materia = ultima_ordem_votada\ + if last_ordem_voto.pk >= last_expediente_voto.pk\ + else ultimo_expediente_votado # Caso somente um deles tenha resultado, prioriza a Ordem do Dia - if last_ordem_voto: - return JsonResponse(get_presentes( - pk, response, ultima_ordem_votada)) + elif last_ordem_voto: + materia = ultima_ordem_votada + # Caso a Ordem do dia não tenha resultado, mostra o último expediente - if last_expediente_voto: - return JsonResponse(get_presentes_expediente( - pk, response, - ultimo_expediente_votado)) + elif last_expediente_voto: + materia = ultimo_expediente_votado + + return JsonResponse(get_votos( + get_presentes(pk, response, materia), + materia)) # Retorna que não há nenhuma matéria já votada ou aberta - return response_nenhuma_materia(response) + return response_nenhuma_materia(get_presentes(pk, response, None)) diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index 59069aca5..0f004be14 100644 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -1,4 +1,4 @@ -from datetime import date, timedelta +from datetime import timedelta from crispy_forms.helper import FormHelper from crispy_forms.layout import Fieldset, Layout @@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError from django.db import transaction from django.db.models import Q from django.forms import ModelForm +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from floppyforms.widgets import ClearableFileInput @@ -108,52 +109,16 @@ class LegislaturaForm(ModelForm): data_fim = data['data_fim'] data_eleicao = data['data_eleicao'] - if data_eleicao.year >= data_inicio.year: - raise ValidationError(_("Data eleição não pode ser inferior a " - "data início da legislatura")) + pk = self.instance.pk - if data_inicio > data_fim or (data_fim.year - data_inicio.year != 4): - raise ValidationError(_("Intervalo de início e fim inválido para " - "legislatura.")) - - return data - - -class LegislaturaCreateForm(LegislaturaForm): - - def clean(self): - super(LegislaturaCreateForm, self).clean() - - cleaned_data = self.cleaned_data - - if not self.is_valid(): - return cleaned_data - - eleicao = cleaned_data['data_eleicao'] - inicio = cleaned_data['data_inicio'] - fim = cleaned_data['data_fim'] - - valida_datas = validar_datas_legislatura(eleicao, inicio, fim) + valida_datas = validar_datas_legislatura(data_eleicao, + data_inicio, + data_fim, + pk=pk) if not valida_datas[0]: raise ValidationError(valida_datas[1]) - return cleaned_data - - -class LegislaturaUpdateForm(LegislaturaCreateForm): - - def clean(self): - super(LegislaturaUpdateForm, self).clean() - - cleaned_data = super(LegislaturaCreateForm, self).clean() - eleicao = cleaned_data['data_eleicao'] - inicio = cleaned_data['data_inicio'] - fim = cleaned_data['data_fim'] - valida_datas = validar_datas_legislatura( - eleicao, inicio, fim, pk=self.instance.pk) - if not valida_datas[0]: - raise ValidationError(valida_datas[1]) - return cleaned_data + return data class ParlamentarForm(ModelForm): @@ -204,7 +169,8 @@ def validar_datas(data_filiacao, data_desfiliacao, parlamentar, filiacao): return [True, ''] # data ficticia de desfiliacao - df_desfiliacao = data_desfiliacao if data_desfiliacao else date.today() + today = timezone.now() + df_desfiliacao = data_desfiliacao if data_desfiliacao else today # se não puder haver filiação no mesmo dia de desfiliação, basta # retirar os timedelta abaixo @@ -332,7 +298,7 @@ class VotanteForm(ModelForm): self.helper = FormHelper() self.helper.layout = Layout( Fieldset(_('Votante'), - row1, form_actions(save_label='Salvar')) + row1, form_actions(label='Salvar')) ) super(VotanteForm, self).__init__(*args, **kwargs) diff --git a/sapl/parlamentares/migrations/0009_auto_20170905_1617.py b/sapl/parlamentares/migrations/0009_auto_20170905_1617.py new file mode 100644 index 000000000..1290f069a --- /dev/null +++ b/sapl/parlamentares/migrations/0009_auto_20170905_1617.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-05 16:17 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0008_adiciona_cargos_mesa'), + ] + + operations = [ + migrations.AlterModelOptions( + name='situacaomilitar', + options={'ordering': ['descricao'], 'verbose_name': 'Tipo Situação Militar', 'verbose_name_plural': 'Tipos Situações Militares'}, + ), + migrations.AlterModelOptions( + name='tipoafastamento', + options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Afastamento', 'verbose_name_plural': 'Tipos de Afastamento'}, + ), + migrations.AlterModelOptions( + name='tipodependente', + options={'ordering': ['descricao'], 'verbose_name': 'Tipo de Dependente', 'verbose_name_plural': 'Tipos de Dependente'}, + ), + ] diff --git a/sapl/parlamentares/migrations/0010_corrige_data_inicio_mandato.py b/sapl/parlamentares/migrations/0010_corrige_data_inicio_mandato.py new file mode 100644 index 000000000..ae2e83d50 --- /dev/null +++ b/sapl/parlamentares/migrations/0010_corrige_data_inicio_mandato.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +from django.db import migrations + + +def altera_data_inicio_mandato(apps, schema_editor): + Mandato = apps.get_model("parlamentares", "Mandato") + mandatos = Mandato.objects.all() + + for mandato in mandatos: + data_inicio = mandato.data_inicio_mandato + data_inicio_legislatura = mandato.legislatura.data_inicio + + days = abs((data_inicio - data_inicio_legislatura + ).days) if data_inicio else 60 + + if days >= 60: + mandato.data_inicio_mandato = data_inicio_legislatura + mandato.save() + + +class Migration(migrations.Migration): + + dependencies = [ + # A dependencia real desse script é o arquivo 0001_initial.py, mas + # isso gera um erro (Conflicting migrations detected; multiple leaf + # nodes in the migration graph). para não ocasionar problemas de migração, + # vamos manter a ordem padrão do django. + ('parlamentares', '0009_auto_20170905_1617'), + ] + + operations = [ + migrations.RunPython(altera_data_inicio_mandato), + ] diff --git a/sapl/parlamentares/migrations/0011_auto_20171010_1433.py b/sapl/parlamentares/migrations/0011_auto_20171010_1433.py new file mode 100644 index 000000000..bba5cfc99 --- /dev/null +++ b/sapl/parlamentares/migrations/0011_auto_20171010_1433.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.3 on 2017-10-10 17:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0010_corrige_data_inicio_mandato'), + ] + + operations = [ + migrations.AlterField( + model_name='mandato', + name='data_inicio_mandato', + field=models.DateField(null=True, verbose_name='Início do Mandato'), + ), + ] diff --git a/sapl/parlamentares/migrations/0012_auto_20171020_1045.py b/sapl/parlamentares/migrations/0012_auto_20171020_1045.py new file mode 100644 index 000000000..a1915ed92 --- /dev/null +++ b/sapl/parlamentares/migrations/0012_auto_20171020_1045.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2017-10-20 12:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('parlamentares', '0011_auto_20171010_1433'), + ] + + operations = [ + migrations.AlterField( + model_name='dependente', + name='nome', + field=models.CharField(max_length=150, verbose_name='Nome'), + ), + migrations.AlterField( + model_name='tipodependente', + name='descricao', + field=models.CharField(max_length=150, verbose_name='Descrição'), + ), + ] diff --git a/sapl/parlamentares/models.py b/sapl/parlamentares/models.py index db8ea8242..2162c73b6 100644 --- a/sapl/parlamentares/models.py +++ b/sapl/parlamentares/models.py @@ -1,7 +1,7 @@ -from datetime import datetime import reversion from django.db import models +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from model_utils import Choices @@ -26,7 +26,7 @@ class Legislatura(models.Model): verbose_name_plural = _('Legislaturas') def atual(self): - current_year = datetime.now().year + current_year = timezone.now().year return self.data_inicio.year <= current_year <= self.data_fim.year @vigencia_atual @@ -190,6 +190,7 @@ class SituacaoMilitar(models.Model): class Meta: verbose_name = _('Tipo Situação Militar') verbose_name_plural = _('Tipos Situações Militares') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -355,11 +356,12 @@ class Parlamentar(models.Model): @reversion.register() class TipoDependente(models.Model): - descricao = models.CharField(max_length=50, verbose_name=_('Descrição')) + descricao = models.CharField(max_length=150, verbose_name=_('Descrição')) class Meta: verbose_name = _('Tipo de Dependente') verbose_name_plural = _('Tipos de Dependente') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -375,7 +377,7 @@ class Dependente(models.Model): tipo = models.ForeignKey(TipoDependente, on_delete=models.PROTECT, verbose_name=_('Tipo')) parlamentar = models.ForeignKey(Parlamentar, on_delete=models.PROTECT) - nome = models.CharField(max_length=50, verbose_name=_('Nome')) + nome = models.CharField(max_length=150, verbose_name=_('Nome')) sexo = models.CharField( max_length=1, verbose_name=_('Sexo'), choices=SEXO_CHOICE) data_nascimento = models.DateField( @@ -432,6 +434,7 @@ class TipoAfastamento(models.Model): class Meta: verbose_name = _('Tipo de Afastamento') verbose_name_plural = _('Tipos de Afastamento') + ordering = ['descricao'] def __str__(self): return self.descricao @@ -450,7 +453,7 @@ class Mandato(models.Model): # TODO what is this field?????? tipo_causa_fim_mandato = models.PositiveIntegerField(blank=True, null=True) data_inicio_mandato = models.DateField(verbose_name=_('Início do Mandato'), - blank=True, + blank=False, null=True) data_fim_mandato = models.DateField(verbose_name=_('Fim do Mandato'), blank=True, @@ -486,7 +489,7 @@ class Mandato(models.Model): self.legislatura.data_inicio, self.legislatura.data_fim, f.data, - f.data_desfiliacao or datetime.max.date())] + f.data_desfiliacao or timezone.datetime.max.date())] @reversion.register() diff --git a/sapl/parlamentares/tests/test_parlamentares.py b/sapl/parlamentares/tests/test_parlamentares.py index a10d235e5..ba1f560b9 100644 --- a/sapl/parlamentares/tests/test_parlamentares.py +++ b/sapl/parlamentares/tests/test_parlamentares.py @@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from model_mommy import mommy -from sapl.parlamentares.forms import (FrenteForm, LegislaturaForm, MandatoForm) +from sapl.parlamentares.forms import FrenteForm, LegislaturaForm, MandatoForm from sapl.parlamentares.models import (Dependente, Filiacao, Legislatura, Mandato, Parlamentar, Partido, TipoDependente) @@ -128,6 +128,8 @@ def test_mandato_submit(admin_client): kwargs={'pk': 14}), {'parlamentar': 14, # hidden field 'legislatura': 5, + 'data_inicio_mandato': \ + Legislatura.objects.get(id=5).data_inicio, 'data_expedicao_diploma': '2016-03-22', 'observacao': 'Observação do mandato', 'salvar': 'salvar'}, @@ -171,12 +173,14 @@ def test_mandato_form_duplicado(): Mandato.objects.create(parlamentar=parlamentar, legislatura=legislatura, - data_expedicao_diploma='2017-07-25') + data_expedicao_diploma='2017-07-25', + data_inicio_mandato=legislatura.data_inicio,) form = MandatoForm(data={ 'parlamentar': str(parlamentar.pk), 'legislatura': str(legislatura.pk), - 'data_expedicao_diploma': '01/07/2015' + 'data_expedicao_diploma': '01/07/2015', + 'data_inicio_mandato': legislatura.data_inicio, }) assert not form.is_valid() @@ -225,10 +229,10 @@ def test_legislatura_form_invalido(): errors = legislatura_form.errors - errors['numero'] == [_('Este campo é obrigatório.')] - errors['data_inicio'] == [_('Este campo é obrigatório.')] - errors['data_fim'] == [_('Este campo é obrigatório.')] - errors['data_eleicao'] == [_('Este campo é obrigatório.')] + assert errors['numero'] == [_('Este campo é obrigatório.')] + assert errors['data_inicio'] == [_('Este campo é obrigatório.')] + assert errors['data_fim'] == [_('Este campo é obrigatório.')] + assert errors['data_eleicao'] == [_('Este campo é obrigatório.')] assert len(errors) == 4 @@ -244,7 +248,8 @@ def test_legislatura_form_datas_invalidas(): assert not legislatura_form.is_valid() expected = \ - _("Data eleição não pode ser inferior a data início da legislatura") + _("A data início deve ser menor que a data fim, " + "e a data eleição deve ser menor que a data início") assert legislatura_form.errors['__all__'] == [expected] legislatura_form = LegislaturaForm(data={'numero': '1', @@ -255,8 +260,7 @@ def test_legislatura_form_datas_invalidas(): assert not legislatura_form.is_valid() - assert legislatura_form.errors['__all__'] == \ - [_("Intervalo de início e fim inválido para legislatura.")] + assert legislatura_form.errors['__all__'] == [expected] @pytest.mark.django_db(transaction=False) diff --git a/sapl/parlamentares/urls.py b/sapl/parlamentares/urls.py index e383421c4..552257c61 100644 --- a/sapl/parlamentares/urls.py +++ b/sapl/parlamentares/urls.py @@ -17,7 +17,7 @@ from sapl.parlamentares.views import (CargoMesaCrud, ColigacaoCrud, frente_atualiza_lista_parlamentares, insere_parlamentar_composicao, parlamentares_frente_selected, - remove_parlamentar_composicao) + remove_parlamentar_composicao, MunicipioCrud) from .apps import AppConfig @@ -60,6 +60,7 @@ urlpatterns = [ url(r'^sistema/parlamentar/tipo-militar/', include(TipoMilitarCrud.get_urls())), url(r'^sistema/parlamentar/partido/', include(PartidoCrud.get_urls())), + url(r'^sistema/parlamentar/municipio/', include(MunicipioCrud.get_urls())), url(r'^sistema/mesa-diretora/sessao-legislativa/', include(SessaoLegislativaCrud.get_urls())), diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index fff00b775..9a289cc20 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -1,5 +1,5 @@ -from datetime import datetime import json +from datetime import datetime from django.contrib import messages from django.contrib.contenttypes.models import ContentType @@ -10,6 +10,7 @@ from django.db.models.aggregates import Count from django.http import JsonResponse from django.http.response import HttpResponseRedirect from django.templatetags.static import static +from django.utils import timezone from django.utils.datastructures import MultiValueDictKeyError from django.utils.translation import ugettext_lazy as _ from django.views.decorators.clickjacking import xframe_options_exempt @@ -25,13 +26,12 @@ from sapl.materia.models import Autoria, Proposicao, Relatoria from sapl.parlamentares.apps import AppConfig from sapl.utils import parlamentares_ativos -from .forms import (FiliacaoForm, LegislaturaCreateForm, LegislaturaUpdateForm, - MandatoForm, ParlamentarCreateForm, ParlamentarForm, - VotanteForm) +from .forms import (FiliacaoForm, LegislaturaForm, MandatoForm, + ParlamentarCreateForm, ParlamentarForm, VotanteForm) from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa, Dependente, Filiacao, Frente, Legislatura, Mandato, NivelInstrucao, Parlamentar, Partido, SessaoLegislativa, - SituacaoMilitar, TipoAfastamento, TipoDependente, Votante) + SituacaoMilitar, TipoAfastamento, TipoDependente, Votante, Municipio) CargoMesaCrud = CrudAux.build(CargoMesa, 'cargo_mesa') PartidoCrud = CrudAux.build(Partido, 'partidos') @@ -40,6 +40,7 @@ TipoDependenteCrud = CrudAux.build(TipoDependente, 'tipo_dependente') NivelInstrucaoCrud = CrudAux.build(NivelInstrucao, 'nivel_instrucao') TipoAfastamentoCrud = CrudAux.build(TipoAfastamento, 'tipo_afastamento') TipoMilitarCrud = CrudAux.build(SituacaoMilitar, 'tipo_situa_militar') +MunicipioCrud = CrudAux.build(Municipio, 'municipio') DependenteCrud = MasterDetailCrud.build( Dependente, 'parlamentar', 'dependente') @@ -89,7 +90,7 @@ class FrenteList(MasterDetailCrud): class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView): model = Relatoria parent_field = 'parlamentar' - help_path = 'relatoria_parlamentar' + help_topic = 'tramitacao_relatoria' namespace = AppConfig.name class BaseMixin(CrudBaseForListAndDetailExternalAppView.BaseMixin): @@ -181,7 +182,7 @@ class ParticipacaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): class ColigacaoCrud(CrudAux): model = Coligacao - help_path = 'tabelas_auxiliares#coligacao' + help_topic = 'coligacao' class ListView(CrudAux.ListView): ordering = ('-numero_votos', 'nome') @@ -222,22 +223,20 @@ class ColigacaoCrud(CrudAux): def json_date_convert(date): - ''' + """ :param date: recebe a data de uma chamada ajax no formato de string "dd/mm/yyyy" :return: - ''' - dia, mes, ano = date.split('/') - return datetime.date(day=int(dia), - month=int(mes), - year=int(ano)) + """ + + return datetime.strptime(date, "%d/%m/%Y").date() def frente_atualiza_lista_parlamentares(request): - ''' + """ :param request: recebe os parâmetros do GET da chamada Ajax :return: retorna a lista atualizada dos parlamentares - ''' + """ ativos = json.loads(request.GET['ativos']) parlamentares = Parlamentar.objects.all() @@ -259,9 +258,9 @@ def frente_atualiza_lista_parlamentares(request): def parlamentares_frente_selected(request): - ''' + """ :return: Lista com o id dos parlamentares em uma frente - ''' + """ try: frente = Frente.objects.get(id=int(request.GET['frente_id'])) except ObjectDoesNotExist: @@ -274,7 +273,7 @@ def parlamentares_frente_selected(request): class FrenteCrud(CrudAux): model = Frente - help_path = 'tabelas_auxiliares#tipo_situa_militar' + help_topic = 'tipo_situa_militar' list_field_names = ['nome', 'data_criacao', 'parlamentares'] class CreateView(CrudAux.CreateView): @@ -339,7 +338,7 @@ class MandatoCrud(MasterDetailCrud): class ComposicaoColigacaoCrud(MasterDetailCrud): model = ComposicaoColigacao parent_field = 'coligacao' - help_path = '' + help_topic = 'coligacao' class BaseMixin(MasterDetailCrud.BaseMixin): @@ -355,10 +354,10 @@ class ComposicaoColigacaoCrud(MasterDetailCrud): class LegislaturaCrud(CrudAux): model = Legislatura - help_path = 'tabelas_auxiliares#legislatura' + help_topic = 'legislatura' class CreateView(CrudAux.CreateView): - form_class = LegislaturaCreateForm + form_class = LegislaturaForm def get_initial(self): try: @@ -369,7 +368,7 @@ class LegislaturaCrud(CrudAux): return {'numero': numero} class UpdateView(CrudAux.UpdateView): - form_class = LegislaturaUpdateForm + form_class = LegislaturaForm class DetailView(CrudAux.DetailView): @@ -393,7 +392,7 @@ class LegislaturaCrud(CrudAux): class FiliacaoCrud(MasterDetailCrud): model = Filiacao parent_field = 'parlamentar' - help_path = '' + help_topic = 'filiacoes_partidarias' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -446,12 +445,12 @@ class ParlamentarCrud(Crud): return 'ParlamentarCreate' def form_valid(self, form): - ''' + """ Reimplementa form_valid devido ao save de ParlamentarCreateForm ser específico, sendo necessário isolar padrão do crud que aciona form.save(commit=False) para registrar dados de auditoria se o model implementá-los, bem como de container se também implement. - ''' + """ return super(Crud.CreateView, self).form_valid(form) class ListView(Crud.ListView): @@ -661,8 +660,8 @@ class MesaDiretoraView(FormView): sessoes = SessaoLegislativa.objects.filter( legislatura=legislatura).order_by("data_inicio") - today = datetime.now() - sessao_atual = sessoes.filter(data_inicio__year=today.year).first() + year = timezone.now().year + sessao_atual = sessoes.filter(data_inicio__year=year).first() mesa = sessao_atual.composicaomesa_set.all() if sessao_atual else [] @@ -716,9 +715,9 @@ def altera_field_mesa(request): # Caso a mudança tenha sido no campo legislatura, a sessão # atual deve ser a primeira daquela legislatura else: - today = datetime.now() + year = timezone.now().year try: - sessao_selecionada = sessoes.get(data_inicio__year=today.year).id + sessao_selecionada = sessoes.get(data_inicio__year=year).id except ObjectDoesNotExist: sessao_selecionada = sessoes.first().id @@ -884,8 +883,8 @@ def altera_field_mesa_public_view(request): # atual deve ser a primeira daquela legislatura else: try: - today = datetime.now() - sessao_selecionada = sessoes.get(data_inicio__year=today.year).id + year = timezone.now().year + sessao_selecionada = sessoes.get(data_inicio__year=year).id except ObjectDoesNotExist as e: sessao_selecionada = sessoes.first().id diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index 626c25186..6a0dc1b9a 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -1,17 +1,18 @@ -from datetime import datetime +import django_filters from crispy_forms.bootstrap import InlineRadios from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Button, Fieldset, Layout, Submit +from crispy_forms.layout import HTML, Button, Fieldset, Layout from django import forms -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import (MultipleObjectsReturned, + ObjectDoesNotExist, ValidationError) from django.db import models from django.forms import ModelForm +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -import django_filters from sapl.base.models import Autor, TipoAutor -from sapl.crispy_layout_mixin import form_actions, to_row, SaplFormLayout +from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row from sapl.materia.models import (MateriaLegislativa, TipoMateriaLegislativa, UnidadeTramitacao) from sapl.utils import (RANGE_ANOS, AnoNumeroOrderingFilter, @@ -21,7 +22,6 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) - TIPOS_PROTOCOLO = [('0', 'Recebido'), ('1', 'Enviado'), ('', 'Ambos')] TIPOS_PROTOCOLO_CREATE = [('0', 'Recebido'), ('1', 'Enviado')] @@ -123,7 +123,7 @@ class ProtocoloFilterSet(django_filters.FilterSet): HTML(autor_label), HTML(autor_modal), row4, row5, row6, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @@ -154,7 +154,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): model = DocumentoAdministrativo fields = ['tipo', 'numero', - 'numero_protocolo', + 'protocolo__numero', 'data', 'tramitacaoadministrativo__unidade_tramitacao_destino', 'tramitacaoadministrativo__status'] @@ -173,7 +173,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): row2 = to_row( [('ano', 4), - ('numero_protocolo', 4), + ('protocolo__numero', 4), ('data', 4)]) row3 = to_row( @@ -194,7 +194,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): Fieldset(_('Pesquisar Documento'), row1, row2, row3, row4, row5, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @@ -276,7 +276,7 @@ class AnularProcoloAdmForm(ModelForm): row1, row2, HTML(" "), - form_actions(save_label='Anular') + form_actions(label='Anular') ) ) super(AnularProcoloAdmForm, self).__init__( @@ -339,7 +339,7 @@ class ProtocoloDocumentForm(ModelForm): row4, row5, HTML(" "), - form_actions(save_label=_('Protocolar Documento')) + form_actions(label=_('Protocolar Documento')) ) ) super(ProtocoloDocumentForm, self).__init__( @@ -407,7 +407,7 @@ class ProtocoloMateriaForm(ModelForm): self.helper.layout = Layout( Fieldset(_('Identificação da Matéria'), row1, row3, - row4, form_actions(save_label='Protocolar Matéria'))) + row4, form_actions(label='Protocolar Matéria'))) super(ProtocoloMateriaForm, self).__init__( *args, **kwargs) @@ -442,28 +442,26 @@ class TramitacaoAdmForm(ModelForm): 'texto', ] - widgets = { - 'data_tramitacao': forms.DateInput(format='%d/%m/%Y'), - 'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'), - 'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'), - } - def clean(self): - super(TramitacaoAdmForm, self).clean() + cleaned_data = super(TramitacaoAdmForm, self).clean() - data_enc_form = self.cleaned_data['data_encaminhamento'] - data_prazo_form = self.cleaned_data['data_fim_prazo'] - data_tram_form = self.cleaned_data['data_tramitacao'] + if 'data_encaminhamento' in cleaned_data: + data_enc_form = cleaned_data['data_encaminhamento'] + if 'data_fim_prazo' in cleaned_data: + data_prazo_form = cleaned_data['data_fim_prazo'] + if 'data_tramitacao' in cleaned_data: + data_tram_form = cleaned_data['data_tramitacao'] - if self.errors: - return self.errors + if not self.is_valid(): + return cleaned_data ultima_tramitacao = TramitacaoAdministrativo.objects.filter( documento_id=self.instance.documento_id).exclude( - id=self.instance.id).last() + id=self.instance.id).order_by( + '-data_tramitacao', + '-id').first() if not self.instance.data_tramitacao: - if ultima_tramitacao: destino = ultima_tramitacao.unidade_tramitacao_destino if (destino != self.cleaned_data['unidade_tramitacao_local']): @@ -471,7 +469,7 @@ class TramitacaoAdmForm(ModelForm): 'destino da última adicionada!') raise ValidationError(msg) - if self.cleaned_data['data_tramitacao'] > datetime.now().date(): + if self.cleaned_data['data_tramitacao'] > timezone.now().date(): msg = _( 'A data de tramitação deve ser ' + 'menor ou igual a data de hoje!') @@ -517,25 +515,33 @@ class TramitacaoAdmEditForm(TramitacaoAdmForm): 'texto', ] - widgets = { - 'data_encaminhamento': forms.DateInput(format='%d/%m/%Y'), - 'data_fim_prazo': forms.DateInput(format='%d/%m/%Y'), - } - def clean(self): - super(TramitacaoAdmEditForm, self).clean() - - local = self.instance.unidade_tramitacao_local - data_tram = self.instance.data_tramitacao + ultima_tramitacao = TramitacaoAdministrativo.objects.filter( + documento_id=self.instance.documento_id).order_by( + '-data_tramitacao', + '-id').first() + + # Se a Tramitação que está sendo editada não for a mais recente, + # ela não pode ter seu destino alterado. + if ultima_tramitacao != self.instance: + if self.cleaned_data['unidade_tramitacao_destino'] != \ + self.instance.unidade_tramitacao_destino: + raise ValidationError( + 'Você não pode mudar a Unidade de Destino desta ' + 'tramitação, pois irá conflitar com a Unidade ' + 'Local da tramitação seguinte') + + self.cleaned_data['data_tramitacao'] = \ + self.instance.data_tramitacao + self.cleaned_data['unidade_tramitacao_local'] = \ + self.instance.unidade_tramitacao_local - self.cleaned_data['data_tramitacao'] = data_tram - self.cleaned_data['unidade_tramitacao_local'] = local return super(TramitacaoAdmEditForm, self).clean() class DocumentoAdministrativoForm(ModelForm): - data = forms.DateField(initial=datetime.today) + data = forms.DateField(initial=timezone.now) ano_protocolo = forms.ChoiceField(required=False, label=Protocolo._meta. @@ -544,6 +550,10 @@ class DocumentoAdministrativoForm(ModelForm): widget=forms.Select( attrs={'class': 'selector'})) + numero_protocolo = forms.IntegerField(required=False, + label=Protocolo._meta. + get_field('numero').verbose_name) + class Meta: model = DocumentoAdministrativo fields = ['tipo', @@ -572,8 +582,8 @@ class DocumentoAdministrativoForm(ModelForm): if not self.is_valid(): return cleaned_data - numero_protocolo = cleaned_data['numero_protocolo'] - ano_protocolo = cleaned_data['ano_protocolo'] + numero_protocolo = self.data['numero_protocolo'] + ano_protocolo = self.data['ano_protocolo'] # campos opcionais, mas que se informados devem ser válidos if numero_protocolo and ano_protocolo: @@ -585,6 +595,11 @@ class DocumentoAdministrativoForm(ModelForm): msg = _('Protocolo %s/%s inexistente.' % ( numero_protocolo, ano_protocolo)) raise ValidationError(msg) + except MultipleObjectsReturned: + msg = _( + 'Existe mais de um Protocolo com este ano e número.' % ( + numero_protocolo, ano_protocolo)) + raise ValidationError(msg) return self.cleaned_data diff --git a/sapl/protocoloadm/migrations/0002_remove_documentoadministrativo_numero_protocolo.py b/sapl/protocoloadm/migrations/0002_remove_documentoadministrativo_numero_protocolo.py new file mode 100644 index 000000000..de12f4139 --- /dev/null +++ b/sapl/protocoloadm/migrations/0002_remove_documentoadministrativo_numero_protocolo.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.3 on 2017-09-20 21:52 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='documentoadministrativo', + name='numero_protocolo', + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 6294399f8..d4d45b3ad 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -120,8 +120,7 @@ class DocumentoAdministrativo(models.Model): on_delete=models.PROTECT, verbose_name=_('Protocolo')) data = models.DateField(verbose_name=_('Data')) - numero_protocolo = models.PositiveIntegerField( - blank=True, null=True, verbose_name=_('Núm. Protocolo')) + interessado = models.CharField( max_length=50, blank=True, verbose_name=_('Interessado')) autor = models.ForeignKey(Autor, blank=True, null=True, diff --git a/sapl/protocoloadm/tests/test_protocoloadm.py b/sapl/protocoloadm/tests/test_protocoloadm.py index 0ffbb5e4b..12f49b39a 100644 --- a/sapl/protocoloadm/tests/test_protocoloadm.py +++ b/sapl/protocoloadm/tests/test_protocoloadm.py @@ -1,4 +1,4 @@ -import datetime +from datetime import date, timedelta import pytest from django.core.urlresolvers import reverse @@ -9,8 +9,7 @@ from model_mommy import mommy from sapl.materia.models import UnidadeTramitacao from sapl.protocoloadm.forms import (AnularProcoloAdmForm, DocumentoAdministrativoForm, - MateriaLegislativa, - ProtocoloDocumentForm, + MateriaLegislativa, ProtocoloDocumentForm, ProtocoloMateriaForm) from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo, @@ -146,7 +145,7 @@ def test_create_tramitacao(admin_client): unidade_tramitacao_destino=unidade_tramitacao_destino_1, status=status, documento=documento_adm, - data_tramitacao=datetime.date(2016, 8, 21)) + data_tramitacao=date(2016, 8, 21)) response = admin_client.post( reverse( @@ -156,7 +155,7 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_local_1.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date(2016, 8, 21)}, + 'data_tramitacao': date(2016, 8, 21)}, follow=True) msg = force_text(_('A origem da nova tramitação deve ser igual ao ' @@ -174,7 +173,7 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date(2016, 8, 20)}, + 'data_tramitacao': date(2016, 8, 20)}, follow=True) msg = _('A data da nova tramitação deve ser ' + @@ -192,7 +191,7 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date.today() + datetime.timedelta( + 'data_tramitacao': date.today() + timedelta( days=1)}, follow=True) @@ -211,8 +210,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date(2016, 8, 21), - 'data_encaminhamento': datetime.date(2016, 8, 20)}, + 'data_tramitacao': date(2016, 8, 21), + 'data_encaminhamento': date(2016, 8, 20)}, follow=True) msg = force_text(_('A data de encaminhamento deve ser ' + @@ -230,8 +229,8 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date(2016, 8, 21), - 'data_fim_prazo': datetime.date(2016, 8, 20)}, + 'data_tramitacao': date(2016, 8, 21), + 'data_fim_prazo': date(2016, 8, 20)}, follow=True) msg = _('A data fim de prazo deve ser ' + @@ -249,12 +248,12 @@ def test_create_tramitacao(admin_client): 'unidade_tramitacao_destino': unidade_tramitacao_destino_2.pk, 'documento': documento_adm.pk, 'status': status.pk, - 'data_tramitacao': datetime.date(2016, 8, 21)}, + 'data_tramitacao': date(2016, 8, 21)}, follow=True) tramitacao = TramitacaoAdministrativo.objects.last() # Verifica se a tramitacao que obedece as regras de negócios é criada - assert tramitacao.data_tramitacao == datetime.date(2016, 8, 21) + assert tramitacao.data_tramitacao == date(2016, 8, 21) @pytest.mark.django_db(transaction=False) @@ -288,7 +287,7 @@ def test_anular_protocolo_form_anula_protocolo_inexistente(): @pytest.mark.django_db(transaction=False) def test_anular_protocolo_form_anula_protocolo_anulado(): - protocolo = mommy.make(Protocolo, numero=1, ano=2017, anulado=True) + mommy.make(Protocolo, numero=1, ano=2017, anulado=True) form = AnularProcoloAdmForm(data={'numero': '1', 'ano': '2017', @@ -305,15 +304,15 @@ def test_anular_protocolo_form_anula_protocolo_anulado(): def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado(): tipo_materia = mommy.make(TipoMateriaLegislativa) - protocolo_materia = mommy.make(Protocolo, - numero=1, - ano=2017, - tipo_materia=tipo_materia, - anulado=False) + mommy.make(Protocolo, + numero=1, + ano=2017, + tipo_materia=tipo_materia, + anulado=False) - materia_legislativa = mommy.make(MateriaLegislativa, - ano=2017, - numero_protocolo=1) + mommy.make(MateriaLegislativa, + ano=2017, + numero_protocolo=1) form = AnularProcoloAdmForm(data={'numero': '1', 'ano': '2017', @@ -334,8 +333,8 @@ def test_anular_protocolo_form_anula_protocolo_com_doc_vinculado(): tipo_documento=tipo_documento, anulado=False) - documento_administrativo = mommy.make(DocumentoAdministrativo, - protocolo=protocolo_documento) + mommy.make(DocumentoAdministrativo, + protocolo=protocolo_documento) form = AnularProcoloAdmForm(data={'numero': '2', 'ano': '2017', diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index 043333323..eb2a18852 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -1,4 +1,3 @@ -from datetime import date, datetime from braces.views import FormValidMessageMixin from django.contrib import messages @@ -8,21 +7,22 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse from django.db.models import Max, Q from django.http import Http404, HttpResponse, JsonResponse +from django.http.response import HttpResponseRedirect from django.shortcuts import redirect +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.views.generic import CreateView, ListView -from django.views.generic.base import TemplateView, RedirectView +from django.views.generic.base import RedirectView, TemplateView from django_filters.views import FilterView +import sapl from sapl.base.models import Autor from sapl.comissoes.models import Comissao from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Parlamentar from sapl.protocoloadm.models import Protocolo -from sapl.utils import create_barcode, get_client_ip -import sapl - +from sapl.utils import create_barcode, get_client_ip, show_results_filter_set, get_mime_type_from_file_extension from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoFilterSet, DocumentoAdministrativoForm, ProtocoloDocumentForm, @@ -32,7 +32,6 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) - TipoDocumentoAdministrativoCrud = CrudAux.build( TipoDocumentoAdministrativo, '') @@ -55,12 +54,7 @@ def doc_texto_integral(request, pk): if documento.texto_integral: arquivo = documento.texto_integral - ext = arquivo.name.split('.')[-1] - mime = '' - if ext == 'odt': - mime = 'application/vnd.oasis.opendocument.text' - else: - mime = "application/%s" % (ext,) + mime = get_mime_type_from_file_extension(arquivo.name) with open(arquivo.path, 'rb') as f: data = f.read() @@ -84,11 +78,11 @@ class DocumentoAdministrativoMixin: class DocumentoAdministrativoCrud(Crud): model = DocumentoAdministrativo - help_path = '' + help_topic = 'numeracao_docsacess' class BaseMixin(Crud.BaseMixin): list_field_names = ['tipo', 'numero', 'ano', 'data', - 'numero_protocolo', 'assunto', + 'protocolo__numero', 'assunto', 'interessado', 'tramitacao', 'texto_integral'] @property @@ -142,7 +136,7 @@ class DocumentoAdministrativoCrud(Crud): class StatusTramitacaoAdministrativoCrud(CrudAux): model = StatusTramitacaoAdministrativo - help_path = '' + help_topic = 'status_tramitacao' class BaseMixin(CrudAux.BaseMixin): list_field_names = ['sigla', 'indicador', 'descricao'] @@ -163,9 +157,7 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView): kwargs = {'data': self.request.GET or None} - qs = self.get_queryset().order_by('ano', 'numero') - - qs = qs.distinct() + qs = self.get_queryset().order_by('ano', 'numero').distinct() if 'o' in self.request.GET and not self.request.GET['o']: qs = qs.order_by('-ano', '-numero') @@ -197,7 +189,7 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView): # Provavelmente você criou um novo campo no Form/FilterSet # Então a ordem da URL está diferente data = self.filterset.data - if (data and data.get('numero') is not None): + if data and data.get('numero') is not None: url = "&" + str(self.request.environ['QUERY_STRING']) if url.startswith("&page"): ponto_comeco = url.find('numero=') - 1 @@ -213,6 +205,9 @@ class ProtocoloPesquisaView(PermissionRequiredMixin, FilterView): numero_res=len(self.object_list) ) + context['show_results'] = show_results_filter_set( + self.request.GET.copy()) + return self.render_to_response(context) @@ -293,7 +288,7 @@ class ProtocoloDocumentoView(PermissionRequiredMixin, if numeracao == 'A': numero = Protocolo.objects.filter( - ano=date.today().year).aggregate(Max('numero')) + ano=timezone.now().year).aggregate(Max('numero')) elif numeracao == 'L': legislatura = Legislatura.objects.last() data_inicio = legislatura.data_inicio @@ -307,10 +302,10 @@ class ProtocoloDocumentoView(PermissionRequiredMixin, f.tipo_processo = '0' # TODO validar o significado f.anulado = False f.numero = (numero['numero__max'] + 1) if numero['numero__max'] else 1 - f.ano = datetime.now().year - f.data = datetime.now().date() - f.hora = datetime.now().time() - f.timestamp = datetime.now() + f.ano = timezone.now().year + f.data = timezone.now() + f.hora = timezone.now().time() + f.timestamp = timezone.now() f.assunto_ementa = self.request.POST['assunto'] f.save() @@ -332,7 +327,7 @@ class CriarDocumentoProtocolo(PermissionRequiredMixin, CreateView): kwargs={'pk': self.kwargs['pk']}) def criar_documento(self, protocolo): - curr_year = datetime.now().date().year + curr_year = timezone.now().year numero_max = DocumentoAdministrativo.objects.filter( tipo=protocolo.tipo_documento, ano=curr_year @@ -341,7 +336,7 @@ class CriarDocumentoProtocolo(PermissionRequiredMixin, CreateView): doc = {} doc['tipo'] = protocolo.tipo_documento doc['ano'] = curr_year - doc['data'] = datetime.today() + doc['data'] = timezone.now() doc['numero_protocolo'] = protocolo.numero doc['ano_protocolo'] = protocolo.ano doc['protocolo'] = protocolo.id @@ -428,7 +423,7 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): if numeracao == 'A': numero = Protocolo.objects.filter( - ano=date.today().year).aggregate(Max('numero')) + ano=timezone.now().year).aggregate(Max('numero')) elif numeracao == 'U': numero = Protocolo.objects.all().aggregate(Max('numero')) @@ -439,10 +434,10 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): protocolo.numero = ( numero['numero__max'] + 1) if numero['numero__max'] else 1 - protocolo.ano = datetime.now().year - protocolo.data = datetime.now().date() - protocolo.hora = datetime.now().time() - protocolo.timestamp = datetime.now() + protocolo.ano = timezone.now().year + protocolo.data = timezone.now().date() + protocolo.hora = timezone.now().time() + protocolo.timestamp = timezone.now() protocolo.tipo_protocolo = 0 protocolo.tipo_processo = '1' # TODO validar o significado @@ -480,7 +475,7 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): lista_comissoes = Comissao.objects.filter(Q( data_extincao__isnull=True) | Q( - data_extincao__gt=date.today())).values_list('id', flat=True) + data_extincao__gt=timezone.now())).values_list('id', flat=True) model_comissao = ContentType.objects.get_for_model(Comissao) autor_comissoes = Autor.objects.filter( content_type=model_comissao, object_id__in=lista_comissoes) @@ -549,7 +544,7 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, # Provavelmente você criou um novo campo no Form/FilterSet # Então a ordem da URL está diferente data = self.filterset.data - if (data and data.get('tipo') is not None): + if data and data.get('tipo') is not None: url = "&" + str(self.request.environ['QUERY_STRING']) if url.startswith("&page"): ponto_comeco = url.find('tipo=') - 1 @@ -565,13 +560,16 @@ class PesquisarDocumentoAdministrativoView(DocumentoAdministrativoMixin, numero_res=len(self.object_list) ) + context['show_results'] = show_results_filter_set( + self.request.GET.copy()) + return self.render_to_response(context) class TramitacaoAdmCrud(MasterDetailCrud): model = TramitacaoAdministrativo parent_field = 'documento' - help_path = '' + help_topic = 'unidade_tramitacao' class BaseMixin(MasterDetailCrud.BaseMixin): list_field_names = ['data_tramitacao', 'unidade_tramitacao_local', @@ -580,6 +578,33 @@ class TramitacaoAdmCrud(MasterDetailCrud): class CreateView(MasterDetailCrud.CreateView): form_class = TramitacaoAdmForm + def get_initial(self): + local = DocumentoAdministrativo.objects.get( + pk=self.kwargs['pk']).tramitacaoadministrativo_set.order_by( + '-data_tramitacao', + '-id').first() + + if local: + self.initial['unidade_tramitacao_local' + ] = local.unidade_tramitacao_destino.pk + else: + self.initial['unidade_tramitacao_local'] = '' + self.initial['data_tramitacao'] = timezone.now().date() + return self.initial + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + primeira_tramitacao = not(TramitacaoAdministrativo.objects.filter( + documento_id=int(kwargs['root_pk'])).exists()) + + # Se não for a primeira tramitação daquela matéria, o campo + # não pode ser modificado + if not primeira_tramitacao: + context['form'].fields[ + 'unidade_tramitacao_local'].widget.attrs['disabled'] = True + return context + class UpdateView(MasterDetailCrud.UpdateView): form_class = TramitacaoAdmEditForm @@ -588,17 +613,42 @@ class TramitacaoAdmCrud(MasterDetailCrud): def get_queryset(self): qs = super(MasterDetailCrud.ListView, self).get_queryset() kwargs = {self.crud.parent_field: self.kwargs['pk']} - return qs.filter(**kwargs).order_by('-data_tramitacao', '-id') + return qs.filter(**kwargs).order_by('-data_tramitacao', + '-id') class DetailView(DocumentoAdministrativoMixin, MasterDetailCrud.DetailView): pass + class DeleteView(MasterDetailCrud.DeleteView): + + def delete(self, request, *args, **kwargs): + tramitacao = TramitacaoAdministrativo.objects.get( + id=self.kwargs['pk']) + documento = DocumentoAdministrativo.objects.get( + id=tramitacao.documento.id) + url = reverse( + 'sapl.protocoloadm:tramitacaoadministrativo_list', + kwargs={'pk': tramitacao.documento.id}) + + ultima_tramitacao = \ + documento.tramitacaoadministrativo_set.order_by( + '-data_tramitacao', + '-id').first() + + if tramitacao.pk != ultima_tramitacao.pk: + msg = _('Somente a última tramitação pode ser deletada!') + messages.add_message(request, messages.ERROR, msg) + return HttpResponseRedirect(url) + else: + tramitacao.delete() + return HttpResponseRedirect(url) + class DocumentoAcessorioAdministrativoCrud(MasterDetailCrud): model = DocumentoAcessorioAdministrativo parent_field = 'documento' - help_path = '' + help_topic = 'numeracao_docsacess' class BaseMixin(MasterDetailCrud.BaseMixin): list_field_names = ['nome', 'tipo', diff --git a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py index e659d169e..a8677c5e1 100644 --- a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py +++ b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py @@ -243,8 +243,8 @@ def votacao(lst_votacao): tmp += '\n' tmp += 'MatériaEmentaResultado da Votação\n' for votacao in lst_votacao: - tmp += '' + str(votacao['num_ordem']) + ' - ' + votacao['id_materia'] + '\n' + 'Turno: ' + votacao[ - 'des_turno'] + '\n' + 'Autor: ' + votacao['nom_autor'] + '\n' + tmp += '' + str(votacao['num_ordem']) + ' - ' + votacao['id_materia'] + '\n' + 'Turno: ' + str(votacao[ + 'des_turno']) + '\n' + 'Autor: ' + str(votacao['nom_autor']) + '\n' txt_ementa = votacao['txt_ementa'].replace('&', '&') tmp += '' + txt_ementa + '\n' tmp += '' + \ diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 87510089d..f4ffb03a0 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -1,9 +1,10 @@ import html import re -from datetime import datetime +from datetime import datetime as dt from django.core.exceptions import ObjectDoesNotExist from django.http import Http404, HttpResponse +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from sapl.base.models import Autor, CasaLegislativa @@ -88,7 +89,7 @@ def get_rodape(casa): linha2 = linha2 + " - " linha2 = linha2 + str(_("E-mail: ")) + casa.email - data_emissao = datetime.today().strftime("%d/%m/%Y") + data_emissao = dt.strftime(timezone.now(), "%d/%m/%Y") return [linha1, linha2, data_emissao] @@ -562,21 +563,9 @@ def get_sessao_plenaria(sessao, casa): str(numeracao.numero_materia) + '/' + str( numeracao.ano_materia)) - dic_expediente_materia["des_turno"] = ' ' - tram = Tramitacao.objects.filter( - materia=materia).first() - if tram is not None: - if tram.turno != '': - for turno in [("P", _("Primeiro")), - ("S", _("Segundo")), - ("U", _("Único")), - ("L", _("Suplementar")), - ("A", _("Votação Única em Regime de Urgência")), - ("B", _("1ª Votação")), - ("C", _("2ª e 3ª Votações")), - ("F", "Final")]: - if tram.turno == turno[0]: - dic_expediente_materia["des_turno"] = turno[1] + turno, _ = get_turno(dic_expediente_materia, materia, sessao.data_inicio) + + dic_expediente_materia["des_turno"] = turno dic_expediente_materia["txt_ementa"] = str(materia.ementa) dic_expediente_materia["ordem_observacao"] = ' ' # TODO @@ -614,8 +603,8 @@ def get_sessao_plenaria(sessao, casa): dic_expediente_materia["votacao_observacao"] = ( i.observacao) else: - dic_expediente_materia["nom_resultado"] = _("Matéria não votada") - dic_expediente_materia["votacao_observacao"] = _(" ") + dic_expediente_materia["nom_resultado"] = 'Matéria não votada' + dic_expediente_materia["votacao_observacao"] = ' ' lst_expediente_materia.append(dic_expediente_materia) # Lista dos oradores do Expediente @@ -676,22 +665,10 @@ def get_sessao_plenaria(sessao, casa): str(numeracao.numero_materia) + '/' + str(numeracao.ano_materia)) - dic_votacao["des_turno"] = ' ' - - tramitacao = Tramitacao.objects.filter( - materia=materia).first() - if tramitacao is not None: - if not tramitacao.turno: - for turno in [("P", _("Primeiro")), - ("S", _("Segundo")), - ("U", _("Único")), - ("L", _("Suplementar")), - ("F", _("Final")), - ("A", _("Votação Única em Regime de Urgência")), - ("B", _("1ª Votação")), - ("C", _("2ª e 3ª Votações"))]: - if tramitacao.turno == turno[0]: - dic_votacao["des_turno"] = turno[1] + + turno, _ = get_turno(dic_votacao, materia, sessao.data_inicio) + + dic_votacao["des_turno"] = turno # https://github.com/interlegis/sapl/issues/1009 dic_votacao["txt_ementa"] = html.unescape(materia.ementa) @@ -727,8 +704,8 @@ def get_sessao_plenaria(sessao, casa): if votacao.observacao: dic_votacao["votacao_observacao"] = i.observacao else: - dic_votacao["nom_resultado"] = _("Matéria não votada") - dic_votacao["votacao_observacao"] = _(" ") + dic_votacao["nom_resultado"] = "Matéria não votada" + dic_votacao["votacao_observacao"] = " " lst_votacao.append(dic_votacao) # Lista dos oradores nas Explicações Pessoais @@ -760,6 +737,28 @@ def get_sessao_plenaria(sessao, casa): lst_oradores) +def get_turno(dic, materia, sessao_data_inicio): + descricao_turno = ' ' + descricao_tramitacao = ' ' + tramitacao = Tramitacao.objects.filter(materia=materia, + turno__isnull=False, + data_tramitacao__lte=sessao_data_inicio, + ).exclude(turno__exact='' + ).select_related( + 'materia', + 'status', + 'materia__tipo').order_by( + '-data_tramitacao' + ).first() + if tramitacao is not None: + for t in Tramitacao.TURNO_CHOICES: + if t[0] == tramitacao.turno: + descricao_turno = t[1] + break + descricao_tramitacao = tramitacao.status.descricao if tramitacao.status else ' ' + return (descricao_turno, descricao_tramitacao) + + def relatorio_sessao_plenaria(request, pk): ''' pdf_sessao_plenaria_gerar.py @@ -932,8 +931,11 @@ def get_etiqueta_protocolos(prots): dic = {} dic['titulo'] = str(p.numero) + '/' + str(p.ano) + + tz_hora = timezone.localtime(p.timestamp) + dic['data'] = 'Data: ' + p.data.strftime( - "%d/%m/%Y") + ' - Horário: ' + p.hora.strftime("%H:%M") + "%d/%m/%Y") + ' - Horário: ' + tz_hora.strftime("%H:%M") dic['txt_assunto'] = p.assunto_ementa dic['txt_interessado'] = p.interessado @@ -952,7 +954,7 @@ def get_etiqueta_protocolos(prots): dic['num_documento'] = '' for documento in DocumentoAdministrativo.objects.filter( - numero_protocolo=p.numero): + protocolo=p): dic['num_documento'] = str(documento) dic['ident_processo'] = dic['num_materia'] or dic['num_documento'] @@ -1060,28 +1062,12 @@ def get_pauta_sessao(sessao, casa): elif autoria is None: dic_expediente_materia["nom_autor"] = 'Desconhecido' - dic_expediente_materia["des_turno"] = ' ' - dic_expediente_materia["des_situacao"] = ' ' - - tramitacao = Tramitacao.objects.filter(materia=materia) - if tramitacao is not None: - tramitacao = tramitacao.first() - - if tramitacao.turno != '': - for turno in [("P", _("Primeiro")), - ("S", _("Segundo")), - ("U", _("Único")), - ("F", _("Final")), - ("L", _("Suplementar")), - ("A", _("Votação Única em Regime de Urgência")), - ("B", _("1ª Votação")), - ("C", _("2ª e 3ª Votações"))]: - if tramitacao.turno == turno.first(): - dic_expediente_materia["des_turno"] = turno.first() - - dic_expediente_materia["des_situacao"] = tramitacao.status - if dic_expediente_materia["des_situacao"] is None: - dic_expediente_materia["des_situacao"] = ' ' + turno, tramitacao = get_turno(dic_expediente_materia, materia, sessao.data_inicio) + + dic_expediente_materia["des_turno"] = turno + dic_expediente_materia["des_situacao"] = tramitacao + + lst_expediente_materia.append(dic_expediente_materia) lst_votacao = [] @@ -1125,25 +1111,9 @@ def get_pauta_sessao(sessao, casa): elif autoria is None: dic_votacao["nom_autor"] = 'Desconhecido' - dic_votacao["des_turno"] = ' ' - dic_votacao["des_situacao"] = ' ' - tramitacao = Tramitacao.objects.filter(materia=materia) - if tramitacao is not None: - tramitacao = tramitacao.first() - if tramitacao.turno != '': - for turno in [("P", _("Primeiro")), - ("S", _("Segundo")), - ("U", _("Único")), - ("L", _("Suplementar")), - ("A", _("Votação Única em Regime de Urgência")), - ("B", _("1ª Votação")), - ("C", _("2ª e 3ª Votações"))]: - if tramitacao.turno == turno.first(): - dic_votacao["des_turno"] = turno.first() - - dic_votacao["des_situacao"] = tramitacao.status - if dic_votacao["des_situacao"] is None: - dic_votacao["des_situacao"] = ' ' + turno, tramitacao = get_turno(dic_expediente_materia, materia, sessao.data_inicio) + dic_votacao["des_turno"] = turno + dic_votacao["des_situacao"] = tramitacao lst_votacao.append(dic_votacao) return (lst_expediente_materia, diff --git a/sapl/rules/apps.py b/sapl/rules/apps.py index ab8c6322c..3b6e9d271 100644 --- a/sapl/rules/apps.py +++ b/sapl/rules/apps.py @@ -1,16 +1,15 @@ from builtins import LookupError -import django -import reversion from django.apps import apps -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.management import _get_all_permissions from django.core import exceptions from django.db import models, router from django.db.utils import DEFAULT_DB_ALIAS -from django.utils.translation import ugettext_lazy as _ from django.utils.translation import string_concat +from django.utils.translation import ugettext_lazy as _ +import django +import reversion from sapl.rules import (SAPL_GROUP_ADMINISTRATIVO, SAPL_GROUP_COMISSOES, SAPL_GROUP_GERAL, SAPL_GROUP_MATERIA, SAPL_GROUP_NORMA, @@ -79,7 +78,19 @@ def create_proxy_permissions( ctype = ContentType.objects.db_manager(using).get_for_model(klass) ctypes.add(ctype) - for perm in _get_all_permissions(klass._meta, ctype): + + # FIXME: Retirar try except quando sapl passar a usar django 1.11 + try: + # Função não existe mais em Django 1.11 + # como sapl ainda não foi para Django 1.11 + # esta excessão foi adicionada para caso o + # Sapl esteja rodando em um projeto 1.11 não ocorra erros + _all_perms_of_klass = _get_all_permissions(klass._meta, ctype) + except: + # Nova função usada em projetos com Django 1.11 e o sapl é uma app + _all_perms_of_klass = _get_all_permissions(klass._meta) + + for perm in _all_perms_of_klass: searched_perms.append((ctype, perm)) # Find all the Permissions that have a content_type for a model we're @@ -116,7 +127,8 @@ def create_proxy_permissions( def update_groups(app_config, verbosity=2, interactive=True, - using=DEFAULT_DB_ALIAS, **kwargs): + using=DEFAULT_DB_ALIAS, cria_usuarios_padrao=False, + **kwargs): if app_config != AppConfig and not isinstance(app_config, AppConfig): return @@ -149,41 +161,16 @@ def update_groups(app_config, verbosity=2, interactive=True, if not group_name: return - g = Group.objects.get_or_create(name=group_name) - if not isinstance(g, Group): - g = g[0] - g.permissions.clear() + group, created = Group.objects.get_or_create(name=group_name) + group.permissions.clear() try: - print(' ', group_name) for model, perms in rules_list: - self.associar(g, model, perms) + self.associar(group, model, perms) except Exception as e: print(group_name, e) - if settings.DEBUG: - user = '' - if group_name == SAPL_GROUP_ADMINISTRATIVO: - user = 'operador_administrativo' - elif group_name == SAPL_GROUP_PROTOCOLO: - user = 'operador_protocoloadm' - elif group_name == SAPL_GROUP_COMISSOES: - user = 'operador_comissoes' - elif group_name == SAPL_GROUP_MATERIA: - user = 'operador_materia' - elif group_name == SAPL_GROUP_NORMA: - user = 'operador_norma' - elif group_name == SAPL_GROUP_SESSAO: - user = 'operador_sessao' - elif group_name == SAPL_GROUP_PAINEL: - user = 'operador_painel' - elif group_name == SAPL_GROUP_GERAL: - user = 'operador_geral' - - if user: - self.cria_usuario(user, g) - def groups_add_user(self, user, groups_name): if not isinstance(groups_name, list): groups_name = [groups_name, ] @@ -211,7 +198,21 @@ def update_groups(app_config, verbosity=2, interactive=True, **param_username)[0] usuario.set_password('interlegis') usuario.save() - grupo.user_set.add(usuario) + g = Group.objects.get_or_create(name=grupo)[0] + g.user_set.add(usuario) + + def cria_usuarios_padrao(self): + for group, user in ( + (SAPL_GROUP_ADMINISTRATIVO, 'operador_administrativo'), + (SAPL_GROUP_PROTOCOLO, 'operador_protocoloadm'), + (SAPL_GROUP_COMISSOES, 'operador_comissoes'), + (SAPL_GROUP_MATERIA, 'operador_materia'), + (SAPL_GROUP_NORMA, 'operador_norma'), + (SAPL_GROUP_SESSAO, 'operador_sessao'), + (SAPL_GROUP_PAINEL, 'operador_painel'), + (SAPL_GROUP_GERAL, 'operador_geral'), + ): + self.cria_usuario(user, group) def update_groups(self): print('') @@ -225,6 +226,8 @@ def update_groups(app_config, verbosity=2, interactive=True, rules = Rules(rules_patterns) rules.update_groups() + if cria_usuarios_padrao: + rules.cria_usuarios_padrao() def revision_pre_delete_signal(sender, **kwargs): diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index 6702a1f27..02c6c0dfa 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -195,7 +195,7 @@ rules_group_geral = { 'view_tabelas_auxiliares' ]), - (base.CasaLegislativa, __listdetailchange__), + (base.CasaLegislativa, __listdetailchange__ + [RP_ADD]), (base.ProblemaMigracao, []), (base.Argumento, []), (base.Constraint, []), diff --git a/sapl/sessao/forms.py b/sapl/sessao/forms.py index 9d4a7c0d3..a9c0bc164 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -15,10 +15,10 @@ from sapl.materia.models import (MateriaLegislativa, StatusTramitacao, from sapl.parlamentares.models import Parlamentar from sapl.utils import (RANGE_DIAS_MES, RANGE_MESES, MateriaPesquisaOrderingFilter, autor_label, - autor_modal) + autor_modal, timezone) from .models import (Bancada, ExpedienteMateria, Orador, OradorExpediente, - OrdemDia, SessaoPlenaria, SessaoPlenariaPresenca) + OrdemDia, SessaoPlenaria, SessaoPlenariaPresenca, TipoResultadoVotacao) def recupera_anos(): @@ -116,6 +116,7 @@ class BancadaForm(ModelForm): class ExpedienteMateriaForm(ModelForm): _model = ExpedienteMateria + data_atual = timezone.now() tipo_materia = forms.ModelChoiceField( label=_('Tipo Matéria'), @@ -128,11 +129,13 @@ class ExpedienteMateriaForm(ModelForm): label='Número Matéria', required=True) ano_materia = forms.CharField( - label='Ano Matéria', required=True) + label='Ano Matéria', + initial=int(data_atual.year), + required=True) data_ordem = forms.CharField( label='Data Sessão', - initial=datetime.now().strftime('%d/%m/%Y'), + initial=datetime.strftime(timezone.now(), '%d/%m/%Y'), widget=forms.TextInput(attrs={'readonly': 'readonly'})) class Meta: @@ -157,9 +160,10 @@ class ExpedienteMateriaForm(ModelForm): return self.instance.sessao_plenaria.data_inicio def clean(self): - super(ExpedienteMateriaForm, self).clean() + cleaned_data = super(ExpedienteMateriaForm, self).clean() + if not self.is_valid(): + return cleaned_data - cleaned_data = self.cleaned_data sessao = self.instance.sessao_plenaria try: @@ -217,7 +221,9 @@ class OrdemDiaForm(ExpedienteMateriaForm): return self.cleaned_data['numero_ordem'] def clean(self): - super(OrdemDiaForm, self).clean() + cleaned_data = super(OrdemDiaForm, self).clean() + if not self.is_valid(): + return cleaned_data return self.cleaned_data def save(self, commit=False): @@ -232,10 +238,6 @@ class PresencaForm(forms.Form): parlamentar = forms.CharField(required=False, max_length=20) -class VotacaoNominalForm(forms.Form): - pass - - class ListMateriaForm(forms.Form): error_message = forms.CharField(required=False, label='votacao_aberta') @@ -250,10 +252,17 @@ class ExpedienteForm(forms.Form): class VotacaoForm(forms.Form): - votos_sim = forms.CharField(required=True, label='Sim') - votos_nao = forms.CharField(required=True, label='Não') - abstencoes = forms.CharField(required=True, label='Abstenções') + votos_sim = forms.CharField(label='Sim') + votos_nao = forms.CharField(label='Não') + abstencoes = forms.CharField(label='Abstenções') total_votos = forms.CharField(required=False, label='total') + resultado_votacao = forms.CharField(label='Resultado da Votação') + + +class VotacaoNominalForm(forms.Form): + resultado_votacao = forms.ModelChoiceField(label='Resultado da Votação', + required=True, + queryset=TipoResultadoVotacao.objects.all()) class VotacaoEditForm(forms.Form): @@ -291,7 +300,7 @@ class SessaoPlenariaFilterSet(django_filters.FilterSet): self.form.helper.layout = Layout( Fieldset(self.titulo, row1, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @@ -367,7 +376,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet): HTML(autor_label), HTML(autor_modal), row4, row5, row6, row7, row8, row9, - form_actions(save_label='Pesquisar')) + form_actions(label='Pesquisar')) ) @@ -458,7 +467,7 @@ class ResumoOrdenacaoForm(forms.Form): Fieldset(_(''), row1, row2, row3, row4, row5, row6, row7, row8, row9, row10, - form_actions(save_label='Atualizar')) + form_actions(label='Atualizar')) ) def clean(self): diff --git a/sapl/sessao/migrations/0014_auto_20170905_1617.py b/sapl/sessao/migrations/0014_auto_20170905_1617.py new file mode 100644 index 000000000..6568001ee --- /dev/null +++ b/sapl/sessao/migrations/0014_auto_20170905_1617.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-05 16:17 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0013_merge'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tipoexpediente', + options={'ordering': ['nome'], 'verbose_name': 'Tipo de Expediente', 'verbose_name_plural': 'Tipos de Expediente'}, + ), + migrations.AlterModelOptions( + name='tiposessaoplenaria', + options={'ordering': ['nome'], 'verbose_name': 'Tipo de Sessão Plenária', 'verbose_name_plural': 'Tipos de Sessão Plenária'}, + ), + ] diff --git a/sapl/sessao/migrations/0015_sessaoplenaria_painel_aberto.py b/sapl/sessao/migrations/0015_sessaoplenaria_painel_aberto.py new file mode 100644 index 000000000..73f7b1cc2 --- /dev/null +++ b/sapl/sessao/migrations/0015_sessaoplenaria_painel_aberto.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2017-09-21 17:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sessao', '0014_auto_20170905_1617'), + ] + + operations = [ + migrations.AddField( + model_name='sessaoplenaria', + name='painel_aberto', + field=models.BooleanField(default=False, verbose_name='Painel está aberto?'), + ), + ] diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index dd136d3fd..7d87ecef2 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -76,6 +76,7 @@ class TipoSessaoPlenaria(models.Model): class Meta: verbose_name = _('Tipo de Sessão Plenária') verbose_name_plural = _('Tipos de Sessão Plenária') + ordering = ['nome'] def __str__(self): return self.nome @@ -110,6 +111,8 @@ class SessaoPlenaria(models.Model): # TODO analyze querying all hosted databases ! cod_andamento_sessao = models.PositiveIntegerField(blank=True, null=True) + painel_aberto = models.BooleanField(blank=True, default=False, + verbose_name=_('Painel está aberto?')) tipo = models.ForeignKey(TipoSessaoPlenaria, on_delete=models.PROTECT, verbose_name=_('Tipo')) @@ -264,6 +267,7 @@ class TipoExpediente(models.Model): class Meta: verbose_name = _('Tipo de Expediente') verbose_name_plural = _('Tipos de Expediente') + ordering = ['nome'] def __str__(self): return self.nome diff --git a/sapl/sessao/tests/test_sessao.py b/sapl/sessao/tests/test_sessao.py index 7142aa48f..038ad7424 100644 --- a/sapl/sessao/tests/test_sessao.py +++ b/sapl/sessao/tests/test_sessao.py @@ -2,9 +2,11 @@ import pytest from django.utils.translation import ugettext_lazy as _ from model_mommy import mommy +from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.parlamentares.models import Legislatura, Partido, SessaoLegislativa from sapl.sessao import forms -from sapl.sessao.models import SessaoPlenaria, TipoSessaoPlenaria +from sapl.sessao.models import (ExpedienteMateria, SessaoPlenaria, + TipoSessaoPlenaria) def test_valida_campos_obrigatorios_sessao_plenaria_form(): @@ -35,7 +37,8 @@ def test_sessao_plenaria_form_valido(): 'tipo': str(tipo.pk), 'sessao_legislativa': str(sessao.pk), 'data_inicio': '10/11/2017', - 'hora_inicio': '10:10' + 'hora_inicio': '10:10', + 'painel_aberto': False }) assert form.is_valid() @@ -46,11 +49,11 @@ def test_numero_duplicado_sessao_plenaria_form(): legislatura = mommy.make(Legislatura) sessao = mommy.make(SessaoLegislativa) tipo = mommy.make(TipoSessaoPlenaria) - sessao_plenaria = mommy.make(SessaoPlenaria, - legislatura=legislatura, - sessao_legislativa=sessao, - tipo=tipo, - numero=1) + mommy.make(SessaoPlenaria, + legislatura=legislatura, + sessao_legislativa=sessao, + tipo=tipo, + numero=1) form = forms.SessaoPlenariaForm(data={'legislatura': str(legislatura.pk), 'numero': '1', @@ -113,3 +116,25 @@ def test_bancada_form_datas_invalidas(): assert not form.is_valid() assert form.errors['__all__'] == [_('Data de extinção não pode ser menor ' 'que a de criação')] + + +@pytest.mark.django_db(transaction=False) +def test_expediente_materia_form_valido(): + tipo_materia = mommy.make(TipoMateriaLegislativa) + materia = mommy.make(MateriaLegislativa, tipo=tipo_materia) + + sessao = mommy.make(SessaoPlenaria) + + instance = mommy.make(ExpedienteMateria, sessao_plenaria=sessao, + materia=materia) + + form = forms.ExpedienteMateriaForm(data={'data_ordem': '28/12/2009', + 'numero_ordem': 1, + 'tipo_materia': tipo_materia.pk, + 'numero_materia': materia.numero, + 'ano_materia': materia.ano, + 'tipo_votacao': 1, + 'sessao_plenaria': sessao.pk + }, + instance=instance) + assert form.is_valid() diff --git a/sapl/sessao/tests/test_sessao_view.py b/sapl/sessao/tests/test_sessao_view.py new file mode 100644 index 000000000..082652c19 --- /dev/null +++ b/sapl/sessao/tests/test_sessao_view.py @@ -0,0 +1,49 @@ +import pytest +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ +from model_mommy import mommy + +from sapl.parlamentares.models import Legislatura, SessaoLegislativa +from sapl.sessao.models import SessaoPlenaria, TipoSessaoPlenaria + + +@pytest.mark.django_db(transaction=False) +def test_incluir_sessao_plenaria_submit(admin_client): + legislatura = mommy.make(Legislatura) + sessao = mommy.make(SessaoLegislativa) + tipo = mommy.make(TipoSessaoPlenaria, id=1) + + response = admin_client.post(reverse('sapl.sessao:sessaoplenaria_create'), + {'legislatura': str(legislatura.pk), + 'numero': '1', + 'tipo': str(tipo.pk), + 'sessao_legislativa': str(sessao.pk), + 'data_inicio': '10/11/2017', + 'hora_inicio': '10:10' + }, follow=True) + + assert response.status_code == 200 + + sessao_plenaria = SessaoPlenaria.objects.first() + assert sessao_plenaria.tipo == tipo + + +@pytest.mark.django_db(transaction=False) +def test_incluir_sessao_errors(admin_client): + + response = admin_client.post(reverse('sapl.sessao:sessaoplenaria_create'), + {'salvar': 'salvar'}, + follow=True) + + assert (response.context_data['form'].errors['legislatura'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['sessao_legislativa'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['tipo'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['numero'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['data_inicio'] == + [_('Este campo é obrigatório.')]) + assert (response.context_data['form'].errors['hora_inicio'] == + [_('Este campo é obrigatório.')]) diff --git a/sapl/sessao/urls.py b/sapl/sessao/urls.py index a28d5eb38..d9a611ba8 100644 --- a/sapl/sessao/urls.py +++ b/sapl/sessao/urls.py @@ -6,7 +6,6 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente, ExpedienteMateriaCrud, ExpedienteView, MateriaOrdemDiaCrud, MesaView, OradorCrud, OradorExpedienteCrud, PainelView, - PautaExpedienteDetail, PautaOrdemDetail, PautaSessaoDetailView, PautaSessaoListView, PesquisarPautaSessaoView, PesquisarSessaoPlenariaView, @@ -19,10 +18,10 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente, VotacaoNominalExpedienteDetailView, VotacaoNominalExpedienteEditView, VotacaoNominalExpedienteView, - VotacaoNominalView, VotacaoView, - abrir_votacao_expediente_view, - abrir_votacao_ordem_view, atualizar_mesa, - insere_parlamentar_composicao, + VotacaoNominalView, + VotacaoNominalTransparenciaDetailView, + VotacaoView, abrir_votacao, + atualizar_mesa, insere_parlamentar_composicao, mudar_ordem_materia_sessao, recuperar_materia, recuperar_numero_sessao, remove_parlamentar_composicao, @@ -60,11 +59,8 @@ urlpatterns = [ url(r'^sessao/sessao-legislativa-legislatura-ajax/', sessao_legislativa_legislatura_ajax), - url(r'^sessao/(?P\d+)/(?P\d+)/abrir-votacao-expediente$', - abrir_votacao_expediente_view, - name="abrir_votacao_exp"), - url(r'^sessao/(?P\d+)/(?P\d+)/abrir-votacao-ordem$', - abrir_votacao_ordem_view, + url(r'^sessao/(?P\d+)/(?P\d+)/abrir-votacao$', + abrir_votacao, name="abrir_votacao"), url(r'^sessao/(?P\d+)/reordenar-expediente$', reordernar_materias_expediente, @@ -100,10 +96,6 @@ urlpatterns = [ PesquisarPautaSessaoView.as_view(), name='pesquisar_pauta'), url(r'^sessao/pauta-sessao/(?P\d+)$', PautaSessaoDetailView.as_view(), name='pauta_sessao_detail'), - url(r'^sessao/pauta-sessao/(?P\d+)/expediente/$', - PautaExpedienteDetail.as_view(), name='pauta_expediente_detail'), - url(r'^sessao/pauta-sessao/(?P\d+)/ordem/$', - PautaOrdemDetail.as_view(), name='pauta_ordem_detail'), # Subnav sessão url(r'^sessao/(?P\d+)/expediente$', @@ -150,6 +142,9 @@ urlpatterns = [ VotacaoExpedienteView.as_view(), name='votacaosecretaexp'), url(r'^sessao/(?P\d+)/matexp/votsec/view/(?P\d+)/(?P\d+)$', VotacaoExpedienteEditView.as_view(), name='votacaosecretaexpedit'), + url(r'^sessao/(?P\d+)/votacao-nominal-transparencia/(?P\d+)/(?P\d+)$', + VotacaoNominalTransparenciaDetailView.as_view(), + name='votacao_nominal_transparencia'), url(r'^sessao/mudar-ordem-materia-sessao/', mudar_ordem_materia_sessao, diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index 2d8e2faea..887ade0fc 100644 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -1,4 +1,3 @@ -from datetime import datetime from re import sub from django.contrib import messages @@ -10,6 +9,7 @@ from django.db.models import Max, Q from django.forms.utils import ErrorList from django.http import JsonResponse from django.http.response import Http404, HttpResponseRedirect +from django.utils import timezone from django.utils.datastructures import MultiValueDictKeyError from django.utils.decorators import method_decorator from django.utils.html import strip_tags @@ -34,6 +34,7 @@ from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa) from sapl.sessao.apps import AppConfig from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm +from sapl.utils import show_results_filter_set from .forms import (AdicionarVariasMateriasFilterSet, ExpedienteForm, ListMateriaForm, MesaForm, OradorExpedienteForm, @@ -90,7 +91,7 @@ def verifica_presenca(request, model, spk): return True -def verifica_votacoes_abertas(request, model, pk): +def verifica_votacoes_abertas(request): votacoes_abertas = SessaoPlenaria.objects.filter( Q(ordemdia__votacao_aberta=True) | Q(expedientemateria__votacao_aberta=True)).distinct() @@ -108,35 +109,99 @@ def verifica_votacoes_abertas(request, model, pk): 'outra, termine ou feche as votações abertas.') messages.add_message(request, messages.INFO, msg) - else: - materia_votacao = model.objects.get(id=pk) - materia_votacao.votacao_aberta = True - materia_votacao.save() + return False + return True -@permission_required('sessao.change_expedientemateria') -def abrir_votacao_expediente_view(request, pk, spk): - if verifica_presenca(request, SessaoPlenariaPresenca, spk): - verifica_votacoes_abertas(request, ExpedienteMateria, pk) - return HttpResponseRedirect( - reverse('sapl.sessao:expedientemateria_list', kwargs={'pk': spk})) +def verifica_sessao_iniciada(request, spk): + sessao = SessaoPlenaria.objects.get(id=spk) + + if not sessao.iniciada or sessao.finalizada: + msg = _('Não é possível abrir matérias para votação. ' + 'Esta Sessão Plenária não foi iniciada ou está finalizada.') + messages.add_message(request, messages.INFO, msg) + return False + + return True + + +@permission_required('sessao.change_expedientemateria', + 'sessao.change_ordemdia') +def abrir_votacao(request, pk, spk): + model = None + + if 'tipo_materia' in request.GET: + if request.GET['tipo_materia'] == 'ordem': + model = OrdemDia + presenca_model = PresencaOrdemDia + redirect_url = 'ordemdia_list' + elif request.GET['tipo_materia'] == 'expediente': + model = ExpedienteMateria + presenca_model = SessaoPlenariaPresenca + redirect_url = 'expedientemateria_list' + if not model: + raise Http404 + + if (verifica_presenca(request, presenca_model, spk) and + verifica_votacoes_abertas(request) and + verifica_sessao_iniciada(request, spk)): + materia_votacao = model.objects.get(id=pk) + materia_votacao.votacao_aberta = True + sessao = SessaoPlenaria.objects.get(id=spk) + sessao.painel_aberto = True + sessao.save() + materia_votacao.save() -@permission_required('sessao.change_ordemdia') -def abrir_votacao_ordem_view(request, pk, spk): - if verifica_presenca(request, PresencaOrdemDia, spk): - verifica_votacoes_abertas(request, OrdemDia, pk) return HttpResponseRedirect( - reverse('sapl.sessao:ordemdia_list', kwargs={'pk': spk})) + reverse('sapl.sessao:' + redirect_url, kwargs={'pk': spk})) -def put_link_materia(context): +def customize_link_materia(context, pk): for i, row in enumerate(context['rows']): materia = context['object_list'][i].materia url_materia = reverse('sapl.materia:materialegislativa_detail', kwargs={'pk': materia.id}) - - context['rows'][i][1] = (row[1][0], url_materia) + numeracao = materia.numeracao_set.first() + autoria = materia.autoria_set.filter( + primeiro_autor=True).first() + autor = autoria.autor if autoria else None + num_protocolo = materia.numero_protocolo + + data_inicio_sessao = SessaoPlenaria.objects.get(id=pk).data_inicio + + tramitacao = Tramitacao.objects.filter(materia=materia, + turno__isnull=False, + data_tramitacao__lte=data_inicio_sessao + ).exclude(turno__exact='' + ).select_related( + 'materia', + 'status', + 'materia__tipo').order_by( + '-data_tramitacao' + ).first() + turno = ' ' + if tramitacao is not None: + for t in Tramitacao.TURNO_CHOICES: + if t[0] == tramitacao.turno: + turno = t[1] + break + + title_materia = '''%s
    + Processo: %s
    + Autor: %s
    + Protocolo: %s
    + Turno: %s
    + ''' % (url_materia, + row[1][0], + numeracao if numeracao else '', + autor if autor else '', + num_protocolo if num_protocolo else '', + turno) + + # Na linha abaixo, o segundo argumento é None para não colocar + # url em toda a string de title_materia + context['rows'][i][1] = (title_materia, None) return context @@ -159,7 +224,7 @@ def get_presencas_generic(model, sessao, legislatura): class MateriaOrdemDiaCrud(MasterDetailCrud): model = OrdemDia parent_field = 'sessao_plenaria' - help_path = '' + help_topic = 'sessao_plenaria_materias_ordem_dia' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -204,8 +269,7 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - - return put_link_materia(context) + return customize_link_materia(context, self.kwargs['pk']) def get_rows(self, object_list): for obj in object_list: @@ -243,7 +307,9 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): obj.resultado = '''Não há resultado''' else: url = reverse('sapl.sessao:abrir_votacao', kwargs={ - 'pk': obj.pk, 'spk': obj.sessao_plenaria_id}) + 'pk': obj.pk, + 'spk': obj.sessao_plenaria_id + }) + '?tipo_materia=ordem' if self.request.user.has_module_perms(AppConfig.label): btn_abrir = ''' @@ -285,9 +351,17 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): resultado_descricao, resultado_observacao)) else: - obj.resultado = ('%s
    %s' % - (resultado_descricao, - resultado_observacao)) + if obj.tipo_votacao == 2: + url = reverse('sapl.sessao:votacao_nominal_transparencia', + kwargs={ + 'pk': obj.sessao_plenaria_id, + 'oid': obj.pk, + 'mid': obj.materia_id}) +\ + '?&materia=expediente' + else: + obj.resultado = ('%s
    %s' % + (resultado_descricao, + resultado_observacao)) return [self._as_row(obj) for obj in object_list] @@ -312,7 +386,7 @@ def recuperar_materia(request): class ExpedienteMateriaCrud(MasterDetailCrud): model = ExpedienteMateria parent_field = 'sessao_plenaria' - help_path = '' + help_topic = 'sessao_plenaria_materia_expediente' public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): @@ -326,12 +400,13 @@ class ExpedienteMateriaCrud(MasterDetailCrud): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - return put_link_materia(context) + return customize_link_materia(context, self.kwargs['pk']) def get_rows(self, object_list): for obj in object_list: exist_resultado = obj.registrovotacao_set.filter( materia=obj.materia).exists() + if not exist_resultado: if obj.votacao_aberta: url = '' @@ -361,8 +436,10 @@ class ExpedienteMateriaCrud(MasterDetailCrud): Registrar Votação''' % (url) obj.resultado = btn_registrar else: - url = reverse('sapl.sessao:abrir_votacao_exp', kwargs={ - 'pk': obj.pk, 'spk': obj.sessao_plenaria_id}) + url = reverse('sapl.sessao:abrir_votacao', kwargs={ + 'pk': obj.pk, + 'spk': obj.sessao_plenaria_id + }) + '?tipo_materia=expediente' btn_abrir = '''Matéria não votada
    ''' if self.request.user.has_module_perms(AppConfig.label): @@ -405,11 +482,12 @@ class ExpedienteMateriaCrud(MasterDetailCrud): else: if obj.tipo_votacao == 2: url = reverse( - 'sapl.sessao:votacaonominalexpdetail', + 'sapl.sessao:votacao_nominal_transparencia', kwargs={ 'pk': obj.sessao_plenaria_id, 'oid': obj.pk, - 'mid': obj.materia_id}) + 'mid': obj.materia_id}) +\ + '?&materia=expediente' obj.resultado = ('%s
    %s' % (url, resultado_descricao, @@ -456,7 +534,7 @@ class ExpedienteMateriaCrud(MasterDetailCrud): class OradorCrud(MasterDetailCrud): model = '' parent_field = 'sessao_plenaria' - help_path = '' + help_topic = 'sessao_plenaria_oradores' public = [RP_LIST, RP_DETAIL] class ListView(MasterDetailCrud.ListView): @@ -499,14 +577,14 @@ def recuperar_numero_sessao(request): tipo__pk=request.GET['tipo'], sessao_legislativa=request.GET['sessao_legislativa']).last() except ObjectDoesNotExist: - response = JsonResponse({'numero': 1}) + numero = 1 else: if sessao: - response = JsonResponse({'numero': sessao.numero + 1}) + numero = sessao.numero + 1 else: - response = JsonResponse({'numero': 1}) + numero = 1 - return response + return JsonResponse({'numero': numero}) def sessao_legislativa_legislatura_ajax(request): @@ -523,7 +601,7 @@ def sessao_legislativa_legislatura_ajax(request): class SessaoCrud(Crud): model = SessaoPlenaria - help_path = 'sessao_plenaria' + help_topic = 'sessao_legislativa' public = [RP_DETAIL] class BaseMixin(Crud.BaseMixin): @@ -660,14 +738,6 @@ class PainelView(PermissionRequiredForAppCrudMixin, TemplateView): template_name = 'sessao/painel.html' app_label = 'painel' - def has_permission(self): - painel_aberto = AppsAppConfig.attr('painel_aberto') - - if painel_aberto and self.request.user.is_anonymous(): - return True - - return PermissionRequiredForAppCrudMixin.has_permission(self) - def get(self, request, *args, **kwargs): if request.user.is_anonymous(): self.template_name = 'painel/index.html' @@ -1084,6 +1154,14 @@ class ResumoOrdenacaoView(PermissionRequiredMixin, FormView): return HttpResponseRedirect(self.get_success_url()) +def get_turno(turno): + for i in Tramitacao.TURNO_CHOICES: + if i[0] == turno: + return str(i[1]) + else: + return '' + + class ResumoView(DetailView): template_name = 'sessao/resumo.html' model = SessaoPlenaria @@ -1171,6 +1249,10 @@ class ResumoView(DetailView): ementa = m.materia.ementa titulo = m.materia numero = m.numero_ordem + tramitacao = m.materia.tramitacao_set.last() + turno = None + if tramitacao: + turno = get_turno(tramitacao.turno) rv = m.registrovotacao_set.first() if rv: @@ -1187,9 +1269,12 @@ class ResumoView(DetailView): mat = {'ementa': ementa, 'titulo': titulo, 'numero': numero, + 'turno': turno, 'resultado': resultado, 'resultado_observacao': resultado_observacao, - 'autor': autor + 'autor': autor, + 'numero_protocolo': m.materia.numero_protocolo, + 'numero_processo': m.materia.numeracao_set.last() } materias_expediente.append(mat) @@ -1231,6 +1316,10 @@ class ResumoView(DetailView): ementa = o.materia.ementa titulo = o.materia numero = o.numero_ordem + tramitacao = o.materia.tramitacao_set.last() + turno = None + if tramitacao: + turno = get_turno(tramitacao.turno) # Verificar resultado rv = o.registrovotacao_set.filter(materia=o.materia).first() @@ -1248,9 +1337,12 @@ class ResumoView(DetailView): mat = {'ementa': ementa, 'titulo': titulo, 'numero': numero, + 'turno': turno, 'resultado': resultado, 'resultado_observacao': resultado_observacao, - 'autor': autor + 'autor': autor, + 'numero_protocolo': o.materia.numero_protocolo, + 'numero_processo': o.materia.numeracao_set.last() } materias_ordem.append(mat) @@ -1611,6 +1703,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): template_name = 'sessao/votacao/nominal.html' ordem = None expediente = None + form_class = VotacaoNominalForm def get(self, request, *args, **kwargs): if self.ordem: @@ -1659,33 +1752,28 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): materia_votacao.materia.ementa))} context = {'materia': materia, 'object': self.get_object(), 'parlamentares': self.get_parlamentares(presentes), - 'tipos': self.get_tipos_votacao(), + 'form': self.get_form(), 'total': total} return self.render_to_response(context) def post(self, request, *args, **kwargs): self.object = self.get_object() + form = self.get_form() if self.ordem: ordem_id = kwargs['oid'] try: - ordem = OrdemDia.objects.get(id=ordem_id) + materia_votacao = OrdemDia.objects.get(id=ordem_id) except ObjectDoesNotExist: raise Http404() - - materia_votacao = ordem - elif self.expediente: expediente_id = kwargs['oid'] try: - expediente = ExpedienteMateria.objects.get(id=expediente_id) + materia_votacao = ExpedienteMateria.objects.get(id=expediente_id) except ObjectDoesNotExist: raise Http404() - materia_votacao = expediente - - form = VotacaoNominalForm(request.POST) if 'cancelar-votacao' in request.POST: fechar_votacao_materia(materia_votacao) @@ -1702,19 +1790,20 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): voto = v[0] parlamentar_id = v[1] - if(voto == 'Sim'): + if voto == 'Sim': votos_sim += 1 - elif(voto == 'Não'): + elif voto == 'Não': votos_nao += 1 - elif(voto == 'Abstenção'): + elif voto == 'Abstenção': abstencoes += 1 - elif(voto == 'Não Votou'): + elif voto == 'Não Votou': nao_votou += 1 # Caso todas as opções sejam 'Não votou', fecha a votação if nao_votou == len(request.POST.getlist('voto_parlamentar')): - fechar_votacao_materia(materia_votacao) - return self.form_valid(form) + form.add_error(None, 'Não é possível finalizar a votação sem '\ + 'nenhum voto') + return self.form_invalid(form) if self.ordem: votacao = RegistroVotacao.objects.filter( @@ -1731,7 +1820,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): votacao.numero_votos_sim = votos_sim votacao.numero_votos_nao = votos_nao votacao.numero_abstencoes = abstencoes - votacao.observacao = request.POST['observacao'] + votacao.observacao = request.POST.get('observacao', None) if self.ordem: votacao.materia_id = ordem.materia.id @@ -1740,8 +1829,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): votacao.materia_id = expediente.materia.id votacao.expediente_id = expediente_id - votacao.tipo_resultado_votacao_id = int( - request.POST['resultado_votacao']) + votacao.tipo_resultado_votacao = form.cleaned_data['resultado_votacao'] votacao.save() for votos in request.POST.getlist('voto_parlamentar'): @@ -1763,8 +1851,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): voto_parlamentar.votacao_id = votacao.id voto_parlamentar.save() - resultado = TipoResultadoVotacao.objects.get( - id=request.POST['resultado_votacao']) + resultado = form.cleaned_data['resultado_votacao'] materia_votacao.resultado = resultado.nome materia_votacao.votacao_aberta = False @@ -1783,9 +1870,35 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): expediente=expediente, votacao__isnull=True).delete() return self.form_valid(form) + else: return self.form_invalid(form) + def form_invalid(self, form): + errors_tuple = [(form[e].label, form.errors[e]) for e in form.errors if e in form.fields] + error_message = '''
      ''' + for e in errors_tuple: + error_message += '''
    • %s: %s
    • ''' % (e[0], e[1][0]) + for e in form.non_field_errors(): + error_message += '''
    • %s
    • ''' % e + error_message += '''
    ''' + + messages.add_message(self.request, messages.ERROR, error_message) + + if self.ordem: + view = 'sapl.sessao:votacaonominal' + elif self.expediente: + view = 'sapl.sessao:votacaonominalexp' + else: + view = None + + return HttpResponseRedirect(reverse( + view, + kwargs={'pk': self.kwargs['pk'], + 'oid': self.kwargs['oid'], + 'mid': self.kwargs['mid']})) + + def get_parlamentares(self, presencas): self.object = self.get_object() @@ -1808,10 +1921,6 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): else: yield [parlamentar, voto.voto] - def get_tipos_votacao(self): - for tipo in TipoResultadoVotacao.objects.all(): - yield tipo - def get_success_url(self): pk = self.kwargs['pk'] @@ -1869,11 +1978,27 @@ class VotacaoNominalEditAbstract(SessaoPermissionMixin): ' ', ' ', strip_tags(ementa))} context.update({'materia': materia}) + votosSim = votosNao = abstencoes = naoRegistrados = 0 + for v in votos: + if v.voto == 'Sim': + votosSim += 1 + elif v.voto == 'Não': + votosNao += 1 + elif v.voto == 'Abstenção': + abstencoes += 1 + elif v.voto == 'Não Votou': + naoRegistrados += 1 + + list_contagem = {'votosSim': votosSim, 'votosNao': votosNao, 'abstencoes': abstencoes, + 'naoRegistrados': naoRegistrados} + + context.update({'contagem': list_contagem}) + votacao_existente = {'observacao': sub( ' ', ' ', strip_tags(votacao.observacao)), 'resultado': votacao.tipo_resultado_votacao.nome, 'tipo_resultado': - votacao.tipo_resultado_votacao_id} + votacao.tipo_resultado_votacao_id} context.update({'votacao': votacao_existente, 'tipos': self.get_tipos_votacao()}) @@ -1940,6 +2065,45 @@ class VotacaoNominalExpedienteEditView(VotacaoNominalEditAbstract): ordem = False +class VotacaoNominalTransparenciaDetailView(TemplateView): + template_name = 'sessao/votacao/nominal_transparencia.html' + + def get_context_data(self, **kwargs): + context = super(VotacaoNominalTransparenciaDetailView, + self).get_context_data(**kwargs) + + materia_votacao = self.request.GET.get('materia', None) + + if materia_votacao == 'ordem': + votacao = RegistroVotacao.objects.get(ordem=self.kwargs['oid']) + if materia_votacao == 'expediente': + votacao = RegistroVotacao.objects.get(expediente=self.kwargs['oid']) + else: + raise Http404() + + context['votacao'] = votacao + + voto_parlamentar = VotoParlamentar.objects.filter( + votacao=votacao) + + context['voto_parlamentar'] = voto_parlamentar + + votacao_existente = {'observacao': sub( + ' ', ' ', strip_tags(votacao.observacao)), + 'resultado': votacao.tipo_resultado_votacao.nome, + 'tipo_resultado': + votacao.tipo_resultado_votacao_id} + context.update({'resultado_votacao': votacao_existente, + 'tipos': self.get_tipos_votacao()}) + + return context + + def get_tipos_votacao(self): + for tipo in TipoResultadoVotacao.objects.all(): + yield tipo + + + class VotacaoNominalExpedienteDetailView(DetailView): template_name = 'sessao/votacao/nominal_detail.html' @@ -1988,9 +2152,9 @@ class VotacaoNominalExpedienteDetailView(DetailView): class VotacaoExpedienteView(SessaoPermissionMixin): - ''' + """ Votação Simbólica e Secreta - ''' + """ template_name = 'sessao/votacao/votacao.html' form_class = VotacaoForm @@ -2109,9 +2273,9 @@ class VotacaoExpedienteView(SessaoPermissionMixin): class VotacaoExpedienteEditView(SessaoPermissionMixin): - ''' + """ Votação Simbólica e Secreta - ''' + """ template_name = 'sessao/votacao/votacao_edit.html' form_class = VotacaoEditForm @@ -2243,7 +2407,11 @@ class PautaSessaoDetailView(DetailView): ementa = m.materia.ementa titulo = m.materia numero = m.numero_ordem - situacao = m.materia.tramitacao_set.last().status + + ultima_tramitacao = m.materia.tramitacao_set.last() + + situacao = ultima_tramitacao.status if ultima_tramitacao else None + if situacao is None: situacao = _("Não informada") rv = m.registrovotacao_set.all() @@ -2257,7 +2425,7 @@ class PautaSessaoDetailView(DetailView): autoria = Autoria.objects.filter(materia_id=m.materia_id) autor = [str(x.autor) for x in autoria] - mat = {'id': m.id, + mat = {'id': m.materia_id, 'ementa': ementa, 'titulo': titulo, 'numero': numero, @@ -2300,21 +2468,27 @@ class PautaSessaoDetailView(DetailView): ementa = o.materia.ementa titulo = o.materia numero = o.numero_ordem - situacao = o.materia.tramitacao_set.last().status + + ultima_tramitacao = o.materia.tramitacao_set.last() + + situacao = ultima_tramitacao.status if ultima_tramitacao else None + if situacao is None: situacao = _("Não informada") # Verificar resultado rv = o.registrovotacao_set.all() if rv: resultado = rv[0].tipo_resultado_votacao.nome + resultado_observacao = rv[0].observacao else: resultado = _('Matéria não votada') + resultado_observacao = _(' ') autoria = Autoria.objects.filter( materia_id=o.materia_id) autor = [str(x.autor) for x in autoria] - mat = {'id': o.id, + mat = {'id': o.materia_id, 'ementa': ementa, 'titulo': titulo, 'numero': numero, @@ -2331,46 +2505,6 @@ class PautaSessaoDetailView(DetailView): return self.render_to_response(context) -class PautaExpedienteDetail(DetailView): - template_name = "sessao/pauta/expediente.html" - model = SessaoPlenaria - - def get(self, request, *args, **kwargs): - pk = self.kwargs['pk'] - - expediente = ExpedienteMateria.objects.get(id=pk) - doc_ace = DocumentoAcessorio.objects.filter( - materia=expediente.materia) - tramitacao = Tramitacao.objects.filter( - materia=expediente.materia) - - return self.render_to_response( - {'expediente': expediente, - 'doc_ace': doc_ace, - 'tramitacao': tramitacao}) - - -class PautaOrdemDetail(DetailView): - template_name = "sessao/pauta/ordem.html" - model = SessaoPlenaria - - def get(self, request, *args, **kwargs): - pk = self.kwargs['pk'] - - ordem = OrdemDia.objects.get(id=pk) - norma = NormaJuridica.objects.filter( - materia=ordem.materia) - doc_ace = DocumentoAcessorio.objects.filter( - materia=ordem.materia) - tramitacao = Tramitacao.objects.filter( - materia=ordem.materia) - - return self.render_to_response( - {'ordem': ordem, - 'norma': norma, - 'doc_ace': doc_ace, - 'tramitacao': tramitacao}) - class PesquisarSessaoPlenariaView(FilterView): model = SessaoPlenaria @@ -2383,7 +2517,8 @@ class PesquisarSessaoPlenariaView(FilterView): kwargs = {'data': self.request.GET or None} - qs = self.get_queryset() + qs = self.get_queryset().select_related( + 'tipo', 'sessao_legislativa', 'legislatura') qs = qs.distinct().order_by( '-legislatura__numero', '-data_inicio', '-numero') @@ -2414,7 +2549,7 @@ class PesquisarSessaoPlenariaView(FilterView): # Provavelmente você criou um novo campo no Form/FilterSet # Então a ordem da URL está diferente data = self.filterset.data - if (data and data.get('data_inicio__year') is not None): + if data and data.get('data_inicio__year') is not None: url = "&" + str(self.request.environ['QUERY_STRING']) if url.startswith("&page"): ponto_comeco = url.find('data_inicio__year=') - 1 @@ -2428,6 +2563,9 @@ class PesquisarSessaoPlenariaView(FilterView): numero_res=len(self.object_list) ) + context['show_results'] = show_results_filter_set( + self.request.GET.copy()) + return self.render_to_response(context) @@ -2526,7 +2664,7 @@ class AdicionarVariasMateriasExpediente(PermissionRequiredForAppCrudMixin, expediente.numero_ordem = posicao else: expediente.numero_ordem = 1 - expediente.data_ordem = datetime.now() + expediente.data_ordem = timezone.now() expediente.tipo_votacao = request.POST['tipo_votacao_%s' % m] expediente.save() @@ -2595,7 +2733,7 @@ class AdicionarVariasMateriasOrdemDia(AdicionarVariasMateriasExpediente): ordem_dia.numero_ordem = posicao else: ordem_dia.numero_ordem = 1 - ordem_dia.data_ordem = datetime.now() + ordem_dia.data_ordem = timezone.now() ordem_dia.tipo_votacao = tipo_votacao ordem_dia.save() diff --git a/sapl/settings.py b/sapl/settings.py index 274e2ed0a..5c933d57f 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -209,7 +209,7 @@ LANGUAGES = ( TIME_ZONE = 'America/Sao_Paulo' USE_I18N = True USE_L10N = True -USE_TZ = False +USE_TZ = True # DATE_FORMAT = 'N j, Y' DATE_FORMAT = 'd/m/Y' SHORT_DATE_FORMAT = 'd/m/Y' @@ -299,3 +299,8 @@ def excepthook(*args): 'Uncaught exception:', exc_info=args) # sys.excepthook = excepthook + +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', # default + 'sapl.hashers.ZopeSHA1PasswordHasher', +] diff --git a/sapl/static/styles/app.scss b/sapl/static/styles/app.scss index 56b5a002f..8d50f217d 100644 --- a/sapl/static/styles/app.scss +++ b/sapl/static/styles/app.scss @@ -123,6 +123,13 @@ h6, .h6 { display: block; } } + +.help-block-danger { + margin: $grid-gutter-width / 2; + padding: $grid-gutter-width / 2; + border: 2px dashed #f00; +} + .controls-radio-checkbox { padding: 0px; border: 1px solid #d6e1e5; @@ -212,6 +219,18 @@ body { } } +label { + margin-bottom: 0; + line-height: 1; +} +.control-label { + margin: 0; +} +.form-control-static { + padding-top: 0; + min-height: auto; +} + // #### pagination ######################################## .pagination { padding-top: 25px; @@ -493,3 +512,25 @@ p { width: 1070px; } } + +/* Estilização da Listagem de Votos em sessões plenárias */ + +#styleparlamentar { + border: 0px solid #d6e1e5; + border-top-color: rgb(214, 225, 229); + border-right-color: rgb(214, 225, 229); + border-bottom-color: rgb(214, 225, 229); + border-left-color: rgb(214, 225, 229); + border-image-source: initial; + border-image-slice: initial; + border-image-repeat: initial; + font-size: 16px; + line-height: 1.467; + padding: 7px 12px; + height: 40px; + -webkit-appearance: none; + border-radius: 4px; + -webkit-box-shadow: none; + box-shadow: none; + margin-left: 1.0em; +} diff --git a/sapl/templates/ajuda.html b/sapl/templates/ajuda.html index b90f4db25..df849303d 100644 --- a/sapl/templates/ajuda.html +++ b/sapl/templates/ajuda.html @@ -7,141 +7,141 @@
    diff --git a/sapl/templates/ajuda/acessando_alimenta.html b/sapl/templates/ajuda/acessando_alimenta.html index ef4248d49..4f888bb30 100644 --- a/sapl/templates/ajuda/acessando_alimenta.html +++ b/sapl/templates/ajuda/acessando_alimenta.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/acomp_materia.html b/sapl/templates/ajuda/acomp_materia.html index d8dcb77fc..766e40c94 100644 --- a/sapl/templates/ajuda/acomp_materia.html +++ b/sapl/templates/ajuda/acomp_materia.html @@ -24,11 +24,11 @@ poderá ser feito clicando-se no link apropriado, que consta no e-mail rec informando sobre a movimentação da matéria.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/anexos.html b/sapl/templates/ajuda/anexos.html index 0802dbb66..cb073465e 100644 --- a/sapl/templates/ajuda/anexos.html +++ b/sapl/templates/ajuda/anexos.html @@ -1,7 +1,7 @@ -{% extends "base.html" % -{% load "i18n" % +{% extends "base.html" %} +{% load i18n crispy_forms_tags %} -{% block base_content % +{% block base_content %}
    @@ -90,9 +90,9 @@ banco de dados.
    Qualquer dúvida, entre em contato conosco.

    - Anterior | + Anterior | - Índice + Índice
    diff --git a/sapl/templates/ajuda/autor.html b/sapl/templates/ajuda/autor.html index 3c93e0503..13efa4e29 100644 --- a/sapl/templates/ajuda/autor.html +++ b/sapl/templates/ajuda/autor.html @@ -163,11 +163,11 @@ retorna a tela com a relação de autores já cadastrado.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cadastro_comissoes.html b/sapl/templates/ajuda/cadastro_comissoes.html index 236d98ecd..76b2b68b2 100644 --- a/sapl/templates/ajuda/cadastro_comissoes.html +++ b/sapl/templates/ajuda/cadastro_comissoes.html @@ -601,11 +601,11 @@ A função “Início” retorna a tela de cadastramento da Comissão.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cadastro_materia.html b/sapl/templates/ajuda/cadastro_materia.html index bb794b20d..aaaaaf062 100644 --- a/sapl/templates/ajuda/cadastro_materia.html +++ b/sapl/templates/ajuda/cadastro_materia.html @@ -504,18 +504,22 @@ inicial do sistema.
    Selecione a Matéria já cadastrada para continuar alimentando as outras informações sobre a matéria tais como: - Despacho Inicial, Autoria, -Legislação Citada, Anexada, -Tramitação, Relatoria, Numeração e - Documentos Acessórios. + Despacho Inicial, +Autoria, +Legislação Citada, +Anexada, +Tramitação, +Relatoria, +Numeração e + Documentos Acessórios.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cadastro_mesa_diretora.html b/sapl/templates/ajuda/cadastro_mesa_diretora.html index 2197a4f7e..a0b658b78 100644 --- a/sapl/templates/ajuda/cadastro_mesa_diretora.html +++ b/sapl/templates/ajuda/cadastro_mesa_diretora.html @@ -75,11 +75,11 @@ Acione a função Continuar para repetir a operação para outros parlamentares.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cadastro_parlamentares.html b/sapl/templates/ajuda/cadastro_parlamentares.html index 44a1ab9b2..1818c5218 100644 --- a/sapl/templates/ajuda/cadastro_parlamentares.html +++ b/sapl/templates/ajuda/cadastro_parlamentares.html @@ -463,15 +463,16 @@ Quando for informado OK será enviada a mensagem !Parlamentar excluído com sucesso!
    Selecione um Parlamentar já cadastrado para continuar alimentando outras informações tais como: -Mandato, -Filiação Partidária e Dependente.
    +Mandato, +Filiação Partidária e +Dependente.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cargo_comissao.html b/sapl/templates/ajuda/cargo_comissao.html index 7dd888c6f..53db3dbc4 100644 --- a/sapl/templates/ajuda/cargo_comissao.html +++ b/sapl/templates/ajuda/cargo_comissao.html @@ -49,11 +49,11 @@ Quando for informado OK será enviada a mensagem !Cargo Comissão excluído com sucesso!.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/cargo_mesa.html b/sapl/templates/ajuda/cargo_mesa.html index 38ef6a986..2abe38ab7 100644 --- a/sapl/templates/ajuda/cargo_mesa.html +++ b/sapl/templates/ajuda/cargo_mesa.html @@ -51,11 +51,11 @@ Quando for informado OK será enviada a mensagem !Cargo excluído com sucesso!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/casa_legislativa.html b/sapl/templates/ajuda/casa_legislativa.html index 02afdc5b4..4472b16a7 100644 --- a/sapl/templates/ajuda/casa_legislativa.html +++ b/sapl/templates/ajuda/casa_legislativa.html @@ -220,11 +220,11 @@ A Busca por palavra chave só pesquisa o cadastro de matérias legis Quando a pesquisa é realizada dentro do módulo de atualização das informações, o operador tem a possibilidade de fazer alterações nas informações das matérias que resultou da pesquisa.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/coligacao.html b/sapl/templates/ajuda/coligacao.html index 4d289b25b..56c23227b 100644 --- a/sapl/templates/ajuda/coligacao.html +++ b/sapl/templates/ajuda/coligacao.html @@ -173,11 +173,11 @@ retorna a tela com a relação de Coligações já cadastradas.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/consultas.html b/sapl/templates/ajuda/consultas.html index dd6fa162b..66e9574b8 100644 --- a/sapl/templates/ajuda/consultas.html +++ b/sapl/templates/ajuda/consultas.html @@ -108,9 +108,9 @@ Parlamentar.
    Ao selecionar este módulo Protocolo Geral é exibida uma tela com o seguinte menu de opções:


    Também, apresenta uma tela com campos a serem preenchidos com argumentos de pesquisa sobre registros de protocolo.

    @@ -176,11 +176,11 @@ Ex: Para que se possa encontrar protocolos que mencionem a palavra 'asfalto' no Apresenta o resultado da pesquisa feita. Além disso, exibe o seguinte menu de opções:


    O conteúdo a seguir é a lista do resultado da pesquisa feita.

    @@ -707,9 +707,9 @@ Preenchimento


    - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/dependentes.html b/sapl/templates/ajuda/dependentes.html index 4ab425b24..336408889 100644 --- a/sapl/templates/ajuda/dependentes.html +++ b/sapl/templates/ajuda/dependentes.html @@ -166,11 +166,11 @@ A função “Início” retorna a tela com os dados básicos do parlamentar.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/despacho_autoria.html b/sapl/templates/ajuda/despacho_autoria.html index 94ebbef9a..f1e2ff2b8 100644 --- a/sapl/templates/ajuda/despacho_autoria.html +++ b/sapl/templates/ajuda/despacho_autoria.html @@ -173,11 +173,11 @@ retorna a tela com a relação de autores já designados para a matéria.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/envio_proposicao.html b/sapl/templates/ajuda/envio_proposicao.html index 977893b05..b943dc469 100644 --- a/sapl/templates/ajuda/envio_proposicao.html +++ b/sapl/templates/ajuda/envio_proposicao.html @@ -144,11 +144,11 @@ no campo subjacente, ou clique no botão

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/filiacoes_partidarias.html b/sapl/templates/ajuda/filiacoes_partidarias.html index 2eabd2e08..87bdc2e3f 100644 --- a/sapl/templates/ajuda/filiacoes_partidarias.html +++ b/sapl/templates/ajuda/filiacoes_partidarias.html @@ -131,11 +131,11 @@ A função “Início” retorna a tela com os dados básicos do parlamentar.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/fim_relatoria.html b/sapl/templates/ajuda/fim_relatoria.html index 9be3be776..932f576a9 100644 --- a/sapl/templates/ajuda/fim_relatoria.html +++ b/sapl/templates/ajuda/fim_relatoria.html @@ -35,11 +35,11 @@ excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/gerenciamento_usuarios.html b/sapl/templates/ajuda/gerenciamento_usuarios.html index 8ed88c7e2..6fd87ce76 100644 --- a/sapl/templates/ajuda/gerenciamento_usuarios.html +++ b/sapl/templates/ajuda/gerenciamento_usuarios.html @@ -249,11 +249,11 @@ manutenção.



    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/glossario.html b/sapl/templates/ajuda/glossario.html index 1710c323a..3a1d6eccd 100644 --- a/sapl/templates/ajuda/glossario.html +++ b/sapl/templates/ajuda/glossario.html @@ -51,11 +51,11 @@ localização de informações específicas. (Dicionário Aurélio).

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/legislacao_cita_matanexada.html b/sapl/templates/ajuda/legislacao_cita_matanexada.html index e579136f4..9bc4af6c5 100644 --- a/sapl/templates/ajuda/legislacao_cita_matanexada.html +++ b/sapl/templates/ajuda/legislacao_cita_matanexada.html @@ -292,11 +292,11 @@ A função “Início” retorna a tela com os dados da matéria legislativa selecionada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/legislatura.html b/sapl/templates/ajuda/legislatura.html index 20d8ea259..2ab59f6ca 100644 --- a/sapl/templates/ajuda/legislatura.html +++ b/sapl/templates/ajuda/legislatura.html @@ -129,11 +129,11 @@ Quando for informado OK será enviada a mensagem !Legislatura excluída com sucesso!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/lexml.html b/sapl/templates/ajuda/lexml.html index e0198089b..f37122f66 100644 --- a/sapl/templates/ajuda/lexml.html +++ b/sapl/templates/ajuda/lexml.html @@ -16,11 +16,11 @@

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/mandatos_parlamentar.html b/sapl/templates/ajuda/mandatos_parlamentar.html index b885aa444..66ffc9308 100644 --- a/sapl/templates/ajuda/mandatos_parlamentar.html +++ b/sapl/templates/ajuda/mandatos_parlamentar.html @@ -194,11 +194,11 @@ A função “Início” retorna a tela com os dados básicos do parlamentar.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/materia_anexada.html b/sapl/templates/ajuda/materia_anexada.html index fd549549a..031c43227 100644 --- a/sapl/templates/ajuda/materia_anexada.html +++ b/sapl/templates/ajuda/materia_anexada.html @@ -177,11 +177,11 @@ retorna a tela com os dados da matéria legislativa selecionada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/modulo_comissoes.html b/sapl/templates/ajuda/modulo_comissoes.html index 1b5e72448..a469e9bef 100644 --- a/sapl/templates/ajuda/modulo_comissoes.html +++ b/sapl/templates/ajuda/modulo_comissoes.html @@ -17,14 +17,14 @@ esta transação, que são:
    - Tabelas Auxiliares



    @@ -37,11 +37,11 @@ data a partir da qual ela ficou inativa; devendo, nesse caso, ser posterior a da A exclusão da comissão somente será possível se não tiver havido qualquer tramitação de matéria para ela. Então, deve-se exclui-la antes da tabela de unidades de tramitação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/modulo_mesa_diretora.html b/sapl/templates/ajuda/modulo_mesa_diretora.html index 09e9bbe7c..5b2cce0ca 100644 --- a/sapl/templates/ajuda/modulo_mesa_diretora.html +++ b/sapl/templates/ajuda/modulo_mesa_diretora.html @@ -16,20 +16,20 @@ transação, que são:
    - Tabelas Auxiliares



    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/modulo_norma_juridica.html b/sapl/templates/ajuda/modulo_norma_juridica.html index d84e09851..c8662e517 100644 --- a/sapl/templates/ajuda/modulo_norma_juridica.html +++ b/sapl/templates/ajuda/modulo_norma_juridica.html @@ -16,16 +16,16 @@ transação, que são:

    - Tabela Auxiliar


    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/modulo_parlamentares.html b/sapl/templates/ajuda/modulo_parlamentares.html index 42df0acf7..0e5770641 100644 --- a/sapl/templates/ajuda/modulo_parlamentares.html +++ b/sapl/templates/ajuda/modulo_parlamentares.html @@ -17,23 +17,23 @@ transação, que são:
    - Tabelas Auxiliares


    Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/modulo_tramitacao_materias.html b/sapl/templates/ajuda/modulo_tramitacao_materias.html index fb70c7b05..81fc0c978 100644 --- a/sapl/templates/ajuda/modulo_tramitacao_materias.html +++ b/sapl/templates/ajuda/modulo_tramitacao_materias.html @@ -19,40 +19,40 @@ esta transação, que são:
    - Tabelas Auxiliares



    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/nivel_instrucao.html b/sapl/templates/ajuda/nivel_instrucao.html index d8875f61c..84372818a 100644 --- a/sapl/templates/ajuda/nivel_instrucao.html +++ b/sapl/templates/ajuda/nivel_instrucao.html @@ -43,11 +43,11 @@ instrução  foi excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/norma_juridica.html b/sapl/templates/ajuda/norma_juridica.html index 8d434029a..dd4e84ed8 100644 --- a/sapl/templates/ajuda/norma_juridica.html +++ b/sapl/templates/ajuda/norma_juridica.html @@ -331,11 +331,11 @@ Quando for informado OK será enviada a mensagem !Norma Jurídica excluída com sucesso!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/numeracao_docsacess.html b/sapl/templates/ajuda/numeracao_docsacess.html index 7291cd56c..0e932e08c 100644 --- a/sapl/templates/ajuda/numeracao_docsacess.html +++ b/sapl/templates/ajuda/numeracao_docsacess.html @@ -544,11 +544,11 @@ Acessórios, acione a função “Documentos Acessórios”.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/ordem_dia.html b/sapl/templates/ajuda/ordem_dia.html index 29436c082..bdf59ef8c 100644 --- a/sapl/templates/ajuda/ordem_dia.html +++ b/sapl/templates/ajuda/ordem_dia.html @@ -162,11 +162,11 @@ Quando for informado OK será enviada a mensagem !Matéria excluída com sucesso da ordem do dia!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/orgao.html b/sapl/templates/ajuda/orgao.html index d1fa8a9c9..ce3dae02b 100644 --- a/sapl/templates/ajuda/orgao.html +++ b/sapl/templates/ajuda/orgao.html @@ -1,4 +1,4 @@ -{% extends "base.html% } +{% extends "base.html" %} {% load i18n %} {% block base_content %} @@ -141,11 +141,11 @@ retorna a tela com a relação de órgãos já cadastrados.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/origem.html b/sapl/templates/ajuda/origem.html index 0313b4bf8..f3f4ba5f7 100644 --- a/sapl/templates/ajuda/origem.html +++ b/sapl/templates/ajuda/origem.html @@ -89,11 +89,11 @@ retorna a tela com a relação de origem já cadastrada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/partidos.html b/sapl/templates/ajuda/partidos.html index fc3f1651d..ed75351a8 100644 --- a/sapl/templates/ajuda/partidos.html +++ b/sapl/templates/ajuda/partidos.html @@ -124,11 +124,11 @@ retorna à tela com a relação de Partidos já cadastrados.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/periodo_composicao_comissao.html b/sapl/templates/ajuda/periodo_composicao_comissao.html index 4b7d05b94..c93feb4bf 100644 --- a/sapl/templates/ajuda/periodo_composicao_comissao.html +++ b/sapl/templates/ajuda/periodo_composicao_comissao.html @@ -103,11 +103,11 @@ retorna a tela com a relação de períodos de comissão já cadastradas.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/proposicao.html b/sapl/templates/ajuda/proposicao.html index 9e3dc2e45..bc5f2c926 100644 --- a/sapl/templates/ajuda/proposicao.html +++ b/sapl/templates/ajuda/proposicao.html @@ -10,11 +10,11 @@

    Proposição - elaboração / atualização

    A tela exibe um conjunto de dados básicos para a elaboração de uma nova proposição ou de um documento acessório. No caso de uma proposição ou se um documento estiver sido consultado, o sapl permitirá a sua atualização . Os dados básicos são os seguintes:

    Tipo - combo para a seleção do tipo de proposição legislativa ou documento acessório.

    Descrição - deve conter uma breve descrição sobre a proposição ou documento que está sendo elaborado ou atualizado.

    Matéria Legislativa, número e ano - essas informações somente deverão ser indicadas quando tratar-se de um documento do tipo parecer, caso contrário, o sistema mantém os respectivos campos inibidos, ou seja, sem acesso. Caso tenham que ser informados, a matéria legislativa já deverá ter sido cadastrada no sistema.

    Criar texto em XML ou Carregar Arquivo Externo - indicar assinalando uma das opções. Caso seja indicada a opção - Criar texto em XML - o sapl irá disponibilizar um editor de textos - orientado por um modelo próprio, o qual deve ter sido previamente associado - para o tipo de documento a ser digitado, ou seja, com a formatação final que foi previamente ajustada para o tipo de documento. Se a segunda opção for a indicada, então o sapl irá habilitar o botão Arquivo de modo a permitir realizar a carga de um documento previamente digitado; e, neste caso, não entrará no mérito do modelo e nem da respectiva folha de estilo - formato final - do documento uploaded.

    Ao final, deve ser acionado o botão salvar dados básicos e criar texto integral para dar seguimento a execução da função. Se a opção foi a de criar o texto, o sapl abrirá o editor de textos para que o documento seja digitado. No caso do upload de documento já digitado, a função emitirá mensagem de finalização e de envio para protocolo informando o número de identificação gerado de modo automático.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/proposicao_editor.html b/sapl/templates/ajuda/proposicao_editor.html index 2ad5faa31..054c61aaf 100644 --- a/sapl/templates/ajuda/proposicao_editor.html +++ b/sapl/templates/ajuda/proposicao_editor.html @@ -10,11 +10,11 @@

    Proposição - edição / atualização

    A tela exibe um conjunto de dados básicos para a atualização da proposição ou documento acessório. A seguir, as situações possíveis:

    - Proposição ainda não enviada - Neste caso o autor tem disponível as seguintes ações: Editar o Texto Integral, sendo que, antes da edição, os dados básicos eventualmente modificados devem ser salvos; retornar a tela anterior, clicando em Início; clicar em Salvar para salvar os dados básicos; ou, clicar em Excluir para excluir a proposição ou o documento acessório, conforme o caso.

    - Proposição já enviada, porém, ainda não recebida - Neste estado, as ações disponíveis são: clicar em Imprimir Recibo para imprimir o recibo referente ao envio para protocolo; clicar em Imprimir Texto Integral para imprimir a proposição ou documento; clicar em Início para retornar a tela anterior; ou, ainda, clicar em retomar a proposição enviada para retomar a proposição e realizar modificações no seu texto. Neste caso, o Sapl irá desprezar o número anteriormente gerado, no momento do envio, e ao ser reenviada a proposição, gerar nova numeração. Após essa ação, a proposição ou documento retorna a situação de proposição ainda não enviada.

    - Proposição já enviada e recebida - Nesta situação, todas as ações relativas a situação anterior - de recebida - estão disponíveis, exceto que não será permitida retomar a proposição e nem realizar alterações nos dados básicos.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/proposicao_legislativa.html b/sapl/templates/ajuda/proposicao_legislativa.html index 2c107fe46..b0ca6d600 100644 --- a/sapl/templates/ajuda/proposicao_legislativa.html +++ b/sapl/templates/ajuda/proposicao_legislativa.html @@ -11,11 +11,11 @@ Esta tela exibe uma lista de proposições legislativas, que foram, ou estão sendo, elaboradas por um usuário com perfil de autor. Contém as seguintes colunas de dados:

    Data de envio - refere-se a data na qual o autor enviou a proposição para protocolo. Caso ainda, a mesma não tenha sido enviada, o sistema exibe a palavra em elaboração. Essa coluna possui um link para a tela que contém o detalhamento da proposição referida, o qual é acionado quando se clica sobre o mesmo.

    Tipo - indica o tipo de proposição.

    Descrição - contém a descrição da proposição.

    Recebida - sim ou não, indicando a informação de recebimento ou não da proposição pela Secretaria Legislativa da Casa, para que seja protocolada, no caso da Proposição já ter sido enviada.

    Além das colunas de dados acima, possui os seguintes links de acionamento de funcionalidades:

    Próxima página - avança para apresentar mais 8 proposições - paginando para frente, no caso de haver mais proposições.

    Página anterior - retrocede para apresentar as 8 proposições anteriores - paginando para trás, se houver.

    Elaborar nova proposição - link que, ao ser acionado, direciona o usuário para a tela de inclusão de nova proposição.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/protocolo_administrativo.html b/sapl/templates/ajuda/protocolo_administrativo.html index ef4248d49..5d43134fe 100644 --- a/sapl/templates/ajuda/protocolo_administrativo.html +++ b/sapl/templates/ajuda/protocolo_administrativo.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/protocolo_anular.html b/sapl/templates/ajuda/protocolo_anular.html index ef4248d49..5d43134fe 100644 --- a/sapl/templates/ajuda/protocolo_anular.html +++ b/sapl/templates/ajuda/protocolo_anular.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/protocolo_geral.html b/sapl/templates/ajuda/protocolo_geral.html index b89fe5472..15393822b 100644 --- a/sapl/templates/ajuda/protocolo_geral.html +++ b/sapl/templates/ajuda/protocolo_geral.html @@ -162,11 +162,11 @@ Quando for informado OK será enviada a mensagem !Matéria excluída com sucesso da ordem do dia!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/protocolo_gerar_etiqueta_processo.html b/sapl/templates/ajuda/protocolo_gerar_etiqueta_processo.html index ef4248d49..5d43134fe 100644 --- a/sapl/templates/ajuda/protocolo_gerar_etiqueta_processo.html +++ b/sapl/templates/ajuda/protocolo_gerar_etiqueta_processo.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/protocolo_gerar_etiqueta_protocolo.html b/sapl/templates/ajuda/protocolo_gerar_etiqueta_protocolo.html index ef4248d49..5d43134fe 100644 --- a/sapl/templates/ajuda/protocolo_gerar_etiqueta_protocolo.html +++ b/sapl/templates/ajuda/protocolo_gerar_etiqueta_protocolo.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/protocolo_legislativo.html b/sapl/templates/ajuda/protocolo_legislativo.html index ef4248d49..5d43134fe 100644 --- a/sapl/templates/ajuda/protocolo_legislativo.html +++ b/sapl/templates/ajuda/protocolo_legislativo.html @@ -22,23 +22,23 @@ informações você tem acesso módulos:



    @@ -50,9 +50,9 @@ informações relativas a casa legislativa, para personalizar as telas do sistema de Apoio ao Processo Legislativo.

    - Anterior | + Anterior | - Índice + Índice | Próxima diff --git a/sapl/templates/ajuda/recebimento_proposicao.html b/sapl/templates/ajuda/recebimento_proposicao.html index 09b0d1f67..24ad025ea 100644 --- a/sapl/templates/ajuda/recebimento_proposicao.html +++ b/sapl/templates/ajuda/recebimento_proposicao.html @@ -75,11 +75,11 @@ acessório.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/regime_tramitacao.html b/sapl/templates/ajuda/regime_tramitacao.html index 63291ea80..abc94e665 100644 --- a/sapl/templates/ajuda/regime_tramitacao.html +++ b/sapl/templates/ajuda/regime_tramitacao.html @@ -39,11 +39,11 @@ tramitação foi excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/sessao_legislativa.html b/sapl/templates/ajuda/sessao_legislativa.html index 4f2cb7f2d..f28e339d9 100644 --- a/sapl/templates/ajuda/sessao_legislativa.html +++ b/sapl/templates/ajuda/sessao_legislativa.html @@ -162,11 +162,11 @@ A função “Início” retorna a tela de cadastramento de Sessão Legislativa.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/sessao_plenaria.html b/sapl/templates/ajuda/sessao_plenaria.html index ffe971a2e..3aa513d0b 100644 --- a/sapl/templates/ajuda/sessao_plenaria.html +++ b/sapl/templates/ajuda/sessao_plenaria.html @@ -1,4 +1,4 @@ -{% extends "base.html" % +{% extends "base.html" %} {% load i18n %} {% block base_content %} @@ -14,26 +14,26 @@
    Além das informações básicas de identificaçáo da Sessão é possível acessar o áudio num dos formatos mp3 e aac e o video num dos formatos: mp4, flv e webM por meio das respectivas URL's, caso tenham sido gerados. Ao selecionar uma sessão específica, o sistema irá apresentar o seguinte menu de opçóes, relativo a sessão plenária:

    Menu de Opções


    Ao ter acesso, para cadastro, será exibida a opção de Incluir, para a inclusão de novas Sessões. Ao ser acionada essa função, será exibida a tela Formulário de Cadastro.

    Para a efetivação do cadastramento serão necessários dados das seguintes tabelas auxiliares:

    Tabelas Auxiliares

    Os seguintes campos deverão ser preenchidos:

    @@ -131,11 +131,11 @@ a data de encerramento da sessão plenária no formato dd/mm/aaaa e
    Acione a função “Salvar” para que as informações sejam salvas no arquivo.

    Será enviada a mensagem !Sessão Plenária salva com sucesso!

    Acione a função “Sair” para sair do cadastramento de Sessões e voltar a tela anterior.
    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_ata.html b/sapl/templates/ajuda/sessao_plenaria_ata.html index 5a76f4cb7..e5a7a43f7 100644 --- a/sapl/templates/ajuda/sessao_plenaria_ata.html +++ b/sapl/templates/ajuda/sessao_plenaria_ata.html @@ -17,11 +17,11 @@
    6. Expedientes - Constam os registros dos fatos ocorridos em cada parte/expediente da Sessão.

    Clique no botão retornar para retornar a tela de cadastro da Sessão Plenária.
    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_expedientes.html b/sapl/templates/ajuda/sessao_plenaria_expedientes.html index 5b178b14f..525bbe7bd 100644 --- a/sapl/templates/ajuda/sessao_plenaria_expedientes.html +++ b/sapl/templates/ajuda/sessao_plenaria_expedientes.html @@ -14,11 +14,11 @@
    Após digitar os textos dentro da caixa, acione o botão 'Salvar' para grava-los.

    Nota: Deve-se evitar copiar textos de outros editores MS Word, etc (Crtl-C e Crtl-V) para dentro da caixa de texto, uma vez que juntamente com o texto vem tags inseridas pelo Editor de Textos, as quais nem sempre são reconhecidas ou passíveis de tradução para o html, que é a linguagem do Navegador Web. Esta sugestão é feita, pois sempre que uma tag existente no texto e não reconhecida pela função de conversão, o relatório em PDF deixa de ser gerado devido a ocorrências de erros decorrentes dessas tags não traduzidas.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_lista_presenca_sessao.html b/sapl/templates/ajuda/sessao_plenaria_lista_presenca_sessao.html index 3d14c8724..d9cfd2354 100644 --- a/sapl/templates/ajuda/sessao_plenaria_lista_presenca_sessao.html +++ b/sapl/templates/ajuda/sessao_plenaria_lista_presenca_sessao.html @@ -17,11 +17,11 @@
    A informação, sobre o quórum, foi previamente cadastrada quando do cadastro do tipo de sessão no sistema.

    Para a execução desta função serão necessários que os Parlamentares bem como os Tipos de Sessão Plenária - em tabelas auxiliares, estejam previamente cadastrados.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_materias_expediente.html b/sapl/templates/ajuda/sessao_plenaria_materias_expediente.html index 32f12d5bf..f19047aa1 100644 --- a/sapl/templates/ajuda/sessao_plenaria_materias_expediente.html +++ b/sapl/templates/ajuda/sessao_plenaria_materias_expediente.html @@ -15,11 +15,11 @@
    Na identificação da matéria há um link que, quando acionado, permite o acesso aos meta dados da matéria propriamente.

    As matérias legislativas são inseridas no Expediente, por meio da função Matérias no Expediente, da Sessão Plenária.

    O retorno a tela anterior é feito ao acionar o botão 'Retornar', que se encontra na parte inferior da tela.
    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_materias_ordem_dia.html b/sapl/templates/ajuda/sessao_plenaria_materias_ordem_dia.html index 9505abd97..015e5e3cd 100644 --- a/sapl/templates/ajuda/sessao_plenaria_materias_ordem_dia.html +++ b/sapl/templates/ajuda/sessao_plenaria_materias_ordem_dia.html @@ -15,11 +15,11 @@
    Esta função permite o acesso as funções inclusão individual ou de várias matérias na mesma transação, conforme o botão que for acionado, via clique do mouse.

    Também, é possível ajustar as matérias na Ordem do Dia, de modo a restaurar o número de ordem sequencial, bastando para isso, clicar no botão 'Ajustar Ordenação na Ordem do Dia', as quais serão renumeradas em ordem de tipo, ano e número.

    O retorno a tela anterior é feito ao acionar o botão 'Retornar', que se encontra na parte inferior da tela.
    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_mesa.html b/sapl/templates/ajuda/sessao_plenaria_mesa.html index 53872b5de..d90ef647d 100644 --- a/sapl/templates/ajuda/sessao_plenaria_mesa.html +++ b/sapl/templates/ajuda/sessao_plenaria_mesa.html @@ -14,11 +14,11 @@
    Para excluir o parlamentar da Mesa da Sessão, basta seleciona-lo e acionar o botão 'Excluir'. O sistema, antes de efetivar, irá perguntar se deseja realmente excluir e, caso afirmativo, efetivará a exclusão e emitirá a mensagem: !Parlamentar excluído com sucesso da composição da mesa.

    Para a execução desta função serão necessários que os Parlamentares bem como os Cargos - em tabelas auxiliares, estejam previamente cadastrados.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_oradores.html b/sapl/templates/ajuda/sessao_plenaria_oradores.html index 15f352076..634eb46a4 100644 --- a/sapl/templates/ajuda/sessao_plenaria_oradores.html +++ b/sapl/templates/ajuda/sessao_plenaria_oradores.html @@ -14,11 +14,11 @@ Essa função possui 3 caixas: Oradores Cadastrados, Cadastro de Ora
    Em Cadastro de Oradores consta no combo box os parlamentares ativos que ainda podem ser selecionados e incluídos na lista de oradores com a indicação da ordem de pronunciamento. Ao acionar o botão incluir orador o sistema irá emitir a mensagem ! Parlamentar incluído com sucesso na lista de oradores!

    A última caixa, Cadastro de Discurso permite a inclusão do discurso do parlamentar. Para tanto, é necessário selecionar o parlamentar no combo box, clicar em arquivo para obter o discurso e, em seguida, clicar no botão Incluir Discurso. O sistema irá emitir a mensagem !Discurso incluído com sucesso! e o link continuar que ao pressionado retorna a tela anterior.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_oradores_expediente.html b/sapl/templates/ajuda/sessao_plenaria_oradores_expediente.html index 15f352076..634eb46a4 100644 --- a/sapl/templates/ajuda/sessao_plenaria_oradores_expediente.html +++ b/sapl/templates/ajuda/sessao_plenaria_oradores_expediente.html @@ -14,11 +14,11 @@ Essa função possui 3 caixas: Oradores Cadastrados, Cadastro de Ora
    Em Cadastro de Oradores consta no combo box os parlamentares ativos que ainda podem ser selecionados e incluídos na lista de oradores com a indicação da ordem de pronunciamento. Ao acionar o botão incluir orador o sistema irá emitir a mensagem ! Parlamentar incluído com sucesso na lista de oradores!

    A última caixa, Cadastro de Discurso permite a inclusão do discurso do parlamentar. Para tanto, é necessário selecionar o parlamentar no combo box, clicar em arquivo para obter o discurso e, em seguida, clicar no botão Incluir Discurso. O sistema irá emitir a mensagem !Discurso incluído com sucesso! e o link continuar que ao pressionado retorna a tela anterior.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_oradores_explicacoes_pessoais.html b/sapl/templates/ajuda/sessao_plenaria_oradores_explicacoes_pessoais.html index 15f352076..634eb46a4 100644 --- a/sapl/templates/ajuda/sessao_plenaria_oradores_explicacoes_pessoais.html +++ b/sapl/templates/ajuda/sessao_plenaria_oradores_explicacoes_pessoais.html @@ -14,11 +14,11 @@ Essa função possui 3 caixas: Oradores Cadastrados, Cadastro de Ora
    Em Cadastro de Oradores consta no combo box os parlamentares ativos que ainda podem ser selecionados e incluídos na lista de oradores com a indicação da ordem de pronunciamento. Ao acionar o botão incluir orador o sistema irá emitir a mensagem ! Parlamentar incluído com sucesso na lista de oradores!

    A última caixa, Cadastro de Discurso permite a inclusão do discurso do parlamentar. Para tanto, é necessário selecionar o parlamentar no combo box, clicar em arquivo para obter o discurso e, em seguida, clicar no botão Incluir Discurso. O sistema irá emitir a mensagem !Discurso incluído com sucesso! e o link continuar que ao pressionado retorna a tela anterior.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_presenca_ordem_dia.html b/sapl/templates/ajuda/sessao_plenaria_presenca_ordem_dia.html index ac691c689..e0572a4b3 100644 --- a/sapl/templates/ajuda/sessao_plenaria_presenca_ordem_dia.html +++ b/sapl/templates/ajuda/sessao_plenaria_presenca_ordem_dia.html @@ -13,13 +13,13 @@
    Para isso, o sistema apresenta uma lista dos parlamentares bastando assinalar - com um clique do mouse - no quadro, que se encontra a esquerda do nome de cada parlamentar, a presença na Ordem do Dia e, ao final, acionar o botão 'Salvar'.

    Para excluir o parlamentar da lista de presenças basta desmarcar - com um clique do mouse - o quadro correspondente ao nome do parlamentar e acionar o botão 'Salvar'.

    O sistema irá exibir a mensagem !Lista de presença da Ordem do Dia cadastrada com sucesso!.
    -
    Para a execução desta função serão necessários que os Parlamentares bem como os Tipos de Sessão Plenária - em tabelas auxiliares, estejam previamente cadastrados.
    +
    Para a execução desta função serão necessários que os Parlamentares bem como os Tipos de Sessão Plenária - em tabelas auxiliares, estejam previamente cadastrados.

    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/sessao_plenaria_votacao.html b/sapl/templates/ajuda/sessao_plenaria_votacao.html index 0f5159afb..4a392d378 100644 --- a/sapl/templates/ajuda/sessao_plenaria_votacao.html +++ b/sapl/templates/ajuda/sessao_plenaria_votacao.html @@ -16,11 +16,11 @@
    O campo observações deve ser preenchido, quando for o caso, com as informações que forem julgadas oportunas e de esclarecimentos adicionais.

    Após preenchidas todas as informações, clicar no botão 'Salvar'. O sistema deverá emitir a mensagem: !Votação salva com sucesso! caso todos os dados estejam corretos, devendo clicar em continuar para retomar o registro da votação de outras matérias.

    Entretanto, emitirá a mensagem: !Quantidade de Votos é diferente do número de parlamentares presentes na Ordem do Dia! caso o total de votantes esteja diferente da lista de presença indicada na Ordem do Dia. Neste caso, clique em voltar para retornar a tela anterior e efetuar as correções.
    -
    Anterior | +
    Anterior | - Índice + Índice - | Próxima + | Próxima

    diff --git a/sapl/templates/ajuda/status_tramitacao.html b/sapl/templates/ajuda/status_tramitacao.html index 5d72ddd83..1d26fcb4a 100644 --- a/sapl/templates/ajuda/status_tramitacao.html +++ b/sapl/templates/ajuda/status_tramitacao.html @@ -116,11 +116,11 @@ retorna a tela com a relação de Status de tramitação já cadastradas.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_afastamento.html b/sapl/templates/ajuda/tipo_afastamento.html index e01e62490..91dd33000 100644 --- a/sapl/templates/ajuda/tipo_afastamento.html +++ b/sapl/templates/ajuda/tipo_afastamento.html @@ -112,11 +112,11 @@ Quando for informado OK será enviada a mensagem !Tipo de Afastamento excluído com sucesso!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_autor.html b/sapl/templates/ajuda/tipo_autor.html index 0062e95c8..d81f74b34 100644 --- a/sapl/templates/ajuda/tipo_autor.html +++ b/sapl/templates/ajuda/tipo_autor.html @@ -95,11 +95,11 @@ retorna a tela com a relação de tipo de autor já cadastrado.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_comissao.html b/sapl/templates/ajuda/tipo_comissao.html index a51591252..345cbcbac 100644 --- a/sapl/templates/ajuda/tipo_comissao.html +++ b/sapl/templates/ajuda/tipo_comissao.html @@ -132,11 +132,11 @@ retorna a tela com a relação de Tipo de comissões já cadastradas.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_dependente.html b/sapl/templates/ajuda/tipo_dependente.html index 1ae7a8005..cc9068f2e 100644 --- a/sapl/templates/ajuda/tipo_dependente.html +++ b/sapl/templates/ajuda/tipo_dependente.html @@ -38,11 +38,11 @@ excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_documento.html b/sapl/templates/ajuda/tipo_documento.html index 0438566de..61cd0dd29 100644 --- a/sapl/templates/ajuda/tipo_documento.html +++ b/sapl/templates/ajuda/tipo_documento.html @@ -33,11 +33,11 @@ excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_materia_legislativa.html b/sapl/templates/ajuda/tipo_materia_legislativa.html index 9f40d93c6..c0ba02ef8 100644 --- a/sapl/templates/ajuda/tipo_materia_legislativa.html +++ b/sapl/templates/ajuda/tipo_materia_legislativa.html @@ -96,11 +96,11 @@ retorna a tela com a relação de tipo de Matéria Legislatura.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_norma_juridica.html b/sapl/templates/ajuda/tipo_norma_juridica.html index 82d69dbd4..20d96269b 100644 --- a/sapl/templates/ajuda/tipo_norma_juridica.html +++ b/sapl/templates/ajuda/tipo_norma_juridica.html @@ -162,11 +162,11 @@ retorna aa tela com os dados da norma jurídica selecionada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_proposicao.html b/sapl/templates/ajuda/tipo_proposicao.html index 8153d2f6e..2008ad7c5 100644 --- a/sapl/templates/ajuda/tipo_proposicao.html +++ b/sapl/templates/ajuda/tipo_proposicao.html @@ -107,11 +107,11 @@ retorna a tela com a relação de tipo de proposições já cadastradas.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tipo_situa_militar.html b/sapl/templates/ajuda/tipo_situa_militar.html index 1135bfd3c..5bbede6ba 100644 --- a/sapl/templates/ajuda/tipo_situa_militar.html +++ b/sapl/templates/ajuda/tipo_situa_militar.html @@ -39,11 +39,11 @@ militar foi excluído com sucesso sem pergunta de confirmação.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tramitacao_lote.html b/sapl/templates/ajuda/tramitacao_lote.html index 098834116..a8a8f5450 100644 --- a/sapl/templates/ajuda/tramitacao_lote.html +++ b/sapl/templates/ajuda/tramitacao_lote.html @@ -273,11 +273,11 @@ Será enviada a mensagem Tramitação efetivada com sucesso!
    Acione a função Continuar repetir a operação para outras tramitações ou Voltar para retornar para tela inicial do sistema.


    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/tramitacao_relatoria.html b/sapl/templates/ajuda/tramitacao_relatoria.html index 394232ada..373ea3e63 100644 --- a/sapl/templates/ajuda/tramitacao_relatoria.html +++ b/sapl/templates/ajuda/tramitacao_relatoria.html @@ -370,11 +370,11 @@ retorna à tela com os dados da matéria legislativa selecionada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/troca_senha.html b/sapl/templates/ajuda/troca_senha.html index 8b4edf79b..72be948f4 100644 --- a/sapl/templates/ajuda/troca_senha.html +++ b/sapl/templates/ajuda/troca_senha.html @@ -70,11 +70,11 @@ informações sejam salvas no arquivo.
    Será enviada a mensagem !Senha salva com sucesso!

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/ajuda/unidade_tramitacao.html b/sapl/templates/ajuda/unidade_tramitacao.html index 71aa1d0cd..76d3a7a85 100644 --- a/sapl/templates/ajuda/unidade_tramitacao.html +++ b/sapl/templates/ajuda/unidade_tramitacao.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load "i18n" %} +{% load i18n %} {% block base_content %} @@ -120,11 +120,11 @@ retorna a tela com a relação de unidade de tramitação já cadastrada.

    - Anterior | + Anterior | - Índice + Índice - | Próxima + | Próxima
    diff --git a/sapl/templates/base.html b/sapl/templates/base.html index 554f08756..efcd0a647 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -55,7 +55,7 @@ -
  • +{#
  • #} {% if not user.is_authenticated %}
  • {% else %} @@ -65,7 +65,7 @@