diff --git a/bug.txt b/bug.txt new file mode 100644 index 000000000..e69de29bb diff --git a/gunicorn_start.sh b/gunicorn_start.sh index b6c86b571..9669644fa 100755 --- a/gunicorn_start.sh +++ b/gunicorn_start.sh @@ -2,6 +2,15 @@ # As seen in http://tutos.readthedocs.org/en/latest/source/ndg.html +SAPL_DIR="/var/interlegis/sapl" + +# Seta um novo diretório foi passado como raiz para o SAPL +# caso esse tenha sido passado como parâmetro +if [ "$1" ] +then + SAPL_DIR="$1" +fi + NAME="SAPL" # Name of the application (*) DJANGODIR=/var/interlegis/sapl/ # Django project directory (*) SOCKFILE=/var/interlegis/sapl/run/gunicorn.sock # we will communicate using this unix socket (*) @@ -12,7 +21,7 @@ NUM_WORKERS=3 # how many worker processes shou DJANGO_SETTINGS_MODULE=sapl.settings # which settings file should Django use (*) DJANGO_WSGI_MODULE=sapl.wsgi # WSGI module name (*) -echo "Starting $NAME as `whoami`" +echo "Starting $NAME as `whoami` on base dir $SAPL_DIR" # Create the run directory if it doesn't exist RUNDIR=$(dirname $SOCKFILE) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3a0e5b446..6d7592cda 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -19,7 +19,7 @@ django-sass-processor==0.4.6 djangorestframework drfdocs easy-thumbnails==2.3 -trml2pdf==0.4.2 +git+git://github.com/interlegis/trml2pdf.git libsass==0.11.1 psycopg2==2.6.2 python-decouple==3.0 diff --git a/sapl/base/migrations/0002_auto_20170331_1900.py b/sapl/base/migrations/0002_auto_20170331_1900.py new file mode 100644 index 000000000..5ee726647 --- /dev/null +++ b/sapl/base/migrations/0002_auto_20170331_1900.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.12 on 2017-03-31 19:00 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Argumento', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('argumento', models.CharField(max_length=50, verbose_name='Argumento')), + ], + options={ + 'verbose_name': 'Argumento da constraint', + 'verbose_name_plural': 'Argumentos da constraint', + }, + ), + migrations.CreateModel( + name='Constraint', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nome_tabela', models.CharField(max_length=50, verbose_name='Nome da tabela')), + ('nome_constraint', models.CharField(max_length=100, verbose_name='Nome da constraint')), + ('nome_model', models.CharField(max_length=50, verbose_name='Nome da model')), + ('tipo_constraint', models.CharField(max_length=50, verbose_name='Tipo da constraint')), + ], + options={ + 'verbose_name': 'Constraint removida', + 'verbose_name_plural': 'Constraints removidas', + }, + ), + migrations.AddField( + model_name='problemamigracao', + name='eh_importante', + field=models.BooleanField(default=False, verbose_name='É importante?'), + ), + migrations.AddField( + model_name='argumento', + name='constraint', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Constraint'), + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 8825ea2a4..9dd0f5ec3 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -61,16 +61,45 @@ class ProblemaMigracao(models.Model): content_object = GenericForeignKey('content_type', 'object_id') nome_campo = models.CharField(max_length=100, blank=True, - verbose_name='Nome do(s) Campo(s)') + verbose_name=_('Nome do(s) Campo(s)')) problema = models.CharField(max_length=300, verbose_name=_('Problema')) descricao = models.CharField(max_length=300, verbose_name=_('Descrição')) - eh_stub = models.BooleanField(verbose_name='É stub?') + eh_stub = models.BooleanField(verbose_name=_('É stub?')) + eh_importante = models.BooleanField( + default=False, verbose_name=_('É importante?')) class Meta: verbose_name = _('Problema na Migração') verbose_name_plural = _('Problemas na Migração') +@reversion.register() +class Constraint(models.Model): + nome_tabela = models.CharField( + max_length=50, verbose_name=_('Nome da tabela')) + nome_constraint = models.CharField( + max_length=100, verbose_name=_('Nome da constraint')) + nome_model = models.CharField( + max_length=50, verbose_name=_('Nome da model')) + tipo_constraint = models.CharField( + max_length=50, verbose_name=_('Tipo da constraint')) + + class Meta: + verbose_name = _('Constraint removida') + verbose_name_plural = _('Constraints removidas') + + +@reversion.register() +class Argumento(models.Model): + constraint = models.ForeignKey(Constraint) + argumento = models.CharField( + max_length=50, verbose_name=_('Argumento')) + + class Meta: + verbose_name = _('Argumento da constraint') + verbose_name_plural = _('Argumentos da constraint') + + @reversion.register() class AppConfig(models.Model): diff --git a/sapl/base/search_indexes.py b/sapl/base/search_indexes.py index 1b2944f48..c85be523b 100644 --- a/sapl/base/search_indexes.py +++ b/sapl/base/search_indexes.py @@ -1,8 +1,9 @@ import os.path -import textract +import textract from django.template import Context, loader from haystack import indexes + from sapl.materia.models import DocumentoAcessorio, MateriaLegislativa from sapl.norma.models import NormaJuridica diff --git a/sapl/compilacao/templatetags/compilacao_filters.py b/sapl/compilacao/templatetags/compilacao_filters.py index f3965c43d..941ec5fe1 100644 --- a/sapl/compilacao/templatetags/compilacao_filters.py +++ b/sapl/compilacao/templatetags/compilacao_filters.py @@ -211,7 +211,7 @@ def heranca(request, d, ignore_ultimo=0, ignore_primeiro=0): ta_id = str(d.ta_id) d_pk = str(d.pk) if ta_id not in ta_dpts_parents or d_pk not in ta_dpts_parents[ta_id]: - #print('recarregando estrutura temporaria de heranças') + # print('recarregando estrutura temporaria de heranças') dpts_parents = {} ta_dpts_parents[ta_id] = dpts_parents update_dispositivos_parents(dpts_parents, ta_id) @@ -296,6 +296,7 @@ def urldetail_content_type(obj): def list(obj): return [obj, ] + @register.filter def can_use_dynamic_editing(texto_articulado, user): return texto_articulado.can_use_dynamic_editing(user) diff --git a/sapl/crispy_layout_mixin.py b/sapl/crispy_layout_mixin.py index e9dbc3f01..369b6fe9e 100644 --- a/sapl/crispy_layout_mixin.py +++ b/sapl/crispy_layout_mixin.py @@ -1,12 +1,12 @@ from math import ceil +import rtyaml from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit from django import template from django.utils import formats from django.utils.translation import ugettext as _ -import rtyaml def heads_and_tails(list_of_lists): diff --git a/sapl/crud/base.py b/sapl/crud/base.py index b2223c31f..1a1225cfc 100644 --- a/sapl/crud/base.py +++ b/sapl/crud/base.py @@ -842,7 +842,7 @@ class CrudDeleteView(PermissionRequiredContainerCrudMixin, def delete(self, request, *args, **kwargs): try: - return super(CrudDeleteView, self).delete(request, args, kwargs) + return super(CrudDeleteView, self).delete(request, args, kwargs) except models.ProtectedError as err: error_msg = 'Registro não pode ser removido, pois\ é referenciado por outros registros:
\ diff --git a/sapl/legacy/management/commands/recria_constraints.py b/sapl/legacy/management/commands/recria_constraints.py new file mode 100644 index 000000000..9e999e5f6 --- /dev/null +++ b/sapl/legacy/management/commands/recria_constraints.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand + +from sapl.legacy.migration import recria_constraints + + +class Command(BaseCommand): + + help = (u'Recria constraints do PostgreSQL excluidas durante ' + 'migração de dados') + + def handle(self, *args, **options): + recria_constraints() diff --git a/sapl/legacy/migracao_documentos.py b/sapl/legacy/migracao_documentos.py index 4d9877fa7..21697dc4f 100644 --- a/sapl/legacy/migracao_documentos.py +++ b/sapl/legacy/migracao_documentos.py @@ -4,6 +4,7 @@ import re import magic +from django.db.models.signals import post_delete, post_save from sapl.base.models import CasaLegislativa from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, Proposicao) @@ -12,6 +13,8 @@ from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import DocumentoAdministrativo from sapl.sessao.models import SessaoPlenaria from sapl.settings import MEDIA_ROOT +from sapl.utils import delete_texto, save_texto + # MIGRAÇÃO DE DOCUMENTOS ################################################### EXTENSOES = { @@ -160,7 +163,29 @@ def migrar_docs_por_ids(tipo): tipo.__name__, id, destino)) +def desconecta_sinais_indexacao(): + post_save.disconnect(save_texto, NormaJuridica) + post_save.disconnect(save_texto, DocumentoAcessorio) + post_save.disconnect(save_texto, MateriaLegislativa) + post_delete.disconnect(delete_texto, NormaJuridica) + post_delete.disconnect(delete_texto, DocumentoAcessorio) + post_delete.disconnect(delete_texto, MateriaLegislativa) + + +def conecta_sinais_indexacao(): + post_save.connect(save_texto, NormaJuridica) + post_save.connect(save_texto, DocumentoAcessorio) + post_save.connect(save_texto, MateriaLegislativa) + post_delete.connect(delete_texto, NormaJuridica) + post_delete.connect(delete_texto, DocumentoAcessorio) + post_delete.connect(delete_texto, MateriaLegislativa) + + def migrar_documentos(): + # precisamos excluir os sinais de post_save e post_delete para não que o + # computador não trave com a criação de threads desnecessárias + desconecta_sinais_indexacao() + # aqui supomos que uma pasta chamada sapl_documentos está em MEDIA_ROOT # com o conteúdo da pasta de mesmo nome do zope # Os arquivos da pasta serão movidos para a nova estrutura e a pasta será @@ -186,3 +211,6 @@ def migrar_documentos(): len(sobrando))) for doc in sobrando: print(' {}'. format(doc)) + # + # reconexão dos sinais desligados no inicio da migração de documentos + conecta_sinais_indexacao() diff --git a/sapl/legacy/migration.py b/sapl/legacy/migration.py index 99d14f8a7..8cfa25a4e 100644 --- a/sapl/legacy/migration.py +++ b/sapl/legacy/migration.py @@ -13,13 +13,15 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import OperationalError, ProgrammingError, connections, models from django.db.models import CharField, Max, ProtectedError, TextField from django.db.models.base import ModelBase +from django.db.models.signals import post_delete, post_save from model_mommy import mommy from model_mommy.mommy import foreign_key_required, make -from sapl.base.models import Autor, ProblemaMigracao +from sapl.base.models import Argumento, Autor, Constraint, ProblemaMigracao from sapl.comissoes.models import Comissao, Composicao, Participacao from sapl.legacy.models import Protocolo as ProtocoloLegado -from sapl.materia.models import (Proposicao, StatusTramitacao, TipoDocumento, +from sapl.materia.models import (DocumentoAcessorio, MateriaLegislativa, + StatusTramitacao, TipoDocumento, TipoMateriaLegislativa, TipoProposicao, Tramitacao) from sapl.norma.models import (AssuntoNorma, NormaJuridica, @@ -28,7 +30,7 @@ from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import Protocolo, StatusTramitacaoAdministrativo from sapl.sessao.models import ExpedienteMateria, OrdemDia from sapl.settings import PROJECT_DIR -from sapl.utils import normalize +from sapl.utils import delete_texto, normalize, save_texto # BASE ###################################################################### # apps to be migrated, in app dependency order (very important) @@ -109,6 +111,10 @@ def warn(msg): print('CUIDADO! ' + msg) +def erro(msg): + print('ERRO: ' + msg) + + def get_fk_related(field, value, label=None): if value is None and field.null is False: value = 0 @@ -197,6 +203,12 @@ def iter_sql_records(sql, db): record.__dict__.update(zip(fieldnames, row)) yield record +# Todos os models têm no máximo uma constraint unique together +# Isso é necessário para que o método delete_constraints funcione corretamente +assert all(len(model._meta.unique_together) <= 1 + for app in appconfs + for model in app.models.values()) + def delete_constraints(model): # pega nome da unique constraint dado o nome da tabela @@ -210,40 +222,66 @@ def delete_constraints(model): for r in result: if r[0].endswith('key'): words_list = r[0].split('_') - one_to_one_constraints.append([table, r[0], words_list, model]) + constraint = Constraint.objects.create( + nome_tabela=table, nome_constraint=r[0], + nome_model=model.__name__, tipo_constraint='one_to_one') + for w in words_list: + Argumento.objects.create(constraint=constraint, argumento=w) else: - args = None - args_list = [] if model._meta.unique_together: - args = model._meta.unique_together[0] - args_list = list(args) - unique_constraints.append([table, r[0], args_list, model]) + args_list = model._meta.unique_together[0] + constraint = Constraint.objects.create( + nome_tabela=table, nome_constraint=r[0], + nome_model=model.__name__, + tipo_constraint='unique_together') + for a in args_list: + Argumento.objects.create(constraint=constraint, + argumento=a) warn('Excluindo unique constraint de nome %s' % r[0]) exec_sql("ALTER TABLE %s DROP CONSTRAINT %s;" % (table, r[0])) -def recreate_constraints(): - if one_to_one_constraints: - for constraint in one_to_one_constraints: - table, name, args, model = constraint +def recria_constraints(): + constraints = Constraint.objects.all() + for con in constraints: + if con.tipo_constraint == 'one_to_one': + nome_tabela = con.nome_tabela + nome_constraint = con.nome_constraint + args = [a.argumento for a in con.argumento_set.all()] args_string = '' args_string = "(" + "_".join(map(str, args[2:-1])) + ")" - exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % - (table, name, args_string)) - if unique_constraints: - for constraint in unique_constraints: - table, name, args, model = constraint + try: + exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % + (nome_tabela, nome_constraint, args_string)) + except ProgrammingError: + info('A constraint %s já foi recriada!' % nome_constraint) + if con.tipo_constraint == 'unique_together': + nome_tabela = con.nome_tabela + nome_constraint = con.nome_constraint + # Pegando explicitamente o primeiro valor do filter, + # pois pode ser que haja mais de uma ocorrência + model = ContentType.objects.filter( + model=con.nome_model.lower())[0].model_class() + args = [a.argumento for a in con.argumento_set.all()] for i in range(len(args)): if isinstance(model._meta.get_field(args[i]), models.ForeignKey): args[i] = args[i] + '_id' args_string = '' args_string += "(" + ', '.join(map(str, args)) + ")" - exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % - (table, name, args_string)) - one_to_one_constraints.clear() - unique_constraints.clear() + try: + exec_sql("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE %s;" % + (nome_tabela, nome_constraint, args_string)) + except ProgrammingError: + info('A constraint %s já foi recriada!' % nome_constraint) + except Exception as err: + problema = re.findall('\(.*?\)', err.args[0]) + erro('A constraint [%s] da tabela [%s] não pode ser recriada' % + (nome_constraint, nome_tabela)) + erro('Os dados %s = %s estão duplicados. ' + 'Arrume antes de recriar as constraints!' % + (problema[0], problema[1])) def obj_desnecessario(obj): @@ -262,8 +300,8 @@ def get_last_value(model): def alter_sequence(model, id): sequence_name = '%s_id_seq' % model._meta.db_table - exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE %s;' % ( - sequence_name, id, id)) + exec_sql('ALTER SEQUENCE %s RESTART WITH %s MINVALUE -1;' % ( + sequence_name, id)) def save_with_id(new, id): @@ -278,8 +316,7 @@ def save_relation(obj, nome_campo='', problema='', descricao='', eh_stub=False): link = ProblemaMigracao( content_object=obj, nome_campo=nome_campo, problema=problema, - descricao=descricao, eh_stub=eh_stub, - ) + descricao=descricao, eh_stub=eh_stub,) link.save() @@ -416,6 +453,8 @@ class DataMigrator: call([PROJECT_DIR.child('manage.py'), 'flush', '--database=default', '--no-input'], stdout=PIPE) + desconecta_sinais_indexacao() + info('Começando migração: %s...' % obj) self._do_migrate(obj) @@ -427,7 +466,7 @@ class DataMigrator: for obj in self.to_delete: msg = 'A entrada de PK %s da model %s não pode ser ' \ 'excluida' % (obj.pk, obj._meta.model_name) - descricao = 'Um ou mais objetos protegidos ' + descricao = 'Um ou mais objetos protegidos' warn(msg + ' => ' + descricao) save_relation(obj=obj, problema=msg, descricao=descricao, eh_stub=False) @@ -435,8 +474,8 @@ class DataMigrator: info('Deletando stubs desnecessários...') while self.delete_stubs(): pass - info('Recriando unique constraints...') - # recreate_constraints() + + conecta_sinais_indexacao() def _do_migrate(self, obj): if isinstance(obj, AppConfig): @@ -731,3 +770,23 @@ def make_with_log(model, _quantity=None, make_m2m=False, **attrs): return stub make_with_log.required = foreign_key_required + +# DISCONNECT SIGNAL ######################################################## + + +def desconecta_sinais_indexacao(): + post_save.disconnect(save_texto, NormaJuridica) + post_save.disconnect(save_texto, DocumentoAcessorio) + post_save.disconnect(save_texto, MateriaLegislativa) + post_delete.disconnect(delete_texto, NormaJuridica) + post_delete.disconnect(delete_texto, DocumentoAcessorio) + post_delete.disconnect(delete_texto, MateriaLegislativa) + + +def conecta_sinais_indexacao(): + post_save.connect(save_texto, NormaJuridica) + post_save.connect(save_texto, DocumentoAcessorio) + post_save.connect(save_texto, MateriaLegislativa) + post_delete.connect(delete_texto, NormaJuridica) + post_delete.connect(delete_texto, DocumentoAcessorio) + post_delete.connect(delete_texto, MateriaLegislativa) diff --git a/sapl/legacy/scripts/migra_um_db.sh b/sapl/legacy/scripts/migra_um_db.sh index 26240d8d4..f55dfb53a 100755 --- a/sapl/legacy/scripts/migra_um_db.sh +++ b/sapl/legacy/scripts/migra_um_db.sh @@ -23,3 +23,9 @@ echo "--- MIGRACAO DE DADOS ---" | tee -a $LOG echo >> $LOG DATABASE_NAME=$1 ./manage.py migracao_25_31 -f --settings sapl.legacy_migration_settings |& tee -a $LOG echo >> $LOG + + +echo "--- RECRIANDO CONSTRAINTS ---" | tee -a $LOG +echo >> $LOG +DATABASE_NAME=$1 ./manage.py recria_constraints --settings sapl.legacy_migration_settings |& tee -a $LOG +echo >> $LOG diff --git a/sapl/materia/models.py b/sapl/materia/models.py index 6b48613ee..1a9d2afff 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -1,5 +1,6 @@ from datetime import datetime +import reversion from django.contrib.auth.models import Group from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -7,7 +8,6 @@ from django.db import models from django.utils import formats from django.utils.translation import ugettext_lazy as _ from model_utils import Choices -import reversion from sapl.base.models import Autor from sapl.comissoes.models import Comissao @@ -18,7 +18,6 @@ from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path) - EM_TRAMITACAO = [(1, 'Sim'), (0, 'Não')] diff --git a/sapl/materia/signals.py b/sapl/materia/signals.py index 96ff85dea..9f08b104c 100644 --- a/sapl/materia/signals.py +++ b/sapl/materia/signals.py @@ -1,26 +1,7 @@ from django.db.models.signals import post_delete, post_save -from sapl.settings import PROJECT_DIR -from subprocess import PIPE, call -from threading import Thread +from sapl.utils import save_texto, delete_texto - -from .models import MateriaLegislativa, DocumentoAcessorio - - -class UpdateIndexCommand(Thread): - def run(self): - call([PROJECT_DIR.child('manage.py'), 'update_index'], - stdout=PIPE) - - -def save_texto(sender, instance, **kwargs): - update_index = UpdateIndexCommand() - update_index.start() - - -def delete_texto(sender, instance, **kwargs): - update_index = UpdateIndexCommand() - update_index.start() +from .models import DocumentoAcessorio, MateriaLegislativa post_save.connect(save_texto, sender=MateriaLegislativa) diff --git a/sapl/materia/urls.py b/sapl/materia/urls.py index 1de8cd689..7eac42e5c 100644 --- a/sapl/materia/urls.py +++ b/sapl/materia/urls.py @@ -4,10 +4,9 @@ from sapl.materia.views import (AcompanhamentoConfirmarView, AcompanhamentoExcluirView, AcompanhamentoMateriaView, AdicionarVariasAutorias, AnexadaCrud, - AssuntoMateriaCrud, - AutoriaCrud, ConfirmarProposicao, - CriarProtocoloMateriaView, DespachoInicialCrud, - DocumentoAcessorioCrud, + AssuntoMateriaCrud, AutoriaCrud, + ConfirmarProposicao, CriarProtocoloMateriaView, + DespachoInicialCrud, DocumentoAcessorioCrud, DocumentoAcessorioEmLoteView, LegislacaoCitadaCrud, MateriaAssuntoCrud, MateriaLegislativaCrud, diff --git a/sapl/materia/views.py b/sapl/materia/views.py index 305b4b7b0..b93d81289 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -43,21 +43,19 @@ from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, AdicionarVariasAutoriasFilterSet, DespachoInicialForm, - DocumentoAcessorioForm, - MateriaAssuntoForm, MateriaLegislativaFilterSet, - MateriaSimplificadaForm, PrimeiraTramitacaoEmLoteFilterSet, - ReceberProposicaoForm, RelatoriaForm, - TramitacaoEmLoteFilterSet, filtra_tramitacao_destino, + DocumentoAcessorioForm, MateriaAssuntoForm, + MateriaLegislativaFilterSet, MateriaSimplificadaForm, + PrimeiraTramitacaoEmLoteFilterSet, ReceberProposicaoForm, + RelatoriaForm, TramitacaoEmLoteFilterSet, + filtra_tramitacao_destino, filtra_tramitacao_destino_and_status, filtra_tramitacao_status) -from .models import (AssuntoMateria, AcompanhamentoMateria, - Anexada, Autoria, DespachoInicial, - DocumentoAcessorio, MateriaLegislativa, - MateriaAssunto, Numeracao, Orgao, - Origem, Proposicao, RegimeTramitacao, Relatoria, - StatusTramitacao, TipoDocumento, TipoFimRelatoria, - TipoMateriaLegislativa, TipoProposicao, Tramitacao, - UnidadeTramitacao) +from .models import (AcompanhamentoMateria, Anexada, AssuntoMateria, Autoria, + DespachoInicial, DocumentoAcessorio, MateriaAssunto, + MateriaLegislativa, Numeracao, Orgao, Origem, Proposicao, + RegimeTramitacao, Relatoria, StatusTramitacao, + TipoDocumento, TipoFimRelatoria, TipoMateriaLegislativa, + TipoProposicao, Tramitacao, UnidadeTramitacao) AssuntoMateriaCrud = Crud.build(AssuntoMateria, 'assunto_materia') diff --git a/sapl/norma/models.py b/sapl/norma/models.py index 20b59956f..d91ee2da2 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -7,8 +7,8 @@ from model_utils import Choices from sapl.compilacao.models import TextoArticulado from sapl.materia.models import MateriaLegislativa -from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path, - restringe_tipos_de_arquivo_txt) +from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, + restringe_tipos_de_arquivo_txt, texto_upload_path) @reversion.register() diff --git a/sapl/norma/signals.py b/sapl/norma/signals.py index fc16f89b4..3089e563e 100644 --- a/sapl/norma/signals.py +++ b/sapl/norma/signals.py @@ -1,27 +1,8 @@ from django.db.models.signals import post_delete, post_save -from sapl.settings import PROJECT_DIR -from subprocess import PIPE, call -from threading import Thread - +from sapl.utils import save_texto, delete_texto from .models import NormaJuridica -class UpdateIndexCommand(Thread): - def run(self): - call([PROJECT_DIR.child('manage.py'), 'update_index'], - stdout=PIPE) - - -def save_texto(sender, instance, **kwargs): - update_index = UpdateIndexCommand() - update_index.start() - - -def delete_texto(sender, instance, **kwargs): - update_index = UpdateIndexCommand() - update_index.start() - - post_save.connect(save_texto, sender=NormaJuridica) post_delete.connect(delete_texto, sender=NormaJuridica) diff --git a/sapl/norma/views.py b/sapl/norma/views.py index 461c1b9ef..9120da4e9 100644 --- a/sapl/norma/views.py +++ b/sapl/norma/views.py @@ -4,6 +4,7 @@ from django.http import JsonResponse from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import RedirectView from django_filters.views import FilterView + from sapl.base.models import AppConfig from sapl.compilacao.views import IntegracaoTaView from sapl.crud.base import (RP_DETAIL, RP_LIST, Crud, CrudAux, diff --git a/sapl/parlamentares/views.py b/sapl/parlamentares/views.py index a4f698b5a..d703e1c2c 100644 --- a/sapl/parlamentares/views.py +++ b/sapl/parlamentares/views.py @@ -22,7 +22,6 @@ from .models import (CargoMesa, Coligacao, ComposicaoColigacao, ComposicaoMesa, NivelInstrucao, Parlamentar, Partido, SessaoLegislativa, SituacaoMilitar, TipoAfastamento, TipoDependente, Votante) - CargoMesaCrud = CrudAux.build(CargoMesa, 'cargo_mesa') PartidoCrud = CrudAux.build(Partido, 'partidos') SessaoLegislativaCrud = CrudAux.build(SessaoLegislativa, 'sessao_legislativa') diff --git a/sapl/protocoloadm/forms.py b/sapl/protocoloadm/forms.py index bca51df31..d8f996fe3 100644 --- a/sapl/protocoloadm/forms.py +++ b/sapl/protocoloadm/forms.py @@ -544,10 +544,10 @@ class DocumentoAdministrativoForm(ModelForm): ano_protocolo = forms.ChoiceField(required=False, label=Protocolo._meta. - get_field('ano').verbose_name, + get_field('ano').verbose_name, choices=RANGE_ANOS, widget=forms.Select( - attrs={'class': 'selector'})) + attrs={'class': 'selector'})) class Meta: model = DocumentoAdministrativo diff --git a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py index 24796bb13..5917d0f8b 100644 --- a/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py +++ b/sapl/relatorios/templates/pdf_sessao_plenaria_gerar.py @@ -101,7 +101,7 @@ def inf_basicas(inf_basicas_dic): tmp += '\t\tInformações Básicas\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' tmp += '\t\tTipo da Sessão: ' + \ nom_sessao + '\n' @@ -120,7 +120,7 @@ def mesa(lst_mesa): tmp = '' tmp += '\t\tMesa Diretora\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' for mesa in lst_mesa: tmp += '\t\t' + \ @@ -136,7 +136,7 @@ def presenca(lst_presenca_sessao): tmp = '' tmp += '\t\tLista de Presença da Sessão\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' for presenca in lst_presenca_sessao: tmp += '\t\t' + \ @@ -154,10 +154,10 @@ def expedientes(lst_expedientes): tmp += '\t\t\n' tmp += '\t\t\t \n' tmp += '\t\t\n' - for expediente in lst_expedientes: - tmp += '\t\t' + \ + for idx, expediente in enumerate(lst_expedientes): + tmp += '\t\t' + '
' + \ expediente['nom_expediente'] + ':
\n' + \ - '' + \ + '' + \ expediente['txt_expediente'] + '\n' tmp += '\t\t\n' tmp += '\t\t\t \n' @@ -171,7 +171,7 @@ def expediente_materia(lst_expediente_materia): tmp = '' tmp += '\t\tMatérias do Expediente\n\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' tmp += '\n' tmp += 'MatériaEmentaResultado da Votação\n' @@ -181,8 +181,12 @@ def expediente_materia(lst_expediente_materia): txt_ementa = expediente_materia['txt_ementa'].replace('&', '&') tmp += '' + txt_ementa + '\n' tmp += '' + \ - str(expediente_materia['nom_resultado']) + '\n' + '' + \ - str(expediente_materia['votacao_observacao']) + '\n' + str(expediente_materia['nom_resultado']) + '
\n' + '' + if expediente_materia['votacao_observacao'] != txt_ementa: + tmp += str(expediente_materia['votacao_observacao']) + else: + tmp += ' ' + tmp += '\n' tmp += '\t\t\n' return tmp @@ -195,7 +199,7 @@ def oradores_expediente(lst_oradores_expediente): tmp = '' tmp += '\t\tOradores do Expediente\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' for orador_expediente in lst_oradores_expediente: tmp += '\t\t' + str(orador_expediente['num_ordem']) + ' - ' + orador_expediente[ @@ -210,7 +214,7 @@ def presenca_ordem_dia(lst_presenca_ordem_dia): tmp = '' tmp += '\t\tLista de Presença da Ordem do Dia\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' for presenca_ordem_dia in lst_presenca_ordem_dia: tmp += '\t\t' + \ @@ -226,7 +230,7 @@ def votacao(lst_votacao): tmp = '' tmp += 'Matérias da Ordem do Dia\n\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' tmp += '\n' tmp += 'MatériaEmentaResultado da Votação\n' @@ -236,8 +240,12 @@ def votacao(lst_votacao): txt_ementa = votacao['txt_ementa'].replace('&', '&') tmp += '' + txt_ementa + '\n' tmp += '' + \ - str(votacao['nom_resultado']) + '\n' + '' + \ - str(votacao['votacao_observacao']) + '\n' + str(votacao['nom_resultado']) + '
\n' + '' + if votacao['votacao_observacao'] != txt_ementa: + tmp += str(votacao['votacao_observacao']) + else: + tmp += ' ' + tmp += '\n' tmp += '\t\t\n' return tmp @@ -250,7 +258,7 @@ def oradores(lst_oradores): tmp = '' tmp += '\t\tOradores das Explicações Pessoais\n' tmp += '\t\t\n' - tmp += '\t\t\t \n' + tmp += '\t\t\t
\n' tmp += '\t\t
\n' for orador in lst_oradores: tmp += '\t\t' + \ @@ -290,6 +298,7 @@ def principal(cabecalho_dic, rodape_dic, imagem, sessao, inf_basicas_dic, lst_me tmp += oradores(lst_oradores) tmp += '\t\n' tmp += '\n' + tmp_pdf = parseString(tmp) return tmp_pdf # if hasattr(context.temp_folder,arquivoPdf): diff --git a/sapl/relatorios/views.py b/sapl/relatorios/views.py index 53b9ef54b..3a2ccd047 100644 --- a/sapl/relatorios/views.py +++ b/sapl/relatorios/views.py @@ -8,15 +8,14 @@ from sapl.base.models import Autor, CasaLegislativa from sapl.comissoes.models import Comissao from sapl.materia.models import (Autoria, MateriaLegislativa, Numeracao, Tramitacao, UnidadeTramitacao) -from sapl.parlamentares.models import (CargoMesa, ComposicaoMesa, Filiacao, - Parlamentar) +from sapl.parlamentares.models import CargoMesa, Filiacao, Parlamentar from sapl.protocoloadm.models import (DocumentoAdministrativo, Protocolo, TramitacaoAdministrativo) from sapl.sessao.models import (ExpedienteMateria, ExpedienteSessao, - IntegranteMesa, Orador, - OradorExpediente, OrdemDia, PresencaOrdemDia, - RegistroVotacao, SessaoPlenaria, - SessaoPlenariaPresenca, TipoExpediente) + IntegranteMesa, Orador, OradorExpediente, + OrdemDia, PresencaOrdemDia, RegistroVotacao, + SessaoPlenaria, SessaoPlenariaPresenca, + TipoExpediente) from sapl.settings import STATIC_ROOT from sapl.utils import UF @@ -519,14 +518,14 @@ def get_sessao_plenaria(sessao, casa): # Exibe os Expedientes lst_expedientes = [] - for tip_expediente in TipoExpediente.objects.all(): - for expediente in ExpedienteSessao.objects.filter( - sessao_plenaria=sessao, tipo=tip_expediente): - dic_expedientes = {} - dic_expedientes["nom_expediente"] = str(tip_expediente) - dic_expedientes["txt_expediente"] = (expediente.conteudo) - if dic_expedientes: - lst_expedientes.append(dic_expedientes) + expedientes = ExpedienteSessao.objects.filter( + sessao_plenaria=sessao).order_by('tipo__nome') + for e in expedientes: + dic_expedientes = {} + dic_expedientes["nom_expediente"] = e.tipo.nome + dic_expedientes["txt_expediente"] = e.conteudo + if dic_expedientes: + lst_expedientes.append(dic_expedientes) # Lista das matérias do Expediente, incluindo o resultado das votacoes lst_expediente_materia = [] @@ -595,18 +594,16 @@ def get_sessao_plenaria(sessao, casa): dic_expediente_materia["nom_autor"] = 'Desconhecido' dic_expediente_materia["votacao_observacao"] = ' ' - if not expediente_materia.resultado: - resultado = RegistroVotacao.objects.filter( - tipo_resultado_votacao=expediente_materia.tipo_votacao) - - for i in resultado: + resultados = expediente_materia.registrovotacao_set.all() + if resultados: + for i in resultados: dic_expediente_materia["nom_resultado"] = ( i.tipo_resultado_votacao.nome) dic_expediente_materia["votacao_observacao"] = ( - expediente_materia.observacao) + i.observacao) else: dic_expediente_materia["nom_resultado"] = _("Matéria não votada") - dic_expediente_materia["votacao_observacao"] = _("Vazio") + dic_expediente_materia["votacao_observacao"] = _(" ") lst_expediente_materia.append(dic_expediente_materia) # Lista dos oradores do Expediente @@ -711,16 +708,15 @@ def get_sessao_plenaria(sessao, casa): dic_votacao["nom_autor"] = 'Desconhecido' dic_votacao["votacao_observacao"] = ' ' - if not votacao.resultado: - resultado = RegistroVotacao.objects.filter( - tipo_resultado_votacao=votacao.tipo_votacao) - for i in resultado: + resultados = votacao.registrovotacao_set.all() + if resultados: + for i in resultados: dic_votacao["nom_resultado"] = i.tipo_resultado_votacao.nome if votacao.observacao: - dic_votacao["votacao_observacao"] = votacao.observacao + dic_votacao["votacao_observacao"] = i.observacao else: dic_votacao["nom_resultado"] = _("Matéria não votada") - dic_votacao["votacao_observacao"] = _("Vazio") + dic_votacao["votacao_observacao"] = _(" ") lst_votacao.append(dic_votacao) # Lista dos oradores nas Explicações Pessoais diff --git a/sapl/rules/map_rules.py b/sapl/rules/map_rules.py index 906592fd0..05d869fd8 100644 --- a/sapl/rules/map_rules.py +++ b/sapl/rules/map_rules.py @@ -197,6 +197,8 @@ rules_group_geral = { (base.CasaLegislativa, __listdetailchange__), (base.ProblemaMigracao, []), + (base.Argumento, []), + (base.Constraint, []), (base.TipoAutor, __base__), (base.Autor, __base__), diff --git a/sapl/rules/tests/test_rules.py b/sapl/rules/tests/test_rules.py index 3662477b9..6fc7eb3fe 100644 --- a/sapl/rules/tests/test_rules.py +++ b/sapl/rules/tests/test_rules.py @@ -6,7 +6,8 @@ from django.contrib.contenttypes.models import ContentType from django.utils import six from django.utils.translation import ugettext_lazy as _ -from sapl.base.models import CasaLegislativa, ProblemaMigracao +from sapl.base.models import (CasaLegislativa, ProblemaMigracao, Argumento, + Constraint) from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, TipoDispositivo, TipoDispositivoRelationship) @@ -56,11 +57,15 @@ def test_models_in_rules_patterns(model_item): __fp__in__test_permission_of_models_in_rules_patterns = { map_rules.RP_ADD: [CasaLegislativa, ProblemaMigracao, + Argumento, + Constraint, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado], map_rules.RP_CHANGE: [ProblemaMigracao, + Argumento, + Constraint, AcompanhamentoMateria, TipoDispositivo, TipoDispositivoRelationship, @@ -68,17 +73,23 @@ __fp__in__test_permission_of_models_in_rules_patterns = { map_rules.RP_DELETE: [CasaLegislativa, ProblemaMigracao, + Argumento, + Constraint, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado], map_rules.RP_LIST: [ProblemaMigracao, + Argumento, + Constraint, AcompanhamentoMateria, TipoDispositivo, TipoDispositivoRelationship, PerfilEstruturalTextoArticulado], map_rules.RP_DETAIL: [ProblemaMigracao, + Argumento, + Constraint, AcompanhamentoMateria, TipoDispositivo, TipoDispositivoRelationship, diff --git a/sapl/sessao/views.py b/sapl/sessao/views.py index fedc8829e..419ea4fa0 100644 --- a/sapl/sessao/views.py +++ b/sapl/sessao/views.py @@ -164,7 +164,8 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): def get_rows(self, object_list): for obj in object_list: - if not obj.resultado: + resultados = obj.registrovotacao_set.all() + if not resultados: if obj.votacao_aberta: url = '' if obj.tipo_votacao == 1: @@ -208,6 +209,8 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): else: obj.resultado = '''Não há resultado''' else: + resultado = resultados[0].tipo_resultado_votacao.nome + resultado_observacao = resultados[0].observacao if self.request.user.has_module_perms(AppConfig.label): url = '' if obj.tipo_votacao == 1: @@ -228,10 +231,13 @@ class MateriaOrdemDiaCrud(MasterDetailCrud): 'pk': obj.sessao_plenaria_id, 'oid': obj.materia_id, 'mid': obj.pk}) - obj.resultado = '%s' % (url, - obj.resultado) + obj.resultado = ('%s
%s' % + (url, + resultado, + resultado_observacao)) else: - obj.resultado = '%s' % (obj.resultado) + obj.resultado = ('%s
%s' % + (resultado, resultado_observacao)) return [self._as_row(obj) for obj in object_list] @@ -268,7 +274,8 @@ class ExpedienteMateriaCrud(MasterDetailCrud): def get_rows(self, object_list): for obj in object_list: - if not obj.resultado: + resultados = obj.registrovotacao_set.all() + if not resultados: if obj.votacao_aberta: url = '' if obj.tipo_votacao == 1: @@ -310,7 +317,7 @@ class ExpedienteMateriaCrud(MasterDetailCrud): obj.resultado = btn_abrir else: url = '' - + resultado = resultados[0].tipo_resultado_votacao.nome if self.request.user.has_module_perms(AppConfig.label): if obj.tipo_votacao == 1: url = reverse( @@ -332,7 +339,9 @@ class ExpedienteMateriaCrud(MasterDetailCrud): 'oid': obj.materia_id, 'mid': obj.pk}) obj.resultado = '%s' % (url, - obj.resultado) + resultado) + else: + obj.resultado = '%s' % (resultado) return [self._as_row(obj) for obj in object_list] class CreateView(MasterDetailCrud.CreateView): @@ -604,7 +613,7 @@ class PainelView(PermissionRequiredForAppCrudMixin, TemplateView): cronometro_ordem = AppsAppConfig.attr('cronometro_ordem') if (not cronometro_discurso or not cronometro_aparte - or not cronometro_ordem): + or not cronometro_ordem): msg = _( 'Você precisa primeiro configurar os cronômetros \ nas Configurações da Aplicação') @@ -982,12 +991,14 @@ class ResumoView(DetailView): materias_expediente = [] for m in materias: + ementa = m.observacao titulo = m.materia numero = m.numero_ordem - if m.resultado: - resultado = m.resultado + resultado = m.registrovotacao_set.all() + if resultado: + resultado = resultado[0].tipo_resultado_votacao.nome else: resultado = _('Matéria não votada') @@ -1039,7 +1050,6 @@ class ResumoView(DetailView): # Matérias Ordem do Dia ordem = OrdemDia.objects.filter( sessao_plenaria_id=self.object.id) - materias_ordem = [] for o in ordem: ementa = o.observacao @@ -1047,8 +1057,9 @@ class ResumoView(DetailView): numero = o.numero_ordem # Verificar resultado - if o.resultado: - resultado = o.resultado + resultado = o.registrovotacao_set.all() + if resultado: + resultado = resultado[0].tipo_resultado_votacao.nome else: resultado = _('Matéria não votada') @@ -2065,9 +2076,9 @@ class PautaSessaoDetailView(DetailView): situacao = m.materia.tramitacao_set.last().status if situacao is None: situacao = _("Não informada") - - if m.resultado: - resultado = m.resultado + resultado = m.registrovotacao_set.all() + if resultado: + resultado = resultado[0].tipo_resultado_votacao.nome else: resultado = _('Matéria não votada') @@ -2118,8 +2129,9 @@ class PautaSessaoDetailView(DetailView): numero = o.numero_ordem # Verificar resultado - if o.resultado: - resultado = o.resultado + resultado = o.registrovotacao_set.all() + if resultado: + resultado = resultado[0].tipo_resultado_votacao.nome else: resultado = _('Matéria não votada') @@ -2436,7 +2448,7 @@ def mudar_ordem_materia_sessao(request): sessao_plenaria=pk_sessao, numero_ordem=posicao_inicial) except ObjectDoesNotExist: - raise # TODO tratar essa exceção + raise # TODO tratar essa exceção # Se a posição inicial for menor que a final, todos que # estiverem acima da nova posição devem ter sua ordem decrementada diff --git a/sapl/settings.py b/sapl/settings.py index d9875994b..11b94b3de 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -237,6 +237,7 @@ BOWER_INSTALLED_APPS = ( 'jQuery-Mask-Plugin#1.14.0', 'jsdiff#2.2.2', 'https://github.com/interlegis/drunken-parrot-flat-ui.git', + 'jquery-query-object#2.2.3', ) # Additional search paths for SASS files when using the @import statement diff --git a/sapl/templates/base.html b/sapl/templates/base.html index a96815f62..51351e7a0 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -224,6 +224,8 @@ + + {% block extra_js %}{% endblock %} {% endblock foot_js %} diff --git a/sapl/templates/sessao/sessaoplenaria_filter.html b/sapl/templates/sessao/sessaoplenaria_filter.html index b4aa4ca98..8c77a3ee2 100644 --- a/sapl/templates/sessao/sessaoplenaria_filter.html +++ b/sapl/templates/sessao/sessaoplenaria_filter.html @@ -9,16 +9,11 @@ {% blocktrans with verbose_name=view.verbose_name %} Adicionar Sessão Plenária {% endblocktrans %} {% endif %} - {% if filter_url %} - {% trans 'Fazer nova pesquisa' %} - {% endif %} {% endblock %} {% block detail_content %} - {% if not filter_url %} - {% crispy filter.form %} - {% endif %} + {% crispy filter.form %} {% if filter_url %}

@@ -51,3 +46,41 @@ {% endblock detail_content %} {% block table_content %} {% endblock table_content %} + + +{% block extra_js %} + + + +{% endblock %} \ No newline at end of file diff --git a/sapl/templates/sessao/sessaoplenaria_form.html b/sapl/templates/sessao/sessaoplenaria_form.html index cf8cbd64f..81057ba57 100644 --- a/sapl/templates/sessao/sessaoplenaria_form.html +++ b/sapl/templates/sessao/sessaoplenaria_form.html @@ -31,7 +31,6 @@ $(function() { function altera_legislatura(){ - console.debug('hey'); var id_legislatura = $("#id_legislatura").val(); var id_sessao_leg = $("#id_sessao_legislativa").val(); $("#id_sessao_legislativa option").remove(); diff --git a/sapl/test_urls.py b/sapl/test_urls.py index bcddfcc9e..40339cef7 100644 --- a/sapl/test_urls.py +++ b/sapl/test_urls.py @@ -361,6 +361,7 @@ for item in _lista_urls: __lista_urls.append((oper, item))""" +@pytest.mark.skip(reason="TODO: Lento demais. Precisa ser refatorado") @pytest.mark.django_db(transaction=False) @pytest.mark.parametrize('url_item', _lista_urls) def test_permissions_urls_for_users_by_apps(url_item, client): @@ -402,7 +403,7 @@ def test_permissions_urls_for_users_by_apps(url_item, client): container, como é o caso de proposições que possui restrição por usuário e não só por, ou não tem, o campo permission_required """ - if PermissionRequiredForAppCrudMixin in type.mro(key.view_class): + if issubclass(key.view_class, PermissionRequiredForAppCrudMixin): # essa classe deve informar app_label assert hasattr(key.view_class, 'app_label') # app_label deve ter conteudo diff --git a/sapl/utils.py b/sapl/utils.py index 3a857f337..81ebea6cc 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -5,6 +5,8 @@ import re from datetime import date from functools import wraps from unicodedata import normalize as unicodedata_normalize +from subprocess import PIPE, call +from threading import Thread import django_filters import magic @@ -22,7 +24,7 @@ from floppyforms import ClearableFileInput from reversion.admin import VersionAdmin from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row -from sapl.settings import BASE_DIR +from sapl.settings import BASE_DIR, PROJECT_DIR sapl_logger = logging.getLogger(BASE_DIR.name) @@ -632,3 +634,19 @@ def texto_upload_path(instance, filename, subpath=''): } return path + + +class UpdateIndexCommand(Thread): + def run(self): + call([PROJECT_DIR.child('manage.py'), 'update_index'], + stdout=PIPE) + + +def save_texto(sender, instance, **kwargs): + update_index = UpdateIndexCommand() + update_index.start() + + +def delete_texto(sender, instance, **kwargs): + update_index = UpdateIndexCommand() + update_index.start()