Browse Source

Alterações dos tíquetes de agosto de 2025

master
Sesóstris Vieira 2 months ago
parent
commit
f99b42e37f
  1. 3
      sigi/apps/casas/models.py
  2. 58
      sigi/apps/eventos/admin.py
  3. 5
      sigi/apps/eventos/jobs/daily/sincroniza_saberes.py
  4. 21
      sigi/apps/eventos/management/commands/carga_participantes.py
  5. 80
      sigi/apps/eventos/migrations/0064_alter_modelodeclaracao_texto_participante.py
  6. 168
      sigi/apps/eventos/migrations/0065_participantes_visitas.py
  7. 27
      sigi/apps/eventos/migrations/0066_viw_eventos_paricipante.py
  8. 153
      sigi/apps/eventos/models.py
  9. 281
      sigi/apps/eventos/saberes.py
  10. 51
      sigi/apps/eventos/templates/admin/eventos/evento/change_form_object_tools.html
  11. 0
      sigi/apps/eventos/templates/admin/eventos/evento/checklist_report.html
  12. 0
      sigi/apps/eventos/templates/admin/eventos/evento/custos_report.html
  13. 0
      sigi/apps/eventos/templates/admin/eventos/evento/gant_report.html
  14. 0
      sigi/apps/eventos/templates/admin/eventos/evento/gant_report_classes.html
  15. 0
      sigi/apps/eventos/templates/admin/eventos/evento/plano_comunicacao.html
  16. 26
      sigi/apps/eventos/templates/admin/eventos/evento/seleciona_modelo.html
  17. 9
      sigi/apps/eventos/templates/eventos/declaracao_pdf.html
  18. 20
      sigi/apps/eventos/templates/eventos/snippets/comitiva.html
  19. 49
      sigi/apps/eventos/templates3/admin/eventos/evento/seleciona_modelo.html
  20. 22
      sigi/apps/utils/mixins.py
  21. 17
      sigi/settings.py

3
sigi/apps/casas/models.py

@ -136,7 +136,8 @@ class Orgao(models.Model):
brasao_largura = models.SmallIntegerField(editable=False, null=True)
brasao_altura = models.SmallIntegerField(editable=False, null=True)
def _mathnames(nome, orgaos):
@classmethod
def _mathnames(cls, nome, orgaos):
for o, nome_canonico in orgaos:
ratio = SequenceMatcher(
None, to_ascii(nome).lower(), nome_canonico

58
sigi/apps/eventos/admin.py

@ -40,6 +40,7 @@ from sigi.apps.eventos.models import (
Cronograma,
ModeloDeclaracao,
Modulo,
Participante,
TipoEvento,
Solicitacao,
AnexoSolicitacao,
@ -56,6 +57,7 @@ from sigi.apps.eventos.views import (
context_custos_eventos,
context_custos_servidor,
)
from sigi.apps.eventos.saberes import SaberesSyncException
from sigi.apps.utils import abreviatura
from sigi.apps.utils.filters import DateRangeFilter
from sigi.apps.utils.mixins import AsciifyQParameter
@ -334,27 +336,60 @@ class ChecklistInline(admin.StackedInline):
class EquipeInline(admin.StackedInline):
model = Equipe
fields = (
("membro", "funcao"),
"assina_oficio",
("qtde_diarias", "valor_diaria"),
("emissao_passagens", "total_passagens"),
"observacoes",
)
autocomplete_fields = ("membro", "funcao")
stacked_cols = "1"
class ConviteInline(admin.StackedInline):
model = Convite
fields = [("casa", "qtde_participantes"), "nomes_participantes"]
autocomplete_fields = ("casa",)
readonly_fields = ["nomes_participantes"]
stacked_cols = "1"
class ModuloInline(admin.StackedInline):
model = Modulo
fields = (
"nome",
"descricao",
"tipo",
("inicio", "termino", "carga_horaria"),
("apresentador", "monitor"),
"qtde_participantes",
)
autocomplete_fields = ("apresentador", "monitor")
stacked_cols = "1"
class AnexoInline(admin.StackedInline):
class AnexoInline(admin.TabularInline):
model = Anexo
exclude = ("data_pub", "convite")
class CronogramaInline(admin.StackedInline):
model = Cronograma
extra = 0
fields = (
("etapa", "nome"),
"descricao",
"duracao",
("data_prevista_inicio", "data_prevista_termino"),
("data_inicio", "data_termino"),
"dependencia",
"responsaveis",
"comunicar_inicio",
"comunicar_termino",
"recursos",
)
extra = 1
stacked_cols = "1"
class ParticipantesEventoInline(admin.TabularInline):
@ -363,6 +398,13 @@ class ParticipantesEventoInline(admin.TabularInline):
autocomplete_fields = ["uf"]
class ParticipanteInline(admin.StackedInline):
model = Participante
fields = ("casa_legislativa", ("cpf", "email"), "nome", "local_trabalho")
autocomplete_fields = ["casa_legislativa"]
stacked_cols = "1"
class ItemSolicitadoInline(admin.StackedInline):
model = ItemSolicitado
fields = (
@ -996,6 +1038,7 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
inlines = (
EquipeInline,
ConviteInline,
ParticipanteInline,
ParticipantesEventoInline,
ModuloInline,
AnexoInline,
@ -1210,10 +1253,10 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
return deleted_objects
def declaracao_report(self, request, object_id):
evento = get_object_or_404(Evento, id=object_id)
if request.method == "POST":
form = SelecionaModeloForm(request.POST)
if form.is_valid():
evento = get_object_or_404(Evento, id=object_id)
modelo = form.cleaned_data["modelo"]
membro = (
evento.equipe_set.filter(assina_oficio=True).first()
@ -1263,7 +1306,12 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
)
context = {
**self.admin_site.each_context(request),
"title": _("Emitir declaração para os participantes da visita"),
"subtitle": str(evento) if evento else None,
"form": form,
"object_id": object_id,
"original": evento,
"evento_id": object_id,
"opts": self.model._meta,
"preserved_filters": self.get_preserved_filters(request),
@ -1429,7 +1477,7 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
)
if not evento.equipe_set.filter(
Q(valor_diaria__gte=0) | Q(total_passagens__gte=0)
Q(valor_diaria__gt=0) | Q(total_passagens__gt=0)
).exists():
self.message_user(
request,
@ -1684,7 +1732,7 @@ class EventoAdmin(AsciifyQParameter, ExportActionMixin, admin.ModelAdmin):
f"{request.user.get_full_name()}"
)
)
except Evento.SaberesSyncException as e:
except SaberesSyncException as e:
self.message_user(
request,
_(f"Erro ao sincronizar dados do Saberes: '{e.message}'"),

5
sigi/apps/eventos/jobs/daily/sincroniza_saberes.py

@ -6,6 +6,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from sigi.apps.utils.management.jobs import JobReportMixin
from sigi.apps.eventos.models import Evento
from sigi.apps.eventos.saberes import SaberesSyncException
class Job(JobReportMixin, DailyJob):
@ -42,11 +43,11 @@ class Job(JobReportMixin, DailyJob):
total_sinc += 1
else:
total_ok += 1
except Evento.SaberesSyncException as err:
except SaberesSyncException as err:
errors.append(
_(
f"Erro ao sincronizar evento {evento.nome} "
f"({evento,id}), com a mensagem '{err.message}'"
f"({evento.id}), com a mensagem '{err.message}'"
)
)
total_erros += 1

21
sigi/apps/eventos/management/commands/carga_participantes.py

@ -1,21 +1,30 @@
from django.core.management.base import BaseCommand
from sigi.apps.eventos.models import Evento
from django.utils import timezone
from sigi.apps.eventos.models import Evento
from sigi.apps.eventos.saberes import SaberesSyncException
class Command(BaseCommand):
help = "Carrega dados de participantes de eventos do Moodle para o SIGI"
def handle(self, *args, **options):
for evento in Evento.objects.exclude(moodle_courseid=None).filter(
eventos = Evento.objects.exclude(moodle_courseid=None).filter(
data_termino__lt=timezone.localtime()
):
)
self.stdout.write(f"Processando {eventos.count()} eventos:")
counter = 0
for evento in eventos:
counter += 1
try:
evento.sincroniza_saberes()
self.stdout.write(
self.style.SUCCESS(f"{evento.nome} sincronizado.")
self.style.SUCCESS(
f"{counter}: {evento.nome} sincronizado."
)
except Evento.SaberesSyncException as err:
)
except SaberesSyncException as err:
self.stdout.write(
self.style.ERROR(f"{evento.nome}: {err.message}")
self.style.ERROR(
f"{counter}: {evento.nome}: {err.message}"
)
)

80
sigi/apps/eventos/migrations/0064_alter_modelodeclaracao_texto_participante.py

@ -0,0 +1,80 @@
# Generated by Django 5.2.1 on 2025-08-29 14:08
import django.db.models.deletion
import tinymce.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("casas", "0027_alter_orgao_email"),
("eventos", "0063_participantesevento_and_more"),
]
operations = [
migrations.AlterField(
model_name="modelodeclaracao",
name="texto",
field=tinymce.models.HTMLField(
help_text="Use as seguintes marcações:<ul><li>{{ casa.nome }} para o nome da Casa Legislativa / órgão</li><li>{{ casa.municipio.uf.sigla }} para a sigla da UF da Casa legislativa</li><li>{{ participante.cpf }} para o CPF do visitante</li><li>{{ participante.nome }} para o nome do visitante</li><li>{{ participante.email }} para o e-mail do visitante</li><li>{{ participante.local_trabalho }} para o cargo / função / local de trabalho do visitante</li><li>{{ data }} para a data de emissão da declaração</li><li>{{ evento.data_inicio }} para a data/hora do início da visita</li><li>{{ evento.data_termino }} para a data/hora do término da visita</li><li>{{ evento.nome }} para o nome do evento</li><li>{{ evento.descricao }} para a descrição do evento</li><li>{% include 'eventos/snippets/comitiva.html' %} para a tabela com toda a comitiva da visita</li></ul>",
verbose_name="Texto da declaração",
),
),
migrations.CreateModel(
name="Participante",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("cpf", models.CharField(max_length=30, verbose_name="CPF")),
(
"nome",
models.CharField(
max_length=100, verbose_name="nome completo"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="e-mail"
),
),
(
"local_trabalho",
models.TextField(
blank=True, verbose_name="local de trabalho / cargo"
),
),
(
"casa_legislativa",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="casas.orgao",
verbose_name="casa legislativa",
),
),
(
"evento",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="eventos.evento",
verbose_name="evento",
),
),
],
options={
"verbose_name": "participante",
"verbose_name_plural": "participantes",
"ordering": ("casa_legislativa", "nome"),
},
),
]

168
sigi/apps/eventos/migrations/0065_participantes_visitas.py

@ -0,0 +1,168 @@
# Generated by Django 5.2.1 on 2025-08-28 01:37
import re
from django.db import migrations
def forward(apps, schema_editor):
# Monta regexps #
cargos = r"|".join(
[
"AUXILIAR LEGISLATIVA",
"Acessor Parlamentar",
"Administrador",
"Agente Administrativo de Eventos",
"Agente Legislativo",
"Analista de Revisão Pessoal",
"Analista de TI",
"Analista jurídico",
"Assessora Especial da Secretaria Legislativa e Presidente da Comissão de Regulamentação de Cargos",
"ASSESSOR JURÍDICO DA PRESIDÊNCIA",
"Assessor Jurídico",
"Assessor Legislativo",
"Assessor Parlamentar",
"Assessor de Comunicação",
"Assessora Jurídica",
"Assessora",
"Assessor",
"Auxiliar Administrativa",
"Chefe de Gabinete da Presidência",
"Chefe de Gabinete",
"Controlador Geral da Câmara",
"Controlador Interno",
"Controlador",
"Coord do Departamento Legislativo",
"Coordenador de Tecnologia da Informação",
"Coordenador do Departamento Legislativo da Câmara",
"Coordenadora",
"Coordenadora da Escola do Parlamento",
"DIRETOR GERAL",
"Deputado",
"Diretor Administrativo",
"Diretor",
"Diretor Geral",
"Diretor Hospital",
"Diretor Legislativo",
"Diretor Tesoureiro",
"Diretor de Compras e Licitações",
"Diretor de Matérias e Protocolo",
"Diretor de TI",
"Diretor de Tecnologia e Informação",
"Diretor do Departamento Legislativo",
"Diretor do Legislativo",
"Diretora Administrativa",
"Diretora",
"Diretora Legislativa",
"Diretora da Escola do Legislativo",
"Diretora geral da Câmara",
"Motorista",
"O Subprocurador Geral",
"Ouvidor",
"Ouvidor Geral",
"PRESIDENTE",
"Prefeito",
"Presidente",
"Presidente Vereador",
"Presidente Vereadora",
"Presidente da Escola do Legislativo",
"Presidente do InGEPE",
"Primeira Secretária",
"Procurador Geral",
"Procurador",
"Procuradora",
"Secretária",
"Secretária Saúde",
"Secretária da Casa Civil",
"Secretária-Geral",
"Secretário Administrativo",
"Secretário",
"Secretário de Administração",
"Secretário de Planejamento",
"Secretário de Saúde",
"Secretário-geral da Câmara",
"Servidor",
"Servidora",
"Tesoureiro",
"Técnica Administrativa",
"Técnico Legislativo da Secretaria Legislativa e Presidente da Comissão Sistêmica de Sustentabilidade Legislativa",
"Técnico Legislativo",
"VEREADOR",
"Verador",
"Vereadora",
"Vice Prefeita",
"Vice Prefeito",
"Vice Presidente",
"Vice-Prefeito",
"a Vereadora",
"o Assessor Jurídico",
"o Presidente",
"o Servidor",
"o Vereador",
]
)
patterns = [
re.compile(p, re.IGNORECASE)
for p in [
r"(?P<nome>.+?)[,?][ ?]inscrit[o|a] no CPF[ ?](?P<cpf>.+?)[,?][ ?](?P<local_trabalho>.+)",
r"(?P<nome>.+?)[ ]*-[ ]*(?P<local_trabalho>.+)",
r"(?P<local_trabalho>.+?)( *):( *)(?P<nome>.+)",
r"(?P<nome>.+?)( *);( *)[cpf?][ *](?P<cpf>.+)",
r"(?P<nome>.+?)( *),( *)(?P<local_trabalho>.+)",
r"(?P<nome>.+?) CPF (?P<cpf>.+)",
r"(?P<nome>.+?)(?P<cpf>[\d.]*[-]*\d+)",
r"(?P<nome>.+?)[ ]*\((?P<local_trabalho>.+)\)",
rf"(?P<local_trabalho>{cargos})[ ]*(?P<nome>.+)",
rf"(?P<nome>.+?)[ ]*(?P<local_trabalho>{cargos})",
]
]
Evento = apps.get_model("eventos", "Evento")
eventos = (
Evento.objects.filter(tipo_evento__categoria="V")
.exclude(convite=None)
.exclude(convite__nomes_participantes="")
).prefetch_related("convite_set")
for evento in eventos:
evento.participante_set.all().delete()
for convite in evento.convite_set.all():
participantes = convite.nomes_participantes.strip().splitlines()
for nome in participantes:
if nome.strip() == "":
continue
for pattern in patterns:
match = pattern.match(nome)
if match is not None:
break
if match is None:
dados = {"nome": nome}
else:
dados = {
k: v.strip() for k, v in match.groupdict().items()
}
if len(dados["nome"]) > 100:
dados["nome"] = dados["nome"][:100]
if "cpf" in dados and len(dados["cpf"]) > 30:
dados["cpf"] = dados["cpf"][:30]
evento.participante_set.create(
casa_legislativa_id=convite.casa_id, **dados
)
def backward(apps, schema_editor):
Evento = apps.get_model("eventos", "Evento")
Participante = apps.get_model("eventos", "Participante")
eventos = (
Evento.objects.filter(tipo_evento__categoria="V")
.exclude(convite=None)
.exclude(convite__nomes_participantes="")
)
Participante.objects.filter(evento__in=eventos).delete()
class Migration(migrations.Migration):
dependencies = [
("eventos", "0064_alter_modelodeclaracao_texto_participante"),
]
operations = [migrations.RunPython(forward, backward)]

27
sigi/apps/eventos/migrations/0066_viw_eventos_paricipante.py

@ -0,0 +1,27 @@
# Generated by Django 5.2.1 on 2025-08-29 13:47
from django.db import migrations
SQL_STMT = """
create view viw_eventos_participante as
select ep.evento_id as id_evento,
ep.casa_legislativa_id as id_casa,
ep.cpf,
ep.nome,
ep.email,
ep.local_trabalho
from eventos_participante ep;
grant select on viw_eventos_participante to sigi_qs;
"""
SQL_REVERSE_STMT = "DROP VIEW viw_eventos_participante;"
class Migration(migrations.Migration):
dependencies = [
("eventos", "0065_participantes_visitas"),
]
operations = [
migrations.RunSQL(sql=SQL_STMT, reverse_sql=SQL_REVERSE_STMT)
]

153
sigi/apps/eventos/models.py

@ -1,6 +1,8 @@
from collections.abc import Iterable
import datetime
import re
import lxml
import lxml.html
from moodle import Moodle
from tinymce.models import HTMLField
from django.conf import settings
@ -16,6 +18,8 @@ from django.utils.translation import gettext as _
from sigi.apps.casas.models import Orgao, Servidor
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.espacos.models import Reserva
from sigi.apps.utils.templatetags.model_fields import verbose_name
from sigi.apps.eventos.saberes import EventoSaberes
class TipoEvento(models.Model):
@ -281,11 +285,6 @@ class AnexoSolicitacao(models.Model):
class Evento(models.Model):
class SaberesSyncException(Exception):
@property
def message(self):
return str(self)
STATUS_PREVISTO = "P"
STATUS_AUTORIZADO = "O"
STATUS_REALIZADO = "R"
@ -546,81 +545,39 @@ class Evento(models.Model):
)
def sincroniza_saberes(self, origem="Cronjob"):
if self.moodle_courseid is None:
raise Evento.SaberesSyncException(
_("Este evento não tem curso associado no Saberes"),
)
api_url = f"{settings.MOODLE_BASE_URL}/webservice/rest/server.php"
mws = Moodle(api_url, settings.MOODLE_API_TOKEN)
try:
inscritos = mws.post(
"core_enrol_get_enrolled_users",
courseid=self.moodle_courseid,
)
except Exception as e:
raise Evento.SaberesSyncException(
_(
"Ocorreu um erro ao acessar o curso no Saberes com "
f"a mensagem {e.message}"
),
)
participantes = list(
filter(
lambda u: any(
r["roleid"] in settings.MOODLE_STUDENT_ROLES
for r in u["roles"]
),
inscritos,
)
)
aprovados = 0
saberes = EventoSaberes(self)
participantes = saberes.get_participantes()
tot_aprovados = len(saberes.get_aprovados())
self.participantesevento_set.update(inscritos=0, aprovados=0)
self.participante_set.all().delete()
saberes.identifica_orgaos()
for participante in participantes:
try:
nome_uf = [
f["value"].lower()
for f in participante["customfields"]
if f["shortname"] == settings.MOODLE_UF_CUSTOMFIELD
][0]
uf = UnidadeFederativa.objects.get(nome__iexact=nome_uf)
except (
IndexError,
UnidadeFederativa.DoesNotExist,
UnidadeFederativa.MultipleObjectsReturned,
):
uf = None
uf = participante["uf"]
part_uf, created = ParticipantesEvento.objects.get_or_create(
evento=self, uf=uf
)
part_uf.inscritos += 1
try:
completion_data = mws.post(
"core_completion_get_course_completion_status",
courseid=self.moodle_courseid,
userid=participante["id"],
)
except Exception:
completion_data = None
if completion_data and (
completion_data["completionstatus"]["completed"]
or any(
filter(
lambda c: c["type"]
== settings.MOODLE_COMPLETE_CRITERIA_TYPE
and c["complete"],
completion_data["completionstatus"]["completions"],
)
)
):
aprovados += 1
if participante["completed"]:
part_uf.aprovados += 1
part_uf.save()
part = Participante(evento=self)
if "orgao" in participante:
part.casa_legislativa = participante["orgao"]
part.cpf = participante["username"]
part.nome = participante["fullname"]
part.email = participante["email"]
local_trabalho = []
if "customfields" in participante:
for cf in participante["customfields"]:
value = lxml.html.fromstring(cf["value"]).text_content()
local_trabalho.append(f"{cf['name']}: {value}")
if "city" in participante:
local_trabalho.append(_(f"Cidade: {participante['city']}"))
part.local_trabalho = "\n".join(local_trabalho)
part.save()
self.inscritos_saberes = len(participantes)
self.aprovados_saberes = aprovados
self.aprovados_saberes = tot_aprovados
self.data_sincronizacao = timezone.localtime()
self.origem_sincronizacao = origem
@ -897,6 +854,36 @@ class Convite(models.Model):
verbose_name = _("Casa convidada")
verbose_name_plural = _("Casas convidadas")
def __str__(self):
return str(self.id)
class Participante(models.Model):
evento = models.ForeignKey(
Evento, verbose_name=_("evento"), on_delete=models.CASCADE
)
casa_legislativa = models.ForeignKey(
Orgao,
verbose_name=_("casa legislativa"),
on_delete=models.SET_NULL,
blank=True,
null=True,
)
cpf = models.CharField(_("CPF"), max_length=30)
nome = models.CharField(_("nome completo"), max_length=100)
email = models.EmailField(_("e-mail"), blank=True)
local_trabalho = models.TextField(
_("local de trabalho / cargo"), blank=True
)
class Meta:
verbose_name = _("participante")
verbose_name_plural = _("participantes")
ordering = ("casa_legislativa", "nome")
def __str__(self):
return self.nome
class Modulo(models.Model):
TIPO_AULA = "A"
@ -1011,17 +998,25 @@ class ModeloDeclaracao(models.Model):
texto = HTMLField(
_("Texto da declaração"),
help_text=_(
"Use as seguintes marcações:<ul><li>{{ casa.nome }} para o"
" nome da Casa Legislativa / órgão</li>"
"Use as seguintes marcações:<ul>"
"<li>{{ casa.nome }} para o nome da Casa Legislativa / órgão</li>"
"<li>{{ casa.municipio.uf.sigla }} para a sigla da UF da "
"Casa legislativa</li><li>{{ nome }} "
"para o nome do visitante</li><li>{{ data }} para a data "
"de emissão da declaração</li><li>{{ evento.data_inicio }}"
" para a data/hora do início da visita</li>"
"Casa legislativa</li>"
"<li>{{ participante.cpf }} para o CPF do visitante</li>"
"<li>{{ participante.nome }} para o nome do visitante</li>"
"<li>{{ participante.email }} para o e-mail do visitante</li>"
"<li>{{ participante.local_trabalho }} para o cargo / função / "
"local de trabalho do visitante</li>"
"<li>{{ data }} para a data de emissão da declaração</li>"
"<li>{{ evento.data_inicio }} para a data/hora do início "
"da visita</li>"
"<li>{{ evento.data_termino }} para a data/hora do "
"término da visita</li><li>{{ evento.nome }} para o nome "
"do evento</li><li>{{ evento.descricao }} para a descrição"
" do evento</li></ul>"
"término da visita</li>"
"<li>{{ evento.nome }} para o nome do evento</li>"
"<li>{{ evento.descricao }} para a descrição do evento</li>"
"<li>{% include 'eventos/snippets/comitiva.html' %} para a tabela com toda"
" a comitiva da visita</li>"
"</ul>"
),
)

281
sigi/apps/eventos/saberes.py

@ -0,0 +1,281 @@
import lxml
from difflib import SequenceMatcher
from moodle import Moodle
from django.db import models
from django.conf import settings
from django.utils.translation import gettext as _
from sigi.apps.utils import to_ascii
from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.casas.models import Orgao
CAR_ESP = {x: " " for x in range(33, 65) if x < 48 or x > 58}
CONECTIVOS = ["a", "e", "o", "da", "de", "do", "na", "no", "em"]
def canonize_full(s):
"""canoniza uma string removendo símbolos, artigos e outros conectivos
Args:
s (str): A string a ser canonizada
Returns:
(str, list): a string canonizada e a lista de palavras
"""
s = to_ascii(s.lower()).strip().translate(CAR_ESP)
palavras = [
p.strip()
for p in s.split(" ")
if p.strip() != "" and p.strip() not in CONECTIVOS
]
s = " ".join(palavras)
return (s, palavras)
def canonize(s):
"""canoniza uma string retornando apenas a string canonizada
Args:
s (str): A string a ser canonizada
Returns:
str: a string canonizada
"""
return canonize_full(s)[0]
class SaberesSyncException(Exception):
@property
def message(self):
return str(self)
class EventoSaberes(Moodle):
_inscritos = None
_participantes = None
_aprovados = None
evento = None
_ufs = None
def __init__(self, evento):
url = f"{settings.MOODLE_BASE_URL}/webservice/rest/server.php"
super().__init__(url, settings.MOODLE_API_TOKEN)
self.evento = evento
self._inscritos = None
self._participantes = None
self._aprovados = None
self._ufs = {
canonize(uf.nome): uf for uf in UnidadeFederativa.objects.all()
}
def get_inscritos(self):
if self.evento.moodle_courseid is None:
raise SaberesSyncException(
_(
f"O evento {self.evento} não tem curso associado no Saberes"
),
)
if self._inscritos is None:
try:
self._inscritos = self.post(
"core_enrol_get_enrolled_users",
courseid=self.evento.moodle_courseid,
)
except Exception as e:
raise SaberesSyncException(
_(
"Ocorreu um erro ao acessar o curso no Saberes com "
f"a mensagem {e.message}"
),
)
for i in self._inscritos:
if "customfields" in i:
i["dictcustomfields"] = {
f["shortname"]: canonize(
lxml.html.fromstring(f["value"]).text_content()
)
for f in i["customfields"]
}
uf_nome = (
i["dictcustomfields"][settings.MOODLE_UF_CUSTOMFIELD]
if settings.MOODLE_UF_CUSTOMFIELD
in i["dictcustomfields"]
else None
)
i["uf"] = (
self._ufs[uf_nome] if uf_nome in self._ufs else None
)
return self._inscritos
def get_participantes(self):
if self._participantes is None:
self._participantes = list(
filter(
lambda u: any(
r["roleid"] in settings.MOODLE_STUDENT_ROLES
for r in u["roles"]
),
self.get_inscritos(),
)
)
return self._participantes
def get_aprovados(self):
if self._aprovados is None:
for participante in self.get_participantes():
try:
participante["completion_data"] = self.post(
"core_completion_get_course_completion_status",
courseid=self.evento.moodle_courseid,
userid=participante["id"],
)
except Exception:
participante["completed"] = False
participante["completion_data"] = None
continue
participante["completed"] = participante["completion_data"][
"completionstatus"
]["completed"] or any(
filter(
lambda c: c["type"]
== settings.MOODLE_COMPLETE_CRITERIA_TYPE
and c["complete"],
participante["completion_data"]["completionstatus"][
"completions"
],
)
)
self._aprovados = list(
filter(lambda p: p["completed"], self.get_participantes())
)
return self._aprovados
def identifica_orgaos(self):
obj_list = (
Orgao.objects.all()
.order_by()
.annotate(uf_sigla=models.F("municipio__uf__sigla"))
)
assembleias = obj_list.filter(tipo__sigla="AL")
orgaos = [(o, canonize(f"{o.nome} {o.uf_sigla}")) for o in obj_list]
siglados = {canonize(o.sigla): o for o in obj_list if o.sigla != ""}
siglados.update({canonize(f"ALE{o.uf_sigla}"): o for o in assembleias})
siglados.update({canonize(f"AL{o.uf_sigla}"): o for o in assembleias})
kcm = ["camara", "municipal", "vereadores"]
kal = ["assembleia", "legislativa", "estado"]
ufs = self._ufs
try:
senado = Orgao.objects.get(nome__iexact="senado federal")
except Exception:
senado = None
def get_names(name, uf, municipio):
municipio = canonize(municipio)
uf_sigla = canonize(uf.sigla) if uf else None
name, palavras = canonize_full(name)
names = [name]
# Acrescenta uma versão com a sigla do estado se já não tiver #
if uf_sigla and uf_sigla not in palavras:
names.insert(0, f"{name} {uf_sigla}") # Coloca como primeiro
# Corrige grafia das palavras-chave para Câmara
matches = {
s: [
p
for p in palavras
if SequenceMatcher(a=s, b=p).ratio() > 0.8
]
for s in kcm
}
for kw in matches:
for s in matches[kw]:
name = name.replace(s, kw)
# Elimina o termo vereadores
if "vereadores" in name:
if "municipal" in name:
name = name.replace("vereadores", "") # Só elimina
else:
name = name.replace(
"vereadores", "municipal"
) # troca por municipal
names.append(canonize(name))
if "camara" in name:
if "municipal" not in name:
name = name.replace("camara", "camara municipal")
names.append(canonize(name))
# Cria versão canonica com o nome do municipio e a UF
if uf_sigla:
names.append(
canonize(f"camara municipal {municipio} {uf_sigla}")
)
# Corrige grafia das palavras-chave para Assembleia
matches = {
s: [
p
for p in palavras
if SequenceMatcher(a=s, b=p).ratio() > 0.8
]
for s in kal
}
for kw in matches:
for s in matches[kw]:
name = name.replace(s, kw)
if "assembleia" in name:
name = name.replace("estado", "") # Elimina o termo "estado"
# Adiciona "legislativa" se necessário
if "legislativa" not in name:
name = name.replace("assembleia", "assembleia legislativa")
names.append(canonize(name))
# Cria versão canonica com o nome e sigla da UF
if uf_sigla:
names.append(
canonize(f"assembleia legislativa {uf} {uf_sigla}")
)
# remove duplicados sem mudar a ordem
names = list(dict.fromkeys(names))
return names
for p in self.get_participantes():
if (
"dictcustomfields" in p
and settings.MOODLE_ORGAO_CUSTOMFIELD in p["dictcustomfields"]
and p["dictcustomfields"][
settings.MOODLE_ORGAO_CUSTOMFIELD
].strip()
!= ""
):
nome_orgao = p["dictcustomfields"][
settings.MOODLE_ORGAO_CUSTOMFIELD
]
municipio = (
p["dictcustomfields"][
settings.MOODLE_MUNICIPIO_CUSTOMFIELD
]
if settings.MOODLE_MUNICIPIO_CUSTOMFIELD
in p["dictcustomfields"]
else p["city"] if "city" in p else ""
)
nomes_possiveis = get_names(nome_orgao, p["uf"], municipio)
for nome in nomes_possiveis:
semelhantes = Orgao.get_semelhantes(nome, orgaos)
if len(semelhantes) > 0:
p["orgao"] = semelhantes[-1][0]
break
if "orgao" not in p:
# Buscar por sigla
nome, palavras = canonize_full(nome_orgao)
for nome in palavras:
if nome in siglados:
p["orgao"] = siglados[nome]
break
# Pode ser servidor do Senado - última chance ;D
if (
"orgao" not in p
and senado is not None
and settings.MOODLE_SERVSENADO_CUSTOMFIELD
in p["dictcustomfields"]
and not p["dictcustomfields"][
settings.MOODLE_SERVSENADO_CUSTOMFIELD
].startswith("nao ")
):
p["orgao"] = senado

51
sigi/apps/eventos/templates/admin/eventos/evento/change_form_object_tools.html

@ -0,0 +1,51 @@
{% extends "admin/change_form_object_tools.html" %}
{% load i18n admin_urls djbs_extras %}
{% block object-tools-items %}
{{ block.super }}
{% if original.equipe_set.exists %}
{% url opts|admin_urlname:'custos' original.pk|admin_urlquote as custos_url %}
<a class="addlink nav-link custos" href="{% add_preserved_filters custos_url %}" aria-labelledby="text-tool-custos" title="{% translate 'Relatório de custos' %}">
{% icon "money" %} <span id="text-tool-custos" class="d-lg-none">{% translate "Relatório de custos" %}</span>
</a>
{% endif %}
{% if original.cronograma_set.exists %}
{% url opts|admin_urlname:'gantreport' original.pk|admin_urlquote as gant_url %}
{% url opts|admin_urlname:'checklistreport' original.pk|admin_urlquote as checklist_url %}
{% url opts|admin_urlname:'comunicacaoreport' original.pk|admin_urlquote as comunicacao_url %}
<a class="addlink nav-link gant" href="{% add_preserved_filters gant_url %}" aria-labelledby="text-tool-gant" title="{% translate 'Gráfico de gant' %}">
{% icon "chart" %} <span id="text-tool-gant" class="d-lg-none">{% translate "Gráfico de gant" %}</span>
</a>
<a class="addlink nav-link checklist" href="{% add_preserved_filters checklist_url %}" aria-labelledby="text-tool-checklist" title="{% translate 'Checklist' %}">
{% icon "checklist" %} <span id="text-tool-checklist" class="d-lg-none">{% translate "Checklist" %}</span>
</a>
<a class="addlink nav-link comunicacao" href="{% add_preserved_filters comunicacao_url %}" aria-labelledby="text-tool-comunicacao" title="{% translate 'Plano de comunicação' %}">
{% icon "speak" %} <span id="text-tool-comunicacao" class="d-lg-none">{% translate "Plano de comunicação" %}</span>
</a>
{% endif %}
{% if original.moodle_courseid is None %}
{% if original.tipo_evento.moodle_template_courseid is not None and evento.tipo_evento.moodle_categoryid is not None %}
{% url opts|admin_urlname:'createcourse' original.pk|admin_urlquote as createcourse_url %}
<a class="addlink nav-link createcourse" href="{% add_preserved_filters createcourse_url %}" aria-labelledby="text-tool-createcourse" title="{% translate 'Criar curso no Saberes' %}">
{% icon "create" %} <span id="text-tool-createcourse" class="d-lg-none">{% translate "Criar curso no Saberes" %}</span>
</a>
{% endif %}
{% endif %}
{% if original.moodle_courseid is not None %}
{% url opts|admin_urlname:'updateparticipantes' original.pk|admin_urlquote as updateparticipantes_url %}
<a class="addlink nav-link updateparticipantes" href="{% add_preserved_filters updateparticipantes_url %}" aria-labelledby="text-tool-updateparticipantes" title="{% translate 'Atualizar lista de participantes (Saberes)' %}">
{% icon "refresh" %} <span id="text-tool-updateparticipantes" class="d-lg-none">{% translate "Atualizar lista de participantes (Saberes)" %}</span>
</a>
{% endif %}
{% if original.tipo_evento.categoria == "V" %}
{% url opts|admin_urlname:'declaracaoreport' original.pk|admin_urlquote as declaracao_url %}
<a class="addlink nav-link declaracao" href="{% add_preserved_filters declaracao_url %}" aria-labelledby="text-tool-declaracao" title="{% translate 'Declaração' %}">
{% icon "pdf" %} <span id="text-tool-declaracao" class="d-lg-none">{% translate "Declaração" %}</span>
</a>
{% endif %}
{% endblock %}

0
sigi/apps/eventos/templates3/admin/eventos/evento/checklist_report.html → sigi/apps/eventos/templates/admin/eventos/evento/checklist_report.html

0
sigi/apps/eventos/templates3/admin/eventos/evento/custos_report.html → sigi/apps/eventos/templates/admin/eventos/evento/custos_report.html

0
sigi/apps/eventos/templates3/admin/eventos/evento/gant_report.html → sigi/apps/eventos/templates/admin/eventos/evento/gant_report.html

0
sigi/apps/eventos/templates3/admin/eventos/evento/gant_report_classes.html → sigi/apps/eventos/templates/admin/eventos/evento/gant_report_classes.html

0
sigi/apps/eventos/templates3/admin/eventos/evento/plano_comunicacao.html → sigi/apps/eventos/templates/admin/eventos/evento/plano_comunicacao.html

26
sigi/apps/eventos/templates/admin/eventos/evento/seleciona_modelo.html

@ -0,0 +1,26 @@
{% extends "admin/change_form.html" %}
{% load i18n static admin_urls djbs_extras %}
{% block content %}
<div id="content-main" class="container">
<div class="card">
<div class="card-body">
<form id="select-form" name="select-form" action="" method="post" novalidate>
{% csrf_token %}
{{ form }}
</form>
</div>
</div>
<div class="submit-row btn-toolbar hstack justify-content-end mx-2 my-5 gap-2">
<button class="default btn btn-outline-success" type="submit" form="select-form" name="submit" value="print">
{% icon "print" %} {% trans "Imprimir" %}
</button>
{% url opts|admin_urlname:'change' evento_id|admin_urlquote as change_url %}
<a href="{% add_preserved_filters change_url %}" class="closelink btn btn-outline-success" title="{% translate 'Close' %}">
{% icon "dismiss" %} {% translate 'Close' %}
</a>
</div>
</div>
{% endblock %}

9
sigi/apps/eventos/templates3/eventos/declaracao_pdf.html → sigi/apps/eventos/templates/eventos/declaracao_pdf.html

@ -17,16 +17,11 @@
{% block page_margin %}3cm {{ pagemargin }}cm 2cm {{ pagemargin }}cm{% endblock page_margin %}
{% block main_content %}
{% for convite in evento.convite_set.all %}
{% with convite.casa as casa %}
{% for nome in convite.nomes_participantes.splitlines %}
{% for participante in evento.participante_set.all %}
{% with casa=participante.casa_legislativa %}
<div class="text-box">
{% block text_body %}{% endblock %}
</div>
{% if not forloop.last %}
<div class="new-page"/>
{% endif %}
{% endfor %}
{% endwith %}
{% if not forloop.last %}
<div class="new-page"/>

20
sigi/apps/eventos/templates/eventos/snippets/comitiva.html

@ -0,0 +1,20 @@
{% load i18n %}
<table>
<tr>
<th>{% trans "CPF" %}</th>
<th>{% trans "Nome" %}</th>
<th>{% trans "Cargo / função / setor" %}</th>
</tr>
{% for membro in evento.participante_set.all %}
{% ifchanged membro.casa_legislativa %}
<tr>
<td colspan="3">{{ membro.casa_legislativa.nome }}</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ membro.cpf }}</td>
<td>{{ membro.nome }}</td>
<td>{{ membro.local_trabalho }}</td>
</tr>
{% endfor %}
</table>

49
sigi/apps/eventos/templates3/admin/eventos/evento/seleciona_modelo.html

@ -1,49 +0,0 @@
{% extends "admin/base_site.html" %}
{% load i18n static admin_urls %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'material/admin/css/submit_line.min.css' %}">
{% endblock %}
{% block breadcrumbs %}{% endblock %}
{% block messages %}
{% if error %}
<ul class="messagelist">
<li class="error">{{ error|capfirst }}</li>
</ul>
{% endif %}
{% endblock messages %}
{% block content %}
<div class="container">
<div class="card">
<div class="card-content">
<span class="card-title">{% trans 'Emitir declaração de comparecimento' %}</span>
<form id="select-form" name="select-form" action="" method="post" novalidate>
{% csrf_token %}
<div class="form-group">
{{ form }}
</div>
</form>
</div>
<div class="card-action">
<div class="submit-row">
<div class="open-actions">
<button class="default waves-effect waves-light btn" type="submit" form="select-form" name="submit" value="print">
<i class="material-icons">picture_as_pdf</i>
{% trans "Imprimir" %}
</button>
{% url opts|admin_urlname:'change' evento_id|admin_urlquote as change_url %}
<a class="default waves-effect waves-light btn" role="button" href="{% add_preserved_filters change_url %}">
<i class="material-icons">undo</i>
{% trans "Voltar" %}
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

22
sigi/apps/utils/mixins.py

@ -1,25 +1,13 @@
from collections import OrderedDict
from django import forms
from django.contrib import admin
from django.contrib.admin import helpers
from django.contrib.admin.options import csrf_protect_m
from django.contrib.admin.utils import pretty_name
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.http import Http404
from django.http.response import HttpResponse, HttpResponseRedirect
from django.core.exceptions import ImproperlyConfigured
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path
from django.urls import path, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.translation import gettext as _, ngettext
from django.utils.translation import gettext as _
from django_weasyprint.views import WeasyTemplateResponse
from import_export import resources
from import_export.admin import ImportMixin, ExportMixin
from import_export.fields import Field
from import_export.forms import ExportForm
from import_export.signals import post_export
from sigi.apps.utils import field_label, to_ascii
from sigi.apps.utils import to_ascii
class ReturnMixin:

17
sigi/settings.py

@ -259,6 +259,14 @@ DJBSTHEME = {
"FIELDSET_STYLE": djbs_constants.STYLE_TAB,
"INLINESET_STYLE": djbs_constants.STYLE_TAB,
"BADGERIZE_FACETS": True,
"ICONS": {
"chart": "bi bi-graph-up",
"checklist": "bi bi-list-check",
"create": "bi bi-node-plus",
"money": "bi bi-coin",
"refresh": "bi bi-arrow-clockwise",
"speak": "bi bi-megaphone",
},
}
# tinyMCE rich text editor settings
@ -299,7 +307,16 @@ MOODLE_STUDENT_ROLES = env("MOODLE_STUDENT_ROLES", eval, default=(5, 9))
MOODLE_COMPLETE_CRITERIA_TYPE = env(
"MOODLE_COMPLETE_CRITERIA_TYPE", int, default=6 # Type Grade
)
MOODLE_ORGAO_CUSTOMFIELD = env(
"MOODLE_ORGAO_CUSTOMFIELD", str, default="nomeorgao"
)
MOODLE_UF_CUSTOMFIELD = env("MOODLE_UF_CUSTOMFIELD", str, default="estado")
MOODLE_MUNICIPIO_CUSTOMFIELD = env(
"MOODLE_MUNICIPIO_CUSTOMFIELD", str, default="Municipio"
)
MOODLE_SERVSENADO_CUSTOMFIELD = env(
"MOODLE_SERVSENADO_CUSTOMFIELD", str, default="servidorsenado"
)
# Integração com reserva de salas

Loading…
Cancel
Save