diff --git a/.gitignore b/.gitignore index 45262ed7..79b884be 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,11 @@ db.* .vagrant sigi/settings/prod.py +bower/ +collected_static/ +eav/ +geraldo/ + +*.bak +*~ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4ee11fc0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +FROM ubuntu:15.04 + +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 + +RUN mkdir /sigi + +RUN apt-get update && \ + apt-get install -y -f \ + build-essential \ + curl \ + git \ + graphviz-dev \ + graphviz \ + libz-dev \ + libffi-dev \ + libfreetype6-dev \ + libjpeg62 \ + libjpeg-dev \ + libldap2-dev \ + libpq-dev \ + libsasl2-dev \ + libssl-dev \ + libxft-dev \ + libxml2-dev \ + libxslt1-dev \ + nginx \ + pkg-config \ + python-dev \ + python-setuptools \ + software-properties-common \ + npm \ + nodejs + +# install nodejs +RUN DEBIAN_FRONTEND=noninteractive curl -sL https://deb.nodesource.com/setup_5.x | bash - +RUN DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs + +# install bower +RUN npm install -g bower + +# Bower aceitar root +RUN touch /root/.bowerrc +RUN chmod 751 /root/.bowerrc +RUN echo "{ \"allow_root\": true }" >> /root/.bowerrc + +ADD . /sigi + +WORKDIR /sigi + +RUN easy_install pip +RUN pip2 install -r requirements/dev-requirements.txt +RUN pip2 install --upgrade setuptools + +RUN mkdir -p /var/log/sigi/ +RUN touch /var/log/sigi/application.log +RUN chmod -x /var/log/sigi/application.log + +RUN git clone https://github.com/marinho/geraldo.git +WORKDIR /sigi/geraldo/ +RUN python setup.py install +RUN cp -Rfv reporting geraldo `python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"` +WORKDIR /sigi +RUN rm -rf geraldo/ diff --git a/check_qa.sh b/check_qa.sh new file mode 100755 index 00000000..607bb52b --- /dev/null +++ b/check_qa.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Check if there's some debug breakpoint in codebase +me=`basename "$0"` +stmts=`grep --exclude=$me -r -l "ipdb.set_trace()" * | wc -l` +if [ $stmts != '0' ] +then + echo "==================================================================" + echo "ERROR: ipdb.set_trace() call in codebase! Remove, please." + grep --exclude=$me -r -n "ipdb.set_trace()" * + echo "==================================================================" +fi + +# QA checks: run this before every commit +./manage.py check +flake8 --exclude='ipython_log.py*,migrations,templates' . +isort --recursive --check-only --skip='migrations' --skip='templates' --skip='ipython_log.py' . diff --git a/conftest.py b/conftest.py index b9ff21fc..a59c9995 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,6 @@ import pytest from django_webtest import DjangoTestApp, WebTestMixin - DEFAULT_MARK = object() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2bd636b1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +#localhost: +# image: postgres +# environment: +# POSTGRES_PASSWORD: sigi +# POSTGRES_USER: sigi +# POSTGRES_DB: sigi +# ports: +# - "5532:5432" +#web: +# build: . +# command: bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" +# volumes: +# - .:/sigi +# ports: +# - "8000:8000" +# links: +# - localhost + +osticket: + image: campbellsoftwaresolutions/osticket + ports: + - "80:80" + links: + - mysql + +mysql: + image: mysql + environment: + - MYSQL_ROOT_PASSWORD=secret + - MYSQL_DATABASE=osticket + - MYSQL_USER=osticket + - MYSQL_PASSWORD=secret diff --git a/etc/migracao/migra.py b/etc/migracao/migra.py index 47216db4..693fa256 100755 --- a/etc/migracao/migra.py +++ b/etc/migracao/migra.py @@ -19,19 +19,21 @@ banco de dados em produção. """ +import csv +from datetime import datetime + from django.core.management import setup_environ from sigi import settings -setup_environ(settings) - -import csv -from datetime import datetime from sigi.apps.casas.models import * from sigi.apps.contatos.models import * from sigi.apps.convenios.models import * from sigi.apps.inventario.models import * from sigi.apps.parlamentares.models import * +setup_environ(settings) + + ERROR_MSG_0 = (' %s[%s]: erro desconhecido! Possível erro de integridade ' 'do banco de dados. Favor verificar e inserir manualmente caso ' 'necessário.') diff --git a/fix_qa.sh b/fix_qa.sh new file mode 100755 index 00000000..a3ac9d84 --- /dev/null +++ b/fix_qa.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# QA fix: Use ese script para corrigir automaticamente vários +# problemas de estilo e boas práticas no código. +# +# Sempre guarde suas mudanças de alguma forma antes de aplicar esse script, +# de modo que possa revisar cada alteração que ele fez. +# Uma forma simples de fazer isso é adicionando antes suas mudanças à +# "staging area" do git, com `git add .` e após usar o script `git diff`. + +isort --recursive --skip='migrations' --skip='templates' --skip='ipython_log.py' . diff --git a/moodlerouter.py b/moodlerouter.py index 4ea4aa14..c5ba3465 100644 --- a/moodlerouter.py +++ b/moodlerouter.py @@ -18,7 +18,7 @@ def allow_relation(self, obj1, obj2, **hints): return True return None - def allow_migrate(self, db, model): - if model._meta.app_label == 'mdl': + def allow_migrate(self, db, app_label, model_name=None, **hints): + if app_label == 'mdl': return False return None diff --git a/prod.py b/prod.py new file mode 120000 index 00000000..01f8fd64 --- /dev/null +++ b/prod.py @@ -0,0 +1 @@ +dev.py \ No newline at end of file diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index 547f372f..983588b8 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -1,5 +1,7 @@ -r test-requirements.txt -django-debug-toolbar==1.2.2 -ipdb==0.8 -ipython==2.3.1 -pygraphviz==1.2 +django-debug-toolbar==1.5 +ipdb==0.10.0 +ipython==4.2.0 +isort==4.2.5 +model_mommy==1.2.6 +pygraphviz==1.3.1 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index abfd1d45..037703e5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,23 +1,41 @@ -e git://github.com/marinho/geraldo.git@868ebdce67176d9b6205cddc92476f642c783fff#egg=geraldo -django-bootstrap3==6.2.2 -django-admin-bootstrapped==2.4.0 -django-auth-ldap==1.2.7 +git+git://github.com/hellpanderrr/django-admin-bootstrapped-1.9-compatible.git@master#egg=django_admin_bootstrapped +git+git://github.com/interlegis/eav-django +django-auth-ldap==1.2.8 django-autoslug==1.9.3 -django-extensions==1.5.7 -django-image-cropping==1.0.2 -django-localflavor==1.1 -Django==1.7.10 -easy-thumbnails==2.2 -eav-django==1.4.7 -gunicorn==19.3.0 +django-bootstrap3==7.0.1 +django-bower==5.1.0 +django-braces==1.8.1 +django-compressor==2.0 +django-crispy-forms==1.6.0 +dj-database-url==0.4.1 +django-dotenv==1.4.1 +django-extensions==1.6.7 +django-extra-views==0.7.1 +django-filter==0.13.0 +django-floppyforms==1.6.1 +django-image-cropping==1.0.3 +django-localflavor==1.3 +django-model-utils==2.4 +django-sass-processor==0.3.4 +django-easy-select2==1.3.2 +django-simple-captcha==0.5.1 +Django==1.10.1 +libsass==0.11.0 +easy-thumbnails==2.3 +gunicorn==19.6.0 html5lib==0.9999999 -Pillow==2.9.0 +Pillow==3.2.0 pisa==3.0.33 psycopg2==2.6.1 -python-memcached==1.53 +python-decouple==3.0 +python-memcached==1.58 +pytz==2016.3 PyYAML==3.11 reportlab==2.7 -requests==2.8.1 +requests==2.10.0 +rtyaml==0.0.2 +unipath==1.1 six==1.10.0 djangorestframework==2.4.8 django-ipware==1.1.6 diff --git a/requirements/test-requirements.txt b/requirements/test-requirements.txt index 6a2cb2fa..80e69360 100644 --- a/requirements/test-requirements.txt +++ b/requirements/test-requirements.txt @@ -1,10 +1,10 @@ -r requirements.txt -coverage==3.7.1 -django-dynamic-fixture==1.8.1 -django-webtest==1.7.7 +git+git://github.com/paulocheque/django-dynamic-fixture/@master +coverage==4.1 +django-webtest==1.7.9 pyPdf==1.13 -pyquery==1.2.9 -pytest-cov==1.8.1 -pytest-django==2.8.0 -pytest==2.6.4 -WebTest==2.0.17 +pyquery==1.2.13 +pytest-cov==2.2.1 +pytest-django==2.9.1 +pytest==2.9.2 +WebTest==2.0.21 diff --git a/scripts/atendimento/criar_grupos.py b/scripts/atendimento/criar_grupos.py new file mode 100644 index 00000000..e870269f --- /dev/null +++ b/scripts/atendimento/criar_grupos.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import Group + + +def criar_grupos(): + # COPLAF = Atestar usuário + if not Group.objects.filter(name='COPLAF').exists(): + Group.objects.create(name='COPLAF') + + # COADFI = Atestar convênio + if not Group.objects.filter(name='COADFI').exists(): + Group.objects.create(name='COADFI') + + # Já recebeu aprovação dos dois grupos de cima + if not Group.objects.filter(name='Usuario_Habilitado').exists(): + Group.objects.create(name='Usuario_Habilitado') diff --git a/scripts/casas_com_PM_e_SAPL.py b/scripts/casas_com_PM_e_SAPL.py index b382da34..8c915c00 100644 --- a/scripts/casas_com_PM_e_SAPL.py +++ b/scripts/casas_com_PM_e_SAPL.py @@ -1,7 +1,6 @@ from csv_writer import CsvWriter from sigi.apps.servicos.models import Servico - ARQUIVO_CSV = '/tmp/casas_que_usam_PM_e_SAPL.csv' diff --git a/scripts/casas_que_usam_LEGBR_ou_PM.py b/scripts/casas_que_usam_LEGBR_ou_PM.py index 2dc8aeba..8d1de620 100644 --- a/scripts/casas_que_usam_LEGBR_ou_PM.py +++ b/scripts/casas_que_usam_LEGBR_ou_PM.py @@ -1,9 +1,8 @@ -import cStringIO import codecs +import cStringIO import csv -from sigi.apps.servicos.models import TipoServico, Servico - +from sigi.apps.servicos.models import Servico, TipoServico # cria um CSV com contatos das casas legislativas que possuem portal modelo ativo # rodar em um shell: diff --git a/scripts/contatos_de_casas_que_usam_portalmodelo.py b/scripts/contatos_de_casas_que_usam_portalmodelo.py index 32dc761a..4736e40f 100644 --- a/scripts/contatos_de_casas_que_usam_portalmodelo.py +++ b/scripts/contatos_de_casas_que_usam_portalmodelo.py @@ -1,10 +1,10 @@ -import cStringIO import codecs +import cStringIO import csv from sigi.apps.casas.models import Funcionario from sigi.apps.contatos.models import Telefone -from sigi.apps.servicos.models import TipoServico, Servico +from sigi.apps.servicos.models import Servico, TipoServico # cria um CSV com contatos das casas legislativas que possuem portal modelo ativo # rodar em um shell: diff --git a/scripts/csv_writer.py b/scripts/csv_writer.py index acf485dd..a825e5cc 100644 --- a/scripts/csv_writer.py +++ b/scripts/csv_writer.py @@ -1,5 +1,5 @@ -import cStringIO import codecs +import cStringIO import csv diff --git a/scripts/eav_models.py b/scripts/eav_models.py new file mode 100644 index 00000000..5ddb08a9 --- /dev/null +++ b/scripts/eav_models.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# +# EAV-Django is a reusable Django application which implements EAV data model +# Copyright © 2009—2010 Andrey Mikhaylenko +# +# This file is part of EAV-Django. +# +# EAV-Django is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# EAV-Django is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with EAV-Django. If not, see . +""" +Models +~~~~~~ +""" + +# django +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import fields +from django.db.models import (BooleanField, CharField, DateField, FloatField, + ForeignKey, IntegerField, Model, NullBooleanField, + TextField) +from django.utils.translation import ugettext_lazy as _ + +# 3rd-party +from autoslug.fields import AutoSlugField +from autoslug.settings import slugify +#from view_shortcuts.decorators import cached_property + +# this app +from managers import BaseEntityManager + + +__all__ = ['BaseAttribute', 'BaseChoice', 'BaseEntity', 'BaseSchema'] + + +def slugify_attr_name(name): + return slugify(name.replace('_', '-')).replace('-', '_') + + +def get_entity_lookups(entity): + ctype = ContentType.objects.get_for_model(entity) + return {'entity_type': ctype, 'entity_id': entity.pk} + + +class BaseSchema(Model): + """ + Metadata for an attribute. + """ + TYPE_TEXT = 'text' + TYPE_FLOAT = 'float' + TYPE_DATE = 'date' + TYPE_BOOLEAN = 'bool' + TYPE_ONE = 'one' + TYPE_MANY = 'many' + TYPE_RANGE = 'range' + + DATATYPE_CHOICES = ( + (TYPE_TEXT, _('text')), + (TYPE_FLOAT, _('number')), + (TYPE_DATE, _('date')), + (TYPE_BOOLEAN, _('boolean')), + (TYPE_ONE, _('choice')), + (TYPE_MANY, _('multiple choices')), + (TYPE_RANGE, _('numeric range')), + ) + + title = CharField(_('title'), max_length=250, help_text=_('user-friendly attribute name')) + name = AutoSlugField(_('name'), max_length=250, populate_from='title', + editable=True, blank=True, slugify=slugify_attr_name) + help_text = CharField(_('help text'), max_length=250, blank=True, + help_text=_('short description for administrator')) + datatype = CharField(_('data type'), max_length=5, choices=DATATYPE_CHOICES) + + required = BooleanField(_('required'), default=False) + searched = BooleanField(_('include in search'), default=False) # i.e. full-text search? mb for text only + filtered = BooleanField(_('include in filters'), default=False) + sortable = BooleanField(_('allow sorting'), default=False) + + class Meta: + abstract = True + verbose_name, verbose_name_plural = _('schema'), _('schemata') + ordering = ['title'] + + def __unicode__(self): + return u'%s (%s)%s' % (self.title, self.get_datatype_display(), + u' %s'%_('required') if self.required else '') + + def get_choices(self): + """ + Returns a queryset of choice objects bound to this schema. + """ + return self.choices.all() + + def get_attrs(self, entity): + """ + Returns available attributes for given entity instance. + Handles many-to-one relations transparently. + """ + return self.attrs.filter(**get_entity_lookups(entity)) + + def save_attr(self, entity, value): + """ + Saves given EAV attribute with given value for given entity. + + If schema is not a choice, the value is saved to the corresponding + Attr instance (which is created or updated). + + If schema is an cvhoice (one-to-one or many-to-one), the value is + processed thusly: + + * if value is iterable, all Attr instances for corresponding managed choice + schemata are updated (those with names from the value list are set to + True, others to False). If a list item is not in available choices, + ValueError is raised; + * if the value is None, all corresponding Attr instances are reset to False; + * if the value is neither a list nor None, it is wrapped into a list and + processed as above (i.e. "foo" --> ["foo"]). + """ + + if self.datatype in (self.TYPE_ONE, self.TYPE_MANY): + self._save_choice_attr(entity, value) + else: + self._save_single_attr(entity, value) + + def _save_single_attr(self, entity, value=None, schema=None, + create_nulls=False, extra={}): + """ + Creates or updates an EAV attribute for given entity with given value. + + :param schema: schema for attribute. Default it current schema instance. + :param create_nulls: boolean: if True, even attributes with value=None + are created (by default they are skipped). + :param extra: dict: additional data for Attr instance (e.g. title). + """ + # If schema is not many-to-one, the value is saved to the corresponding + # Attr instance (which is created or updated). + + schema = schema or self + lookups = dict(get_entity_lookups(entity), schema=schema, **extra) + try: + attr = self.attrs.get(**lookups) + except self.attrs.model.DoesNotExist: + attr = self.attrs.model(**lookups) + if create_nulls or value != attr.value: + attr.value = value + for k,v in extra.items(): + setattr(attr, k, v) + attr.save() + + def _save_choice_attr(self, entity, value): + """ + Creates or updates BaseChoice(s) attribute(s) for given entity. + """ + + # value can be None to reset choices from schema + if value == None: + value = [] + + if not hasattr(value, '__iter__'): + value = [value] + + if self.datatype == self.TYPE_ONE and len(value) > 1: + raise TypeError('Cannot assign multiple values "%s" to TYPE_ONE ' + 'must be only one BaseChoice instance.' + % value) + + if not all(isinstance(x, BaseChoice) for x in value): + raise TypeError('Cannot assign "%s": "Attr.choice" ' + 'must be a BaseChoice instance.' + % value) + + # drop all attributes for this entity/schema pair + self.get_attrs(entity).delete() + + # Attr instances for corresponding managed choice schemata are updated + for choice in value: + self._save_single_attr( + entity, + schema = self, + create_nulls = True, + extra = {'choice': choice} + ) + + +class BaseEntity(Model): + """ + Entity, the "E" in EAV. This model is abstract and must be subclassed. + See tests for examples. + """ + + objects = BaseEntityManager() + + class Meta: + abstract = True + + def save(self, force_eav=False, **kwargs): + """ + Saves entity instance and creates/updates related attribute instances. + + :param eav: if True (default), EAV attributes are saved along with entity. + """ + # save entity + super(BaseEntity, self).save(**kwargs) + + # TODO: think about use cases; are we doing it right? + #if not self.check_eav_allowed(): + # import warnings + # warnings.warn('EAV attributes are going to be saved along with entity' + # ' despite %s.check_eav_allowed() returned False.' + # % type(self), RuntimeWarning) + + + # create/update EAV attributes + for schema in self.get_schemata(): + value = getattr(self, schema.name, None) + schema.save_attr(self, value) + + def __getattr__(self, name): + if not name.startswith('_'): + if name in self.get_schema_names(): + schema = self.get_schema(name) + attrs = schema.get_attrs(self) + if schema.datatype == schema.TYPE_MANY: + return [a.value for a in attrs if a.value] + else: + return attrs[0].value if attrs else None + raise AttributeError('%s does not have attribute named "%s".' % + (self._meta.object_name, name)) + + def __iter__(self): + "Iterates over non-empty EAV attributes. Normal fields are not included." + for attr in self.attrs.select_related(): + if getattr(self, attr.schema.name, None): + yield attr + + @classmethod + def get_schemata_for_model(cls): + return NotImplementedError('BaseEntity subclasses must define method ' + '"get_schemata_for_model" which returns a ' + 'QuerySet for a BaseSchema subclass.') + + def get_schemata_for_instance(self, qs): + return qs + + def get_schemata(self): + if hasattr(self, '_schemata_cache') and self._schemata_cache is not None: + return self._schemata_cache + all_schemata = self.get_schemata_for_model().select_related() + self._schemata_cache = self.get_schemata_for_instance(all_schemata) + self._schemata_cache_dict = dict((s.name, s) for s in self._schemata_cache) + return self._schemata_cache + + def get_schema_names(self): + if not hasattr(self, '_schemata_cache_dict'): + self.get_schemata() + return self._schemata_cache_dict.keys() + + def get_schema(self, name): + if not hasattr(self, '_schemata_cache_dict'): + self.get_schemata() + return self._schemata_cache_dict[name] + + def get_schema_by_id(self, schema_id): + for schema in self.get_schemata(): + if schema.pk == schema_id: + return schema + + def check_eav_allowed(self): + """ + Returns True if entity instance allows EAV attributes to be attached. + + Can be useful if some external data is required to determine available + schemata and that data may be missing. In such cases this method should + be overloaded to check whether the data is available. + """ + return True + + def is_valid(self): + "Returns True if attributes and their values conform with schema." + + raise NotImplementedError() + + ''' + schemata = self.rubric.schemata.all() + return all(x.is_valid for x in self.attributes) + # 1. check if all required attributes are present + for schema in schemata: + pass + # 2. check if all attributes have appropriate values + for schema in schemata: + pass + return True + ''' + + +class BaseChoice(Model): + """ Base class for choices. Concrete choice class must overload the + `schema` attribute. + """ + title = CharField(max_length=100) + schema = NotImplemented + + class Meta: + abstract = True + ordering = ('title',) + + def __unicode__(self): + return self.title #u'%s "%s"' % (self.schema.title, self.title) + + +class BaseAttribute(Model): + """ Base class for choices. Concrete choice class must overload the + `schema` and `choice` attributes. + """ + entity_type = ForeignKey(ContentType) + entity_id = IntegerField() + entity = fields.GenericForeignKey(ct_field="entity_type", fk_field='entity_id') + + value_text = TextField(blank=True, null=True) + value_float = FloatField(blank=True, null=True) + value_date = DateField(blank=True, null=True) + value_bool = NullBooleanField(blank=True) # TODO: ensure that form invalidates null booleans (??) + value_range_min = FloatField(blank=True, null=True) + value_range_max = FloatField(blank=True, null=True) + + schema = NotImplemented # must be FK + choice = NotImplemented # must be nullable FK + + class Meta: + abstract = True + verbose_name, verbose_name_plural = _('attribute'), _('attributes') + ordering = ['entity_type', 'entity_id', 'schema'] + unique_together = ('entity_type', 'entity_id', 'schema', 'choice') + + def __unicode__(self): + return u'%s: %s "%s"' % (self.entity, self.schema.title, self.value) + + def _get_value(self): + if self.schema.datatype in (self.schema.TYPE_ONE, self.schema.TYPE_MANY): + return self.choice + if self.schema.datatype == self.schema.TYPE_RANGE: + names = ('value_range_%s' % x for x in ('min', 'max')) + value = tuple(getattr(self, x, None) for x in names) + return None if value == (None, None) else value + return getattr(self, 'value_%s' % self.schema.datatype) + + def _set_value(self, new_value): + if self.schema.datatype == self.schema.TYPE_RANGE: + new_value = new_value or (None, None) + + # validate range value -- expecting a tuple of two numbers + try: + validate_range_value(new_value) + except (TypeError, ValueError): + raise + + for k,v in zip('min max'.split(), new_value): + v = v if v is None else float(v) + setattr(self, 'value_range_%s' % k, v) + else: + setattr(self, 'value_%s' % self.schema.datatype, new_value) + + value = property(_get_value, _set_value) + + +def validate_range_value(value): + """ + Validates given value against `Schema.TYPE_RANGE` data type. Raises + TypeError or ValueError if something is wrong. Returns None if everything + is OK. + """ + if value == (None, None): + return + + if not hasattr(value, '__iter__'): + raise TypeError('Range value must be an iterable, got "%s".' % value) + if not 2 == len(value): + raise ValueError('Range value must consist of two elements, got %d.' % + len(value)) + if not all(isinstance(x, (int,float)) for x in value): + raise TypeError('Range value must consist of two numbers, got "%s" ' + 'and "%s" instead.' % value) + if not value[0] <= value[1]: + raise ValueError('Range must consist of min and max values (min <= ' + 'max) but got "%s" and "%s" instead.' % value) + return + + +# xxx catch signal Attr.post_save() --> update attr.item.attribute_cache (JSONField or such) diff --git a/scripts/importa_pesquisa.py b/scripts/importa_pesquisa.py index cac1c324..b0f79554 100644 --- a/scripts/importa_pesquisa.py +++ b/scripts/importa_pesquisa.py @@ -29,6 +29,7 @@ from sigi.apps.casas.models import CasaLegislativa from sigi.apps.servidores.models import Servidor + def importa(file_list): ''' Este script importa dados de um arquivo CSV e dá carga no model casas.CasaLegislativa @@ -128,4 +129,3 @@ def importa(file_list): casa.save() print 'O arquivo '+filename+'.out foi criado com os registros que nao puderam ser importados' - diff --git a/scripts/servicos_seit_ate_2013_2014.py b/scripts/servicos_seit_ate_2013_2014.py index 24b4f7fd..c2d2e188 100644 --- a/scripts/servicos_seit_ate_2013_2014.py +++ b/scripts/servicos_seit_ate_2013_2014.py @@ -1,4 +1,5 @@ from datetime import date + from django.db.models import Q from sigi.apps.servicos.models import TipoServico diff --git a/scripts/setgerentes/dados_gerentes.py b/scripts/setgerentes/dados_gerentes.py index 20009146..6616f24b 100644 --- a/scripts/setgerentes/dados_gerentes.py +++ b/scripts/setgerentes/dados_gerentes.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from sigi.apps.servidores.models import Servidor - gerentes = {k: Servidor.objects.get(nome_completo=n) for k, n in ( ('ADAL', u'Adalberto Alves de Oliveira'), ('janarycn', u'Janary Carvão Nunes'), diff --git a/scripts/user_from_session.py b/scripts/user_from_session.py index 853aeb7e..7c9b7895 100644 --- a/scripts/user_from_session.py +++ b/scripts/user_from_session.py @@ -1,5 +1,5 @@ -from django.contrib.sessions.models import Session from django.contrib.auth.models import User +from django.contrib.sessions.models import Session def user_from_session(session_key): diff --git a/scripts/verificacao_arquivos_faltando_media.py b/scripts/verificacao_arquivos_faltando_media.py index 90cc79f4..6a686c60 100644 --- a/scripts/verificacao_arquivos_faltando_media.py +++ b/scripts/verificacao_arquivos_faltando_media.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- +from datetime import datetime # Dependência: # pip install terminaltables from os.path import isfile -from datetime import datetime from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse -from terminaltables import AsciiTable from sigi.apps.casas.models import CasaLegislativa from sigi.apps.convenios.models import Anexo as AnexoConvenios @@ -15,6 +14,7 @@ from sigi.apps.ocorrencias.models import Anexo as AnexoOcorrencias from sigi.apps.parlamentares.models import Parlamentar from sigi.apps.servidores.models import Servidor +from terminaltables import AsciiTable def print_table(msg, relacao): diff --git a/setup.py b/setup.py index 2ca58d5a..c23c2bbe 100644 --- a/setup.py +++ b/setup.py @@ -4,5 +4,4 @@ # from distutils.core import setup - setup(name='sigi', version='2.x') diff --git a/sigi/apps/casas/admin.py b/sigi/apps/casas/admin.py index 392d752b..5099368b 100644 --- a/sigi/apps/casas/admin.py +++ b/sigi/apps/casas/admin.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- -from django.core.urlresolvers import reverse from django.contrib import admin -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.admin import GenericTabularInline +from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.utils.translation import ugettext as _ from image_cropping import ImageCroppingMixin from sigi.apps.casas.forms import CasaLegislativaForm -from sigi.apps.casas.models import CasaLegislativa, Presidente, Funcionario, TipoCasaLegislativa -from sigi.apps.casas.views import report_complete, labels_report, export_csv, \ - labels_report_sem_presidente, report, \ - adicionar_casas_carrinho +from sigi.apps.casas.models import (CasaLegislativa, Funcionario, Presidente, + TipoCasaLegislativa) +from sigi.apps.casas.views import (adicionar_casas_carrinho, export_csv, + labels_report, labels_report_sem_presidente, + report, report_complete) from sigi.apps.contatos.models import Telefone from sigi.apps.convenios.models import Convenio from sigi.apps.diagnosticos.models import Diagnostico @@ -24,7 +25,7 @@ from sigi.apps.utils.base_admin import BaseModelAdmin -class TelefonesInline(generic.GenericTabularInline): +class TelefonesInline(GenericTabularInline): model = Telefone readonly_fields = ('ult_alteracao',) extra = 1 diff --git a/sigi/apps/casas/management/commands/importa_gerentes.py b/sigi/apps/casas/management/commands/importa_gerentes.py index f43744cd..2016d98c 100644 --- a/sigi/apps/casas/management/commands/importa_gerentes.py +++ b/sigi/apps/casas/management/commands/importa_gerentes.py @@ -24,10 +24,13 @@ import csv import os + from django.core.management.base import BaseCommand, CommandError + from sigi.apps.casas.models import CasaLegislativa -from sigi.apps.servidores.models import Servidor from sigi.apps.contatos.models import Municipio +from sigi.apps.servidores.models import Servidor + class Command(BaseCommand): args = u"data_file.csv" @@ -81,4 +84,4 @@ def handle(self, *args, **options): casa.gerente_contas = gerente casa.save() - self.stdout.write(u"Importação concluída. %s erros em %s linhas" % (erros, reader.line_num,)) \ No newline at end of file + self.stdout.write(u"Importação concluída. %s erros em %s linhas" % (erros, reader.line_num,)) diff --git a/sigi/apps/casas/models.py b/sigi/apps/casas/models.py index b1995798..e13cabf3 100644 --- a/sigi/apps/casas/models.py +++ b/sigi/apps/casas/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import random +from datetime import datetime from string import ascii_uppercase from unicodedata import normalize -from datetime import datetime -from django.contrib.contenttypes import generic +from django.contrib.contenttypes import fields from django.db import models from image_cropping import ImageRatioField @@ -85,7 +85,7 @@ class CasaLegislativa(models.Model): pesquisador = models.ForeignKey(Servidor, verbose_name=u"Pesquisador", null=True, blank=True) obs_pesquisa = models.TextField(u"Observações do pesquisador", blank=True) ult_alt_endereco = models.DateTimeField(u'Última alteração do endereço', null=True, blank=True, editable=True) - telefones = generic.GenericRelation('contatos.Telefone') + telefones = fields.GenericRelation('contatos.Telefone') foto = models.ImageField( upload_to='imagens/casas', @@ -243,7 +243,7 @@ def gerarCodigoInterlegis(self): return codigo def __unicode__(self): - return self.nome + return self.nome + ' - ' + self.municipio.uf.sigla def save(self, *args, **kwargs): address_changed = False @@ -293,8 +293,8 @@ class Funcionario(models.Model): sexo = models.CharField(max_length=1, choices=SEXO_CHOICES, default="M") nota = models.CharField(max_length=70, null=True, blank=True) email = models.CharField('e-mail', max_length=75, blank=True) - telefones = generic.GenericRelation('contatos.Telefone') - endereco = generic.GenericRelation('contatos.Endereco') + telefones = fields.GenericRelation('contatos.Telefone') + endereco = fields.GenericRelation('contatos.Endereco') cargo = models.CharField(max_length=100, null=True, blank=True) funcao = models.CharField(u'função', max_length=100, null=True, blank=True) setor = models.CharField(max_length=100, choices=SETOR_CHOICES, default="outros") diff --git a/sigi/apps/casas/reports.py b/sigi/apps/casas/reports.py index 2c7a0437..3e5e66fd 100644 --- a/sigi/apps/casas/reports.py +++ b/sigi/apps/casas/reports.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from django.templatetags.static import static from django.utils.translation import ugettext as _ -from geraldo import Report, DetailBand, Label, ObjectValue, ReportGroup, ReportBand, landscape, SubReport, BAND_WIDTH, SystemField +from geraldo import (BAND_WIDTH, DetailBand, Label, ObjectValue, Report, + ReportBand, ReportGroup, SubReport, SystemField, + landscape) from geraldo.graphics import Image from reportlab.lib.enums import TA_CENTER from reportlab.lib.pagesizes import A4 diff --git a/sigi/apps/casas/test_casas.py b/sigi/apps/casas/test_casas.py index 9e113c15..ad95dee8 100644 --- a/sigi/apps/casas/test_casas.py +++ b/sigi/apps/casas/test_casas.py @@ -14,4 +14,5 @@ def some_parliaments(): def parliaments_from_names(names): - return [G(CasaLegislativa, nome=name, foto=None, gerente_contas=None,) for name in names] + return [G(CasaLegislativa, nome=name, foto=None, gerente_contas=None, + pesquisador=None,) for name in names] diff --git a/sigi/apps/casas/urls.py b/sigi/apps/casas/urls.py index 53813647..fd22f503 100644 --- a/sigi/apps/casas/urls.py +++ b/sigi/apps/casas/urls.py @@ -1,38 +1,37 @@ # coding: utf-8 -from django.conf.urls import patterns, url +from django.conf.urls import url +from sigi.apps.casas import views - -urlpatterns = patterns( - 'sigi.apps.casas.views', +urlpatterns = [ # Informacoes de uma casa legislativa - url(r'^casalegislativa/report_complete/$', 'report_complete', name='report-complete-all'), - url(r'^casalegislativa/(?P\w+)/report_complete/$', 'report_complete', name='report-complete-id'), + url(r'^casalegislativa/report_complete/$', views.report_complete, name='report-complete-all'), + url(r'^casalegislativa/(?P\w+)/report_complete/$', views.report_complete, name='report-complete-id'), # Reports Labels - url(r'^casalegislativa/labels/$', 'labels_report', name='labels-report-all'), - url(r'^casalegislativa/(?P\w+)/labels/$', 'labels_report', name='labels-report-id'), + url(r'^casalegislativa/labels/$', views.labels_report, name='labels-report-all'), + url(r'^casalegislativa/(?P\w+)/labels/$', views.labels_report, name='labels-report-id'), # Reports Labels Parlamentar - url(r'^casalegislativa/labels_parlamentar/$', 'labels_report_parlamentar', name='lebels-report-parlamentar-all'), - url(r'^casalegislativa/(?P\w+)/labels_parlamentar/$', 'labels_report_parlamentar', name='labels-report-parlamentar-id'), + url(r'^casalegislativa/labels_parlamentar/$', views.labels_report_parlamentar, name='lebels-report-parlamentar-all'), + url(r'^casalegislativa/(?P\w+)/labels_parlamentar/$', views.labels_report_parlamentar, name='labels-report-parlamentar-id'), # Reports labels sem presidente - url(r'^casalegislativa/labels_sem_presidente/$', 'labels_report_sem_presidente', name='labels-report-sem-presidente-all'), - url(r'^casalegislativa/(?P\w+)/labels_sem_presidente/$', 'labels_report_sem_presidente', name='labels-report-sem-presidente-id'), + url(r'^casalegislativa/labels_sem_presidente/$', views.labels_report_sem_presidente, name='labels-report-sem-presidente-all'), + url(r'^casalegislativa/(?P\w+)/labels_sem_presidente/$', views.labels_report_sem_presidente, name='labels-report-sem-presidente-id'), # Reports casas sem convenio - url(r'^casalegislativa/reports/$', 'report', name='casa-report'), - url(r'^casalegislativa/casas_sem_convenio_report/$', 'casas_sem_convenio_report', name='casas-sem-convenio-report'), + url(r'^casalegislativa/reports/$', views.report, name='casa-report'), + url(r'^casalegislativa/casas_sem_convenio_report/$', views.casas_sem_convenio_report, name='casas-sem-convenio-report'), # CSV - url(r'^casalegislativa/csv/$', 'export_csv', name='casa-export-csv'), # Error + url(r'^casalegislativa/csv/$', views.export_csv, name='casa-export-csv'), # Error # Carrinho - url(r'^casalegislativa/carrinho/$', 'visualizar_carrinho', name='visualizar-carrinho'), - url(r'^casalegislativa/carrinho/excluir_carrinho/$', 'excluir_carrinho', name='excluir-carrinho'), # Error - url(r'^casalegislativa/carrinho/deleta_itens_carrinho$', 'deleta_itens_carrinho', name='deleta-itens-carrinho'), # Error - url(r'^portfolio/$', 'portfolio', name='casas-portfolio'), - url(r'^carteira/$', 'painel_relacionamento', name='casas-carteira'), -) + url(r'^casalegislativa/carrinho/$', views.visualizar_carrinho, name='visualizar-carrinho'), + url(r'^casalegislativa/carrinho/excluir_carrinho/$', views.excluir_carrinho, name='excluir-carrinho'), # Error + url(r'^casalegislativa/carrinho/deleta_itens_carrinho$', views.deleta_itens_carrinho, name='deleta-itens-carrinho'), # Error + url(r'^portfolio/$', views.portfolio, name='casas-portfolio'), + url(r'^carteira/$', views.painel_relacionamento, name='casas-carteira'), +] diff --git a/sigi/apps/casas/views.py b/sigi/apps/casas/views.py index d8cea61a..d218cc3c 100644 --- a/sigi/apps/casas/views.py +++ b/sigi/apps/casas/views.py @@ -2,23 +2,29 @@ import csv from functools import reduce -from django.core.paginator import Paginator, InvalidPage, EmptyPage -from django.http import HttpResponse, HttpResponseRedirect, Http404 -from django.shortcuts import render, get_object_or_404 -from django.utils.translation import ugettext as _, ungettext +from django.contrib.auth.decorators import login_required +from django.core.paginator import EmptyPage, InvalidPage, Paginator +from django.db.models import Count, Q +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http.response import JsonResponse +from django.shortcuts import get_object_or_404, render +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext from geraldo.generators import PDFGenerator +from sigi.apps.casas.forms import PortfolioForm from sigi.apps.casas.models import CasaLegislativa -from sigi.apps.casas.reports import CasasLegislativasLabels, CasasLegislativasLabelsSemPresidente, CasasLegislativasReport, CasasSemConvenioReport, InfoCasaLegislativa +from sigi.apps.casas.reports import (CasasLegislativasLabels, + CasasLegislativasLabelsSemPresidente, + CasasLegislativasReport, + CasasSemConvenioReport, + InfoCasaLegislativa) +from sigi.apps.contatos.models import (Mesorregiao, Microrregiao, + UnidadeFederativa) +from sigi.apps.ocorrencias.models import Ocorrencia from sigi.apps.parlamentares.reports import ParlamentaresLabels -from sigi.apps.contatos.models import UnidadeFederativa, Mesorregiao, Microrregiao -from sigi.apps.casas.forms import PortfolioForm -from django.contrib.auth.decorators import login_required from sigi.apps.servicos.models import TipoServico from sigi.apps.servidores.models import Servidor -from sigi.apps.ocorrencias.models import Ocorrencia -from django.db.models import Count, Q -from django.http.response import JsonResponse # @param qs: queryset @@ -678,4 +684,4 @@ def painel_relacionamento(request): if snippet == 'resumo': return render(request, 'casas/resumo_carteira_snippet.html', context) - return render(request, 'casas/painel.html', context) \ No newline at end of file + return render(request, 'casas/painel.html', context) diff --git a/sigi/apps/contatos/admin.py b/sigi/apps/contatos/admin.py index 768956fa..7506f42e 100644 --- a/sigi/apps/contatos/admin.py +++ b/sigi/apps/contatos/admin.py @@ -3,11 +3,12 @@ from django.utils.translation import ugettext as _ from sigi.apps.contatos.filters import PopulationFilter -from sigi.apps.contatos.models import (UnidadeFederativa, Mesorregiao, Microrregiao, - Municipio, Telefone, Contato) +from sigi.apps.contatos.models import (Contato, Mesorregiao, Microrregiao, + Municipio, Telefone, UnidadeFederativa) from sigi.apps.utils import queryset_ascii from sigi.apps.utils.base_admin import BaseModelAdmin + class MesorregiaoInline(admin.TabularInline): model = Mesorregiao diff --git a/sigi/apps/contatos/management/commands/importa_mesomicro.py b/sigi/apps/contatos/management/commands/importa_mesomicro.py index d4bc2b90..ffea4193 100644 --- a/sigi/apps/contatos/management/commands/importa_mesomicro.py +++ b/sigi/apps/contatos/management/commands/importa_mesomicro.py @@ -23,8 +23,12 @@ # import csv import os + from django.core.management.base import BaseCommand, CommandError -from sigi.apps.contatos.models import Municipio, UnidadeFederativa, Mesorregiao, Microrregiao + +from sigi.apps.contatos.models import (Mesorregiao, Microrregiao, Municipio, + UnidadeFederativa) + class Command(BaseCommand): args = u"data_file.csv" @@ -97,4 +101,4 @@ def handle(self, *args, **options): municipio.microrregiao = micro municipio.save() - self.stdout.write(u"Importação concluída. %s erros em %s linhas" % (erros, reader.line_num,)) \ No newline at end of file + self.stdout.write(u"Importação concluída. %s erros em %s linhas" % (erros, reader.line_num,)) diff --git a/sigi/apps/contatos/migrations/0003_auto_20160623_0829.py b/sigi/apps/contatos/migrations/0003_auto_20160623_0829.py new file mode 100644 index 00000000..8508c790 --- /dev/null +++ b/sigi/apps/contatos/migrations/0003_auto_20160623_0829.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-23 08:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contatos', '0002_auto_20151104_0810'), + ] + + operations = [ + migrations.AlterField( + model_name='contato', + name='email', + field=models.EmailField(blank=True, max_length=254, verbose_name='e-mail'), + ), + ] diff --git a/sigi/apps/contatos/models.py b/sigi/apps/contatos/models.py index 9b3becf3..04cd2ae4 100644 --- a/sigi/apps/contatos/models.py +++ b/sigi/apps/contatos/models.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from django.contrib.contenttypes import generic +from django.contrib.contenttypes import fields from django.contrib.contenttypes.models import ContentType from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -176,7 +176,7 @@ class Telefone(models.Model): content_type = models.ForeignKey(ContentType) # identificador do registro na classe vinculado a esse registro object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') + content_object = fields.GenericForeignKey('content_type', 'object_id') class Meta: ordering = ('numero',) @@ -196,7 +196,7 @@ class Contato(models.Model): nota = models.CharField(max_length=70, blank=True) email = models.EmailField(_(u'e-mail'), blank=True) - telefones = generic.GenericRelation(Telefone) + telefones = fields.GenericRelation(Telefone) municipio = models.ForeignKey( Municipio, @@ -209,7 +209,7 @@ class Contato(models.Model): content_type = models.ForeignKey(ContentType) # identificador do registro na classe vinculado a esse registro object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') + content_object = fields.GenericForeignKey('content_type', 'object_id') class Meta: ordering = ('nome',) @@ -301,7 +301,7 @@ class Endereco(models.Model): content_type = models.ForeignKey(ContentType) # identificador do registro na classe vinculado a esse registro object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') + content_object = fields.GenericForeignKey('content_type', 'object_id') class Meta: ordering = ('logradouro', 'numero') diff --git a/sigi/apps/convenios/admin.py b/sigi/apps/convenios/admin.py index 6eeac0bd..77dd8ee8 100644 --- a/sigi/apps/convenios/admin.py +++ b/sigi/apps/convenios/admin.py @@ -4,7 +4,8 @@ from django.utils.translation import ugettext as _ from geraldo.generators import PDFGenerator -from sigi.apps.convenios.models import Projeto, Convenio, EquipamentoPrevisto, Anexo, Tramitacao +from sigi.apps.convenios.models import (Anexo, Convenio, EquipamentoPrevisto, + Projeto, Tramitacao) from sigi.apps.convenios.reports import ConvenioReport from sigi.apps.convenios.views import adicionar_convenios_carrinho from sigi.apps.utils import queryset_ascii diff --git a/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py b/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py new file mode 100644 index 00000000..94c9ae39 --- /dev/null +++ b/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('convenios', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='anexo', + name='descricao', + field=models.CharField(max_length=70, verbose_name='descri\xe7\xe3o'), + preserve_default=True, + ), + migrations.AlterField( + model_name='tramitacao', + name='observacao', + field=models.CharField(max_length=512, null=True, verbose_name='observa\xe7\xe3o', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='unidadeadministrativa', + name='nome', + field=models.CharField(max_length=100), + preserve_default=True, + ), + migrations.AlterField( + model_name='unidadeadministrativa', + name='sigla', + field=models.CharField(max_length=10), + preserve_default=True, + ), + ] diff --git a/sigi/apps/convenios/models.py b/sigi/apps/convenios/models.py index 07ddcd3e..ad490c9c 100644 --- a/sigi/apps/convenios/models.py +++ b/sigi/apps/convenios/models.py @@ -1,5 +1,6 @@ # style="list-style-type: noneo -*- coding: utf-8 -*- from datetime import datetime + from django.db import models from django.utils.translation import ugettext as _ @@ -148,7 +149,7 @@ class Anexo(models.Model): convenio = models.ForeignKey(Convenio, verbose_name=_(u'convênio')) # caminho no sistema para o documento anexo arquivo = models.FileField(upload_to='apps/convenios/anexo/arquivo', max_length=500) - descricao = models.CharField(_(u'descrição'), max_length='70') + descricao = models.CharField(_(u'descrição'), max_length=70) data_pub = models.DateTimeField( _(u'data da publicação do anexo'), default=datetime.now @@ -167,8 +168,8 @@ class UnidadeAdministrativa(models.Model): que pode ser um servivo do próprio Interlegis, assim como uma unidade do Senado Federal """ - sigla = models.CharField(max_length='10') - nome = models.CharField(max_length='100') + sigla = models.CharField(max_length=10) + nome = models.CharField(max_length=100) def __unicode__(self): return unicode(self.sigla) @@ -184,7 +185,7 @@ class Tramitacao(models.Model): data = models.DateField() observacao = models.CharField( _(u'observação'), - max_length='512', + max_length=512, null=True, blank=True, ) diff --git a/sigi/apps/convenios/reports.py b/sigi/apps/convenios/reports.py index 15f68a81..a4dce186 100644 --- a/sigi/apps/convenios/reports.py +++ b/sigi/apps/convenios/reports.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from django.utils.translation import ugettext as _ -from geraldo import ReportBand, ObjectValue, Label, ReportGroup, FIELD_ACTION_SUM +from geraldo import (FIELD_ACTION_SUM, Label, ObjectValue, ReportBand, + ReportGroup) from reportlab.lib.units import cm from sigi.apps.relatorios.reports import ReportDefault diff --git a/sigi/apps/convenios/templates/convenios/change_list.html b/sigi/apps/convenios/templates/convenios/change_list.html index 3f0ce3e4..3dc6737f 100644 --- a/sigi/apps/convenios/templates/convenios/change_list.html +++ b/sigi/apps/convenios/templates/convenios/change_list.html @@ -5,7 +5,7 @@