Sistema de Apoio ao Processo Legislativo
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1214 lines
38 KiB

from functools import wraps
import hashlib
from itertools import groupby, chain
import logging
from operator import itemgetter
import os
import platform
import re
import sys
import tempfile
from time import time
from unicodedata import normalize as unicodedata_normalize
import unicodedata
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Button, HTML, Fieldset, Div
from django import forms
from django.apps import apps
from django.conf import settings
from django.contrib import admin
8 years ago
from django.contrib.contenttypes.fields import (GenericForeignKey, GenericRel,
GenericRelation)
8 years ago
from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage
from django.core.files.uploadedfile import UploadedFile
Fix #2342 (#2354) * Fix #2432 Insere verificação de serviço de email configurado. Assume que EMAIL_HOST vazio é serviço desativado não enviando, assim, email na edição/adição de autor, bem como desativando acesso via get/post e por link de "Acompanhar Matéria" na lista de resultados da pesquisa de matéria e na tela de detalhes de matérias. Comportamento análogo para Documentos Administrativos. * impl test de conexão * Corrige tipo de remetente rementente de um email é único e seu envio não é feito através de uma lista... estudando a execução do send_mail descobri a RFC 2822 que define rementente da seguinte forma: From: "Joe Q. Public" <john.q.public@example.com> portanto, ou se envia uma string neste formato, o que faz com que mais execução deva ser feita para um padrão interno de processamento, ou envia-se uma tupla e não uma lista da seguinte forma ("Joe Q. Public", "john.q.public@exemple.com") então por que com um servidor configurado o remetente estava funcionando? possivelmente ninguem ainda tinha seguindo o formato completo acima apresentado na definição de EMAIL_SEND_USER... fazendo isso: EMAIL_SEND_USER = john.q.public@example.com em vez disso: EMAIL_SEND_USER = Joe Q. Public <john.q.public@example.com> ocorre que esta segunda forma (a correta), melhor apresenta o email ao usuário, colocando em sua caixa de entrada de email um nome significativo e não o username. * faz teste de conexão de email apenas no início do sapl
6 years ago
from django.core.mail import get_connection
from django.db import models
from django.db.models import Q
from django.db.models.fields.related import ForeignKey
from django.forms import BaseForm
from django.forms.widgets import SplitDateTimeWidget
from django.utils import six, timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
import django_filters
from easy_thumbnails import source_generators
from floppyforms import ClearableFileInput
import magic
import requests
from reversion_compare.admin import CompareVersionAdmin
from unipath.path import Path
from sapl.crispy_layout_mixin import (form_actions, SaplFormHelper,
SaplFormLayout, to_row)
from sapl.settings import MAX_DOC_UPLOAD_SIZE
# (26/10/2018): O separador foi mudador de '/' para 'K'
# por conta dos leitores de códigos de barra, que trocavam
# a '/' por '&' ou ';'
SEPARADOR_HASH_PROPOSICAO = 'K'
TIME_PATTERN = '^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$'
Fix #2342 (#2354) * Fix #2432 Insere verificação de serviço de email configurado. Assume que EMAIL_HOST vazio é serviço desativado não enviando, assim, email na edição/adição de autor, bem como desativando acesso via get/post e por link de "Acompanhar Matéria" na lista de resultados da pesquisa de matéria e na tela de detalhes de matérias. Comportamento análogo para Documentos Administrativos. * impl test de conexão * Corrige tipo de remetente rementente de um email é único e seu envio não é feito através de uma lista... estudando a execução do send_mail descobri a RFC 2822 que define rementente da seguinte forma: From: "Joe Q. Public" <john.q.public@example.com> portanto, ou se envia uma string neste formato, o que faz com que mais execução deva ser feita para um padrão interno de processamento, ou envia-se uma tupla e não uma lista da seguinte forma ("Joe Q. Public", "john.q.public@exemple.com") então por que com um servidor configurado o remetente estava funcionando? possivelmente ninguem ainda tinha seguindo o formato completo acima apresentado na definição de EMAIL_SEND_USER... fazendo isso: EMAIL_SEND_USER = john.q.public@example.com em vez disso: EMAIL_SEND_USER = Joe Q. Public <john.q.public@example.com> ocorre que esta segunda forma (a correta), melhor apresenta o email ao usuário, colocando em sua caixa de entrada de email um nome significativo e não o username. * faz teste de conexão de email apenas no início do sapl
6 years ago
def groups_remove_user(user, groups_name):
from django.contrib.auth.models import Group
if not isinstance(groups_name, list):
groups_name = [groups_name, ]
for group_name in groups_name:
if not group_name or not user.groups.filter(
name=group_name).exists():
continue
g = Group.objects.get_or_create(name=group_name)[0]
user.groups.remove(g)
def groups_add_user(user, groups_name):
from django.contrib.auth.models import Group
if not isinstance(groups_name, list):
groups_name = [groups_name, ]
for group_name in groups_name:
if not group_name or user.groups.filter(
name=group_name).exists():
continue
g = Group.objects.get_or_create(name=group_name)[0]
user.groups.add(g)
def num_materias_por_tipo(qs, attr_tipo='tipo'):
"""
:argument um QuerySet em MateriaLegislativa
:return um dict com o mapeamento {tipo: <quantidade de materias>},
se não existir o matéria para o tipo informado então não
haverá entrada no dicionário.
"""
qtdes = {}
if attr_tipo == 'tipo':
def sort_function(m): return m.tipo
else:
def sort_function(m): return m.materia.tipo
# select_related eh importante por questoes de desempenho, pois caso
# contrario ele realizara uma consulta ao banco para cada iteracao,
# na key do groupby (uma alternativa é só usar tipo_id, na chave).
qs2 = qs.select_related(attr_tipo).order_by(attr_tipo + '_id')
for key, values in groupby(qs2, key=sort_function):
# poderia usar qtdes[key] = len(list(values)) aqui, mas
# desse modo economiza memória RAM
qtdes[key] = sum(1 for x in values)
return qtdes
def validar_arquivo(arquivo, nome_campo):
if len(arquivo.name) > 200:
raise ValidationError(
"Certifique-se de que o nome do arquivo no "
"campo '" + nome_campo + "' tenha no máximo 200 caracteres "
"(ele possui {})".format(len(arquivo.name))
)
if arquivo.size > MAX_DOC_UPLOAD_SIZE:
raise ValidationError(
"O arquivo " + nome_campo + " deve ser menor que "
"{0:.1f} mb, o tamanho atual desse arquivo é {1:.1f} mb".format(
(MAX_DOC_UPLOAD_SIZE / 1024) / 1024,
(arquivo.size / 1024) / 1024
)
)
def pil_image(source, exif_orientation=False, **options):
return source_generators.pil_image(source, exif_orientation, **options)
def dont_break_out(value, max_part=50):
_safe = value.split()
def chunkstring(string):
return re.findall('.{%d}' % max_part, string)
def __map(a):
if len(a) <= max_part:
return a
return '<br>' + '<br>'.join(chunkstring(a))
_safe = map(__map, _safe)
_safe = ' '.join(_safe)
_safe = mark_safe(_safe)
return value
def clear_thumbnails_cache(queryset, field):
for r in queryset:
assert hasattr(r, field), _(
'Objeto da listagem não possui o campo informado')
if not getattr(r, field):
continue
path = Path(getattr(r, field).path)
if not path.exists():
7 years ago
continue
cache_files = path.parent.walk()
for cf in cache_files:
if cf != path:
cf.remove()
def normalize(txt):
return unicodedata_normalize(
'NFKD', txt).encode('ASCII', 'ignore').decode('ASCII')
9 years ago
def get_settings_auth_user_model():
return getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
autor_label = '''
<div class="col-xs-12">
Autor: <span id="nome_autor" name="nome_autor">
{% if form.autor.value %}
{{form.autor.value}}
{% endif %}
</span>
</div>
'''
autor_modal = '''
<div id="modal_autor" title="Selecione o Autor" align="center">
<form>
<input id="q" type="text" />
<input id="pesquisar" type="submit" value="Pesquisar"
class="btn btn-primary btn-sm"/>
</form>
<div id="div-resultado"></div>
<input type="submit" id="selecionar" value="Selecionar"
hidden="true" />
</div>
'''
def montar_row_autor(name):
autor_row = to_row(
[(name, 0),
(Button('pesquisar',
'Pesquisar Autor',
css_class='btn btn-primary btn-sm'), 2),
(Button('limpar',
'Limpar Autor',
css_class='btn btn-primary btn-sm'), 10)])
return autor_row
# TODO: Esta função é utilizada?
def montar_helper_autor(self):
autor_row = montar_row_autor('nome')
self.helper = SaplFormHelper()
self.helper.layout = SaplFormLayout(*self.get_layout())
# Adiciona o novo campo 'autor' e mecanismo de busca
self.helper.layout[0][0].append(HTML(autor_label))
self.helper.layout[0][0].append(HTML(autor_modal))
self.helper.layout[0][1] = autor_row
# Adiciona espaço entre o novo campo e os botões
# self.helper.layout[0][4][1].append(HTML('<br /><br />'))
# Remove botões que estão fora do form
self.helper.layout[1].pop()
# Adiciona novos botões dentro do form
self.helper.layout[0][4][0].insert(2, form_actions(more=[
HTML('<a href="{{ view.cancel_url }}"'
Fix #1540 migra frontend (#2485) * retira django-bootstrap3 do projeto sapl * retira bower sass-processor e libsass do projeto sapl * altera origem do django-admin-bootstrapped * commenta todas as entradas css e js em base.html * vue create sapl-frontend * add e configura django-webpack-loader * add vue.config.js e configura comunicação com webpack-loader * adequa staticfiles_dirs e ignora webpack-stats * cria bloco de expansão para vue_content * remove rotas do app hellow e impl multiplas entradas * add nova entrada para gereciamento de temas * cria plugin para carregar THEME_CUSTOM em .env * add bootstrap ao projeto e a configuração fixa de temas * add imports básicos para bootstrap * move assets para sapl-frontend * move assets para projetosexterno de tema e configura sapl project para leitura de temas através do .env * remove theme do sapl frontend project * retira link simbólico de entre projeto frontend e projeto de tema * retira scss do projeto django * altera btn-default/bootstrap3 para btn-outline-dark/bootstrap4 * altera btns de outline-dark para outlink-primary * migra pull-right para float-right * migra pull-left para float-left * migra btn-excluir para btn-outline-danger * add global entry and configure jquery * add jquery na entrada global e migra getCookie * remove row-fluid * muda active para link a * ajustes do botão salvar * ajuste botão add participação em comissão * ajuste do layout das mensagens de alerta e botao add detail detail * ajusta script de votação individual * ajuste eslint * altera origem da imagem para etiqueta.png * corrige eslint * migra tela de busca de autores para sapl-frontend * corrige fragmento de html para paginação * ajuste de js e html em protocolo * ajuste de layout no menu protocolo * ajuste no formulário de proposições para sapl_frontend * migra jquery-mask-plugin * remove código antigo do jquery-mask-plugin * migra jquery e algumas funções dependentes * migra tinymce * ajustes eslint * ajuste de sub menus * add campo de hora em mask * move jquery.runner.js para sapl-frontend * primeira versão com jquery runner funcionando * ajusta painel ao sapl-frontend * ajuste de tela de login * cria entr compilação p sapl-frontend e migra jsdiff * migra js das notas * add import bootstrap em module global * altera nome de função para iniciar editor de texto rico * move scss de compilação do tema para o frontend * adequa crud de notas de compilação ao form do django 1.11 * retira render padrão scripts css e js de image_cropping * integra jquery runner e image cropping com jquery e lodash global * ajustes de eslint em compilação * ajuste de layout para notas e de código eslint * ajuste de layout para Vides da app compilação * altera configurações para pasta público de tema ser importada no build * ajustes de eslint * ajusta TAs embedded de normas para sapl-frontend * migra botões de compilação para bootstrap4 * migra componente alert para bootstrap 4 * altera chamada a jQuery e resolve conflito com bootstrap * altera travis.yaml * corrige entrada doo sapl-oficial-theme para instalação trivial * testa modo de definir versão do node no travis * teste de configuração do travis * ajuste de teste e das telas de erro 404 e 500 * Configura sapl-frontend e webpackloader Configura sapl-frontend e webpack-loader com código de produção já esportado para a sapl/static. Desta forma, tanto no travis como no docker não existe a necessidade de instalação de node, npm e/ou yarn * configura docker e localização do webpack-stats.json * inicia migração da edição de dispositivos * ajuste de chunks * resume forma de lidar com com chunks css e js * cria template tag para tratar chunk-vendors * corrige menu de configuração dos TAs * migra barra inferior da vigencia * migra linha do tempo de vigências * ajuste nos menus do topo da edição dinamica da compilação * frontend produção * ajuste de blocos * migra layout da edição dinamica de dispositivos originais * ajuste da seleção de tipos de vigências * migra frontend de edição dinâmica de dispositivos * ajst dropdown de seleção de editor de dispositivos * migra subnav da edição avançada de dispositivos * separa frontend em outro projeto * Altera how-to de instalação do ambiente dev Altera how-to de instalação do ambiente de desenvolvimento com novas instruções para colaboração com o sapl-frontend * migra barra de navegação de parents de dispositivos * Alterações no how-to para colab com sapl-frontend Alterações no how-to para colab com sapl-frontend * ajuste de docs * sapl-frontend reinseriu language pt-br datepicker * ajuste no form autoria multicreate para sapl-frontend * add webpack blocks no index de painel * refatora layout do painel para bootstrap4 * migra cadastro de autor * migra frontend da manutenção de tipos de proposição * migra tela de incorporação * remove templates de compilação * ajusta layout do form de pesquisa textual * ajustes de layout no painel * primeira versão checada do frontend
6 years ago
' class="btn btn-dark">Cancelar</a>')]))
class SaplGenericForeignKey(GenericForeignKey):
8 years ago
def __init__(
self,
ct_field='content_type',
fk_field='object_id',
for_concrete_model=True,
verbose_name=''):
super().__init__(ct_field, fk_field, for_concrete_model)
self.verbose_name = verbose_name
class SaplGenericRelation(GenericRelation):
"""
Extenção da class GenericRelation para implmentar o atributo fields_search
fields_search é uma tupla de tuplas de dois strings no padrão de construção
de campos porém com [Field Lookups][ref_1]
exemplo:
[No Model Parlamentar em][ref_2] existe a implementação dessa
classe no atributo autor. Parlamentar possui três informações
relevantes para buscas realacionadas a Autor:
- nome_completo;
- nome_parlamentar; e
- filiacao__partido__sigla
que devem ser pesquisados, coincidentemente
pelo FieldLookup __icontains
portanto a estrutura de fields_search seria:
fields_search=(
('nome_completo', '__icontains'),
('nome_parlamentar', '__icontains'),
('filiacao__partido__sigla', '__icontains'),
)
8 years ago
[ref_1]: https://docs.djangoproject.com/el/1.10/topics/db/queries/
#field-lookups
[ref_2]: https://github.com/interlegis/sapl/blob/master/sapl/
parlamentares/models.py
"""
def __init__(self, to, fields_search=(), **kwargs):
assert 'related_query_name' in kwargs, _(
'SaplGenericRelation não pode ser instanciada sem '
'related_query_name.')
assert fields_search, _(
'SaplGenericRelation não pode ser instanciada sem fields_search.')
for field in fields_search:
# descomente para ver todas os campos que são elementos de busca
8 years ago
# print(kwargs['related_query_name'], field)
assert isinstance(field, (tuple, list)), _(
'fields_search deve ser um array de tuplas ou listas.')
assert len(field) <= 3, _(
'cada tupla de fields_search deve possuir até 3 strings')
# TODO implementar assert para validar campos do Model e lookups
self.fields_search = fields_search
super().__init__(to, **kwargs)
class ImageThumbnailFileInput(ClearableFileInput):
template_name = 'floppyforms/image_thumbnail.html'
class RangeWidgetOverride(forms.MultiWidget):
def __init__(self, attrs=None):
widgets = (forms.DateInput(format='%d/%m/%Y',
attrs={'class': 'dateinput form-control',
'placeholder': 'Inicial'}),
forms.DateInput(format='%d/%m/%Y',
attrs={'class': 'dateinput form-control',
'placeholder': 'Final'}))
super(RangeWidgetOverride, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return [value.start, value.stop]
return []
def render(self, name, value, attrs=None, renderer=None):
rendered_widgets = []
for i, x in enumerate(self.widgets):
rendered_widgets.append(
x.render(
'%s_%d' % (name, i), value[i] if value else ''
)
)
html = '<div class="col-sm-6">%s</div><div class="col-sm-6">%s</div>'\
% tuple(rendered_widgets)
return '<div class="row">%s</div>' % html
class CustomSplitDateTimeWidget(SplitDateTimeWidget):
def render(self, name, value, attrs=None, renderer=None):
rendered_widgets = []
for i, x in enumerate(self.widgets):
x.attrs['class'] += ' form-control'
rendered_widgets.append(
x.render(
'%s_%d' % (name, i), self.decompress(
value)[i] if value else ''
)
)
html = '<div class="col-6">%s</div><div class="col-6">%s</div>'\
% tuple(rendered_widgets)
return '<div class="row">%s</div>' % html
def register_all_models_in_admin(module_name, exclude_list=[]):
appname = module_name.split('.')
appname = appname[1] if appname[0] == 'sapl' else appname[0]
app = apps.get_app_config(appname)
for model in app.get_models():
class CustomModelAdmin(CompareVersionAdmin):
list_display = [f.name for f in model._meta.fields
if f.name != 'id' and f.name not in exclude_list]
if not admin.site.is_registered(model):
admin.site.register(model, CustomModelAdmin)
def xstr(s):
return '' if s is None else str(s)
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def get_base_url(request):
# TODO substituir por Site.objects.get_current().domain
# from django.contrib.sites.models import Site
current_domain = request.get_host()
protocol = 'https' if request.is_secure() else 'http'
return "{0}://{1}".format(protocol, current_domain)
def create_barcode(value, width=170, height=50):
9 years ago
'''
creates a base64 encoded barcode PNG image
'''
from base64 import b64encode
from reportlab.graphics.barcode import createBarcodeDrawing
value_bytes = bytes(value, "ascii")
barcode = createBarcodeDrawing('Code128',
value=value_bytes,
barWidth=width,
height=height,
fontSize=2,
humanReadable=True)
data = b64encode(barcode.asString('png'))
return data.decode('utf-8')
YES_NO_CHOICES = [(True, _('Sim')), (False, _('Não'))]
def listify(function):
@wraps(function)
def f(*args, **kwargs):
return list(function(*args, **kwargs))
return f
7 years ago
LISTA_DE_UFS = [
('AC', 'Acre'),
('AL', 'Alagoas'),
('AP', 'Amapá'),
('AM', 'Amazonas'),
('BA', 'Bahia'),
('CE', 'Ceará'),
('DF', 'Distrito Federal'),
('ES', 'Espírito Santo'),
('GO', 'Goiás'),
('MA', 'Maranhão'),
('MT', 'Mato Grosso'),
('MS', 'Mato Grosso do Sul'),
('MG', 'Minas Gerais'),
('PR', 'Paraná'),
('PB', 'Paraíba'),
('PA', 'Pará'),
('PE', 'Pernambuco'),
('PI', 'Piauí'),
('RJ', 'Rio de Janeiro'),
('RN', 'Rio Grande do Norte'),
('RS', 'Rio Grande do Sul'),
('RO', 'Rondônia'),
('RR', 'Roraima'),
('SC', 'Santa Catarina'),
('SE', 'Sergipe'),
('SP', 'São Paulo'),
('TO', 'Tocantins'),
('EX', 'Exterior'),
]
6 years ago
RANGE_ANOS = [(year, year) for year in range(timezone.now().year + 1,
1889, -1)]
9 years ago
RANGE_MESES = [
(1, 'Janeiro'),
(2, 'Fevereiro'),
(3, 'Março'),
(4, 'Abril'),
(5, 'Maio'),
(6, 'Junho'),
(7, 'Julho'),
(8, 'Agosto'),
(9, 'Setembro'),
(10, 'Outubro'),
(11, 'Novembro'),
(12, 'Dezembro'),
]
RANGE_DIAS_MES = [(n, n) for n in range(1, 32)]
6 years ago
def ANO_CHOICES():
return [('', '---------')] + RANGE_ANOS
def choice_anos(model):
try:
anos_list = model.objects.all().distinct(
'ano').order_by('-ano').values_list('ano', 'ano')
return list(anos_list)
6 years ago
except Exception:
return []
def choice_anos_com_materias():
from sapl.materia.models import MateriaLegislativa
return choice_anos(MateriaLegislativa)
def choice_anos_com_normas():
from sapl.norma.models import NormaJuridica
return choice_anos(NormaJuridica)
def choice_tipos_normas():
from sapl.norma.models import TipoNormaJuridica
return [(id, descricao) for (id, descricao) in
TipoNormaJuridica.objects.all().values_list('id', 'descricao')]
6 years ago
def choice_anos_com_protocolo():
from sapl.protocoloadm.models import Protocolo
return choice_anos(Protocolo)
def choice_anos_com_documentoadministrativo():
from sapl.protocoloadm.models import DocumentoAdministrativo
return choice_anos(DocumentoAdministrativo)
6 years ago
def choice_anos_com_sessaoplenaria():
try:
from sapl.sessao.models import SessaoPlenaria
anos_list = SessaoPlenaria.objects.all().dates('data_inicio', 'year')
# a listagem deve ser em ordem descrescente, mas por algum motivo
# a adicao de .order_by acima depois do all() nao surte efeito
# apos a adicao do .dates(), por isso o reversed() abaixo
anos = [(k.year, k.year) for k in reversed(anos_list)]
return anos
except Exception:
return []
def choice_force_optional(callable):
""" Django-filter faz algo que tenha o mesmo sentido em ChoiceFilter,
no entanto, as funções choice_anos_... podem ser usadas em formulários
comuns de adição e/ou edição, com a particularidade de terem
required=False.
Neste caso para ser possível contar com a otimização de apenas mostrar anos
que estejam na base de dados e ainda colocar o item opcional '---------',
é necessário encapsular então, as funções choice_anos_... com a
esta função choice_force_optional... isso ocorre e foi aplicado
inicialmente no cadastro de documentos administrativos onde tem-se
opcionalmente a possibilidade de colocar o ano do protocolo.
Em ChoiceFilter choice_force_optional não deve ser usado pois duplicaria
o item opcional '---------' que ChoiceFilter o adiciona, como dito
anteriormente.
"""
def _func():
return [('', '---------')] + callable()
return _func
FILTER_OVERRIDES_DATEFIELD = {
'filter_class': django_filters.DateFromToRangeFilter,
'extra': lambda f: {
'label': '%s (%s)' % (f.verbose_name, _('Inicial - Final')),
'widget': RangeWidgetOverride
}
}
class FilterOverridesMetaMixin:
filter_overrides = {
models.DateField: FILTER_OVERRIDES_DATEFIELD,
models.DateTimeField: FILTER_OVERRIDES_DATEFIELD
}
9 years ago
TIPOS_TEXTO_PERMITIDOS = (
'application/vnd.oasis.opendocument.text',
'application/x-vnd.oasis.opendocument.text',
'application/pdf',
'application/x-pdf',
'application/zip',
9 years ago
'application/acrobat',
'applications/vnd.pdf',
'text/pdf',
'text/x-pdf',
'text/plain',
'application/txt',
'browser/internal',
'text/anytext',
'widetext/plain',
'widetext/paragraph',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/xml',
'application/octet-stream',
'text/xml',
'text/html',
)
9 years ago
TIPOS_IMG_PERMITIDOS = (
'image/jpeg',
'image/jpg',
'image/jpe_',
'image/pjpeg',
'image/vnd.swiftview-jpeg',
'application/jpg',
'application/x-jpg',
'image/pjpeg',
'image/pipeg',
'image/vnd.swiftview-jpeg',
'image/x-xbitmap',
'image/bmp',
'image/x-bmp',
'image/x-bitmap',
'image/png',
'application/png',
'application/x-png',
)
def fabrica_validador_de_tipos_de_arquivo(lista, nome):
9 years ago
def restringe_tipos_de_arquivo(value):
if not os.path.splitext(value.path)[1][:1]:
raise ValidationError(_(
'Não é possível fazer upload de arquivos sem extensão.'))
try:
mime = magic.from_buffer(value.read(), mime=True)
if mime not in lista:
raise ValidationError(_('Tipo de arquivo não suportado'))
except FileNotFoundError:
raise ValidationError(_('Arquivo não encontrado'))
# o nome é importante para as migrations
restringe_tipos_de_arquivo.__name__ = nome
9 years ago
return restringe_tipos_de_arquivo
9 years ago
restringe_tipos_de_arquivo_txt = fabrica_validador_de_tipos_de_arquivo(
TIPOS_TEXTO_PERMITIDOS, 'restringe_tipos_de_arquivo_txt')
9 years ago
restringe_tipos_de_arquivo_img = fabrica_validador_de_tipos_de_arquivo(
TIPOS_IMG_PERMITIDOS, 'restringe_tipos_de_arquivo_img')
def intervalos_tem_intersecao(a_inicio, a_fim, b_inicio, b_fim):
maior_inicio = max(a_inicio, b_inicio)
menor_fim = min(a_fim, b_fim)
return maior_inicio <= menor_fim
class MateriaPesquisaOrderingFilter(django_filters.OrderingFilter):
choices = (
('', 'Selecione'),
('dataC', 'Data, Tipo, Ano, Numero - Ordem Crescente'),
('dataD', 'Data, Tipo, Ano, Numero - Ordem Decrescente'),
('tipoC', 'Tipo, Ano, Numero, Data - Ordem Crescente'),
('tipoD', 'Tipo, Ano, Numero, Data - Ordem Decrescente')
)
order_by_mapping = {
'': [],
'dataC': ['data_apresentacao', 'tipo__sigla', 'ano', 'numero'],
'dataD': ['-data_apresentacao', '-tipo__sigla', '-ano', '-numero'],
'tipoC': ['tipo__sigla', 'ano', 'numero', 'data_apresentacao'],
'tipoD': ['-tipo__sigla', '-ano', '-numero', '-data_apresentacao'],
}
def __init__(self, *args, **kwargs):
kwargs['choices'] = self.choices
super(MateriaPesquisaOrderingFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
_value = self.order_by_mapping[value[0]] if value else value
return super().filter(qs, _value)
class NormaPesquisaOrderingFilter(django_filters.OrderingFilter):
choices = (
('', 'Selecione'),
('dataC', 'Data, Tipo, Ano, Numero - Ordem Crescente'),
('dataD', 'Data, Tipo, Ano, Numero - Ordem Decrescente'),
('tipoC', 'Tipo, Ano, Numero, Data - Ordem Crescente'),
('tipoD', 'Tipo, Ano, Numero, Data - Ordem Decrescente')
)
order_by_mapping = {
'': [],
'dataC': ['data', 'tipo', 'ano', 'numero'],
'dataD': ['-data', '-tipo', '-ano', '-numero'],
'tipoC': ['tipo', 'ano', 'numero', 'data'],
'tipoD': ['-tipo', '-ano', '-numero', '-data'],
}
def __init__(self, *args, **kwargs):
kwargs['choices'] = self.choices
super(NormaPesquisaOrderingFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
_value = self.order_by_mapping[value[0]] if value else value
return super().filter(qs, _value)
class FileFieldCheckMixin(BaseForm):
def _check(self):
cleaned_data = super(FileFieldCheckMixin, self).clean()
errors = []
for name, campo in self.fields.items():
if isinstance(campo, forms.fields.FileField):
error = self.errors.get(name)
if error:
msgs = [e.replace('Certifique-se de que o arquivo',
"Certifique-se de que o nome do "
"arquivo no campo '{}'".format(
campo.label))
for e in error]
for msg in msgs:
errors.append(msg)
arquivo = self.cleaned_data.get(name)
if arquivo and not isinstance(arquivo, UploadedFile):
if not os.path.exists(arquivo.path):
errors.append("Arquivo referenciado no campo "
" '%s' inexistente! Marque a "
"opção Limpar e Salve." % campo.label)
if errors:
raise ValidationError(errors)
return cleaned_data
def clean(self):
""" Alias for _check() """
return self._check()
class AnoNumeroOrderingFilter(django_filters.OrderingFilter):
choices = (('DEC', 'Ordem Decrescente'),
('CRE', 'Ordem Crescente'),)
order_by_mapping = {
'DEC': ['-ano', '-numero'],
'CRE': ['ano', 'numero'],
}
def __init__(self, *args, **kwargs):
kwargs['choices'] = self.choices
super(AnoNumeroOrderingFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
_value = self.order_by_mapping[value[0]] if value else value
return super().filter(qs, _value)
def gerar_hash_arquivo(arquivo, pk, block_size=2 ** 20):
md5 = hashlib.md5()
arq = open(arquivo, 'rb')
while True:
data = arq.read(block_size)
if not data:
break
md5.update(data)
return 'P' + md5.hexdigest() + SEPARADOR_HASH_PROPOSICAO + pk
class ChoiceWithoutValidationField(forms.ChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'], code='required')
def models_with_gr_for_model(model):
return list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
model._meta.get_fields(include_hidden=True))
))
def generic_relations_for_model(model):
"""
Esta função retorna uma lista de tuplas de dois elementos, onde o primeiro
elemento é um model qualquer que implementa SaplGenericRelation (SGR), o
segundo elemento é uma lista de todas as SGR's que pode haver dentro do
model retornado na primeira posição da tupla.
Exemplo: No Sapl, o model Parlamentar tem apenas uma SGR para Autor.
Se no Sapl existisse apenas essa SGR, o resultado dessa função
seria:
[ #Uma Lista de tuplas
( # cada tupla com dois elementos
sapl.parlamentares.models.Parlamentar,
[<sapl.utils.SaplGenericRelation: autor>]
),
]
"""
return list(map(
lambda x: (x,
list(filter(
lambda field: (
isinstance(
field, SaplGenericRelation) and
field.related_model == model),
x._meta.get_fields(include_hidden=True)))),
models_with_gr_for_model(model)
))
8 years ago
def texto_upload_path(instance, filename, subpath='', pk_first=False):
"""
O path gerado por essa função leva em conta a pk de instance.
isso não é possível naturalmente em uma inclusão pois a implementação
do django framework chama essa função antes do metodo save
Por outro lado a forma como vinha sendo formada os paths para os arquivos
são improdutivas e inconsistentes. Exemplo: usava se o valor de __str__
do model Proposicao que retornava a descrição da proposição, não retorna
mais, para uma pasta formar o path do texto_original.
Ora, o resultado do __str__ citado é totalmente impróprio para ser o nome
de uma pasta.
Para colocar a pk no path, a solução encontrada foi implementar o método
save nas classes que possuem atributo do tipo FileField, implementação esta
que guarda o FileField em uma variável independente e temporária para savar
o object sem o arquivo e, logo em seguida, salvá-lo novamente com o arquivo
Ou seja, nas inclusões que acomparem um arquivo, haverá dois saves,
um para armazenar toda a informação e recuperar o pk, e outro logo em
seguida para armazenar o arquivo.
"""
filename = re.sub('\s', '_', normalize(filename.strip()).lower())
from sapl.materia.models import Proposicao
from sapl.protocoloadm.models import DocumentoAdministrativo
if isinstance(instance, (DocumentoAdministrativo, Proposicao)):
prefix = 'private'
else:
prefix = 'public'
str_path = ('./sapl/%(prefix)s/%(model_name)s/'
'%(subpath)s/%(pk)s/%(filename)s')
8 years ago
if pk_first:
str_path = ('./sapl/%(prefix)s/%(model_name)s/'
'%(pk)s/%(subpath)s/%(filename)s')
8 years ago
if subpath is None:
subpath = '_'
path = str_path % \
{
'prefix': prefix,
'model_name': instance._meta.model_name,
'pk': instance.pk,
'subpath': subpath,
'filename': filename
}
return path
7 years ago
def qs_override_django_filter(self):
if not hasattr(self, '_qs'):
valid = self.is_bound and self.form.is_valid()
if self.is_bound and not valid:
"""if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
raise forms.ValidationError(self.form.errors)
elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:"""
self._qs = self.queryset.none()
return self._qs
# else STRICTNESS.IGNORE... ignoring
# start with all the results and filter from there
qs = self.queryset.all()
for name, filter_ in six.iteritems(self.filters):
value = None
if valid:
value = self.form.cleaned_data[name]
else:
raw_value = self.form[name].value()
try:
value = self.form.fields[name].clean(raw_value)
except forms.ValidationError:
"""if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
raise
elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:"""
self._qs = self.queryset.none()
return self._qs
# else STRICTNESS.IGNORE... ignoring
if value is not None: # valid & clean data
qs = qs._next_is_sticky()
qs = filter_.filter(qs, value)
self._qs = qs
7 years ago
return self._qs
def filiacao_data(parlamentar, data_inicio, data_fim=None):
7 years ago
from sapl.parlamentares.models import Filiacao
filiacoes_parlamentar = Filiacao.objects.filter(
parlamentar=parlamentar)
filiacoes = filiacoes_parlamentar.filter(Q(
data__lte=data_inicio,
7 years ago
data_desfiliacao__isnull=True) | Q(
data__lte=data_inicio,
data_desfiliacao__gte=data_inicio))
7 years ago
if data_fim:
filiacoes = filiacoes | filiacoes_parlamentar.filter(
data__gte=data_inicio,
data__lte=data_fim)
return ' | '.join([f.partido.sigla for f in filiacoes])
def parlamentares_ativos(data_inicio, data_fim=None):
from sapl.parlamentares.models import Mandato, Parlamentar
'''
:param data_inicio: define a data de inicial do período desejado
:param data_fim: define a data final do período desejado
:return: queryset dos parlamentares ativos naquele período
'''
mandatos_ativos = Mandato.objects.filter(Q(
data_inicio_mandato__lte=data_inicio,
data_fim_mandato__isnull=True) | Q(
data_inicio_mandato__lte=data_inicio,
data_fim_mandato__gte=data_inicio))
if data_fim:
mandatos_ativos = mandatos_ativos | Mandato.objects.filter(
data_inicio_mandato__gte=data_inicio,
data_inicio_mandato__lte=data_fim)
7 years ago
else:
mandatos_ativos = mandatos_ativos | Mandato.objects.filter(
data_inicio_mandato__gte=data_inicio)
parlamentares_id = mandatos_ativos.values_list(
'parlamentar_id',
4 years ago
flat=True).distinct('parlamentar_id').order_by('parlamentar_id')
return Parlamentar.objects.filter(id__in=parlamentares_id)
def show_results_filter_set(qr):
query_params = set(qr.keys())
if ((len(query_params) == 1 and 'iframe' in query_params) or
len(query_params) == 0):
return False
return True
def sort_lista_chave(lista, chave):
"""
:param lista: Uma list a ser ordenada .
:param chave: Algum atributo (chave) que está presente na lista e qual
deve ser usado para a ordenação da nova
lista.
:return: A lista ordenada pela chave passada.
"""
lista_ordenada = sorted(lista, key=itemgetter(chave))
return lista_ordenada
def get_mime_type_from_file_extension(filename):
ext = filename.split('.')[-1]
if ext == 'odt':
mime = 'application/vnd.oasis.opendocument.text'
else:
mime = "application/%s" % (ext,)
return mime
def ExtraiTag(texto, posicao):
for i in range(posicao, len(texto)):
if (texto[i] == '>'):
return i + 1
def TrocaTag(texto, startTag, endTag, sizeStart, sizeEnd, styleName, subinitiTag, subendTag):
textoSaida = ''
insideTag = 0
i = 0
if texto is None or texto.strip() == '':
return texto
if '<tbody>' in texto:
texto = texto.replace('<tbody>', '')
texto = texto.replace('</tbody>', '')
if '<p>' in texto:
texto = texto.replace('<p>', '')
texto = texto.replace('</p>', '')
while (i < len(texto)):
shard = texto[i:i + sizeStart]
if (shard == startTag):
i = ExtraiTag(texto, i)
textoSaida += subinitiTag + styleName + '">'
insideTag = 1
else:
if (insideTag == 1):
if (texto[i:i + sizeEnd] == endTag):
textoSaida += subendTag
insideTag = 0
i += sizeEnd
else:
textoSaida += texto[i]
i += 1
else:
textoSaida += texto[i]
i += 1
return textoSaida
def RemoveTag(texto):
textoSaida = ''
i = 0
while (i < len(texto)):
if (texto[i] == '<'):
i = ExtraiTag(texto, i)
else:
textoSaida += texto[i]
i += 1
7 years ago
return textoSaida
def remover_acentos(string):
return unicodedata.normalize('NFKD', string).encode('ASCII', 'ignore').decode()
Fix #2342 (#2354) * Fix #2432 Insere verificação de serviço de email configurado. Assume que EMAIL_HOST vazio é serviço desativado não enviando, assim, email na edição/adição de autor, bem como desativando acesso via get/post e por link de &#34;Acompanhar Matéria&#34; na lista de resultados da pesquisa de matéria e na tela de detalhes de matérias. Comportamento análogo para Documentos Administrativos. * impl test de conexão * Corrige tipo de remetente rementente de um email é único e seu envio não é feito através de uma lista... estudando a execução do send_mail descobri a RFC 2822 que define rementente da seguinte forma: From: &#34;Joe Q. Public&#34; &lt;john.q.public@example.com&gt; portanto, ou se envia uma string neste formato, o que faz com que mais execução deva ser feita para um padrão interno de processamento, ou envia-se uma tupla e não uma lista da seguinte forma (&#34;Joe Q. Public&#34;, &#34;john.q.public@exemple.com&#34;) então por que com um servidor configurado o remetente estava funcionando? possivelmente ninguem ainda tinha seguindo o formato completo acima apresentado na definição de EMAIL_SEND_USER... fazendo isso: EMAIL_SEND_USER = john.q.public@example.com em vez disso: EMAIL_SEND_USER = Joe Q. Public &lt;john.q.public@example.com&gt; ocorre que esta segunda forma (a correta), melhor apresenta o email ao usuário, colocando em sua caixa de entrada de email um nome significativo e não o username. * faz teste de conexão de email apenas no início do sapl
6 years ago
def mail_service_configured(request=None):
logger = logging.getLogger(__name__)
Fix #2342 (#2354) * Fix #2432 Insere verificação de serviço de email configurado. Assume que EMAIL_HOST vazio é serviço desativado não enviando, assim, email na edição/adição de autor, bem como desativando acesso via get/post e por link de &#34;Acompanhar Matéria&#34; na lista de resultados da pesquisa de matéria e na tela de detalhes de matérias. Comportamento análogo para Documentos Administrativos. * impl test de conexão * Corrige tipo de remetente rementente de um email é único e seu envio não é feito através de uma lista... estudando a execução do send_mail descobri a RFC 2822 que define rementente da seguinte forma: From: &#34;Joe Q. Public&#34; &lt;john.q.public@example.com&gt; portanto, ou se envia uma string neste formato, o que faz com que mais execução deva ser feita para um padrão interno de processamento, ou envia-se uma tupla e não uma lista da seguinte forma (&#34;Joe Q. Public&#34;, &#34;john.q.public@exemple.com&#34;) então por que com um servidor configurado o remetente estava funcionando? possivelmente ninguem ainda tinha seguindo o formato completo acima apresentado na definição de EMAIL_SEND_USER... fazendo isso: EMAIL_SEND_USER = john.q.public@example.com em vez disso: EMAIL_SEND_USER = Joe Q. Public &lt;john.q.public@example.com&gt; ocorre que esta segunda forma (a correta), melhor apresenta o email ao usuário, colocando em sua caixa de entrada de email um nome significativo e não o username. * faz teste de conexão de email apenas no início do sapl
6 years ago
if settings.EMAIL_RUNNING is None:
result = True
try:
connection = get_connection()
connection.open()
except Exception as e:
logger.error(str(e))
Fix #2342 (#2354) * Fix #2432 Insere verificação de serviço de email configurado. Assume que EMAIL_HOST vazio é serviço desativado não enviando, assim, email na edição/adição de autor, bem como desativando acesso via get/post e por link de &#34;Acompanhar Matéria&#34; na lista de resultados da pesquisa de matéria e na tela de detalhes de matérias. Comportamento análogo para Documentos Administrativos. * impl test de conexão * Corrige tipo de remetente rementente de um email é único e seu envio não é feito através de uma lista... estudando a execução do send_mail descobri a RFC 2822 que define rementente da seguinte forma: From: &#34;Joe Q. Public&#34; &lt;john.q.public@example.com&gt; portanto, ou se envia uma string neste formato, o que faz com que mais execução deva ser feita para um padrão interno de processamento, ou envia-se uma tupla e não uma lista da seguinte forma (&#34;Joe Q. Public&#34;, &#34;john.q.public@exemple.com&#34;) então por que com um servidor configurado o remetente estava funcionando? possivelmente ninguem ainda tinha seguindo o formato completo acima apresentado na definição de EMAIL_SEND_USER... fazendo isso: EMAIL_SEND_USER = john.q.public@example.com em vez disso: EMAIL_SEND_USER = Joe Q. Public &lt;john.q.public@example.com&gt; ocorre que esta segunda forma (a correta), melhor apresenta o email ao usuário, colocando em sua caixa de entrada de email um nome significativo e não o username. * faz teste de conexão de email apenas no início do sapl
6 years ago
result = False
finally:
connection.close()
settings.EMAIL_RUNNING = result
return settings.EMAIL_RUNNING
def google_recaptcha_configured():
from sapl.base.models import AppConfig
return not AppConfig.attr('google_recaptcha_site_key') == ''
def sapl_as_sapn():
from sapl.base.models import AppConfig
return AppConfig.attr('sapl_as_sapn')
def timing(f):
@wraps(f)
def wrap(*args, **kw):
logger = logging.getLogger(__name__)
ts = time()
result = f(*args, **kw)
te = time()
logger.info('funcao:%r args:[%r, %r] took: %2.4f sec' %
(f.__name__, args, kw, te - ts))
return result
return wrap
@timing
def lista_anexados(principal):
from sapl.materia.models import MateriaLegislativa
from sapl.materia.models import Anexada
from sapl.protocoloadm.models import Anexado
if isinstance(principal, MateriaLegislativa):
anexados = {a.materia_anexada for a in Anexada.objects.select_related(
'materia_anexada').filter(materia_principal=principal)}
else:
anexados = {a.documento_anexado for a in Anexado.objects.select_related(
'documento_anexado').filter(documento_principal=principal)}
anexados_total = set()
while anexados:
if isinstance(principal, MateriaLegislativa):
novos_anexados = {a.materia_anexada for a in
Anexada.objects.filter(
materia_principal__in=anexados)
if a.materia_anexada not in anexados_total}
else:
novos_anexados = {a.documento_anexado for a in
Anexado.objects.filter(
documento_principal__in=anexados)
if a.documento_anexado not in anexados_total}
anexados_total.update(anexados)
anexados = novos_anexados
if principal in anexados_total:
anexados_total.remove(principal)
return list(anexados_total)
def from_date_to_datetime_utc(data):
"""
:param data: datetime.date
:return: datetime.timestamp com UTC
"""
import pytz
from datetime import datetime
# from date to datetime
dt_unware = datetime.combine(data, datetime.min.time())
dt_utc = pytz.utc.localize(dt_unware)
return dt_utc
class OverwriteStorage(FileSystemStorage):
'''
Solução derivada do gist: https://gist.github.com/fabiomontefuscolo/1584462
Muda o comportamento padrão do Django e o faz sobrescrever arquivos de
mesmo nome que foram carregados pelo usuário ao invés de renomeá-los.
'''
def get_available_name(self, name, max_length=None):
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
return name
def get_tempfile_dir():
return '/tmp' if platform.system() == 'Darwin' else tempfile.gettempdir()
class GoogleRecapthaMixin:
logger = logging.getLogger(__name__)
def __init__(self, *args, **kwargs):
from sapl.base.models import AppConfig
title_label = kwargs.pop('title_label')
action_label = kwargs.pop('action_label')
row1 = to_row(
[
(Div(
css_class="g-recaptcha float-right", # if not settings.DEBUG else '',
data_sitekey=AppConfig.attr('google_recaptcha_site_key')
), 5),
('email', 7),
]
)
self.helper = FormHelper()
self.helper.layout = SaplFormLayout(
Fieldset(
title_label,
row1
),
actions=form_actions(label=action_label)
)
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
cd = self.cleaned_data
recaptcha = self.data.get('g-recaptcha-response', '')
if not recaptcha:
raise ValidationError(
_('Verificação do reCAPTCHA não efetuada.'))
from sapl.base.models import AppConfig
url = ('https://www.google.com/recaptcha/api/siteverify?'
'secret=%s'
'&response=%s' % (AppConfig.attr('google_recaptcha_secret_key'),
recaptcha))
try:
r = requests.post(url)
if r.ok:
jdata = r.json()
else:
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
except Exception as e:
logging.error(e)
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
if jdata['success']:
return cd
else:
raise ValidationError(
_('Ocorreu um erro na validação do reCAPTCHA.'))
return cd