diff --git a/.travis.yml b/.travis.yml index eb6abe0c3..f54ee6d20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: - - 3.4.3 + - 3.5 services: - postgresql @@ -14,13 +14,13 @@ before_script: - cp sapl/.env_test sapl/.env - psql -c "CREATE USER sapl WITH PASSWORD 'sapl'" -U postgres; - psql -c "CREATE DATABASE sapl OWNER sapl;" -U postgres - - ./check_migrations.sh + - ./scripts/django/check_migrations.sh script: - ./manage.py migrate - ./manage.py bower install - py.test --create-db - # - ./test_and_check_qa.sh + # - ./scripts/django/test_and_check_qa.sh addons: hosts: diff --git a/docker-compose.yml b/docker-compose.yml index a3173316a..3ca5f0f4e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.72 + image: interlegis/sapl:3.1.84 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/docs/instalacao31.rst b/docs/instalacao31.rst index 0b159b91d..e904f8d6c 100644 --- a/docs/instalacao31.rst +++ b/docs/instalacao31.rst @@ -28,10 +28,10 @@ Instalar as seguintes dependências do sistema:: pkg-config postgresql postgresql-contrib pgadmin3 python-psycopg2 \ software-properties-common build-essential libxml2-dev libjpeg-dev \ libmysqlclient-dev libssl-dev libffi-dev libxslt1-dev python3-setuptools \ - python3-pip curl poppler-utils antiword default-jre + python3-pip curl poppler-utils antiword default-jre python3-venv sudo -i - curl -sL https://deb.nodesource.com/setup_6.x | bash - + curl -sL https://deb.nodesource.com/setup_8.x | bash - exit sudo apt-get install nodejs @@ -184,6 +184,8 @@ Copie a chave que aparecerá, edite o arquivo .env e altere o valor do parâmetr * Instalar as dependências do ``bower``:: eval $(echo "sudo chown -R $USER:$USER /home/$USER/") + sudo chown -R $USER:$GROUP ~/.npm + sudo chown -R $USER:$GROUP ~/.config ./manage.py bower install * Atualizar e/ou criar as tabelas da base de dados para refletir o modelo da versão clonada:: diff --git a/gunicorn_start.sh b/gunicorn_start.sh index 6b5c98676..6247a0b27 100755 --- a/gunicorn_start.sh +++ b/gunicorn_start.sh @@ -16,8 +16,9 @@ DJANGODIR=/var/interlegis/sapl/ # Django project directory (* SOCKFILE=/var/interlegis/sapl/run/gunicorn.sock # we will communicate using this unix socket (*) USER=`whoami` # the user to run as (*) GROUP=`whoami` # the group to run as (*) -NUM_WORKERS=9 # how many worker processes should Gunicorn spawn (*) +NUM_WORKERS=4 # how many worker processes should Gunicorn spawn (*) # NUM_WORKERS = 2 * CPUS + 1 +MAX_REQUESTS=100 # number of requests before restarting worker DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*) DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*) @@ -41,6 +42,7 @@ test -d $RUNDIR || mkdir -p $RUNDIR exec gunicorn ${DJANGO_WSGI_MODULE}:application \ --name $NAME \ --workers $NUM_WORKERS \ + --max-requests $MAX_REQUESTS \ --user $USER \ --access-logfile - \ --error-logfile - \ diff --git a/release.sh b/release.sh new file mode 100755 index 000000000..cc3a172b5 --- /dev/null +++ b/release.sh @@ -0,0 +1,45 @@ +#/bin/bash + +VERSION=`git describe --tags --abbrev=0` +LAST_DIGIT=`echo $VERSION | cut -f 3 -d '.'` +MAIN_REV=`echo $VERSION | cut -f 1,2 -d '.'` +NEXT_NUMBER=$(($LAST_DIGIT + 1)) +NEXT_VERSION=$MAIN_REV'.'$NEXT_NUMBER + + +function bump_version { + sed -e s/$VERSION/$NEXT_VERSION/g docker-compose.yml > tmp1 + mv tmp1 docker-compose.yml + + sed -e s/$VERSION/$NEXT_VERSION/g setup.py > tmp2 + mv tmp2 setup.py +} + +function commit_and_push { + echo "committing..." + git add docker-compose.yml setup.py + git commit -m "Release: $NEXT_VERSION" + git tag $NEXT_VERSION + + echo "sending to github..." + git push origin $NEXT_VERSION + git push origin + + echo "done." +} + +case "$1" in + --dry-run) + echo "Dry run" + bump_version + echo "done." + echo "Run git checkout -- docker-compose.yml setup.py to undo the files" + + exit 0 + ;; + --publish) + echo "generating release" + bump_version + commit_and_push +esac + diff --git a/requirements/migration-requirements.txt b/requirements/migration-requirements.txt index d5793dad8..e3f887b14 100644 --- a/requirements/migration-requirements.txt +++ b/requirements/migration-requirements.txt @@ -1,2 +1,4 @@ -r dev-requirements.txt +GitPython mysqlclient==1.3.12 +pyaml diff --git a/requirements/requirements.txt b/requirements/requirements.txt index fe2e139c8..4b80ad130 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -22,7 +22,7 @@ easy-thumbnails==2.3 django-image-cropping==1.1.0 git+git://github.com/interlegis/trml2pdf.git libsass==0.11.1 -psycopg2==2.7.3 +psycopg2-binary==2.7.4 python-decouple==3.0 pytz==2016.4 pyyaml==3.11 diff --git a/sapl/audiencia/views.py b/sapl/audiencia/views.py index c0e353139..2c16b0919 100644 --- a/sapl/audiencia/views.py +++ b/sapl/audiencia/views.py @@ -1,15 +1,10 @@ -from django.shortcuts import render from django.http import HttpResponse -from django.core.urlresolvers import reverse -from django.db.models import F from django.views.decorators.clickjacking import xframe_options_exempt -from django.views.generic import ListView -from sapl.comissoes.forms import ParticipacaoCreateForm, ParticipacaoEditForm -from sapl.crud.base import RP_DETAIL, RP_LIST, Crud, CrudAux, MasterDetailCrud -from sapl.materia.models import MateriaLegislativa +from django.views.generic import UpdateView +from sapl.crud.base import RP_DETAIL, RP_LIST, Crud from .forms import AudienciaForm -from .models import (AudienciaPublica, TipoAudienciaPublica) +from .models import AudienciaPublica def index(request): return HttpResponse("Audiência Pública") @@ -36,10 +31,11 @@ class AudienciaCrud(Crud): form_class = AudienciaForm def get_initial(self): - self.initial['tipo_materia'] = self.object.materia.tipo.id - self.initial['numero_materia'] = self.object.materia.numero - self.initial['ano_materia'] = self.object.materia.ano - return self.initial + initial = super(UpdateView, self).get_initial() + initial['tipo_materia'] = self.object.materia.tipo.id + initial['numero_materia'] = self.object.materia.numero + initial['ano_materia'] = self.object.materia.ano + return initial class DeleteView(Crud.DeleteView): pass diff --git a/sapl/base/forms.py b/sapl/base/forms.py index 1a0637a22..5410ee3fe 100644 --- a/sapl/base/forms.py +++ b/sapl/base/forms.py @@ -525,6 +525,11 @@ class RelatorioAtasFilterSet(django_filters.FilterSet): model = SessaoPlenaria fields = ['data_inicio'] + @property + def qs(self): + parent = super(RelatorioAtasFilterSet, self).qs + return parent.distinct().prefetch_related('tipo').order_by('-ano', 'tipo', 'numero') + def __init__(self, *args, **kwargs): super(RelatorioAtasFilterSet, self).__init__( *args, **kwargs) @@ -588,7 +593,7 @@ class RelatorioHistoricoTramitacaoFilterSet(django_filters.FilterSet): @property def qs(self): parent = super(RelatorioHistoricoTramitacaoFilterSet, self).qs - return parent.distinct().order_by('-ano', 'tipo', 'numero') + return parent.distinct().prefetch_related('tipo').order_by('-ano', 'tipo', 'numero') class Meta: model = MateriaLegislativa @@ -628,7 +633,7 @@ class RelatorioDataFimPrazoTramitacaoFilterSet(django_filters.FilterSet): @property def qs(self): parent = super(RelatorioDataFimPrazoTramitacaoFilterSet, self).qs - return parent.distinct().order_by('-ano', 'tipo', 'numero') + return parent.distinct().prefetch_related('tipo').order_by('-ano', 'tipo', 'numero') class Meta: model = MateriaLegislativa @@ -829,6 +834,7 @@ class ConfiguracoesAppForm(ModelForm): 'cronometro_discurso', 'cronometro_aparte', 'cronometro_ordem', + 'cronometro_consideracoes', 'mostrar_brasao_painel', 'receber_recibo_proposicao'] @@ -837,6 +843,8 @@ class ConfiguracoesAppForm(ModelForm): self.fields['cronometro_discurso'].widget.attrs['class'] = 'cronometro' self.fields['cronometro_aparte'].widget.attrs['class'] = 'cronometro' self.fields['cronometro_ordem'].widget.attrs['class'] = 'cronometro' + self.fields['cronometro_consideracoes'].widget.attrs['class'] = 'cronometro' + def clean_mostrar_brasao_painel(self): mostrar_brasao_painel = self.cleaned_data.get( diff --git a/sapl/base/migrations/0017_appconfig_cronometro_consideracoes.py b/sapl/base/migrations/0017_appconfig_cronometro_consideracoes.py new file mode 100644 index 000000000..057344d9c --- /dev/null +++ b/sapl/base/migrations/0017_appconfig_cronometro_consideracoes.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-05-23 17:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0016_auto_20180326_1840'), + ] + + operations = [ + migrations.AddField( + model_name='appconfig', + name='cronometro_consideracoes', + field=models.TimeField(blank=True, null=True, verbose_name='Cronômetro de Considerações Finais'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index d2a6035a8..bbc03b6c7 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -113,6 +113,11 @@ class AppConfig(models.Model): blank=True, null=True) + cronometro_consideracoes = models.TimeField( + verbose_name=_('Cronômetro de Considerações Finais'), + blank=True, + null=True) + mostrar_brasao_painel = models.BooleanField( default=False, verbose_name=_('Mostrar brasão da Casa no painel?')) diff --git a/sapl/base/urls.py b/sapl/base/urls.py index 0f720071f..b52d46ad6 100644 --- a/sapl/base/urls.py +++ b/sapl/base/urls.py @@ -1,20 +1,22 @@ +import os + from django.conf.urls import include, url from django.contrib.auth import views from django.contrib.auth.decorators import permission_required from django.contrib.auth.views import (password_reset, password_reset_complete, password_reset_confirm, password_reset_done) -from django.views.generic.base import TemplateView +from django.views.generic.base import RedirectView, TemplateView from sapl.base.views import AutorCrud, ConfirmarEmailView, TipoAutorCrud -from sapl.settings import EMAIL_SEND_USER +from sapl.settings import EMAIL_SEND_USER, MEDIA_URL from .apps import AppConfig from .forms import LoginForm, NovaSenhaForm, RecuperarSenhaForm from .views import (AlterarSenha, AppConfigCrud, CasaLegislativaCrud, CreateUsuarioView, DeleteUsuarioView, EditUsuarioView, - HelpTopicView, ListarUsuarioView, RelatorioAtasView, - RelatorioDataFimPrazoTramitacaoView, + HelpTopicView, ListarUsuarioView, LogotipoView, + RelatorioAtasView, RelatorioDataFimPrazoTramitacaoView, RelatorioHistoricoTramitacaoView, RelatorioMateriasPorAnoAutorTipoView, RelatorioMateriasPorAutorView, @@ -120,4 +122,13 @@ urlpatterns = [ url(r'^sistema/search/', SaplSearchView(), name='haystack_search'), + # Folhas XSLT e extras referenciadas por documentos migrados do sapl 2.5 + url(r'^(sapl/)?XSLT/HTML/(?P.*)$', RedirectView.as_view( + url=os.path.join(MEDIA_URL, 'sapl/public/XSLT/HTML/%(path)s'), + permanent=False)), + # url do logotipo usada em documentos migrados do sapl 2.5 + url(r'^(sapl/)?sapl_documentos/props_sapl/logo_casa', + LogotipoView.as_view(), name='logotipo'), + + ] + recuperar_senha + alterar_senha + admin_user diff --git a/sapl/base/views.py b/sapl/base/views.py index 4dbf50ae2..f0e7d2e1b 100644 --- a/sapl/base/views.py +++ b/sapl/base/views.py @@ -1,5 +1,6 @@ -from django.conf import settings -from django.contrib.auth import get_user_model, update_session_auth_hash +import os + +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 @@ -12,14 +13,15 @@ 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 string_concat from django.utils.translation import ugettext_lazy as _ -from django.views.generic import (CreateView, DeleteView, DetailView, FormView, - ListView, UpdateView) -from django.views.generic.base import TemplateView +from django.utils.translation import string_concat +from django.views.generic import (CreateView, DeleteView, FormView, ListView, + UpdateView) +from django.views.generic.base import RedirectView, TemplateView from django_filters.views import FilterView from haystack.views import SearchView +from sapl import settings from sapl.base.forms import AutorForm, AutorFormForAdmin, TipoAutorForm from sapl.base.models import Autor, TipoAutor from sapl.crud.base import CrudAux, make_pagination @@ -759,3 +761,14 @@ class AlterarSenha(FormView): user.save() return super().form_valid(form) + + +STATIC_LOGO = os.path.join(settings.STATIC_URL, 'img/logo.png') + + +class LogotipoView(RedirectView): + + def get_redirect_url(self, *args, **kwargs): + casa = get_casalegislativa() + logo = casa and casa.logotipo and casa.logotipo.name + return os.path.join(settings.MEDIA_URL, logo) if logo else STATIC_LOGO diff --git a/sapl/comissoes/forms.py b/sapl/comissoes/forms.py index 3576873b7..05a0a97b9 100644 --- a/sapl/comissoes/forms.py +++ b/sapl/comissoes/forms.py @@ -241,6 +241,9 @@ class ComissaoForm(forms.ModelForm): if not self.is_valid(): return self.cleaned_data + if len(self.cleaned_data['nome']) > 50: + msg = _('Nome da Comissão deve ter no máximo 50 caracteres.') + raise ValidationError(msg) if self.cleaned_data['data_extincao']: if (self.cleaned_data['data_extincao'] < self.cleaned_data['data_criacao']): diff --git a/sapl/comissoes/migrations/0014_auto_20180503_1055.py b/sapl/comissoes/migrations/0014_auto_20180503_1055.py new file mode 100644 index 000000000..7a2524fed --- /dev/null +++ b/sapl/comissoes/migrations/0014_auto_20180503_1055.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2018-05-03 13:55 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('comissoes', '0013_auto_20180312_1533'), + ] + + operations = [ + migrations.AlterModelOptions( + name='composicao', + options={'ordering': ['periodo'], 'verbose_name': 'Composição de Comissão', 'verbose_name_plural': 'Composições de Comissão'}, + ), + migrations.AlterModelOptions( + name='periodo', + options={'ordering': ['-data_inicio', '-data_fim'], 'verbose_name': 'Período de composição de Comissão', 'verbose_name_plural': 'Períodos de composição de Comissão'}, + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index f0b1b8d8b..0c3240fc6 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -105,6 +105,7 @@ class Periodo(models.Model): # PeriodoCompComissao class Meta: verbose_name = _('Período de composição de Comissão') verbose_name_plural = _('Períodos de composição de Comissão') + ordering = ['-data_inicio', '-data_fim'] def __str__(self): if self.data_inicio and self.data_fim: @@ -140,6 +141,7 @@ class Composicao(models.Model): # IGNORE class Meta: verbose_name = _('Composição de Comissão') verbose_name_plural = _('Composições de Comissão') + ordering = ['periodo'] def __str__(self): return '%s: %s' % (self.comissao.sigla, self.periodo) diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index 82c3a79c1..c5870389e 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -51,6 +51,9 @@ class PeriodoComposicaoCrud(CrudAux): class UpdateView(CrudAux.UpdateView): form_class = PeriodoForm + # class ListView(CrudAux.ListView): + + class ParticipacaoCrud(MasterDetailCrud): model = Participacao parent_field = 'composicao__comissao' @@ -112,7 +115,9 @@ class ComposicaoCrud(MasterDetailCrud): composicao_pk = self.take_composicao_pk() if composicao_pk == 0: - ultima_composicao = context['composicao_list'].last() + # Composicao eh ordenada por Periodo, que por sua vez esta em + # ordem descrescente de data de inicio (issue #1920) + ultima_composicao = context['composicao_list'].first() if ultima_composicao: context['composicao_pk'] = ultima_composicao.pk else: diff --git a/sapl/hashers.py b/sapl/hashers.py index e80642def..3e9cf6c5f 100644 --- a/sapl/hashers.py +++ b/sapl/hashers.py @@ -46,11 +46,12 @@ ZOPE_SHA1_PREFIX = '{SSHA}' def zope_encoded_password_to_django(encoded): "Migra um hash de senha do zope para uso com o ZopeSHA1PasswordHasher" - if encoded.startswith(ZOPE_SHA1_PREFIX): + if encoded and 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 + # a None password blocks login, forcing a password reset 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 27591e058..4541425c4 100644 --- a/sapl/legacy/management/commands/migracao_25_31.py +++ b/sapl/legacy/management/commands/migracao_25_31.py @@ -1,33 +1,13 @@ from django.core import management from django.core.management.base import BaseCommand -from sapl.legacy.migracao import migrar, migrar_dados +from sapl.legacy.migracao import migrar class Command(BaseCommand): help = 'Migração de dados do SAPL 2.5 para o SAPL 3.1' - def add_arguments(self, parser): - parser.add_argument( - '--force', - action='store_true', - default=False, - dest='force', - help='Não interativa: pula confirmação de exclusão dos dados', - ) - parser.add_argument( - '--dados', - action='store_true', - default=False, - dest='dados', - help='migra somente dados', - ) - def handle(self, *args, **options): management.call_command('migrate') - somente_dados, interativo = options['dados'], not options['force'] - if somente_dados: - migrar_dados(interativo=interativo) - else: - migrar(interativo=interativo) + migrar(interativo=False) diff --git a/sapl/legacy/migracao.py b/sapl/legacy/migracao.py index 2690e9a53..ffd0d2a18 100644 --- a/sapl/legacy/migracao.py +++ b/sapl/legacy/migracao.py @@ -1,42 +1,76 @@ import subprocess -import tarfile +from getpass import getpass -from django.conf import settings +import requests +from unipath import Path -from sapl.legacy.migracao_dados import migrar_dados +from sapl.legacy.migracao_dados import (REPO, TAG_MARCO, gravar_marco, info, + migrar_dados) from sapl.legacy.migracao_documentos import migrar_documentos from sapl.legacy.migracao_usuarios import migrar_usuarios +from sapl.legacy.scripts.exporta_zope.variaveis_comuns import TAG_ZOPE +from sapl.legacy_migration_settings import DIR_REPO, NOME_BANCO_LEGADO +from sapl.materia.models import Proposicao -def migrar(interativo=False): - migrar_dados(interativo=interativo) - migrar_usuarios() - migrar_documentos() +def adornar_msg(msg): + return '\n{1}\n{0}\n{1}'.format(msg, '#' * len(msg)) -# fonte: https://stackoverflow.com/a/17081026/1877490 -def make_tarfile(output_filename, source_dir): - with tarfile.open(output_filename, "w:gz") as tar: - tar.add(source_dir, arcname=os.path.basename(source_dir)) - +def migrar(interativo=False): + if TAG_MARCO in REPO.tags: + info('A migração já está feita.') + return + assert TAG_ZOPE in REPO.tags, adornar_msg( + 'Antes de migrar ' + 'é necessário fazer a exportação de documentos do zope') + migrar_dados(interativo=interativo) + migrar_usuarios(REPO.working_dir) + migrar_documentos(REPO) + gravar_marco() + compactar_media() -def gerar_pacote(): - banco = settings.DATABASES['legacy']['NAME'] - # backup do banco - print('Gerando backup do banco... ', end='', flush=True) - arq_backup = settings.MEDIA_ROOT.child('{}.backup'.format(banco)) - backup_cmd = ''' - pg_dump --host localhost --port 5432 --username postgres --no-password - --format custom --blobs --verbose --file {} {}'''.format( - arq_backup, banco) - subprocess.check_output(backup_cmd.split(), stderr=subprocess.DEVNULL) - print('SUCESSO') +def compactar_media(): # tar de media/sapl print('Criando tar de media... ', end='', flush=True) - tar_media = settings.MEDIA_ROOT.child('{}.media.tgz'.format(banco)) - dir_media = settings.MEDIA_ROOT.child('sapl') - with tarfile.open(tar_media, "w:gz") as tar: - tar.add(dir_media, arcname=dir_media.name) + arq_tar = DIR_REPO.child('{}.media.tar'.format(NOME_BANCO_LEGADO)) + arq_tar.remove() + subprocess.check_output(['tar', 'cfh', arq_tar, '-C', DIR_REPO, 'sapl']) print('SUCESSO') + + +PROPOSICAO_UPLOAD_TO = Proposicao._meta.get_field('texto_original').upload_to + + +def salva_conteudo_do_sde(proposicao, conteudo): + caminho_relativo = PROPOSICAO_UPLOAD_TO( + proposicao, 'proposicao_sde_{}.xml'.format(proposicao.pk)) + caminho_absoluto = Path(REPO.working_dir, caminho_relativo) + caminho_absoluto.parent.mkdir(parents=True) + with open(caminho_absoluto, 'wb') as arq: + arq.write(conteudo) + proposicao.texto_original = caminho_relativo + proposicao.save() + + +def scrap_sde(url, usuario, senha=None): + if not senha: + senha = getpass() + + # login + session = requests.session() + res = session.post('{}?retry=1'.format(url), + {'__ac_name': usuario, '__ac_password': senha}) + assert res.status_code == 200 + + url_proposicao = '{}/sapl_documentos/proposicao/{}/renderXML?xsl=__default__' # noqa + total = Proposicao.objects.count() + for num, proposicao in enumerate(Proposicao.objects.all()): + pk = proposicao.pk + res = session.get(url_proposicao.format(url, pk)) + print("pk: {} status: {} (progresso: {:.2%})".format( + pk, res.status_code, num / total)) + if res.status_code == 200: + salva_conteudo_do_sde(proposicao, res.content) diff --git a/sapl/legacy/migracao_dados.py b/sapl/legacy/migracao_dados.py index 9b2846fa7..88758c8b1 100644 --- a/sapl/legacy/migracao_dados.py +++ b/sapl/legacy/migracao_dados.py @@ -1,4 +1,7 @@ +import datetime +import os import re +import subprocess import traceback from collections import OrderedDict, defaultdict, namedtuple from datetime import date @@ -7,10 +10,13 @@ from itertools import groupby from operator import xor from subprocess import PIPE, call +import git import pkg_resources +import pyaml import pytz import reversion import yaml +from bs4 import BeautifulSoup from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.models import Group @@ -18,13 +24,18 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db import connections, transaction from django.db.models import Max, Q +from pyaml import UnsafePrettyYAMLDumper from unipath import Path from sapl.base.models import AppConfig as AppConf from sapl.base.models import Autor, TipoAutor, cria_models_tipo_autor from sapl.comissoes.models import Comissao, Composicao, Participacao +from sapl.legacy import scripts from sapl.legacy.models import NormaJuridica as OldNormaJuridica from sapl.legacy.models import TipoNumeracaoProtocolo +from sapl.legacy_migration_settings import (DATABASES, DIR_DADOS_MIGRACAO, + DIR_REPO, NOME_BANCO_LEGADO, + PROJECT_DIR) from sapl.materia.models import (AcompanhamentoMateria, MateriaLegislativa, Proposicao, StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, @@ -35,11 +46,11 @@ from sapl.parlamentares.models import (Legislatura, Mandato, Parlamentar, Partido, TipoAfastamento) from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, StatusTramitacaoAdministrativo) -from sapl.sessao.models import (ExpedienteMateria, OrdemDia, RegistroVotacao, - TipoResultadoVotacao) -from sapl.settings import DATABASES, PROJECT_DIR +from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, OrdemDia, + RegistroVotacao, TipoResultadoVotacao) from sapl.utils import normalize +from .scripts.normaliza_dump_mysql import normaliza_dump_mysql from .timezonesbrasil import get_timezone # BASE ###################################################################### @@ -136,6 +147,7 @@ for nome_novo, nome_antigo in (('comissao', 'cod_comissao'), class CampoVirtual(namedtuple('CampoVirtual', 'model related_model')): null = True + CAMPOS_VIRTUAIS_PROPOSICAO = { TipoMateriaLegislativa: CampoVirtual(Proposicao, MateriaLegislativa), TipoDocumento: CampoVirtual(Proposicao, DocumentoAdministrativo) @@ -143,6 +155,15 @@ CAMPOS_VIRTUAIS_PROPOSICAO = { for campo_virtual in CAMPOS_VIRTUAIS_PROPOSICAO.values(): campos_novos_para_antigos[campo_virtual] = 'cod_mat_ou_doc' + +CAMPOS_VIRTUAIS_TIPO_PROPOSICAO = { + 'M': CampoVirtual(TipoProposicao, TipoMateriaLegislativa), + 'D': CampoVirtual(TipoProposicao, TipoDocumento) +} +for campo_virtual in CAMPOS_VIRTUAIS_TIPO_PROPOSICAO.values(): + campos_novos_para_antigos[campo_virtual] = 'tip_mat_ou_doc' + + # campos virtuais de Autor para funcionar com get_fk_related CAMPOS_VIRTUAIS_AUTOR = {related: CampoVirtual(Autor, related) for related in (Parlamentar, Comissao, Partido)} @@ -159,6 +180,7 @@ for related, campo_antigo in [(Parlamentar, 'cod_parlamentar'), def info(msg): print('INFO: ' + msg) + ocorrencias = defaultdict(list) @@ -201,7 +223,7 @@ class ForeignKeyFaltando(ObjectDoesNotExist): campo = campos_novos_para_antigos[self.field] _, tabela, campos_pk = get_estrutura_legado(self.field.model) pk = {c: getattr(self.old, c) for c in campos_pk} - sql = 'select * from {} where {}'.format( + sql = 'select * from {} where {};'.format( tabela, ' and '.join(['{} = {}'.format(k, v) for k, v in pk.items()])) return OrderedDict((('campo', campo), @@ -494,6 +516,8 @@ PROPAGACOES_DE_EXCLUSAO = [ ('parlamentar', 'dependente', 'cod_parlamentar'), ('parlamentar', 'filiacao', 'cod_parlamentar'), ('parlamentar', 'mandato', 'cod_parlamentar'), + ('parlamentar', 'composicao_mesa', 'cod_parlamentar'), + ('parlamentar', 'composicao_comissao', 'cod_parlamentar'), # comissao ('comissao', 'composicao_comissao', 'cod_comissao'), @@ -518,6 +542,11 @@ PROPAGACOES_DE_EXCLUSAO = [ ('materia_legislativa', 'anexada', 'cod_materia_principal'), ('materia_legislativa', 'anexada', 'cod_materia_anexada'), ('materia_legislativa', 'documento_acessorio', 'cod_materia'), + ('materia_legislativa', 'numeracao', 'cod_materia'), + + # norma + ('norma_juridica', 'vinculo_norma_juridica', 'cod_norma_referente'), + ('norma_juridica', 'vinculo_norma_juridica', 'cod_norma_referida'), # documento administrativo ('documento_administrativo', 'tramitacao_administrativo', 'cod_documento'), @@ -548,6 +577,9 @@ def uniformiza_banco(): garante_coluna_no_legado('tipo_materia_legislativa', 'quorum_minimo_votacao int(11) NULL') + garante_coluna_no_legado('materia_legislativa', + 'txt_resultado TEXT 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', @@ -695,31 +727,26 @@ def fill_dados_basicos(): appconf.save() -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)) -DIR_DADOS_MIGRACAO = Path('~/migracao_sapl/').expand() -PATH_TABELA_TIMEZONES = DIR_DADOS_MIGRACAO.child('tabela_timezones.yaml') -DIR_RESULTADOS = DIR_DADOS_MIGRACAO.child('resultados') +REPO = git.Repo.init(DIR_REPO) def dict_representer(dumper, data): return dumper.represent_dict(data.items()) + + yaml.add_representer(OrderedDict, dict_representer) # configura timezone de migração -nome_banco_legado = DATABASES['legacy']['NAME'] -match = re.match('sapl_cm_(.*)', nome_banco_legado) +match = re.match('sapl_cm_(.*)', NOME_BANCO_LEGADO) sigla_casa = match.group(1) +PATH_TABELA_TIMEZONES = DIR_DADOS_MIGRACAO.child('tabela_timezones.yaml') with open(PATH_TABELA_TIMEZONES, 'r') as arq: tabela_timezones = yaml.load(arq) municipio, uf, nome_timezone = tabela_timezones[sigla_casa] @@ -762,7 +789,27 @@ def populate_renamed_fields(new, old): setattr(new, field.name, value) +def roda_comando_shell(cmd): + res = os.system(cmd) + assert res == 0, 'O comando falhou: {}'.format(cmd) + + def migrar_dados(interativo=True): + + # restaura dump + arq_dump = Path(DIR_DADOS_MIGRACAO.child( + 'dumps_mysql', '{}.sql'.format(NOME_BANCO_LEGADO))) + assert arq_dump.exists(), 'Dump do mysql faltando: {}'.format(arq_dump) + info('Restaurando dump mysql de [{}]'.format(arq_dump)) + normaliza_dump_mysql(arq_dump) + roda_comando_shell('mysql -uroot < {}'.format(arq_dump)) + + # executa ajustes pré-migração, se existirem + arq_ajustes_pre_migracao = DIR_DADOS_MIGRACAO.child( + 'ajustes_pre_migracao', '{}.sql'.format(sigla_casa)) + if arq_ajustes_pre_migracao.exists(): + exec_legado(arq_ajustes_pre_migracao.read_file()) + uniformiza_banco() # excluindo database antigo. @@ -789,19 +836,16 @@ def migrar_dados(interativo=True): info('Começando migração: ...') try: ocorrencias.clear() - dir_ocorrencias = DIR_RESULTADOS.child(date.today().isoformat()) - dir_ocorrencias.mkdir(parents=True) migrar_todos_os_models() except Exception as e: ocorrencias['traceback'] = str(traceback.format_exc()) raise e finally: # grava ocorrências - arq_ocorrencias = dir_ocorrencias.child( - nome_banco_legado + '.yaml') + arq_ocorrencias = Path(REPO.working_dir, 'ocorrencias.yaml') with open(arq_ocorrencias, 'w') as arq: - dump = yaml.dump(dict(ocorrencias), allow_unicode=True) - arq.write(dump.replace('\n- ', '\n\n- ')) + pyaml.dump(ocorrencias, arq, vspacing=1) + REPO.git.add([arq_ocorrencias.name]) info('Ocorrências salvas em\n {}'.format(arq_ocorrencias)) # recria tipos de autor padrão que não foram criados pela migração @@ -816,7 +860,7 @@ def move_para_depois_de(lista, movido, referencias): return lista -def migrar_todos_os_models(): +def get_models_a_migrar(): models = [model for app in appconfs for model in app.models.values() if model in field_renames] # Devido à referência TipoProposicao.tipo_conteudo_related @@ -829,7 +873,11 @@ def migrar_todos_os_models(): move_para_depois_de(models, Proposicao, [MateriaLegislativa, DocumentoAdministrativo]) - for model in models: + return models + + +def migrar_todos_os_models(): + for model in get_models_a_migrar(): migrar_model(model) @@ -852,10 +900,14 @@ def migrar_model(model): def get_id_do_legado(old): return getattr(old, nome_pk) + + ultima_pk_legado = model_legado.objects.all().aggregate( + Max('pk'))['pk__max'] or 0 else: # a pk no legado tem mais de um campo old_records = iter_sql_records(tabela_legado) get_id_do_legado = None + ultima_pk_legado = model_legado.objects.count() ajuste_antes_salvar = AJUSTE_ANTES_SALVAR.get(model) ajuste_depois_salvar = AJUSTE_DEPOIS_SALVAR.get(model) @@ -898,10 +950,13 @@ def migrar_model(model): if ajuste_depois_salvar: ajuste_depois_salvar() - # se configuramos ids explicitamente devemos reiniciar a sequence + # reiniciamos a sequence logo após a última pk do legado + # + # É importante que seja do legado (e não da nova base), + # pois numa nova versão da migração podemos inserir registros + # não migrados antes sem conflito com pks criadas até lá if get_id_do_legado: - last_pk = get_last_pk(model) - reinicia_sequence(model, last_pk + 1) + reinicia_sequence(model, ultima_pk_legado + 1) # apaga registros migrados do legado if sql_delete_legado: @@ -1061,22 +1116,16 @@ def adjust_tipoafastamento(new, old): new.indicador = 'F' -TIPO_MATERIA_OU_TIPO_DOCUMENTO = {'M': TipoMateriaLegislativa, - 'D': TipoDocumento} +def set_generic_fk(new, campo_virtual, old): + new.content_type = content_types[campo_virtual.related_model] + new.object_id = get_fk_related(campo_virtual, old) def adjust_tipoproposicao(new, old): "Aponta para o tipo relacionado de matéria ou documento" - value = old.tip_mat_ou_doc - model_tipo = TIPO_MATERIA_OU_TIPO_DOCUMENTO[old.ind_mat_ou_doc] - tipo = model_tipo.objects.filter(pk=value) - if tipo: - new.tipo_conteudo_related = tipo[0] - else: - raise ForeignKeyFaltando( - field=TipoProposicao.tipo_conteudo_related, - value=(model_tipo.__name__, value), - label={'ind_mat_ou_doc': old.ind_mat_ou_doc}) + if old.tip_mat_ou_doc: + campo_virtual = CAMPOS_VIRTUAIS_TIPO_PROPOSICAO[old.ind_mat_ou_doc] + set_generic_fk(new, campo_virtual, old) def adjust_proposicao_antes_salvar(new, old): @@ -1085,8 +1134,7 @@ def adjust_proposicao_antes_salvar(new, old): if old.cod_mat_ou_doc: tipo_mat_ou_doc = type(new.tipo.tipo_conteudo_related) campo_virtual = CAMPOS_VIRTUAIS_PROPOSICAO[tipo_mat_ou_doc] - new.content_type = content_types[campo_virtual.related_model] - new.object_id = get_fk_related(campo_virtual, old) + set_generic_fk(new, campo_virtual, old) def adjust_statustramitacao(new, old): @@ -1162,10 +1210,9 @@ def adjust_autor(new, old): break if old.col_username: - 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) + user, created = get_user_model().objects.get_or_create( + username=old.col_username) + if created: # gera uma senha inutilizável, que precisará ser trocada user.set_password(None) with reversion.create_revision(): @@ -1173,8 +1220,9 @@ def adjust_autor(new, old): 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) + grupo_autor = Group.objects.get(name="Autor") + user.groups.add(grupo_autor) + new.user = user def adjust_comissao(new, old): @@ -1199,6 +1247,21 @@ def adjust_tiporesultadovotacao(new, old): {'pk': new.pk, 'nome': new.nome}) +def remove_style(conteudo): + if 'style' not in conteudo: + return conteudo # atalho que acelera muito os casos sem style + + soup = BeautifulSoup(conteudo, 'html.parser') + for tag in soup.recursiveChildGenerator(): + if hasattr(tag, 'attrs'): + tag.attrs = {k: v for k, v in tag.attrs.items() if k != 'style'} + return str(soup) + + +def adjust_expediente_sessao(new, old): + new.conteudo = remove_style(new.conteudo) + + AJUSTE_ANTES_SALVAR = { Autor: adjust_autor, TipoAutor: adjust_tipo_autor, @@ -1220,10 +1283,71 @@ AJUSTE_ANTES_SALVAR = { StatusTramitacaoAdministrativo: adjust_statustramitacaoadm, Tramitacao: adjust_tramitacao, TipoResultadoVotacao: adjust_tiporesultadovotacao, + ExpedienteSessao: adjust_expediente_sessao, } AJUSTE_DEPOIS_SALVAR = { NormaJuridica: adjust_normajuridica_depois_salvar, } -# CHECKS #################################################################### + +# MARCO ###################################################################### + +TIME_FORMAT = '%H:%M:%S' + + +# permite a gravação de tempos puros pelo pretty-yaml +def time_representer(dumper, data): + return dumper.represent_scalar('!time', data.strftime(TIME_FORMAT)) + + +UnsafePrettyYAMLDumper.add_representer(datetime.time, time_representer) + + +# permite a leitura de tempos puros pelo pyyaml (no padrão gravado acima) +def time_constructor(loader, node): + value = loader.construct_scalar(node) + return datetime.datetime.strptime(value, TIME_FORMAT).time() + + +yaml.add_constructor(u'!time', time_constructor) + +TAG_MARCO = 'marco' + + +def gravar_marco(): + """Grava um dump de todos os dados como arquivos yaml no repo de marco + """ + # prepara ou localiza repositorio + dir_dados = Path(REPO.working_dir, 'dados') + + # exporta dados como arquivos yaml + user_model = get_user_model() + models = get_models_a_migrar() + [ + Composicao, user_model, Group, ContentType] + for model in models: + info('Gravando marco de [{}]'.format(model.__name__)) + dir_model = dir_dados.child(model._meta.app_label, model.__name__) + dir_model.mkdir(parents=True) + for data in model.objects.all().values(): + nome_arq = Path(dir_model, '{}.yaml'.format(data['id'])) + with open(nome_arq, 'w') as arq: + pyaml.dump(data, arq) + + # backup do banco + print('Gerando backup do banco... ', end='', flush=True) + arq_backup = DIR_REPO.child('{}.backup'.format(NOME_BANCO_LEGADO)) + arq_backup.remove() + backup_cmd = ''' + pg_dump --host localhost --port 5432 --username postgres --no-password + --format custom --blobs --verbose --file {} {}'''.format( + arq_backup, NOME_BANCO_LEGADO) + subprocess.check_output(backup_cmd.split(), stderr=subprocess.DEVNULL) + print('SUCESSO') + + # salva mudanças + REPO.git.add([dir_dados.name]) + if 'master' not in REPO.heads or REPO.index.diff('HEAD'): + # se de fato existe mudança + REPO.index.commit('Grava marco') + REPO.git.execute('git tag -f'.split() + [TAG_MARCO]) diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 9ae46ef5d..2bb17c53e 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -1,12 +1,14 @@ -import mimetypes import os import re from glob import glob +from os.path import join import yaml +from django.db import transaction +from image_cropping.fields import ImageCropField from sapl.base.models import CasaLegislativa -from sapl.legacy.migracao_dados import exec_legado, warn +from sapl.legacy.migracao_dados import exec_legado from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, Proposicao) from sapl.norma.models import NormaJuridica @@ -14,107 +16,56 @@ from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo) from sapl.sessao.models import SessaoPlenaria -from sapl.settings import MEDIA_ROOT - # MIGRAÇÃO DE DOCUMENTOS ################################################### -def get_ano(obj): - return [obj.ano] - - -def ___(obj): - return [] - - DOCS = { - CasaLegislativa: [ - ('logotipo', - 'props_sapl/{}.*', - 'public/casa/logotipo/', - ___) - ], - Parlamentar: [ - ('fotografia', - 'parlamentar/fotos/{}_foto_parlamentar', - 'public/parlamentar/{0}/', - ___) - ], - MateriaLegislativa: [ - ('texto_original', - 'materia/{}_texto_integral', - 'public/materialegislativa/{1}/{0}/', - get_ano) - ], - DocumentoAcessorio: [ - ('arquivo', - 'materia/{}', - 'public/documentoacessorio/{1}/{0}/', - lambda obj: [obj.materia.ano]) - ], - NormaJuridica: [ - ('texto_integral', - 'norma_juridica/{}_texto_integral', - 'public/normajuridica/{1}/{0}/', - get_ano) - ], - SessaoPlenaria: [ - ('upload_pauta', - 'pauta_sessao/{}_pauta_sessao', - 'public/sessaoplenaria/{0}/pauta/', - ___), - ('upload_ata', - 'ata_sessao/{}_ata_sessao', - 'public/sessaoplenaria/{0}/ata/', - ___), - ('upload_anexo', - 'anexo_sessao/{}_texto_anexado', - 'public/sessaoplenaria/{0}/anexo/', - ___) - ], - Proposicao: [ - ('texto_original', - 'proposicao/{}', - 'private/proposicao/{0}/', - get_ano) - ], - DocumentoAdministrativo: [ - ('texto_integral', - 'administrativo/{}_texto_integral', - 'private/documentoadministrativo/{0}/', - get_ano) - ], - DocumentoAcessorioAdministrativo: [ - ('arquivo', - 'administrativo/{}', - 'private/documentoacessorioadministrativo/{0}/', - ___) - ], + CasaLegislativa: [('logotipo', 'props_sapl/{}.*')], + Parlamentar: [('fotografia', 'parlamentar/fotos/{}_foto_parlamentar')], + MateriaLegislativa: [('texto_original', 'materia/{}_texto_integral')], + DocumentoAcessorio: [('arquivo', 'materia/{}')], + NormaJuridica: [('texto_integral', 'norma_juridica/{}_texto_integral')], + SessaoPlenaria: [('upload_pauta', 'pauta_sessao/{}_pauta_sessao'), + ('upload_ata', 'ata_sessao/{}_ata_sessao'), + ('upload_anexo', 'anexo_sessao/{}_texto_anexado')], + Proposicao: [('texto_original', 'proposicao/{}')], + DocumentoAdministrativo: [('texto_integral', + 'administrativo/{}_texto_integral')], + DocumentoAcessorioAdministrativo: [('arquivo', 'administrativo/{}')], } -DOCS = {model: [(campo, - os.path.join('sapl_documentos', origem), - os.path.join('sapl', destino), - get_extra_args) - for campo, origem, destino, get_extra_args in campos] +DOCS = {model: [(campo, join('sapl_documentos', origem)) + for campo, origem, in campos] for model, campos in DOCS.items()} -def em_media(caminho): - return os.path.join(MEDIA_ROOT, caminho) - - -def mover_documento(origem, destino): - origem, destino = [em_media(c) if not os.path.isabs(c) else c +def mover_documento(repo, origem, destino): + origem, destino = [join(repo.working_dir, c) if not os.path.isabs(c) else c for c in (origem, destino)] os.makedirs(os.path.dirname(destino), exist_ok=True) - os.rename(origem, destino) + repo.git.mv(origem, destino) -def migrar_propriedades_da_casa(): +def migrar_logotipo(repo, casa, propriedades): + print('.... Migrando logotipo da casa ....') + [(campo, origem)] = DOCS[CasaLegislativa] + # a extensão do logo pode ter sido ajustada pelo tipo real do arquivo + nome_nas_propriedades = os.path.splitext(propriedades['id_logo'])[0] + arquivos = glob(join(repo.working_dir, origem.format(nome_nas_propriedades))) + if arquivos: + assert len(arquivos) == 1, 'Há mais de um logotipo para a casa' + [logo] = arquivos + destino = join(CasaLegislativa._meta.get_field(campo).upload_to, + os.path.basename(logo)) + mover_documento(repo, logo, destino) + casa.logotipo = destino + + +def migrar_propriedades_da_casa(repo): print('#### Migrando propriedades da casa ####') - caminho = em_media('sapl_documentos/propriedades.yaml') + caminho = join(repo.working_dir, 'sapl_documentos/propriedades.yaml') + repo.git.execute('git annex get'.split() + [caminho]) with open(caminho, 'r') as arquivo: propriedades = yaml.safe_load(arquivo) casa = CasaLegislativa.objects.first() @@ -134,67 +85,73 @@ def migrar_propriedades_da_casa(): for campo, prop in campos_para_propriedades: setattr(casa, campo, propriedades[prop]) - # Localidade + # localidade sql_localidade = ''' select nom_localidade, sgl_uf from localidade where cod_localidade = {}'''.format(propriedades['cod_localidade']) [(casa.municipio, casa.uf)] = exec_legado(sql_localidade) - print('.... Migrando logotipo da casa ....') - [(_, origem, destino, __)] = DOCS[CasaLegislativa] - # a extensão do logo pode ter sido ajustada pelo tipo real do arquivo - id_logo = os.path.splitext(propriedades['id_logo'])[0] - [origem] = glob(em_media(origem.format(id_logo))) - destino = os.path.join(destino, os.path.basename(origem)) - mover_documento(origem, destino) - casa.logotipo = destino + # logotipo + migrar_logotipo(repo, casa, propriedades) + casa.save() - os.remove(caminho) + repo.git.rm(caminho) -def migrar_docs_por_ids(model): - for campo, base_origem, base_destino, get_extra_args in DOCS[model]: +def migrar_docs_por_ids(repo, model): + for campo, base_origem in DOCS[model]: print('#### Migrando {} de {} ####'.format(campo, model.__name__)) - dir_origem, nome_origem = os.path.split(em_media(base_origem)) + dir_origem, nome_origem = os.path.split( + join(repo.working_dir, base_origem)) nome_origem = nome_origem.format('(\d+)') pat = re.compile('^{}\.\w+$'.format(nome_origem)) - if not os.path.isdir(dir_origem): print(' >>> O diretório {} não existe! Abortado.'.format( dir_origem)) continue - for arq in os.listdir(dir_origem): - match = pat.match(arq) - if match: + matches = [pat.match(arq) for arq in os.listdir(dir_origem)] + ids_origens = [(int(m.group(1)), + join(dir_origem, m.group(0))) + for m in matches if m] + objetos = {obj.id: obj for obj in model.objects.all()} + upload_to = model._meta.get_field(campo).upload_to + tem_cropping = isinstance(model._meta.get_field(campo), ImageCropField) + + with transaction.atomic(): + for id, origem in ids_origens: # associa documento ao objeto - origem = os.path.join(dir_origem, match.group(0)) - id = match.group(1) - try: - 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: - destino = os.path.join( - base_destino.format(id, *get_extra_args(obj)), - os.path.basename(origem)) - mover_documento(origem, destino) + obj = objetos.get(id) + if obj: + destino = upload_to(obj, os.path.basename(origem)) + mover_documento(repo, origem, destino) setattr(obj, campo, destino) + if tem_cropping: + # conserta link do git annex (antes do commit) + # pois o conteúdo das imagens é acessado pelo cropping + repo.git.execute('git annex fix'.split() + [destino]) obj.save() + else: + msg = ' {} (pk={}) não encontrado para documento em [{}]' + print(msg.format(model.__name__, id, origem)) -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! - # A pasta, após conferência do que não foi migrado, deve ser apagada. +def migrar_documentos(repo): + # aqui supomos que as pastas XSLT e sapl_documentos estão em + # com o conteúdo exportado do zope + # Os arquivos das pastas serão (git) MOVIDOS para a nova estrutura! # # Isto significa que para rodar novamente esta função é preciso - # restaurar a pasta sapl_documentos ao estado inicial + # restaurar o repo ao estado anterior + + mover_documento(repo, 'XSLT', 'sapl/public/XSLT') + + migrar_propriedades_da_casa(repo) - migrar_propriedades_da_casa() + # garante que o conteúdo das fotos dos parlamentares esteja presente + # (necessário para o cropping de imagem) + repo.git.execute('git annex get sapl_documentos/parlamentar'.split()) for model in [ Parlamentar, @@ -206,14 +163,13 @@ def migrar_documentos(): DocumentoAdministrativo, DocumentoAcessorioAdministrativo, ]: - migrar_docs_por_ids(model) + migrar_docs_por_ids(repo, model) - sobrando = [os.path.join(dir, file) - for (dir, _, files) in os.walk(em_media('sapl_documentos')) + sobrando = [join(dir, file) + for (dir, _, files) in os.walk(join(repo.working_dir, + 'sapl_documentos')) for file in files] if sobrando: print('\n#### Encerrado ####\n\n' '{} documentos sobraram sem ser migrados!!!'.format( len(sobrando))) - for doc in sobrando: - print(' {}'. format(doc)) diff --git a/sapl/legacy/migracao_usuarios.py b/sapl/legacy/migracao_usuarios.py index 106a2def6..be0478c82 100644 --- a/sapl/legacy/migracao_usuarios.py +++ b/sapl/legacy/migracao_usuarios.py @@ -1,8 +1,8 @@ import yaml from django.contrib.auth.models import Group, User +from unipath import Path from sapl.hashers import zope_encoded_password_to_django -from sapl.settings import MEDIA_ROOT PERFIL_LEGADO_PARA_NOVO = {legado: Group.objects.get(name=novo) for legado, novo in [ @@ -44,9 +44,9 @@ def decode_nome(nome): return nome -def migrar_usuarios(): +def migrar_usuarios(dir_repo): """ - Lê o arquivo media/usuarios.yaml e importa os usuários nele listados, + Lê o arquivo /usuarios.yaml e importa os usuários nele listados, com senhas e perfis. Os usuários são criados se necessário e seus perfis ajustados. @@ -68,7 +68,7 @@ def migrar_usuarios(): Também podemos assumir que essa é uma tarefa de um administrador """ - ARQUIVO_USUARIOS = MEDIA_ROOT.child('usuarios.yaml') + ARQUIVO_USUARIOS = Path(dir_repo).child('usuarios.yaml') with open(ARQUIVO_USUARIOS, 'r') as f: usuarios = yaml.load(f) # conferimos de que só há um nome de usuário diff --git a/sapl/legacy/models.py b/sapl/legacy/models.py index 4c459c9dd..7d341624d 100644 --- a/sapl/legacy/models.py +++ b/sapl/legacy/models.py @@ -433,7 +433,7 @@ class MateriaLegislativa(models.Model): cod_regime_tramitacao = models.IntegerField() dat_publicacao = models.DateField(blank=True, null=True) tip_origem_externa = models.IntegerField(blank=True, null=True) - num_origem_externa = models.CharField(max_length=5, blank=True, null=True) + num_origem_externa = models.CharField(max_length=10, blank=True, null=True) ano_origem_externa = models.SmallIntegerField(blank=True, null=True) dat_origem_externa = models.DateField(blank=True, null=True) cod_local_origem_externa = models.IntegerField(blank=True, null=True) diff --git a/sapl/legacy/router.py b/sapl/legacy/router.py index 68b235059..1aa51a46b 100644 --- a/sapl/legacy/router.py +++ b/sapl/legacy/router.py @@ -16,7 +16,7 @@ class LegacyRouter: return True return None - def allow_migrate(self, db, model): - if model._meta.app_label == 'legacy': + def allow_migrate(self, db, app_label, model_name=None, **hints): + if app_label == 'legacy': return False return None diff --git a/sapl/legacy/scripts/exporta_zope/exporta_zope.py b/sapl/legacy/scripts/exporta_zope/exporta_zope.py index 11ff1ecbb..20c7d21e7 100755 --- a/sapl/legacy/scripts/exporta_zope/exporta_zope.py +++ b/sapl/legacy/scripts/exporta_zope/exporta_zope.py @@ -5,6 +5,8 @@ # Esse script precisa rodar em python 2 # e depende apenas do descrito no arquivo requiments.txt +import cStringIO +import hashlib import mimetypes import os import sys @@ -12,21 +14,32 @@ from collections import defaultdict from contextlib import contextmanager from functools import partial +import git import magic import yaml import ZODB.DB import ZODB.FileStorage +from unipath import Path from ZODB.broken import Broken +from variaveis_comuns import DIR_DADOS_MIGRACAO, TAG_ZOPE + EXTENSOES = { 'application/msword': '.doc', 'application/pdf': '.pdf', 'application/vnd.oasis.opendocument.text': '.odt', 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', # noqa + 'application/vnd.oasis.opendocument.text-template': '.ott', + 'application/vnd.ms-powerpoint': '.ppt', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', # noqa + 'application/vnd.oasis.opendocument.spreadsheet': '.ods', + 'application/xml': '.xml', 'text/xml': '.xml', 'application/zip': '.zip', + 'application/x-rar': '.rar', + 'image/jpeg': '.jpeg', 'image/png': '.png', 'image/gif': '.gif', @@ -38,6 +51,11 @@ EXTENSOES = { 'image/tiff': '.tiff', 'application/tiff': '.tiff', 'audio/x-wav': '.wav', + 'video/mp4': '.mp4', + 'image/x-icon': '.ico', + 'image/x-ms-bmp': '.bmp', + 'video/x-ms-asf': '.asf', + 'audio/mpeg': '.mp3', # TODO rever... 'text/richtext': '.rtf', @@ -45,7 +63,9 @@ EXTENSOES = { # sem extensao 'application/octet-stream': '', # binário 'inode/x-empty': '', # vazio - 'text/x-unknown-content-type': '', + 'application/x-empty': '', # vazio + 'text/x-unknown-content-type': '', # desconhecido + 'application/CDFV2-unknown': '', # desconhecido } @@ -56,29 +76,26 @@ def br(obj): return obj -def guess_extension(caminho): - mime = magic.from_file(caminho, mime=True) - try: - return EXTENSOES[mime] - except KeyError as e: - msg = '\n'.join([ - 'Extensão não conhecida para o arquivo:', - caminho, - 'E mimetype:', - mime, - ' Algumas possibilidades são:', ] + +def guess_extension(fullname, buffer): + mime = magic.from_buffer(buffer, mime=True) + extensao = EXTENSOES.get(mime) + if extensao is not None: + return extensao + else: + possibilidades = '\n'.join( [" '{}': '{}',".format(mime, ext) - for ext in mimetypes.guess_all_extensions(mime)] + - ['Atualize o código do dicionário EXTENSOES!'] - ) - print(msg) - raise Exception(msg, e) - - -def dump_file(doc, path): - name = doc['__name__'] - fullname = os.path.join(path, name) - + for ext in mimetypes.guess_all_extensions(mime)]) + print('''Extensão não conhecida para o arquivo: {} + e mimetype: {} + Algumas possibilidades são: + {} + Atualize o código do dicionário EXTENSOES! + '''.format(fullname, mime, possibilidades) + ) + return '.DESCONHECIDO.{}'.format(mime.replace('/', '__')) + + +def get_conteudo_file(doc): # A partir daqui usamos dict.pop('...') nos __Broken_state__ # para contornar um "vazamento" de memória que ocorre # ao percorrer a árvore de objetos @@ -95,25 +112,28 @@ def dump_file(doc, path): doc['data'] = pdata pdata = doc - with open(fullname, 'w') as arq: - while pdata: - arq.write(pdata.pop('data')) - pdata = br(pdata.pop('next', None)) + output = cStringIO.StringIO() + while pdata: + output.write(pdata.pop('data')) + pdata = br(pdata.pop('next', None)) + + return output.getvalue() - base, original_extension = os.path.splitext(fullname) - extension = guess_extension(fullname) - if extension == '.xml' and original_extension in ['.xsl', '.xslt']: - # não trocamos as extensões XSL e XSLT - final_name = fullname - else: - # trocamos a extensão pela adivinhada - final_name = base + extension - os.rename(fullname, final_name) - print(final_name) +def dump_file(doc, path, salvar, get_conteudo=get_conteudo_file): + name = doc['__name__'] + fullname = os.path.join(path, name) + conteudo = get_conteudo(doc) + if conteudo: + # pula arquivos vazios + salvar(fullname, conteudo) return name +def get_conteudo_dtml_method(doc): + return doc['raw'] + + 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] @@ -131,11 +151,16 @@ enumerate_properties = partial(enumerate_by_key_list, def enumerate_btree(folder): contagem_esperada = folder['_count'].value tree = folder['_tree'] + contagem_real = 0 # para o caso em que não haja itens 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 + if contagem_esperada != contagem_real: + print('ATENÇÃO: contagens diferentes na btree: ' + '{} esperada: {} real: {}'.format(folder['title'], + contagem_esperada, + contagem_real)) nao_identificados = defaultdict(list) @@ -148,14 +173,14 @@ def logando_nao_identificados(): 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('FORAM ENCONTRADOS ARQUIVOS DE FORMATO NÃO IDENTIFICADO!!!') + print('REFAÇA A EXPORTAÇÃO\n') print(nao_identificados) print('#' * 80) print('#' * 80) -def dump_folder(folder, path='', enum=enumerate_folder): +def dump_folder(folder, path, salvar, enum=enumerate_folder): name = folder['id'] path = os.path.join(path, name) if not os.path.exists(path): @@ -165,7 +190,7 @@ def dump_folder(folder, path='', enum=enumerate_folder): if dump == '?': nao_identificados[meta_type].append(path + '/' + id) elif dump: - id_interno = dump(obj, path) + id_interno = dump(obj, path, salvar) assert id == id_interno return name @@ -201,24 +226,24 @@ def read_sde(element): return data -def save_as_yaml(path, name, obj): +def save_as_yaml(path, name, obj, salvar): fullname = os.path.join(path, name) - with open(fullname, 'w') as arquivo: - yaml.safe_dump(obj, arquivo, allow_unicode=True) - print(fullname) - return fullname + conteudo = yaml.safe_dump(obj, allow_unicode=True) + salvar(fullname, conteudo) -def dump_sde(strdoc, path, tipo): +def dump_sde(strdoc, path, salvar, tipo): id = strdoc['id'] sde = read_sde(strdoc) - save_as_yaml(path, '{}.{}.yaml'.format(id, tipo), sde) + save_as_yaml(path, '{}.{}.yaml'.format(id, tipo), sde, salvar) return id DUMP_FUNCTIONS = { 'File': dump_file, 'Image': dump_file, + 'DTML Method': partial(dump_file, + get_conteudo=get_conteudo_dtml_method), 'Folder': partial(dump_folder, enum=enumerate_folder), 'BTreeFolder2': partial(dump_folder, enum=enumerate_btree), 'SDE-Document': partial(dump_sde, tipo='sde.document'), @@ -233,7 +258,7 @@ DUMP_FUNCTIONS = { def get_app(data_fs_path): - storage = ZODB.FileStorage.FileStorage(data_fs_path) + storage = ZODB.FileStorage.FileStorage(data_fs_path, read_only=True) db = ZODB.DB(storage) connection = db.open() root = connection.root() @@ -255,42 +280,115 @@ def find_sapl(app): return sapl -def dump_propriedades(docs, path, encoding='iso-8859-1'): +def dump_propriedades(docs, path, salvar, encoding='iso-8859-1'): 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(encoding) if isinstance(p, str) else p for id, p in props.items()} - save_as_yaml(path, 'sapl_documentos/propriedades.yaml', props) + save_as_yaml(path, 'sapl_documentos/propriedades.yaml', props, salvar) -def dump_usuarios(sapl, path): +def dump_usuarios(sapl, path, salvar): 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) + save_as_yaml(path, 'usuarios.yaml', users, salvar) -def dump_sapl(data_fs_path, destino='../../../../media'): +def _dump_sapl(data_fs_path, destino, salvar): + assert Path(data_fs_path).exists() app, close_db = get_app(data_fs_path) try: sapl = find_sapl(app) # extrai folhas XSLT - dump_folder(br(sapl['XSLT']), destino) + dump_folder(br(sapl['XSLT']), destino, salvar) # extrai usuários com suas senhas e perfis - dump_usuarios(sapl, destino) + dump_usuarios(sapl, destino, salvar) # extrai documentos docs = br(sapl['sapl_documentos']) with logando_nao_identificados(): - dump_folder(docs, destino) - dump_propriedades(docs, destino) + dump_folder(docs, destino, salvar) + dump_propriedades(docs, destino, salvar) finally: close_db() +def repo_execute(repo, cmd, *args): + return repo.git.execute(cmd.split() + list(args)) + + +def get_annex_hashes(repo): + hashes = repo_execute( + repo, 'git annex find', '--format=${keyname}\n', '--include=*') + return {os.path.splitext(h)[0] for h in hashes.splitlines()} + + +def ajusta_extensao(fullname, conteudo): + base, extensao = os.path.splitext(fullname) + if extensao not in ['.xsl', '.xslt', '.yaml', '.css']: + extensao = guess_extension(fullname, conteudo) + return base + extensao + + +def build_salvar(repo): + """Constroi função salvar que pula arquivos que já estão no annex + """ + hashes = get_annex_hashes(repo) + + def salvar(fullname, conteudo): + sha = hashlib.sha256() + sha.update(conteudo) + if sha.hexdigest() in hashes: + print('- hash encontrado - {}'.format(fullname)) + else: + fullname = ajusta_extensao(fullname, conteudo) + if os.path.exists(fullname): + # destrava arquivo pré-existente (o conteúdo mudou) + repo_execute(repo, 'git annex unlock', fullname) + with open(fullname, 'w') as arq: + arq.write(conteudo) + print(fullname) + + return salvar + + +def dump_sapl(sigla): + sigla = sigla[-3:] # ignora prefixo (por ex. 'sapl_cm_') + data_fs_path = DIR_DADOS_MIGRACAO.child('datafs', + 'Data_cm_{}.fs'.format(sigla)) + assert data_fs_path.exists(), 'Origem não existe: {}'.format(data_fs_path) + nome_banco_legado = 'sapl_cm_{}'.format(sigla) + destino = DIR_DADOS_MIGRACAO.child('repos', nome_banco_legado) + destino.mkdir(parents=True) + repo = git.Repo.init(destino) + if TAG_ZOPE in repo.tags: + print('{}: A exportação de documentos já está feita -- abortando'.format(sigla)) + return + + repo_execute(repo, 'git annex init') + repo_execute(repo, 'git config annex.thin true') + + salvar = build_salvar(repo) + try: + finalizado = False + _dump_sapl(data_fs_path, destino, salvar) + finalizado = True + finally: + # grava mundaças + repo_execute(repo, 'git annex add sapl_documentos') + repo.git.add(A=True) + if 'master' not in repo.heads or repo.index.diff('HEAD'): + # se de fato existe mudança + status = 'completa' if finalizado else 'parcial' + repo.index.commit(u'Exportação do zope {}'.format(status)) + if finalizado: + repo.git.execute('git tag -f'.split() + [TAG_ZOPE]) + + if __name__ == "__main__": if len(sys.argv) == 2: - data_fs_path = sys.argv[1] - dump_sapl(data_fs_path) + sigla = sys.argv[1] + dump_sapl(sigla) else: - print('Uso: python exporta_zope ') + print('Uso: python exporta_zope ') diff --git a/sapl/legacy/scripts/exporta_zope/requirements.txt b/sapl/legacy/scripts/exporta_zope/requirements.txt index 4794267ae..69305576b 100644 --- a/sapl/legacy/scripts/exporta_zope/requirements.txt +++ b/sapl/legacy/scripts/exporta_zope/requirements.txt @@ -1,3 +1,8 @@ # ZODB version 3.7.4 -PyYAML==3.12 ZODB==5.3.0 +PyYAML +Unipath +GitPython +pyaml +python-magic +ipython diff --git a/sapl/legacy/scripts/exporta_zope/variaveis_comuns.py b/sapl/legacy/scripts/exporta_zope/variaveis_comuns.py new file mode 100644 index 000000000..e773f0717 --- /dev/null +++ b/sapl/legacy/scripts/exporta_zope/variaveis_comuns.py @@ -0,0 +1,4 @@ +from unipath import Path + +DIR_DADOS_MIGRACAO = Path('~/migracao_sapl/').expand() +TAG_ZOPE = 'zope' diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index ed4db9662..613fd0a4d 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -1,10 +1,7 @@ #!/bin/bash # rodar esse script na raiz do projeto -if [ $# -ge 2 ]; then - - # proteje pasta com dumps de alterações acidentais - # chmod -R -w ~/migracao_sapl/sapl_dumps +if [ $# -eq 1 ]; then DIR_MIGRACAO=~/migracao_sapl @@ -20,28 +17,11 @@ if [ $# -ge 2 ]; then 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" < $DIR_MIGRACAO/dumps_mysql/$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 < $DIR_MIGRACAO/dumps_mysql/$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 - echo "--- MIGRACAO ---" | tee -a $LOG echo >> $LOG - DATABASE_NAME=$1 ./manage.py migracao_25_31 --force --dados --settings sapl.legacy_migration_settings 2>&1 | tee -a $LOG + DATABASE_NAME=$1 ./manage.py migracao_25_31 --settings sapl.legacy_migration_settings 2>&1 | tee -a $LOG echo >> $LOG else echo "USO:" - echo " $0 [senha mysql]" + echo " $0 " fi; diff --git a/sapl/legacy/scripts/normaliza_dump_mysql.py b/sapl/legacy/scripts/normaliza_dump_mysql.py new file mode 100755 index 000000000..a56d74b3a --- /dev/null +++ b/sapl/legacy/scripts/normaliza_dump_mysql.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +import re +import sys + +from unipath import Path + +cabecalho = ''' +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +/*!40000 DROP DATABASE IF EXISTS `{banco}`*/; + +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{banco}` /*!40100 DEFAULT CHARACTER SET latin1 */; + +USE `{banco}`; + +''' + + +def normaliza_dump_mysql(nome_arquivo): + arquivo = Path(nome_arquivo).expand() + banco = arquivo.stem + conteudo = arquivo.read_file() + inicio = re.finditer('--\n-- Table structure for table .*\n--\n', conteudo) + inicio = next(inicio).start() + conteudo = cabecalho.format(banco=banco) + conteudo[inicio:] + arquivo.write_file(conteudo) + + +if __name__ == "__main__": + nome_aquivo = sys.argv[1] + normaliza_dump_mysql(nome_aquivo) diff --git a/sapl/legacy/scripts/normaliza_dump_mysql.sh b/sapl/legacy/scripts/normaliza_dump_mysql.sh deleted file mode 100755 index 75bd5435f..000000000 --- a/sapl/legacy/scripts/normaliza_dump_mysql.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -ARQUIVO=$1 -BANCO=`basename $1 | cut -f1 -d.` -TMP=__tmp.sql - -cat << EOF > $TMP - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!40101 SET NAMES utf8 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - -/*!40000 DROP DATABASE IF EXISTS \`$BANCO\`*/; - -CREATE DATABASE /*!32312 IF NOT EXISTS*/ \`$BANCO\` /*!40100 DEFAULT CHARACTER SET latin1 */; - -USE \`$BANCO\`; -EOF - -sed 1,`grep -n '^USE ' $ARQUIVO |cut -f1 -d:`d $ARQUIVO >> $TMP -mv $TMP $ARQUIVO diff --git a/sapl/legacy/scripts/recria_um_db_postgres.sh b/sapl/legacy/scripts/recria_um_db_postgres.sh index 3ff66e8f3..98defaaa6 100755 --- a/sapl/legacy/scripts/recria_um_db_postgres.sh +++ b/sapl/legacy/scripts/recria_um_db_postgres.sh @@ -4,3 +4,8 @@ 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;" + + +echo "--- DJANGO MIGRATE ---" | tee -a $LOG +DATABASE_NAME=$1 ./manage.py migrate --settings sapl.legacy_migration_settings + diff --git a/sapl/legacy_migration_settings.py b/sapl/legacy_migration_settings.py index 7d911bf7e..96f6a83ad 100644 --- a/sapl/legacy_migration_settings.py +++ b/sapl/legacy_migration_settings.py @@ -3,6 +3,9 @@ import os from decouple import Config, RepositoryEnv from dj_database_url import parse as db_url +from sapl.legacy.scripts.exporta_zope.variaveis_comuns import \ + DIR_DADOS_MIGRACAO + from .settings import * # flake8: noqa config = Config(RepositoryEnv(BASE_DIR.child('legacy', '.env'))) @@ -33,3 +36,10 @@ DEBUG = True # delisga indexação fulltext em tempo real HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.BaseSignalProcessor' + +SHELL_PLUS_DONT_LOAD = ['legacy'] + +NOME_BANCO_LEGADO = DATABASES['legacy']['NAME'] +DIR_REPO = Path(DIR_DADOS_MIGRACAO, 'repos', NOME_BANCO_LEGADO) + +MEDIA_ROOT = DIR_REPO diff --git a/sapl/materia/forms.py b/sapl/materia/forms.py index 3d455ea29..bbfdd3536 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -36,7 +36,7 @@ from sapl.materia.models import (AssuntoMateria, Autoria, MateriaAssunto, from sapl.norma.models import (LegislacaoCitada, NormaJuridica, TipoNormaJuridica) from sapl.parlamentares.models import Legislatura -from sapl.protocoloadm.models import Protocolo +from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo from sapl.settings import MAX_DOC_UPLOAD_SIZE from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, ChoiceWithoutValidationField, @@ -162,6 +162,11 @@ class MateriaSimplificadaForm(ModelForm): class MateriaLegislativaForm(ModelForm): + tipo_autor = ModelChoiceField(label=_('Tipo Autor'), + required=False, + queryset=TipoAutor.objects.all(), + empty_label=_('------'), ) + autor = forms.ModelChoiceField(required=False, empty_label='------', queryset=Autor.objects.all() @@ -172,6 +177,15 @@ class MateriaLegislativaForm(ModelForm): exclude = ['texto_articulado', 'autores', 'proposicao', 'anexadas', 'data_ultima_atualizacao'] + def __init__(self, *args, **kwargs): + super(MateriaLegislativaForm, self).__init__(*args, **kwargs) + + if self.instance and self.instance.pk: + self.fields['tipo_autor'] = forms.CharField(required=False, + widget=forms.TextInput(attrs={'disabled': 'disabled'})) + self.fields['autor'] = forms.CharField(required=False, + widget=forms.TextInput(attrs={'disabled': 'disabled'})) + def clean(self): super(MateriaLegislativaForm, self).clean() @@ -182,29 +196,54 @@ class MateriaLegislativaForm(ModelForm): data_apresentacao = cleaned_data['data_apresentacao'] ano = cleaned_data['ano'] + protocolo = cleaned_data['numero_protocolo'] + protocolo_antigo = self.instance.numero_protocolo + + if protocolo: + if not Protocolo.objects.filter(numero=protocolo,ano=ano).exists(): + raise ValidationError(_('Protocolo %s/%s não' + ' existe' % (protocolo, ano))) + + if protocolo_antigo != protocolo: + exist_materia = MateriaLegislativa.objects.filter( + numero_protocolo=protocolo, + ano=ano).exists() + + exist_doc = DocumentoAdministrativo.objects.filter( + protocolo_id=protocolo, + ano=ano).exists() + if exist_materia or exist_doc: + raise ValidationError(_('Protocolo %s/%s ja possui' + ' documento vinculado' + % (protocolo, ano))) if data_apresentacao.year != ano: - raise ValidationError("O ano da matéria não pode ser " - "diferente do ano na data de apresentação") + raise ValidationError(_("O ano da matéria não pode ser " + "diferente do ano na data de apresentação")) ano_origem_externa = cleaned_data['ano_origem_externa'] data_origem_externa = cleaned_data['data_origem_externa'] if ano_origem_externa and data_origem_externa and \ ano_origem_externa != data_origem_externa.year: - raise ValidationError("O ano de origem externa da matéria não " + raise ValidationError(_("O ano de origem externa da matéria não " "pode ser diferente do ano na data de " - "origem externa") + "origem externa")) return cleaned_data def save(self, commit=False): + if not self.instance.pk: + primeiro_autor = True + else: + primeiro_autor = False + materia = super(MateriaLegislativaForm, self).save(commit) materia.save() if self.cleaned_data['autor']: autoria = Autoria() - autoria.primeiro_autor = True + autoria.primeiro_autor = primeiro_autor autoria.materia = materia autoria.autor = self.cleaned_data['autor'] autoria.save() @@ -582,8 +621,18 @@ class AnexadaForm(ModelForm): msg = _('A matéria a ser anexada não existe no cadastro' ' de matérias legislativas.') raise ValidationError(msg) - else: - cleaned_data['materia_anexada'] = materia_anexada + + materia_principal = self.instance.materia_principal + if materia_principal == materia_anexada: + raise ValidationError(_('Matéria não pode ser anexada a si mesma')) + + is_anexada = Anexada.objects.filter(materia_principal=materia_principal, + materia_anexada=materia_anexada + ).exists() + if is_anexada: + raise ValidationError(_('Materia já se encontra anexada')) + + cleaned_data['materia_anexada'] = materia_anexada return cleaned_data diff --git a/sapl/materia/migrations/0028_auto_20180418_1629.py b/sapl/materia/migrations/0028_auto_20180418_1629.py new file mode 100644 index 000000000..c082ce1e6 --- /dev/null +++ b/sapl/materia/migrations/0028_auto_20180418_1629.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-04-18 19:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('materia', '0027_auto_20180409_1443'), + ] + + operations = [ + migrations.AlterField( + model_name='materialegislativa', + name='numero_origem_externa', + field=models.CharField(blank=True, max_length=10, verbose_name='Número'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index ce4690c39..606fabbd7 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -167,7 +167,7 @@ class MateriaLegislativa(models.Model): on_delete=models.PROTECT, verbose_name=_('Tipo')) numero_origem_externa = models.CharField( - max_length=5, blank=True, verbose_name=_('Número')) + max_length=10, blank=True, verbose_name=_('Número')) ano_origem_externa = models.PositiveSmallIntegerField( blank=True, null=True, verbose_name=_('Ano'), choices=RANGE_ANOS) data_origem_externa = models.DateField( @@ -325,9 +325,17 @@ class AcompanhamentoMateria(models.Model): verbose_name_plural = _('Acompanhamentos de Matéria') def __str__(self): - # FIXME str should be human readable, using hash is very strange - return _('%(materia)s - #%(hash)s') % { - 'materia': self.materia, 'hash': self.hash} + if self.data_cadastro is None: + return _('%(materia)s - %(email)s') % { + 'materia': self.materia, + 'email': self.email + } + else: + return _('%(materia)s - %(email)s - Registrado em: %(data)s') % { + 'materia': self.materia, + 'email': self.email, + 'data': str(self.data_cadastro.strftime('%d/%m/%Y')) + } @reversion.register() diff --git a/sapl/materia/views.py b/sapl/materia/views.py index a23932b42..807db3d9c 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -17,7 +17,7 @@ from django.shortcuts import get_object_or_404, redirect 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 import FormView, ListView, TemplateView, CreateView, UpdateView from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django_filters.views import FilterView @@ -937,7 +937,8 @@ class RelatoriaCrud(MasterDetailCrud): except ObjectDoesNotExist: pass else: - composicao = comissao.composicao_set.last() + composicao = comissao.composicao_set.order_by( + '-periodo__data_inicio').first() participacao = Participacao.objects.filter( composicao=composicao) @@ -1012,18 +1013,19 @@ class TramitacaoCrud(MasterDetailCrud): 'pk': self.kwargs['pk']}) def get_initial(self): + initial = super(CreateView, self).get_initial() local = MateriaLegislativa.objects.get( pk=self.kwargs['pk']).tramitacao_set.order_by( '-data_tramitacao', '-id').first() if local: - self.initial['unidade_tramitacao_local' + 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 + initial['unidade_tramitacao_local'] = '' + initial['data_tramitacao'] = timezone.now().date() + return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -1153,9 +1155,10 @@ class DocumentoAcessorioCrud(MasterDetailCrud): super(MasterDetailCrud.CreateView, self).__init__(**kwargs) def get_initial(self): - self.initial['data'] = timezone.now().date() + initial = super(CreateView, self).get_initial() + initial['data'] = timezone.now().date() - return self.initial + return initial def get_context_data(self, **kwargs): context = super( @@ -1282,10 +1285,11 @@ class LegislacaoCitadaCrud(MasterDetailCrud): form_class = LegislacaoCitadaForm def get_initial(self): - self.initial['tipo'] = self.object.norma.tipo.id - self.initial['numero'] = self.object.norma.numero - self.initial['ano'] = self.object.norma.ano - return self.initial + initial = super(UpdateView, self).get_initial() + initial['tipo'] = self.object.norma.tipo.id + initial['numero'] = self.object.norma.numero + initial['ano'] = self.object.norma.ano + return initial class DetailView(MasterDetailCrud.DetailView): @@ -1318,10 +1322,11 @@ class AnexadaCrud(MasterDetailCrud): form_class = AnexadaForm def get_initial(self): - self.initial['tipo'] = self.object.materia_anexada.tipo.id - self.initial['numero'] = self.object.materia_anexada.numero - self.initial['ano'] = self.object.materia_anexada.ano - return self.initial + initial = super(UpdateView, self).get_initial() + initial['tipo'] = self.object.materia_anexada.tipo.id + initial['numero'] = self.object.materia_anexada.numero + initial['ano'] = self.object.materia_anexada.ano + return initial class DetailView(MasterDetailCrud.DetailView): @@ -1343,16 +1348,18 @@ class MateriaAssuntoCrud(MasterDetailCrud): form_class = MateriaAssuntoForm def get_initial(self): - self.initial['materia'] = self.kwargs['pk'] - return self.initial + initial = super(CreateView, self).get_initial() + initial['materia'] = self.kwargs['pk'] + return initial class UpdateView(MasterDetailCrud.UpdateView): form_class = MateriaAssuntoForm def get_initial(self): - self.initial['materia'] = self.get_object().materia - self.initial['assunto'] = self.get_object().assunto - return self.initial + initial = super(UpdateView, self).get_initial() + initial['materia'] = self.get_object().materia + initial['assunto'] = self.get_object().assunto + return initial class MateriaLegislativaCrud(Crud): diff --git a/sapl/norma/forms.py b/sapl/norma/forms.py index affe07b87..2574edb20 100644 --- a/sapl/norma/forms.py +++ b/sapl/norma/forms.py @@ -83,16 +83,19 @@ class NormaJuridicaForm(ModelForm): label='Matéria', required=False, queryset=TipoMateriaLegislativa.objects.all(), - empty_label='Selecione' + empty_label='Selecione', + widget=forms.Select(attrs={'autocomplete': 'off'}) ) numero_materia = forms.CharField( label='Número Matéria', - required=False + required=False, + widget=forms.TextInput(attrs={'autocomplete': 'off'}) ) ano_materia = forms.ChoiceField( label='Ano Matéria', required=False, choices=ANO_CHOICES, + widget=forms.Select(attrs={'autocomplete': 'off'}) ) class Meta: @@ -122,7 +125,11 @@ class NormaJuridicaForm(ModelForm): if not self.is_valid(): return cleaned_data - + norma = NormaJuridica.objects.filter(ano=cleaned_data['ano'], + numero=cleaned_data['numero'], + tipo=cleaned_data['tipo']).exists() + if norma: + raise ValidationError("Já existe uma norma de mesmo Tipo, Ano e Número no sistema") if (cleaned_data['tipo_materia'] and cleaned_data['numero_materia'] and cleaned_data['ano_materia']): diff --git a/sapl/norma/views.py b/sapl/norma/views.py index d74bc8857..650442676 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -7,7 +7,7 @@ from django.http import HttpResponse, JsonResponse from django.template import RequestContext, loader from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.views.generic import CreateView, ListView, TemplateView, UpdateView +from django.views.generic import TemplateView, UpdateView from django.views.generic.base import RedirectView from django.views.generic.edit import FormView from django_filters.views import FilterView @@ -40,10 +40,9 @@ class NormaRelacionadaCrud(MasterDetailCrud): model = NormaRelacionada parent_field = 'norma_principal' help_topic = 'norma_juridica' - public = [RP_LIST, RP_DETAIL] class BaseMixin(MasterDetailCrud.BaseMixin): - list_field_names = ['norma_relacionada'] + list_field_names = ['norma_relacionada', 'tipo_vinculo'] class CreateView(MasterDetailCrud.CreateView): form_class = NormaRelacionadaForm @@ -52,11 +51,12 @@ class NormaRelacionadaCrud(MasterDetailCrud): form_class = NormaRelacionadaForm def get_initial(self): - self.initial['tipo'] = self.object.norma_relacionada.tipo.id - self.initial['numero'] = self.object.norma_relacionada.numero - self.initial['ano'] = self.object.norma_relacionada.ano - self.initial['ementa'] = self.object.norma_relacionada.ementa - return self.initial + initial = super(UpdateView, self).get_initial() + initial['tipo'] = self.object.norma_relacionada.tipo.id + initial['numero'] = self.object.norma_relacionada.numero + initial['ano'] = self.object.norma_relacionada.ano + initial['ementa'] = self.object.norma_relacionada.ementa + return initial class DetailView(MasterDetailCrud.DetailView): @@ -172,12 +172,13 @@ class NormaCrud(Crud): layout_key = 'NormaJuridicaCreate' def get_initial(self): + initial = super(UpdateView, self).get_initial() norma = NormaJuridica.objects.get(id=self.kwargs['pk']) if norma.materia: - self.initial['tipo_materia'] = norma.materia.tipo - self.initial['ano_materia'] = norma.materia.ano - self.initial['numero_materia'] = norma.materia.numero - return self.initial.copy() + initial['tipo_materia'] = norma.materia.tipo + initial['ano_materia'] = norma.materia.ano + initial['numero_materia'] = norma.materia.numero + return initial def recuperar_norma(request): diff --git a/sapl/painel/migrations/0002_auto_20180523_1430.py b/sapl/painel/migrations/0002_auto_20180523_1430.py new file mode 100644 index 000000000..52074acd1 --- /dev/null +++ b/sapl/painel/migrations/0002_auto_20180523_1430.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.13 on 2018-05-23 17:30 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('painel', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='cronometro', + name='tipo', + field=models.CharField(choices=[('A', 'Aparte'), ('D', 'Discurso'), ('O', 'Ordem do dia'), ('C', 'Considerações finais')], max_length=1, verbose_name='Tipo Cronômetro'), + ), + ] diff --git a/sapl/painel/models.py b/sapl/painel/models.py index 097981520..f999ca480 100644 --- a/sapl/painel/models.py +++ b/sapl/painel/models.py @@ -26,7 +26,8 @@ class Cronometro(models.Model): CRONOMETRO_TYPES = ( ('A', _('Aparte')), ('D', _('Discurso')), - ('O', _('Ordem do dia')) + ('O', _('Ordem do dia')), + ('C', _('Considerações finais')) ) CRONOMETRO_STATUS = ( diff --git a/sapl/painel/views.py b/sapl/painel/views.py index d3f7fc5cc..0cb151382 100644 --- a/sapl/painel/views.py +++ b/sapl/painel/views.py @@ -464,6 +464,7 @@ def get_dados_painel(request, pk): 'cronometro_aparte': get_cronometro_status(request, 'aparte'), 'cronometro_discurso': get_cronometro_status(request, 'discurso'), 'cronometro_ordem': get_cronometro_status(request, 'ordem'), + 'cronometro_consideracoes': get_cronometro_status(request, 'consideracoes'), 'status_painel': sessao.painel_aberto, 'brasao': brasao } diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index e5b8d7d58..36ce94c3a 100644 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -261,6 +261,8 @@ class FiliacaoForm(ModelForm): fields = ['partido', 'data', 'data_desfiliacao'] + widgets = {'data': forms.DateInput(attrs={'autocomplete': 'off'}), + 'data_desfiliacao': forms.DateInput(attrs={'autocomplete': 'off'})} def clean(self): super(FiliacaoForm, self).clean() @@ -321,7 +323,7 @@ class FrenteForm(ModelForm): frente = super(FrenteForm, self).save(commit) content_type = ContentType.objects.get_for_model(Frente) object_id = frente.pk - tipo = TipoAutor.objects.get(descricao='Frente Parlamentar') + tipo = TipoAutor.objects.get(descricao__icontains='Frente') Autor.objects.create( content_type=content_type, object_id=object_id, diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index b56215648..093bb83bd 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -192,7 +192,7 @@ class ColigacaoCrud(CrudAux): help_topic = 'coligacao' class ListView(CrudAux.ListView): - ordering = ('-numero_votos', 'nome') + ordering = ('legislatura', '-nome') def get_context_data(self, **kwargs): context = super(ColigacaoCrud.ListView, self).get_context_data( diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index dc3d5280c..7d68ad9c3 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -155,6 +155,7 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): fields = ['tipo', 'numero', 'protocolo__numero', + 'numero_externo', 'data', 'tramitacaoadministrativo__unidade_tramitacao_destino', 'tramitacaoadministrativo__status'] @@ -173,7 +174,8 @@ class DocumentoAdministrativoFilterSet(django_filters.FilterSet): row2 = to_row( [('ano', 4), - ('protocolo__numero', 4), + ('protocolo__numero', 2), + ('numero_externo', 2), ('data', 4)]) row3 = to_row( @@ -645,6 +647,7 @@ class DocumentoAdministrativoForm(ModelForm): 'tramitacao', 'dias_prazo', 'data_fim_prazo', + 'numero_externo', 'observacao', 'texto_integral', 'protocolo', @@ -655,9 +658,6 @@ class DocumentoAdministrativoForm(ModelForm): def clean(self): super(DocumentoAdministrativoForm, self).clean() - if not self.is_valid(): - return self.cleaned_data - cleaned_data = self.cleaned_data if not self.is_valid(): @@ -665,14 +665,22 @@ class DocumentoAdministrativoForm(ModelForm): numero_protocolo = self.data['numero_protocolo'] ano_protocolo = self.data['ano_protocolo'] - numero_documento = self.cleaned_data['numero'] - tipo_documento = self.data['tipo'] - - documento = DocumentoAdministrativo.objects.filter(numero=numero_documento, - tipo=tipo_documento, ano=ano_protocolo) - - if documento: - raise ValidationError('Documento já existente') + numero_documento = int(self.cleaned_data['numero']) + tipo_documento = int(self.data['tipo']) + ano_documento = int(self.data['ano']) + + # não permite atualizar para numero/ano/tipo existente + if self.instance.pk: + mudanca_doc = numero_documento != self.instance.numero \ + or ano_documento != self.instance.ano \ + or tipo_documento != self.instance.tipo.pk + + if not self.instance.pk or mudanca_doc: + doc_exists = DocumentoAdministrativo.objects.filter(numero=numero_documento, + tipo=tipo_documento, + ano=ano_protocolo).exists() + if doc_exists: + raise ValidationError('Documento já existente') # campos opcionais, mas que se informados devem ser válidos if numero_protocolo and ano_protocolo: @@ -690,6 +698,22 @@ class DocumentoAdministrativoForm(ModelForm): numero_protocolo, ano_protocolo)) raise ValidationError(msg) + inst = self.instance.protocolo + protocolo_antigo = inst.numero if inst else None + + if str(protocolo_antigo) != numero_protocolo: + exist_materia = MateriaLegislativa.objects.filter( + numero_protocolo=numero_protocolo, + ano=ano_protocolo).exists() + + exist_doc = DocumentoAdministrativo.objects.filter( + protocolo_id=numero_protocolo, + ano=ano_protocolo).exists() + if exist_materia or exist_doc: + raise ValidationError(_('Protocolo %s/%s ja possui' + ' documento vinculado' + % (numero_protocolo, ano_protocolo))) + return self.cleaned_data def save(self, commit=True): @@ -720,7 +744,7 @@ class DocumentoAdministrativoForm(ModelForm): [('texto_integral', 12)]) row6 = to_row( - [('dias_prazo', 6), ('data_fim_prazo', 6)]) + [('numero_externo', 4), ('dias_prazo', 6), ('data_fim_prazo', 2)]) row7 = to_row( [('observacao', 12)]) @@ -733,3 +757,118 @@ class DocumentoAdministrativoForm(ModelForm): row6, row7)) super(DocumentoAdministrativoForm, self).__init__( *args, **kwargs) + + +class DesvincularDocumentoForm(ModelForm): + + numero = forms.CharField(required=True, + label=DocumentoAdministrativo._meta. + get_field('numero').verbose_name + ) + ano = forms.ChoiceField(required=True, + label=DocumentoAdministrativo._meta. + get_field('ano').verbose_name, + choices=RANGE_ANOS, + widget=forms.Select(attrs={'class': 'selector'})) + + def clean(self): + super(DesvincularDocumentoForm, self).clean() + + cleaned_data = self.cleaned_data + + if not self.is_valid(): + return cleaned_data + + numero = cleaned_data['numero'] + ano = cleaned_data['ano'] + tipo = cleaned_data['tipo'] + + try: + documento = DocumentoAdministrativo.objects.get(numero=numero, ano=ano, tipo=tipo) + if not documento.protocolo: + raise forms.ValidationError( + _("%s %s/%s não se encontra vinculado a nenhum protocolo" % (tipo, numero, ano))) + except ObjectDoesNotExist: + raise forms.ValidationError( + _("%s %s/%s não existe" % (tipo, numero, ano))) + + return cleaned_data + + class Meta: + model = DocumentoAdministrativo + fields = ['tipo', + 'numero', + 'ano', + ] + + def __init__(self, *args, **kwargs): + + row1 = to_row( + [('numero', 4), + ('ano', 4), + ('tipo', 4)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_('Identificação do Documento'), + row1, + HTML(" "), + form_actions(label='Desvincular') + ) + ) + super(DesvincularDocumentoForm, self).__init__( + *args, **kwargs) + + +class DesvincularMateriaForm(forms.Form): + + numero = forms.CharField(required=True, + label=_('Número da Matéria')) + ano = forms.ChoiceField(required=True, + label=_('Ano da Matéria'), + choices=RANGE_ANOS, + widget=forms.Select(attrs={'class': 'selector'})) + tipo = forms.ModelChoiceField(label=_('Tipo de Matéria'), + required=True, + queryset=TipoMateriaLegislativa.objects.all(), + empty_label='------') + + def clean(self): + super(DesvincularMateriaForm, self).clean() + + cleaned_data = self.cleaned_data + + if not self.is_valid(): + return cleaned_data + + numero = cleaned_data['numero'] + ano = cleaned_data['ano'] + tipo = cleaned_data['tipo'] + + try: + materia = MateriaLegislativa.objects.get(numero=numero, ano=ano, tipo=tipo) + if not materia.numero_protocolo: + raise forms.ValidationError( + _("%s %s/%s não se encontra vinculada a nenhum protocolo" % (tipo, numero, ano))) + except ObjectDoesNotExist: + raise forms.ValidationError( + _("%s %s/%s não existe" % (tipo, numero, ano))) + + return cleaned_data + + def __init__(self, *args, **kwargs): + super(DesvincularMateriaForm, self).__init__(*args, **kwargs) + + row1 = to_row( + [('numero', 4), + ('ano', 4), + ('tipo', 4)]) + + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_('Identificação da Matéria'), + row1, + HTML(" "), + form_actions(label='Desvincular') + ) + ) diff --git a/sapl/protocoloadm/migrations/0004_documentoadministrativo_numero_externo.py b/sapl/protocoloadm/migrations/0004_documentoadministrativo_numero_externo.py new file mode 100644 index 000000000..b3659efbb --- /dev/null +++ b/sapl/protocoloadm/migrations/0004_documentoadministrativo_numero_externo.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.11 on 2018-04-25 18:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('protocoloadm', '0003_auto_20180103_1343'), + ] + + operations = [ + migrations.AddField( + model_name='documentoadministrativo', + name='numero_externo', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='Número Externo'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index d4d45b3ad..cf7616d7a 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -133,6 +133,10 @@ class DocumentoAdministrativo(models.Model): verbose_name=_('Em Tramitação?'), choices=YES_NO_CHOICES) assunto = models.TextField(verbose_name=_('Assunto')) + numero_externo = models.PositiveIntegerField( + blank=True, + null=True, + verbose_name=_('Número Externo')) observacao = models.TextField( blank=True, verbose_name=_('Observação')) texto_integral = models.FileField( diff --git a/sapl/protocoloadm/urls.py b/sapl/protocoloadm/urls.py index 46fec9980..223c38014 100644 --- a/sapl/protocoloadm/urls.py +++ b/sapl/protocoloadm/urls.py @@ -15,7 +15,9 @@ from sapl.protocoloadm.views import (AnularProtocoloAdmView, TipoDocumentoAdministrativoCrud, TramitacaoAdmCrud, atualizar_numero_documento, - doc_texto_integral) + doc_texto_integral, + DesvincularDocumentoView, + DesvincularMateriaView) from .apps import AppConfig @@ -61,6 +63,10 @@ urlpatterns_protocolo = [ url(r'^protocoloadm/anular-protocolo', AnularProtocoloAdmView.as_view(), name='anular_protocolo'), + url(r'^protocoloadm/desvincular-documento', + DesvincularDocumentoView.as_view(), name='desvincular_documento'), + url(r'^protocoloadm/desvincular-materia', + DesvincularMateriaView.as_view(), name='desvincular_materia'), url(r'^protocoloadm/protocolar-mat', ProtocoloMateriaView.as_view(), name='protocolar_mat'), diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index d4f7e96d9..c1a2b185a 100644 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -11,8 +11,9 @@ 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 import ListView, CreateView from django.views.generic.base import RedirectView, TemplateView +from django.views.generic.edit import FormView from django_filters.views import FilterView import sapl @@ -30,7 +31,7 @@ from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm, DocumentoAdministrativoFilterSet, DocumentoAdministrativoForm, ProtocoloDocumentForm, ProtocoloFilterSet, ProtocoloMateriaForm, - TramitacaoAdmEditForm, TramitacaoAdmForm) + TramitacaoAdmEditForm, TramitacaoAdmForm, DesvincularDocumentoForm, DesvincularMateriaForm) from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo, StatusTramitacaoAdministrativo, TipoDocumentoAdministrativo, TramitacaoAdministrativo) @@ -451,10 +452,11 @@ class ProtocoloMateriaView(PermissionRequiredMixin, CreateView): if not protocolo.numero: protocolo.numero = (numero['numero__max'] + 1) if numero['numero__max'] else 1 - if protocolo.numero < (numero['numero__max'] + 1): - msg = _('Número de protocolo deve ser maior que {}').format(numero['numero__max']) - messages.add_message(self.request, messages.ERROR, msg) - return self.render_to_response(self.get_context_data()) + if numero['numero__max']: + if protocolo.numero < (numero['numero__max'] + 1): + msg = _('Número de protocolo deve ser maior que {}').format(numero['numero__max']) + messages.add_message(self.request, messages.ERROR, msg) + return self.render_to_response(self.get_context_data()) protocolo.ano = timezone.now().year protocolo.data = timezone.now().date() protocolo.hora = timezone.now().time() @@ -608,18 +610,19 @@ class TramitacaoAdmCrud(MasterDetailCrud): form_class = TramitacaoAdmForm def get_initial(self): + initial = super(CreateView, self).get_initial() local = DocumentoAdministrativo.objects.get( pk=self.kwargs['pk']).tramitacaoadministrativo_set.order_by( '-data_tramitacao', '-id').first() if local: - self.initial['unidade_tramitacao_local' + 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 + initial['unidade_tramitacao_local'] = '' + initial['data_tramitacao'] = timezone.now().date() + return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -720,3 +723,39 @@ def atualizar_numero_documento(request): {'numero': 1, 'ano': ano}) return response + + +class DesvincularDocumentoView(PermissionRequiredMixin, CreateView): + template_name = 'protocoloadm/anular_protocoloadm.html' + form_class = DesvincularDocumentoForm + form_valid_message = _('Documento desvinculado com sucesso!') + permission_required = ('protocoloadm.action_anular_protocolo', ) + + def get_success_url(self): + return reverse('sapl.protocoloadm:protocolo') + + def form_valid(self, form): + documento = DocumentoAdministrativo.objects.get(numero=form.cleaned_data['numero'], + ano=form.cleaned_data['ano'], + tipo=form.cleaned_data['tipo']) + documento.protocolo = None + documento.save() + return redirect(self.get_success_url()) + + +class DesvincularMateriaView(PermissionRequiredMixin, FormView): + template_name = 'protocoloadm/anular_protocoloadm.html' + form_class = DesvincularMateriaForm + form_valid_message = _('Matéria desvinculado com sucesso!') + permission_required = ('protocoloadm.action_anular_protocolo', ) + + def get_success_url(self): + return reverse('sapl.protocoloadm:protocolo') + + def form_valid(self, form): + materia = MateriaLegislativa.objects.get(numero=form.cleaned_data['numero'], + ano=form.cleaned_data['ano'], + tipo=form.cleaned_data['tipo']) + materia.numero_protocolo = None + materia.save() + return redirect(self.get_success_url()) \ No newline at end of file diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 9f46450b8..0bad27889 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -795,7 +795,8 @@ def relatorio_sessao_plenaria(request, pk): for idx in range(len(lst_expedientes)): txt_expedientes = lst_expedientes[idx]['txt_expediente'] - txt_expedientes = TrocaTag(txt_expedientes, '', 6, 6, 'expedientes') + txt_expedientes = TrocaTag(txt_expedientes, '', 6, 6, + 'expedientes', ' - - - - - - - - - - - - - - - - - -
-
-

-


-

Câmara Municipal de Agudo

-

Estado do Rio Grande do Sul





-
-

- -
- -

-
- - -

-
- - -

- -
- -

-
- - -

-
- -
diff --git a/sapl/static/XSLT/HTML/mocao.xsl b/sapl/static/XSLT/HTML/mocao.xsl deleted file mode 100644 index adb7ef3c9..000000000 --- a/sapl/static/XSLT/HTML/mocao.xsl +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/sapl/static/XSLT/HTML/mocao2.xsl b/sapl/static/XSLT/HTML/mocao2.xsl deleted file mode 100644 index 14f71c982..000000000 --- a/sapl/static/XSLT/HTML/mocao2.xsl +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - -
-
- -


-

Câmara Municipal de Agudo

-

Estado do Rio Grande do Sul





-
- - - -
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- -
\ No newline at end of file diff --git a/sapl/static/XSLT/HTML/parecer.xsl b/sapl/static/XSLT/HTML/parecer.xsl deleted file mode 100644 index 14100320e..000000000 --- a/sapl/static/XSLT/HTML/parecer.xsl +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/sapl/static/XSLT/HTML/pedido.xsl b/sapl/static/XSLT/HTML/pedido.xsl deleted file mode 100644 index 9e5002d01..000000000 --- a/sapl/static/XSLT/HTML/pedido.xsl +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/sapl/static/XSLT/HTML/pedido2.xsl b/sapl/static/XSLT/HTML/pedido2.xsl deleted file mode 100644 index facc154c6..000000000 --- a/sapl/static/XSLT/HTML/pedido2.xsl +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - -
-
- -


-

Câmara Municipal de Agudo

-

Estado do Rio Grande do Sul





-
- - - -
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- -
\ No newline at end of file diff --git a/sapl/static/XSLT/HTML/pl.xsl b/sapl/static/XSLT/HTML/pl.xsl deleted file mode 100644 index 0da6eddb4..000000000 --- a/sapl/static/XSLT/HTML/pl.xsl +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - <xsl:value-of select="@id"/> - - - - - - - - - -
-

PROPOSIÇÃO

-
- -
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -

- -

-
- -
-

JUSTIFICATIVA

-
-

- -

-
- -
-

MENSAGEM

-
-

- -

-
-
\ No newline at end of file diff --git a/sapl/static/XSLT/HTML/pl2.xsl b/sapl/static/XSLT/HTML/pl2.xsl deleted file mode 100644 index f68506f95..000000000 --- a/sapl/static/XSLT/HTML/pl2.xsl +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - -
-
- -


-

Câmara Municipal de Agudo

-

Estado do Rio Grande do Sul





-
- - - - -
- - -

-
- - -

-
- - -

-
- - -

-

-
- - -

-

-
- - -

-

-
- - -

-

-
- - -

-

-
- - -

-

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- - -

-
- -
\ No newline at end of file diff --git a/sapl/static/XSLT/HTML/requerimento.xsl b/sapl/static/XSLT/HTML/requerimento.xsl deleted file mode 100644 index 58f202b09..000000000 --- a/sapl/static/XSLT/HTML/requerimento.xsl +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/sapl/static/XSLT/HTML/requerimento2.xsl b/sapl/static/XSLT/HTML/requerimento2.xsl deleted file mode 100644 index 3d63f6cf8..000000000 --- a/sapl/static/XSLT/HTML/requerimento2.xsl +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - -
-
- -


-

Câmara Municipal de Agudo

-

Estado do Rio Grande do Sul





-
- - - -
- - -

-
- - -

-
- - -

-
- - -

-
- - -

- -
- -

- -
- -

-
- - -

-
- -
\ No newline at end of file diff --git a/sapl/templates/base/layouts.yaml b/sapl/templates/base/layouts.yaml index 3f066289b..c07c15036 100644 --- a/sapl/templates/base/layouts.yaml +++ b/sapl/templates/base/layouts.yaml @@ -21,7 +21,7 @@ AppConfig: - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma {% trans 'Cronômetros do Painel' %}: - - cronometro_discurso cronometro_aparte cronometro_ordem + - cronometro_discurso cronometro_aparte cronometro_ordem cronometro_consideracoes {% trans 'Configurações do Painel' %}: - mostrar_brasao_painel diff --git a/sapl/templates/materia/layouts.yaml b/sapl/templates/materia/layouts.yaml index 74c11e6c8..d31817b94 100644 --- a/sapl/templates/materia/layouts.yaml +++ b/sapl/templates/materia/layouts.yaml @@ -23,7 +23,7 @@ MateriaLegislativa: {% trans 'Identificação Básica' %}: - tipo ano numero - data_apresentacao numero_protocolo tipo_apresentacao - - autor + - tipo_autor autor - texto_original {% trans 'Outras Informações' %}: - apelido dias_prazo polemica diff --git a/sapl/templates/materia/materialegislativa_form.html b/sapl/templates/materia/materialegislativa_form.html index 335350406..bb435be91 100644 --- a/sapl/templates/materia/materialegislativa_form.html +++ b/sapl/templates/materia/materialegislativa_form.html @@ -22,6 +22,44 @@ } } $("#id_tipo, #id_ano").change(recuperar_numero_ano); + + function compare(a, b) { + if (a.text < b.text) + return -1; + if (a.text > b.text) + return 1; + return 0; + } + + $(document).ready(function() { + $("#id_tipo_autor").change(function() { + var tipo_selecionado = $("#id_tipo_autor").val(); + var autor_selecionado = $("#id_autor").val(); + $("#id_autor option").remove() + if (tipo_selecionado !== undefined && tipo_selecionado !== null) { + var json_data = { + tipo : tipo_selecionado, + data_relativa : $("#id_data_apresentacao").val() + } + $.getJSON("/api/autor/possiveis", json_data, function(data){ + if (data) { + var results = data.sort(compare); + if (results.length > 1) { + $("#id_autor").append(""); + } + $.each(results, function(idx, obj) { + $("#id_autor") + .append($("") + .attr("value", obj.value) + .text(obj.text)); + }); + $("#id_autor").val(autor_selecionado); + } + }); + } + }); + $("#id_tipo_autor").trigger('change'); + }); {% endblock %} diff --git a/sapl/templates/norma/normajuridica_detail.html b/sapl/templates/norma/normajuridica_detail.html index ed52564ed..6dcd3d592 100644 --- a/sapl/templates/norma/normajuridica_detail.html +++ b/sapl/templates/norma/normajuridica_detail.html @@ -35,7 +35,7 @@
-

Relacionamentos

+

Normas Relacionadas

{% if object.get_normas_relacionadas.0|length > 0 %} {% for p in object.get_normas_relacionadas.0 %} diff --git a/sapl/templates/norma/subnav.yaml b/sapl/templates/norma/subnav.yaml index 0599efdfb..a89326fd7 100644 --- a/sapl/templates/norma/subnav.yaml +++ b/sapl/templates/norma/subnav.yaml @@ -2,8 +2,9 @@ - title: {% trans 'Início' %} url: normajuridica_detail -- title: {% trans 'Normas Relacionadas' %} +- title: {% trans 'Alterações em Outras Normas' %} url: normarelacionada_list + check_permission: norma.list_normarelacionada # Opção adicionada para chamar o TextoArticulado da norma. # para integração foram necessárias apenas criar a url norma_ta em urls.py diff --git a/sapl/templates/painel/index.html b/sapl/templates/painel/index.html index c50167161..621689ffe 100644 --- a/sapl/templates/painel/index.html +++ b/sapl/templates/painel/index.html @@ -28,7 +28,7 @@ ul, li { list-style-type: none; } - #date, #sessao_plenaria, #sessao_plenaria_data, #sessao_plenaria_hora_inicio, #message, #cronometro_discurso, #cronometro_aparte, #cronometro_ordem, #relogio, #parlamentares, #votacao, #materia_legislativa_texto, #observacao_materia, #resultado_votacao, #orador { + #date, #sessao_plenaria, #sessao_plenaria_data, #sessao_plenaria_hora_inicio, #message, #cronometro_discurso, #cronometro_aparte, #cronometro_ordem, #cronometro_consideracoes, #relogio, #parlamentares, #votacao, #materia_legislativa_texto, #observacao_materia, #resultado_votacao, #orador { font-family: Verdana; } } @@ -108,6 +108,9 @@ Questão de Ordem: + + Considerações Finais: +
@@ -195,9 +198,20 @@ audioAlertFinish.play(); }); + $('#cronometro_consideracoes').runner({ + autostart: false, + countdown: true, + startAt: {{ 'consideracoes'|cronometro_to_seconds }} * 1000, + stopAt: 0, + milliseconds: false + }).on('runnerFinish', function(eventObject, info){ + audioAlertFinish.play(); + }); + var discurso_previous; var ordem_previous; var aparte_previous; + var consideracoes_previous; var counter = 1; (function poll() { @@ -325,6 +339,16 @@ ordem_previous = ordem_current; } + var consideracoes_current = data["cronometro_consideracoes"]; + if (!consideracoes_previous){ + consideracoes_previous = '' + } + + if (consideracoes_current != consideracoes_previous) { + $('#cronometro_consideracoes').runner(consideracoes_current); + consideracoes_previous = consideracoes_current; + } + if($('#cronometro_discurso').runner('info').formattedTime == 30) { audioAlertFinish.play(); } @@ -337,6 +361,10 @@ audioAlertFinish.play(); } + if($('#cronometro_consideracoes').runner('info').formattedTime == 30) { + audioAlertFinish.play(); + } + if (data['materia_legislativa_texto']){ $("#materia_legislativa_texto").text(data["materia_legislativa_texto"]); } diff --git a/sapl/templates/parlamentares/layouts.yaml b/sapl/templates/parlamentares/layouts.yaml index ba7767cd2..2c21ade8d 100644 --- a/sapl/templates/parlamentares/layouts.yaml +++ b/sapl/templates/parlamentares/layouts.yaml @@ -37,10 +37,10 @@ Parlamentar: - situacao_militar profissao - endereco_web - email - - numero_gab_parlamentar telefone fax + - numero_gab_parlamentar telefone - endereco_residencia cep_residencia - municipio_residencia uf_residencia - - telefone_residencia fax_residencia + - telefone_residencia - locais_atuacao - fotografia:5 - biografia @@ -54,10 +54,10 @@ ParlamentarUpdate: - situacao_militar profissao - endereco_web - email - - numero_gab_parlamentar telefone fax + - numero_gab_parlamentar telefone - endereco_residencia cep_residencia - municipio_residencia uf_residencia - - telefone_residencia fax_residencia + - telefone_residencia - locais_atuacao - fotografia cropping - biografia @@ -73,10 +73,10 @@ ParlamentarCreate: - situacao_militar profissao - endereco_web - email - - numero_gab_parlamentar telefone fax + - numero_gab_parlamentar telefone - endereco_residencia cep_residencia - municipio_residencia - - telefone_residencia fax_residencia + - telefone_residencia - locais_atuacao - fotografia - biografia diff --git a/sapl/templates/protocoloadm/comprovante.html b/sapl/templates/protocoloadm/comprovante.html index 031b096c4..1de22d402 100644 --- a/sapl/templates/protocoloadm/comprovante.html +++ b/sapl/templates/protocoloadm/comprovante.html @@ -61,14 +61,14 @@ Data / Horário {{ protocolo.data|date:"d/m/Y" }} - {{ protocolo.timestamp|date:"H:i:s" }} - {% if protocolo.tipo_processo == 0 %} + {% if protocolo.tipo_processo == 1 %} Ementa {{ protocolo.assunto_ementa }} - Interessado - {{ protocolo.interessado }} + Autor + {{ protocolo.autor }} {% endif %} @@ -85,5 +85,9 @@ Número Páginas {{ protocolo.numero_paginas }} + + Comprovante emitido por + {{ request.user.username }} + {% endblock detail_content %} diff --git a/sapl/templates/protocoloadm/layouts.yaml b/sapl/templates/protocoloadm/layouts.yaml index 76b80da18..b1bb2f31b 100644 --- a/sapl/templates/protocoloadm/layouts.yaml +++ b/sapl/templates/protocoloadm/layouts.yaml @@ -11,6 +11,7 @@ DocumentoAdministrativo: - interessado tramitacao - texto_integral {% trans 'Outras Informações' %}: + - numero_externo - dias_prazo data_fim_prazo - observacao diff --git a/sapl/templates/protocoloadm/protocoloadm_detail.html b/sapl/templates/protocoloadm/protocoloadm_detail.html index 5a3fe05d0..3db9ea6d8 100644 --- a/sapl/templates/protocoloadm/protocoloadm_detail.html +++ b/sapl/templates/protocoloadm/protocoloadm_detail.html @@ -5,5 +5,7 @@ {% trans 'Protocolar Matéria' %} {% trans 'Protocolar Documento' %} {% trans 'Anular Protocolo' %} + {% trans 'Desvincular Documentos' %} + {% trans 'Desvincular Matérias' %}
{% endblock editions %} diff --git a/sapl/templates/sessao/blocos_resumo/conteudo_multimidia.html b/sapl/templates/sessao/blocos_resumo/conteudo_multimidia.html index 4749f5ad2..b275ebd79 100644 --- a/sapl/templates/sessao/blocos_resumo/conteudo_multimidia.html +++ b/sapl/templates/sessao/blocos_resumo/conteudo_multimidia.html @@ -1,8 +1,8 @@
Conteúdo Multimídia
-
{{multimidia_audio}}
-
{{multimidia_video}}
+ +
-


\ No newline at end of file +


diff --git a/sapl/templates/sessao/blocos_resumo/expedientes.html b/sapl/templates/sessao/blocos_resumo/expedientes.html index e233fcb58..5462fd3f3 100644 --- a/sapl/templates/sessao/blocos_resumo/expedientes.html +++ b/sapl/templates/sessao/blocos_resumo/expedientes.html @@ -6,7 +6,7 @@ {{e.tipo}}:

-
+

{{e.conteudo|safe}}

diff --git a/sapl/templates/sessao/expediente.html b/sapl/templates/sessao/expediente.html index 940d73d97..106840b88 100644 --- a/sapl/templates/sessao/expediente.html +++ b/sapl/templates/sessao/expediente.html @@ -28,6 +28,7 @@
+ {% endif %} diff --git a/sapl/templates/sessao/mesa.html b/sapl/templates/sessao/mesa.html index 02f313656..56e7f9989 100644 --- a/sapl/templates/sessao/mesa.html +++ b/sapl/templates/sessao/mesa.html @@ -38,7 +38,7 @@
diff --git a/sapl/templates/sessao/painel.html b/sapl/templates/sessao/painel.html index 22dc62dda..d83c99e86 100644 --- a/sapl/templates/sessao/painel.html +++ b/sapl/templates/sessao/painel.html @@ -69,6 +69,20 @@


+
+

Cronômetro de Considerações Finais

+
+ +
+
+
+
+ +
+
+
+
+

@@ -102,6 +116,7 @@ $(function() { $('#discurso').prop('disabled', true); $('#aparte').prop('disabled', true); $('#ordem').prop('disabled', true); + $('#consideracoes').prop('disabled', true); $('#discurso').runner({ autostart: false, @@ -119,6 +134,8 @@ $(function() { $('#aparteReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); }); @@ -135,6 +152,8 @@ $(function() { $('#aparteReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } else { @@ -147,6 +166,8 @@ $(function() { $('#aparteReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } }); @@ -174,6 +195,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); }); @@ -189,6 +212,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } else { $.get('/painel/cronometro', { tipo: 'aparte', action: 'stop' } ); @@ -200,6 +225,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#ordemStart').prop('disabled', false); $('#ordemReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } }); @@ -227,6 +254,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#aparteStart').prop('disabled', false); $('#aparteReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); }); $('#ordemStart').click(function() { @@ -241,6 +270,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#aparteStart').prop('disabled', false); $('#aparteReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } else { @@ -253,6 +284,8 @@ $(function() { $('#discursoReset').prop('disabled', false); $('#aparteStart').prop('disabled', false); $('#aparteReset').prop('disabled', false); + $('#consideracoesStart').prop('disabled', false); + $('#consideracoesReset').prop('disabled', false); } }); @@ -264,6 +297,65 @@ $(function() { $('#ordem').runner('reset'); }); + $('#consideracoes').runner({ + autostart: false, + countdown: true, + startAt: {{cronometro_consideracoes}} * 1000, + stopAt: 0, + milliseconds: false + }).on('runnerFinish', function(eventObject, info){ + $.get('/painel/cronometro', { tipo: 'consideracoes', action: 'stop' } ); + + $('#consideracoesReset').show(); + $('#consideracoes').runner('stop'); + $('#consideracoesStart').text('Iniciar'); + $('#discursoStart').prop('disabled', false); + $('#discursoReset').prop('disabled', false); + $('#ordemStart').prop('disabled', false); + $('#ordemReset').prop('disabled', false); + $('#aparteStart').prop('disabled', false); + $('#aparteReset').prop('disabled', false); + + }); + + $('#consideracoesStart').click(function(){ + if ($('#consideracoesStart').text() == 'Iniciar') { + + $.get('/painel/cronometro', { tipo: 'consideracoes', action: 'start' } ); + + $('#consideracoesReset').hide(); + $('#consideracoes').runner('start'); + $('#consideracoesStart').text('Parar'); + $('#discursoStart').prop('disabled', false); + $('#discursoReset').prop('disabled', false); + $('#ordemStart').prop('disabled', false); + $('#ordemReset').prop('disabled', false); + $('#aparteStart').prop('disabled', false); + $('#aparteReset').prop('disabled', false); + } else { + + $.get('/painel/cronometro', { tipo: 'consideracoes', action: 'stop' } ); + + $('#consideracoesReset').show(); + $('#consideracoes').runner('stop'); + $('#consideracoesStart').text('Iniciar'); + $('#discursoStart').prop('disabled', false); + $('#discursoReset').prop('disabled', false); + $('#ordemStart').prop('disabled', false); + $('#ordemReset').prop('disabled', false); + $('#aparteStart').prop('disabled', false); + $('#aparteReset').prop('disabled', false); + } + }); + + $('#consideracoesReset').click(function() { + + $.get('/painel/cronometro', { tipo: 'consideracoes', action: 'reset' } ); + + $('#consideracoes').runner('stop'); + $('#consideracoes').runner('reset'); + }); + }); function switch_painel(aberto) { diff --git a/sapl/test_urls.py b/sapl/test_urls.py index 8423a98ed..0f1671c98 100644 --- a/sapl/test_urls.py +++ b/sapl/test_urls.py @@ -164,7 +164,9 @@ apps_url_patterns_prefixs_and_users = { '/logout', '/ajuda', '/email', - '/recuperar-senha' + '/recuperar-senha', + '/sapl', + '/XSLT', ]}, 'comissoes': { 'users': {'operador_geral': ['/sistema', '/comissao'], diff --git a/sapl/urls.py b/sapl/urls.py index b6a5af3e4..07f382013 100644 --- a/sapl/urls.py +++ b/sapl/urls.py @@ -21,6 +21,7 @@ from django.views.generic.base import RedirectView, TemplateView from django.views.static import serve as view_static_server import sapl.api.urls +import sapl.audiencia.urls import sapl.base.urls import sapl.comissoes.urls import sapl.compilacao.urls @@ -33,7 +34,6 @@ import sapl.protocoloadm.urls import sapl.redireciona_urls.urls import sapl.relatorios.urls import sapl.sessao.urls -import sapl.audiencia.urls urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html'), @@ -62,9 +62,6 @@ urlpatterns = [ url(r'^favicon\.ico$', RedirectView.as_view( url='/static/img/favicon.ico', permanent=True)), - # Folhas XSLT e extras referenciadas por documentos migrados do sapl 2.5 - url(r'^XSLT/HTML/(?P.*)$', RedirectView.as_view( - url='/static/XSLT/HTML/%(path)s', permanent=False)), url(r'', include(sapl.redireciona_urls.urls)), ] diff --git a/sapl/utils.py b/sapl/utils.py index 0fa20f706..b674481c3 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -2,6 +2,7 @@ import hashlib import logging import os import re +import unicodedata from functools import wraps from operator import itemgetter from unicodedata import normalize as unicodedata_normalize @@ -687,7 +688,7 @@ def ExtraiTag(texto, posicao): return i + 1 -def TrocaTag(texto, startTag, endTag, sizeStart, sizeEnd, styleName): +def TrocaTag(texto, startTag, endTag, sizeStart, sizeEnd, styleName, subinitiTag, subendTag): textoSaida = '' insideTag = 0 i = 0 @@ -696,16 +697,19 @@ def TrocaTag(texto, startTag, endTag, sizeStart, sizeEnd, styleName): if '' in texto: texto = texto.replace('', '') texto = texto.replace('', '') + if '

' in texto: + texto = texto.replace('

', '') + texto = texto.replace('

', '') while (i < len(texto)): shard = texto[i:i + sizeStart] if (shard == startTag): i = ExtraiTag(texto, i) - textoSaida += '' + textoSaida += subinitiTag + styleName + '">' insideTag = 1 else: if (insideTag == 1): if (texto[i:i + sizeEnd] == endTag): - textoSaida += 'blockTable>' + textoSaida += subendTag insideTag = 0 i += sizeEnd else: @@ -732,3 +736,6 @@ def RemoveTag(texto): i += 1 return textoSaida + +def remover_acentos(string): + return unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode() diff --git a/check_migrations.sh b/scripts/django/check_migrations.sh similarity index 61% rename from check_migrations.sh rename to scripts/django/check_migrations.sh index 9db8143b2..dc78641cd 100755 --- a/check_migrations.sh +++ b/scripts/django/check_migrations.sh @@ -11,13 +11,15 @@ # A chamada do django 1.10 INVERTE ISSO. # # https://docs.djangoproject.com/en/1.10/ref/django-admin/#cmdoption-makemigrations-check -if python manage.py makemigrations --dry-run --exit > /dev/null; then + +git_project_root=$(git rev-parse --show-toplevel) +if python ${git_project_root}/manage.py makemigrations --dry-run --exit > /dev/null; then NC='\033[0m' RED='\033[0;31m' 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 -e "${RED}Execute o comando '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 +fi diff --git a/check_qa.sh b/scripts/django/check_qa.sh similarity index 89% rename from check_qa.sh rename to scripts/django/check_qa.sh index 47f2b56d3..a145bee7a 100755 --- a/check_qa.sh +++ b/scripts/django/check_qa.sh @@ -2,6 +2,10 @@ # Verifica se um breakpoint foi esquecido no código me=`basename "$0"` + +git_project_root=$(git rev-parse --show-toplevel) +cd ${git_project_root} + busca=`grep --color=auto --exclude=$me --exclude=ipython_log.py* -r -l "pdb.set_trace()" .` if [ ! -z "$busca" ] diff --git a/fix_qa.sh b/scripts/django/fix_qa.sh similarity index 88% rename from fix_qa.sh rename to scripts/django/fix_qa.sh index 264c01676..cb0f24001 100755 --- a/fix_qa.sh +++ b/scripts/django/fix_qa.sh @@ -8,5 +8,7 @@ # Uma forma simples de fazer isso é adicionando antes suas mudanças à # "staging area" do git, com `git add .` e após usar o script `git diff`. +git_project_root=$(git rev-parse --show-toplevel) +cd ${git_project_root} isort --recursive --skip='migrations' --skip='templates' --skip='ipython_log.py*' . autopep8 --in-place --recursive . --exclude='migrations,ipython_log.py*' diff --git a/scripts/gerar_grafico_apps.sh b/scripts/django/gerar_grafico_apps.sh similarity index 69% rename from scripts/gerar_grafico_apps.sh rename to scripts/django/gerar_grafico_apps.sh index 312f04b0c..ab6bf9a84 100755 --- a/scripts/gerar_grafico_apps.sh +++ b/scripts/django/gerar_grafico_apps.sh @@ -1,3 +1,6 @@ #!/bin/bash +git_project_root=$(git rev-parse --show-toplevel) +cd ${git_project_root} + python -c "from sapl.settings import SAPL_APPS; print(*[s.split('.')[-1] for s in SAPL_APPS])" | xargs -t ./manage.py graph_models -d -g -o zzz.png -l fdp diff --git a/scripts/reset_all_migrations.sh b/scripts/django/reset_all_migrations.sh similarity index 57% rename from scripts/reset_all_migrations.sh rename to scripts/django/reset_all_migrations.sh index 53e274fe9..d3f2cfbe4 100755 --- a/scripts/reset_all_migrations.sh +++ b/scripts/django/reset_all_migrations.sh @@ -3,6 +3,11 @@ # Sends all django migrations to the trash bin # Requires trash-cli. To install: # sudo apt-get install trash-cli +hash trash-put 2>/dev/null || { echo >&2 "I require trash-put but it's not installed. Aborting."; exit 1; } + +git_project_root=$(git rev-parse --show-toplevel) +cd ${git_project_root} + find -name 00*.py | grep /migrations/ | xargs trash-put # Make all migrations from scratch diff --git a/test_and_check_qa.sh b/scripts/django/test_and_check_qa.sh similarity index 53% rename from test_and_check_qa.sh rename to scripts/django/test_and_check_qa.sh index 14231ce80..024139bba 100755 --- a/test_and_check_qa.sh +++ b/scripts/django/test_and_check_qa.sh @@ -2,6 +2,9 @@ # QA checks: run this before every commit +git_project_root=$(git rev-parse --show-toplevel) +cd ${git_project_root} + py.test py.test --ds=sapl.crud.tests.settings sapl/crud/tests -./check_qa.sh +./scripts/django/check_qa.sh diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit index de82032c1..d8efe2cf5 100644 --- a/scripts/hooks/pre-commit +++ b/scripts/hooks/pre-commit @@ -4,5 +4,5 @@ if git diff --cached --name-status | grep -q '^M.*models\.py$'; then # se a checagem de migrations falhar impedimos o commit set -e - ./check_migrations.sh + ./scripts/django/check_migrations.sh fi diff --git a/scripts/redbaron.py b/scripts/redbaron.py index 3229872bc..b02e4cdfa 100644 --- a/scripts/redbaron.py +++ b/scripts/redbaron.py @@ -1,10 +1,14 @@ import os import re +import subprocess from redbaron import RedBaron from redbaron.nodes import EndlNode, ReturnNode, StringNode -root = '/home/mazza/work/sapl' +git_project_root = subprocess.Popen( + ["git", "rev-parse", "--show-toplevel"], + stdout=subprocess.PIPE + ).communicate()[0].decode('utf-8').replace('\n', '') def ignorado(path, name): @@ -13,13 +17,13 @@ def ignorado(path, name): 'relatorios/templates.*', '.*/migrations', ]: - if re.match(os.path.join(root, pattern), path): + if re.match(os.path.join(git_project_root, pattern), path): return True return name.startswith('ipython_log.py') or name == 'manage.py' filenames = [os.path.join(path, name) - for path, subdirs, files in os.walk(root) + for path, subdirs, files in os.walk(git_project_root) for name in files if name.endswith('.py') and not ignorado(path, name)] @@ -37,7 +41,7 @@ def build_red(filename): def write(node): - red = node.root + red = node.git_project_root with open(red.__filename__, "w") as source_code: source_code.write(red.dumps()) @@ -82,7 +86,7 @@ def fix(n): def local(node): - res = '%s:%s' % (node.root.__filename__, + res = '%s:%s' % (node.git_project_root.__filename__, node.absolute_bounding_box.top_left.line) os.system("echo '%s' | xclip -selection c" % res) return res diff --git a/scripts_docker/remove-all-containers.sh b/scripts_docker/remove-all-containers.sh index 64d41cd5c..a3d8fc624 100755 --- a/scripts_docker/remove-all-containers.sh +++ b/scripts_docker/remove-all-containers.sh @@ -1,4 +1,5 @@ #!/bin/bash -sudo docker stop $(docker ps -a -q) # Parar containers -sudo docker rm $(sudo docker ps -a -q) # Remover containers -sudo docker rmi -f $( sudo docker images -q ) # Remover imagens +sudo docker stop $(docker ps -a -q) # Para containers +sudo docker rm $(sudo docker ps -a -q) # Remove containers +sudo docker rmi -f $( sudo docker images -q ) # Remove imagens +sudo docker volume rm $(sudo docker volume ls -q -f dangling=true) # Remove volumes diff --git a/setup.py b/setup.py index 33c6bf7e0..fd4ed2708 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ install_requires = [ ] setup( name='interlegis-sapl', - version='3.1.72', + version='3.1.84', packages=find_packages(), include_package_data=True, license='GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007',