diff --git a/docker-compose.yml b/docker-compose.yml index 36df3e501..f721b66f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.71 + image: interlegis/sapl:3.1.76 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/release.sh b/release.sh new file mode 100755 index 000000000..78fee9187 --- /dev/null +++ b/release.sh @@ -0,0 +1,19 @@ +#/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 + +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 + +git add docker-compose.yml setup.py +git commit -m "Release: $NEXT_VERSION" +git tag $NEXT_VERSION +git push origin $NEXT_VERSION +git push origin 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/sapl/base/forms.py b/sapl/base/forms.py index 1a0637a22..513c3a7ca 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 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/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..059e49164 100644 --- a/sapl/legacy/migracao.py +++ b/sapl/legacy/migracao.py @@ -1,42 +1,75 @@ 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() -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..0f1e4d6dc 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): @@ -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..63d0ed09e 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] @@ -135,7 +155,11 @@ def enumerate_btree(folder): 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, + contagem_esperada, + contagem_real)) nao_identificados = defaultdict(list) @@ -148,14 +172,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 +189,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 +225,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 +257,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 +279,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') + 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 e75c6beb8..b1838218d 100644 --- a/sapl/materia/forms.py +++ b/sapl/materia/forms.py @@ -582,8 +582,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/norma/views.py b/sapl/norma/views.py index d74bc8857..fd5396353 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -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 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 36baa8f3e..06a2f7854 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,21 @@ 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'] - - if not self.instance.pk: - documento = DocumentoAdministrativo.objects.filter(numero=numero_documento, - tipo=tipo_documento, - ano=ano_protocolo) - if documento: + 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 @@ -721,7 +728,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)]) 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/sessao/forms.py b/sapl/sessao/forms.py index 3c1613d5d..70747ce32 100644 --- a/sapl/sessao/forms.py +++ b/sapl/sessao/forms.py @@ -452,7 +452,7 @@ class OradorForm(ModelForm): sessao_plenaria_id=id_sessao)] self.fields['parlamentar'].queryset = Parlamentar.objects.filter( - id__in=ids).order_by('nome_completo') + id__in=ids).order_by('nome_parlamentar') class Meta: model = Orador diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index bf001f8c3..e0b88b853 100644 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -30,12 +30,11 @@ from sapl.materia.forms import filtra_tramitacao_status from sapl.materia.models import (Autoria, DocumentoAcessorio, TipoMateriaLegislativa, Tramitacao) from sapl.materia.views import MateriaLegislativaPesquisaView -from sapl.norma.models import NormaJuridica from sapl.parlamentares.models import (Filiacao, Legislatura, Mandato, Parlamentar, SessaoLegislativa) from sapl.sessao.apps import AppConfig from sapl.sessao.forms import ExpedienteMateriaForm, OrdemDiaForm -from sapl.utils import show_results_filter_set +from sapl.utils import show_results_filter_set, remover_acentos from .forms import (AdicionarVariasMateriasFilterSet, BancadaForm, BlocoForm, ExpedienteForm, ListMateriaForm, MesaForm, @@ -386,13 +385,15 @@ def customize_link_materia(context, pk, has_permission, is_expediente): context['rows'][i][3] = (resultado, None) return context - + def get_presencas_generic(model, sessao, legislatura): presencas = model.objects.filter( sessao_plenaria=sessao) presentes = [p.parlamentar for p in presencas] + presentes = sorted(presentes, key=lambda x: remover_acentos(x.nome_parlamentar)) + mandato = Mandato.objects.filter( legislatura=legislatura).order_by('parlamentar__nome_parlamentar') @@ -1112,15 +1113,13 @@ def remove_parlamentar_composicao(request): if 'composicao_mesa' in request.POST: try: - composicao = IntegranteMesa.objects.get( - id=int(request.POST['composicao_mesa'])) + IntegranteMesa.objects.get( + id=int(request.POST['composicao_mesa'])).delete() except ObjectDoesNotExist: return JsonResponse( {'msg': ( 'Composição da Mesa não pôde ser removida!', 0)}) - composicao.delete() - return JsonResponse( {'msg': ( 'Parlamentar excluido com sucesso!', 1)}) @@ -1449,6 +1448,11 @@ class ExpedienteView(FormMixin, DetailView): self.object = self.get_object() form = ExpedienteForm(request.POST) + if 'apagar-expediente' in request.POST: + ExpedienteSessao.objects.filter( + sessao_plenaria_id=self.object.id).delete() + return self.form_valid(form) + if form.is_valid(): list_tipo = request.POST.getlist('tipo') list_conteudo = request.POST.getlist('conteudo') @@ -1524,8 +1528,7 @@ class VotacaoEditView(SessaoPermissionMixin): ordem_id = kwargs['oid'] if(int(request.POST['anular_votacao']) == 1): - for r in RegistroVotacao.objects.filter(ordem_id=ordem_id): - r.delete() + RegistroVotacao.objects.filter(ordem_id=ordem_id).delete() ordem = OrdemDia.objects.get( sessao_plenaria_id=self.object.id, @@ -1556,9 +1559,8 @@ class VotacaoEditView(SessaoPermissionMixin): materia = {'materia': ordem.materia, 'ementa': ordem.materia.ementa} context.update({'materia': materia}) - votacao = RegistroVotacao.objects.filter( - materia_id=materia_id, - ordem_id=ordem_id).last() + votacao = RegistroVotacao.objects.filter(materia_id=materia_id, + ordem_id=ordem_id).last() votacao_existente = {'observacao': sub( ' ', ' ', strip_tags(votacao.observacao)), 'resultado': votacao.tipo_resultado_votacao.nome, @@ -1705,8 +1707,7 @@ def fechar_votacao_materia(materia): VotoParlamentar.objects.filter(ordem=materia).delete() elif type(materia) == ExpedienteMateria: - RegistroVotacao.objects.filter( - expediente=materia).delete() + RegistroVotacao.objects.filter(expediente=materia).delete() VotoParlamentar.objects.filter(expediente=materia).delete() if materia.resultado: @@ -1754,7 +1755,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): elif self.expediente: expediente_id = kwargs['oid'] if (RegistroVotacao.objects.filter( - expediente_id=expediente_id).exists()): + expediente_id=expediente_id).exists()): msg = _('Esta matéria já foi votada!') messages.add_message(request, messages.ERROR, msg) return HttpResponseRedirect(reverse( @@ -1853,8 +1854,7 @@ class VotacaoNominalAbstract(SessaoPermissionMixin): return self.form_invalid(form) # Remove todas as votação desta matéria, caso existam if self.ordem: - RegistroVotacao.objects.filter( - ordem_id=ordem_id).delete() + RegistroVotacao.objects.filter(ordem_id=ordem_id).delete() elif self.expediente: RegistroVotacao.objects.filter( expediente_id=expediente_id).delete() @@ -1982,11 +1982,10 @@ class VotacaoNominalEditAbstract(SessaoPermissionMixin): if self.ordem: ordem_id = kwargs['oid'] - try: - ordem = OrdemDia.objects.get(id=ordem_id) - votacao = RegistroVotacao.objects.get( - ordem_id=ordem_id) - except ObjectDoesNotExist: + ordem = OrdemDia.objects.filter(id=ordem_id).last() + votacao = RegistroVotacao.objects.filter(ordem_id=ordem_id).last() + + if not ordem or not votacao: raise Http404() materia = ordem.materia @@ -1995,11 +1994,10 @@ class VotacaoNominalEditAbstract(SessaoPermissionMixin): elif self.expediente: expediente_id = kwargs['oid'] - try: - expediente = ExpedienteMateria.objects.get(id=expediente_id) - votacao = RegistroVotacao.objects.get( - expediente_id=expediente_id) - except ObjectDoesNotExist: + expediente = ExpedienteMateria.objects.filter(id=expediente_id).last() + votacao = RegistroVotacao.objects.filter(expediente_id=expediente_id).last() + + if not expediente or not votacao: raise Http404() materia = expediente.materia @@ -2116,9 +2114,9 @@ class VotacaoNominalTransparenciaDetailView(TemplateView): materia_votacao = self.request.GET.get('materia', None) if materia_votacao == 'ordem': - votacao = RegistroVotacao.objects.get(ordem=self.kwargs['oid']) + votacao = RegistroVotacao.objects.filter(ordem=self.kwargs['oid']).last() elif materia_votacao == 'expediente': - votacao = RegistroVotacao.objects.get(expediente=self.kwargs['oid']) + votacao = RegistroVotacao.objects.filter(expediente=self.kwargs['oid']).last() else: raise Http404() @@ -2152,10 +2150,9 @@ class VotacaoNominalExpedienteDetailView(DetailView): materia_id = kwargs['mid'] expediente_id = kwargs['oid'] - votacao = RegistroVotacao.objects.get( - materia_id=materia_id, - expediente_id=expediente_id) - expediente = ExpedienteMateria.objects.get(id=expediente_id) + votacao = RegistroVotacao.objects.filter(materia_id=materia_id, + expediente_id=expediente_id).last() + expediente = ExpedienteMateria.objects.filter(id=expediente_id).last() votos = VotoParlamentar.objects.filter(votacao_id=votacao.id) list_votos = [] @@ -2200,9 +2197,9 @@ class VotacaoSimbolicaTransparenciaDetailView(TemplateView): materia_votacao = self.request.GET.get('materia', None) if materia_votacao == 'ordem': - votacao = RegistroVotacao.objects.get(ordem=self.kwargs['oid']) + votacao = RegistroVotacao.objects.filter(ordem=self.kwargs['oid']).last() elif materia_votacao == 'expediente': - votacao = RegistroVotacao.objects.get(expediente=self.kwargs['oid']) + votacao = RegistroVotacao.objects.filter(expediente=self.kwargs['oid']).last() else: raise Http404() @@ -2388,14 +2385,9 @@ class VotacaoExpedienteEditView(SessaoPermissionMixin): 'ementa': expediente.materia.ementa} context.update({'materia': materia}) - try: - votacao = RegistroVotacao.objects.get( - materia_id=materia_id, - expediente_id=expediente_id) - except MultipleObjectsReturned: - votacao = RegistroVotacao.objects.filter( - materia_id=materia_id, - expediente_id=expediente_id).last() + votacao = RegistroVotacao.objects.filter(materia_id=materia_id, + expediente_id=expediente_id + ).last() votacao_existente = {'observacao': sub( ' ', ' ', strip_tags(votacao.observacao)), 'resultado': votacao.tipo_resultado_votacao.nome, @@ -2414,10 +2406,8 @@ class VotacaoExpedienteEditView(SessaoPermissionMixin): materia_id = kwargs['mid'] expediente_id = kwargs['oid'] - if(int(request.POST['anular_votacao']) == 1): - for r in RegistroVotacao.objects.filter( - expediente_id=expediente_id): - r.delete() + if int(request.POST['anular_votacao']) == 1: + RegistroVotacao.objects.filter(expediente_id=expediente_id).delete() expediente = ExpedienteMateria.objects.get( sessao_plenaria_id=self.object.id, diff --git a/sapl/settings.py b/sapl/settings.py index 38d241005..899954751 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -129,6 +129,7 @@ if DEBUG: INSTALLED_APPS += ('debug_toolbar', 'rest_framework_docs',) MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware', ] + CACHES = { 'default': { 'BACKEND': 'speedinfo.backends.proxy_cache', diff --git a/sapl/static/XSLT/HTML/.objects b/sapl/static/XSLT/HTML/.objects deleted file mode 100644 index ca1ce01f2..000000000 --- a/sapl/static/XSLT/HTML/.objects +++ /dev/null @@ -1,11 +0,0 @@ -estilo.css:DTML Method -indicacao.xsl:File -mocao.xsl:File -mocao2.xsl:File -parecer.xsl:File -pedido.xsl:File -pedido2.xsl:File -pl.xsl:File -pl2.xsl:File -requerimento.xsl:File -requerimento2.xsl:File diff --git a/sapl/static/XSLT/HTML/estilo.css b/sapl/static/XSLT/HTML/estilo.css deleted file mode 100644 index aac85f176..000000000 --- a/sapl/static/XSLT/HTML/estilo.css +++ /dev/null @@ -1,95 +0,0 @@ - body { - font-family: Times; - text-align: justify; - font-size: 12 pt; - margin: 5px 1cm 20px 2cm; -} - - p, - .p{ - font-family: Times; - text-align: justify; - font-size: 12pt; - text-indent: 1.5cm; - margin: 40px 0 20px 0; - } - - .pequeno { - font-family: Times; - text-align: left; - font-size: 13pt; - margin: 0px 0 0px 0; - } - - .cabecalho { - font-family: Times; - font-weight:bold; - text-align: left; - font-size: 15pt; - margin: 10px 0 0px 0; - } - - .texto { - font-family: Times; - text-align: justify; - font-size: 12pt; - margin: 0px 0px 0px 0px; - } - - .data { - text-align: right; - } - - .autor { - text-align: center; - } - - .center { - text-align: center; - } - - .semrecuo { - text-indent: 0; - } - - .ementa { - text-align: justify; - margin-left: 50%; - text-indent: 0; - } - - .titulos1 { - text-align: center; - margin: 10px 0 0px 0; - } - - .titulos2 { - text-align: center; - margin: 0px 0 0px 0; - } - - p.artigo { - text-align: justify; - text-indent: 1cm; - margin: 10px 0 0px 0; -} - - -#imagem { - float:left; - } - -#autores - { - -moz-column-count:3; /* Firefox */ - -webkit-column-count:3; /* Safari and Chrome */ - width:50px; - - } - -#col1 { width: 33%; float: left; center: 10px; } -#col2 { width: 33%; float: left; center: 10px; } -#col3 { width: 33%; float: left; center: 10px; } - - - diff --git a/sapl/static/XSLT/HTML/indicacao.xsl b/sapl/static/XSLT/HTML/indicacao.xsl deleted file mode 100644 index edb4575c7..000000000 --- a/sapl/static/XSLT/HTML/indicacao.xsl +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - -
-
-

-


-

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/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..d050a469f 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.normarelacionada_list # 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/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/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/test_urls.py b/sapl/test_urls.py index 5670f7bff..2bfcb7e15 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..8045f0e0b 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 @@ -732,3 +733,7 @@ def RemoveTag(texto): i += 1 return textoSaida + +def remover_acentos(string): + return ''.join([c for c in unicodedata.normalize('NFD', string) + if unicodedata.category(c) != 'Mn']) \ No newline at end of file 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 7b3240fbf..da929d87c 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ install_requires = [ ] setup( name='interlegis-sapl', - version='3.1.71', + version='3.1.76', packages=find_packages(), include_package_data=True, license='GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007',