Browse Source

Adiciona Solicitações de Oficinas

pull/166/head
Sesóstris Vieira 1 year ago
parent
commit
547ca223be
  1. 235
      sigi/apps/eventos/admin.py
  2. 219
      sigi/apps/eventos/migrations/0037_tipoevento_duracao_tipoevento_sigla_solicitacao_and_more.py
  3. 141
      sigi/apps/eventos/migrations/0038_migra_pedidos.py
  4. 138
      sigi/apps/eventos/models.py
  5. 6
      sigi/menu_conf.yaml

235
sigi/apps/eventos/admin.py

@ -1,6 +1,8 @@
import datetime import datetime
import time import time
from typing import Any
from moodle import Moodle from moodle import Moodle
from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.contrib import admin, messages from django.contrib import admin, messages
from django.http import HttpResponse from django.http import HttpResponse
@ -21,6 +23,8 @@ from sigi.apps.eventos.models import (
ModeloDeclaracao, ModeloDeclaracao,
Modulo, Modulo,
TipoEvento, TipoEvento,
Solicitacao,
ItemSolicitado,
Funcao, Funcao,
Evento, Evento,
Equipe, Equipe,
@ -33,6 +37,52 @@ from sigi.apps.utils.filters import EmptyFilter, DateRangeFilter
from sigi.apps.utils.mixins import CartExportMixin, ValueLabeledResource from sigi.apps.utils.mixins import CartExportMixin, ValueLabeledResource
class SolicitacaoStatusFilter(admin.SimpleListFilter):
title = _("status")
parameter_name = "status"
def lookups(self, request, model_admin):
return (
("aberto", _("Aberto")),
("analise", _("Análise")),
("inconcluso", _("Inconcluso (aberto + análise)")),
("concluido", _("Concluído")),
)
def queryset(self, request, queryset):
if self.value() == "aberto":
return queryset.exclude(
itemsolicitado__status__in=[
ItemSolicitado.STATUS_AUTORIZADO,
ItemSolicitado.STATUS_REJEITADO,
]
).distinct()
elif self.value() == "analise":
return (
queryset.filter(
itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO
)
.filter(
itemsolicitado__status__in=[
ItemSolicitado.STATUS_AUTORIZADO,
ItemSolicitado.STATUS_REJEITADO,
]
)
.distinct()
)
elif self.value() == "inconcluso":
return queryset.exclude(
id__in=Solicitacao.objects.exclude(
itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO
).only("id")
)
elif self.value() == "concluido":
return queryset.exclude(
itemsolicitado__status=ItemSolicitado.STATUS_SOLICITADO
).distinct()
return queryset
class EventoResource(ValueLabeledResource): class EventoResource(ValueLabeledResource):
# categoria_evento = Field(column_name="tipo_evento__categoria") # categoria_evento = Field(column_name="tipo_evento__categoria")
# status = Field(column_name="status") # status = Field(column_name="status")
@ -120,6 +170,21 @@ class CronogramaInline(admin.StackedInline):
extra = 0 extra = 0
class ItemSolicitadoInline(admin.StackedInline):
model = ItemSolicitado
fields = (
"tipo_evento",
"virtual",
"inicio_desejado",
"status",
"justificativa",
"servidor",
"evento",
)
readonly_fields = ("servidor", "evento")
extra = 1
@admin.register(TipoEvento) @admin.register(TipoEvento)
class TipoEventoAdmin(admin.ModelAdmin): class TipoEventoAdmin(admin.ModelAdmin):
list_display = ["nome", "categoria"] list_display = ["nome", "categoria"]
@ -128,6 +193,176 @@ class TipoEventoAdmin(admin.ModelAdmin):
inlines = [ChecklistInline] inlines = [ChecklistInline]
@admin.register(Solicitacao)
class SolicitacaoAdmin(admin.ModelAdmin):
list_display = (
"num_processo",
"casa",
"senador",
"data_pedido",
"get_oficinas",
"get_municipio",
"get_uf",
"get_regiao",
"get_populacao",
"get_oficinas_uf",
"estimativa_casas",
"estimativa_servidores",
"get_status",
)
list_filter = (
"casa__municipio__uf",
"casa__municipio__uf__regiao",
"senador",
"itemsolicitado__tipo_evento",
SolicitacaoStatusFilter,
)
list_select_related = ["casa", "casa__municipio", "casa__municipio__uf"]
search_fields = (
"casa__search_text",
"casa__municipio__search_text",
"casa__municipio__uf__search_text",
"senador",
)
date_hierarchy = "data_pedido"
inlines = (ItemSolicitadoInline,)
autocomplete_fields = ("casa",)
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
if hasattr(request.user, "servidor"):
servidor = request.user.servidor
else:
servidor = None
for item in instances:
if (
item.status == ItemSolicitado.STATUS_SOLICITADO
and item.evento is not None
):
item.evento.status = Evento.STATUS_ACONFIRMAR
self.message_user(
request,
_(
f"Status do evento {item.evento} alterado para "
f"{item.evento.get_status_display()}"
),
messages.INFO,
)
elif item.status == ItemSolicitado.STATUS_AUTORIZADO:
item.servidor = servidor
if item.evento is None:
item.evento = Evento(
tipo_evento=item.tipo_evento,
nome=f"{item.tipo_evento} em {item.solicitacao.casa}",
descricao=f"{item.tipo_evento} em {item.solicitacao.casa}",
virtual=item.virtual,
solicitante=item.solicitacao.senador,
num_processo=item.solicitacao.num_processo,
data_pedido=item.solicitacao.data_pedido,
data_inicio=item.inicio_desejado,
data_termino=item.inicio_desejado
+ datetime.timedelta(days=item.tipo_evento.duracao),
casa_anfitria=item.solicitacao.casa,
municipio=item.solicitacao.casa.municipio,
observacao=f"Autorizado por {servidor} com a justificativa '{item.justificativa}",
status=Evento.STATUS_CONFIRMADO,
contato=item.solicitacao.contato,
telefone=item.solicitacao.telefone_contato,
)
self.message_user(
request,
_(f"Evento {item.evento} criado automaticamente."),
messages.INFO,
)
else:
item.evento.status = Evento.STATUS_CONFIRMADO
self.message_user(
request,
_(
f"Status do evento {item.evento} alterado para "
f"{item.evento.get_status_display()}"
),
messages.INFO,
)
elif ItemSolicitado.STATUS_REJEITADO and item.evento is not None:
item.evento.status = Evento.STATUS_CANCELADO
item.evento.save()
self.message_user(
request,
_(
f"Status do evento {item.evento} alterado para "
f"{item.evento.get_status_display()}"
),
messages.INFO,
)
if item.evento:
item.evento.tipo_evento = item.tipo_evento
item.evento.nome = (
f"{item.tipo_evento} em {item.solicitacao.casa}"
)
item.evento.descricao = (
f"{item.tipo_evento} em {item.solicitacao.casa}"
)
item.evento.virtual = item.virtual
item.evento.solicitante = item.solicitacao.senador
item.evento.num_processo = item.solicitacao.num_processo
item.evento.data_pedido = item.solicitacao.data_pedido
item.evento.data_inicio = item.inicio_desejado
item.evento.data_termino = (
item.inicio_desejado
+ datetime.timedelta(days=item.tipo_evento.duracao)
)
item.evento.casa_anfitria = item.solicitacao.casa
item.evento.municipio = item.solicitacao.casa.municipio
item.evento.observacao = f"Autorizado por {servidor} com a justificativa '{item.justificativa}"
item.evento.contato = item.solicitacao.contato
item.evento.telefone = item.solicitacao.telefone_contato
item.evento.save()
item.save()
return super().save_formset(request, form, formset, change)
@admin.display(description=_("Oficinas solicitadas"))
def get_oficinas(self, obj):
return mark_safe(
"<ul><li>"
+ "</li><li>".join(
[i.tipo_evento.sigla for i in obj.itemsolicitado_set.all()]
)
+ "</li></ul>"
)
@admin.display(
description=_("Município"), ordering="casa__municipio__nome"
)
def get_municipio(self, obj):
return obj.casa.municipio.nome
@admin.display(description=_("UF"), ordering="casa__municipio__uf__nome")
def get_uf(self, obj):
return obj.casa.municipio.uf.nome
@admin.display(
description=_("Região"), ordering="casa__municipio__uf__regiao"
)
def get_regiao(self, obj):
return obj.casa.municipio.uf.get_regiao_display()
@admin.display(
description=_("População"), ordering="casa__municipio__populacao"
)
def get_populacao(self, obj):
return obj.casa.municipio.populacao
@admin.display(description=_("Oficinas atendidas/confirmadas na UF"))
def get_oficinas_uf(self, obj):
return Evento.objects.filter(
status__in=[Evento.STATUS_CONFIRMADO, Evento.STATUS_REALIZADO],
municipio__uf=obj.casa.municipio.uf,
).count()
@admin.register(Funcao) @admin.register(Funcao)
class FuncaoAdmin(admin.ModelAdmin): class FuncaoAdmin(admin.ModelAdmin):
list_display = ( list_display = (

219
sigi/apps/eventos/migrations/0037_tipoevento_duracao_tipoevento_sigla_solicitacao_and_more.py

@ -0,0 +1,219 @@
# Generated by Django 4.2.4 on 2023-08-30 19:21
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("servidores", "0013_servidor_moodle_userid"),
("casas", "0027_alter_orgao_email"),
("eventos", "0036_tipoevento_prefixo_turma_alter_evento_turma"),
]
operations = [
migrations.AddField(
model_name="tipoevento",
name="duracao",
field=models.PositiveIntegerField(
default=1, verbose_name="Duração (dias)"
),
),
migrations.AddField(
model_name="tipoevento",
name="sigla",
field=models.CharField(
blank=True, max_length=20, verbose_name="sigla"
),
),
migrations.CreateModel(
name="Solicitacao",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"senador",
models.CharField(
max_length=100, verbose_name="senador solicitante"
),
),
(
"num_processo",
models.CharField(
blank=True,
help_text="Formato:<em>XXXXX.XXXXXX/XXXX-XX</em>",
max_length=20,
verbose_name="número do processo SIGAD",
),
),
(
"descricao",
models.TextField(verbose_name="descrição da solicitação"),
),
(
"data_pedido",
models.DateField(
help_text="Data em que o pedido do Gabinete chegou à COPERI",
verbose_name="Data do pedido",
),
),
(
"contato",
models.CharField(
max_length=100,
verbose_name="pessoa de contato na Casa",
),
),
(
"email_contato",
models.EmailField(
blank=True,
max_length=254,
verbose_name="e-mail do contato",
),
),
(
"telefone_contato",
models.CharField(
blank=True,
max_length=20,
verbose_name="telefone do contato",
),
),
(
"whatsapp_contato",
models.CharField(
blank=True,
max_length=20,
verbose_name="whatsapp do contato",
),
),
(
"estimativa_casas",
models.PositiveIntegerField(
help_text="estimativa de quantas Casas participarão dos eventos",
verbose_name="estimativa de Casas participantes",
),
),
(
"estimativa_servidores",
models.PositiveIntegerField(
help_text="estimativa de quantos Servidores participarão dos eventos",
verbose_name="estimativa de servidores participantes",
),
),
(
"casa",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="casas.orgao",
verbose_name="casa solicitante",
),
),
],
options={
"verbose_name": "Solicitação de eventos",
"verbose_name_plural": "Solicitações de eventos",
"ordering": ("-data_pedido",),
},
),
migrations.CreateModel(
name="ItemSolicitado",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"virtual",
models.BooleanField(default=False, verbose_name="virtual"),
),
(
"inicio_desejado",
models.DateField(
help_text="Data desejada para o início do evento. Pode ser solicitado pela Casa ou definido pela conveniência do Interlegis. Será usada como data de início do evento, caso seja autorizado.",
verbose_name="início desejado",
),
),
(
"status",
models.CharField(
choices=[
("S", "Solicitado"),
("A", "Autorizado"),
("R", "Rejeitado"),
],
default="S",
verbose_name="status",
),
),
(
"data_analise",
models.DateTimeField(
blank=True,
editable=False,
null=True,
verbose_name="data da autorização/rejeição",
),
),
(
"justificativa",
models.TextField(blank=True, verbose_name="Justificativa"),
),
(
"evento",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="eventos.evento",
),
),
(
"servidor",
models.ForeignKey(
blank=True,
editable=False,
help_text="Servidor que autorizou ou rejeitou a realização do evento",
limit_choices_to={"externo": False},
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="servidores.servidor",
verbose_name="servidor analisador",
),
),
(
"solicitacao",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="eventos.solicitacao",
),
),
(
"tipo_evento",
models.ForeignKey(
limit_choices_to={"casa_solicita": True},
on_delete=django.db.models.deletion.PROTECT,
to="eventos.tipoevento",
),
),
],
options={
"verbose_name": "Evento solicitado",
"verbose_name_plural": "Eventos solicitados",
"ordering": ("status",),
},
),
]

141
sigi/apps/eventos/migrations/0038_migra_pedidos.py

@ -0,0 +1,141 @@
# Generated by Django 4.2.4 on 2023-08-11 20:36
import re
from functools import reduce
from django.db import migrations
from django.utils import timezone
from sigi.apps.utils import to_ascii
def forwards(apps, schema_editor):
TipoEvento = apps.get_model("eventos", "TipoEvento")
Evento = apps.get_model("eventos", "Evento")
Solicitacao = apps.get_model("eventos", "Solicitacao")
ItemSolicitado = apps.get_model("eventos", "ItemSolicitado")
conjuncoes = [
"oficina",
"a",
"e",
"o",
"da",
"de",
"do",
"na",
"no",
"em",
"ao",
"-",
"com",
]
# Siglas para TipoEvento com as iniciais de cada palavra do nome
for t in TipoEvento.objects.all():
t.sigla = "".join(
[s[:1] for s in t.nome.split(" ") if s.lower() not in conjuncoes]
)
t.save()
tipos = {
t.id: {
s
for s in to_ascii(t.nome.lower()).replace("/", " ").split(" ")
if s not in conjuncoes
}
for t in TipoEvento.objects.exclude(id=35)
}
conjugados = [
to_ascii(re.search("(\w+) e (\w+)", t.nome.lower()).group())
for t in TipoEvento.objects.exclude(id=35)
if " e " in t.nome
]
# Tipo_evento_id 35 foi cadastrado como 'pedidos SIGAD' e todos os eventos
# deste tipo serão convertidos em Solicitacao
for e in Evento.objects.filter(tipo_evento_id=35).exclude(
casa_anfitria=None
):
solicitacao = Solicitacao(
casa=e.casa_anfitria,
senador=e.solicitante,
num_processo=e.num_processo,
descricao=e.descricao,
data_pedido=e.data_pedido or timezone.localdate(),
contato=e.contato,
telefone_contato=e.telefone,
estimativa_casas=0,
estimativa_servidores=0,
)
solicitacao.save()
if e.num_processo:
for se in Evento.objects.filter(
num_processo=e.num_processo
).exclude(id=e.id):
ItemSolicitado(
solicitacao=solicitacao,
tipo_evento=se.tipo_evento,
virtual=se.virtual,
inicio_desejado=se.data_inicio
or se.data_pedido
or timezone.localdate(),
status="A", # autorizado
justificativa="Automática na migração dos dados",
evento=se,
).save()
descricoes = (
reduce(
lambda x, y: x.replace(y, y.replace(" e ", " ")),
conjugados,
to_ascii(
e.descricao.lower().replace("\r\n", "").replace(";", ",")
),
)
.replace(" e ", ", ")
.replace("/", " ")
.replace(".", " ")
.replace("(", " ")
.replace(")", " ")
.split(",")
)
for d in descricoes:
termos = {
s.strip() for s in d.strip().split(" ") if s not in conjuncoes
}
similar = min(
[(id, len(termos.difference(t))) for id, t in tipos.items()],
key=lambda x: x[1],
)
if similar and similar[1] < len(termos):
if not ItemSolicitado.objects.filter(
solicitacao=solicitacao, tipo_evento_id=similar[0]
).exists():
ItemSolicitado(
solicitacao=solicitacao,
tipo_evento_id=similar[0],
virtual=e.virtual,
inicio_desejado=e.data_inicio
or e.data_pedido
or timezone.localdate(),
status="S",
).save()
e.delete()
class Migration(migrations.Migration):
dependencies = [
(
"eventos",
"0037_tipoevento_duracao_tipoevento_sigla_solicitacao_and_more",
),
]
operations = [
migrations.RunPython(
forwards,
)
]

138
sigi/apps/eventos/models.py

@ -1,16 +1,17 @@
import datetime import datetime
import re import re
from tinymce.models import HTMLField
from django.contrib import admin
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum, Count
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from sigi.apps.casas.models import Orgao from sigi.apps.casas.models import Orgao, Servidor
from sigi.apps.contatos.models import Municipio from sigi.apps.contatos.models import Municipio
from sigi.apps.servidores.models import Servidor from sigi.apps.servidores.models import Servidor
from django.core.exceptions import ValidationError
from tinymce.models import HTMLField
class TipoEvento(models.Model): class TipoEvento(models.Model):
@ -29,12 +30,14 @@ class TipoEvento(models.Model):
) )
nome = models.CharField(_("Nome"), max_length=100) nome = models.CharField(_("Nome"), max_length=100)
sigla = models.CharField(_("sigla"), max_length=20, blank=True)
categoria = models.CharField( categoria = models.CharField(
_("Categoria"), max_length=1, choices=CATEGORIA_CHOICES _("Categoria"), max_length=1, choices=CATEGORIA_CHOICES
) )
casa_solicita = models.BooleanField( casa_solicita = models.BooleanField(
_("casa pode solicitar"), default=False _("casa pode solicitar"), default=False
) )
duracao = models.PositiveIntegerField(_("Duração (dias)"), default=1)
moodle_template_courseid = models.PositiveBigIntegerField( moodle_template_courseid = models.PositiveBigIntegerField(
_("Curso protótipo"), _("Curso protótipo"),
blank=True, blank=True,
@ -66,6 +69,133 @@ class TipoEvento(models.Model):
return self.nome return self.nome
class Solicitacao(models.Model):
casa = models.ForeignKey(
Orgao, verbose_name=_("casa solicitante"), on_delete=models.PROTECT
)
senador = models.CharField(_("senador solicitante"), max_length=100)
num_processo = models.CharField(
_("número do processo SIGAD"),
max_length=20,
blank=True,
help_text=_("Formato:<em>XXXXX.XXXXXX/XXXX-XX</em>"),
)
descricao = models.TextField(_("descrição da solicitação"))
data_pedido = models.DateField(
_("Data do pedido"),
help_text=_("Data em que o pedido do Gabinete chegou à COPERI"),
)
contato = models.CharField(_("pessoa de contato na Casa"), max_length=100)
email_contato = models.EmailField(_("e-mail do contato"), blank=True)
telefone_contato = models.CharField(
_("telefone do contato"), max_length=20, blank=True
)
whatsapp_contato = models.CharField(
_("whatsapp do contato"), max_length=20, blank=True
)
estimativa_casas = models.PositiveIntegerField(
_("estimativa de Casas participantes"),
help_text=_("estimativa de quantas Casas participarão dos eventos"),
)
estimativa_servidores = models.PositiveIntegerField(
_("estimativa de servidores participantes"),
help_text=_(
"estimativa de quantos Servidores participarão dos eventos"
),
)
class Meta:
ordering = ("-data_pedido",)
verbose_name = _("Solicitação de eventos")
verbose_name_plural = _("Solicitações de eventos")
def __str__(self):
return _(f"{self.num_processo}: {self.casa} / Senador {self.senador}")
@admin.display(description="Status")
def get_status(self):
# TODO: Definir status do pedido com base no status de seus ítens:
# Aberto: Todos os itens estão em estado Solicitado
# Análise: Parte dos pedidos estão Autorizados/Rejeitados e o restante
# está Solicitado
# Concluído: Nenhum pedido está em estado Solicitado
item_status = set(
self.itemsolicitado_set.distinct("status").values_list(
"status", flat=True
)
)
if {ItemSolicitado.STATUS_SOLICITADO} == item_status:
return _("Aberto")
elif ItemSolicitado.STATUS_SOLICITADO in item_status and (
ItemSolicitado.STATUS_AUTORIZADO in item_status
or ItemSolicitado.STATUS_REJEITADO in item_status
):
return _("Análise")
else:
return _("Concluído")
class ItemSolicitado(models.Model):
STATUS_SOLICITADO = "S"
STATUS_AUTORIZADO = "A"
STATUS_REJEITADO = "R"
STATUS_CHOICES = (
(STATUS_SOLICITADO, _("Solicitado")),
(STATUS_AUTORIZADO, _("Autorizado")),
(STATUS_REJEITADO, _("Rejeitado")),
)
solicitacao = models.ForeignKey(Solicitacao, on_delete=models.CASCADE)
tipo_evento = models.ForeignKey(
TipoEvento,
on_delete=models.PROTECT,
limit_choices_to={"casa_solicita": True},
)
virtual = models.BooleanField(_("virtual"), default=False)
inicio_desejado = models.DateField(
_("início desejado"),
help_text=_(
"Data desejada para o início do evento. Pode ser solicitado pela Casa ou definido pela conveniência do Interlegis. Será usada como data de início do evento, caso seja autorizado."
),
)
status = models.CharField(
verbose_name=_("status"),
choices=STATUS_CHOICES,
default=STATUS_SOLICITADO,
)
data_analise = models.DateTimeField(
_("data da autorização/rejeição"),
blank=True,
null=True,
editable=False,
)
servidor = models.ForeignKey(
Servidor,
verbose_name=_("servidor analisador"),
help_text=_(
"Servidor que autorizou ou rejeitou a realização do evento"
),
on_delete=models.PROTECT,
limit_choices_to={"externo": False},
blank=True,
null=True,
editable=False,
)
justificativa = models.TextField(
verbose_name=_("Justificativa"), blank=True
)
evento = models.ForeignKey(
"Evento", on_delete=models.SET_NULL, null=True, editable=False
)
class Meta:
ordering = ("status",)
verbose_name = _("Evento solicitado")
verbose_name_plural = _("Eventos solicitados")
def __str__(self):
return _(f"{self.tipo_evento}: {self.get_status_display()}")
class Evento(models.Model): class Evento(models.Model):
STATUS_PLANEJAMENTO = "E" STATUS_PLANEJAMENTO = "E"
STATUS_AGUARDANDOSIGAD = "G" STATUS_AGUARDANDOSIGAD = "G"

6
sigi/menu_conf.yaml

@ -74,6 +74,9 @@ main_menu:
- title: Eventos - title: Eventos
icon: school icon: school
children: children:
- title: Solicitações
view_name: admin:eventos_solicitacao_changelist
querystr: status=inconcluso
- title: Todos os eventos - title: Todos os eventos
view_name: admin:eventos_evento_changelist view_name: admin:eventos_evento_changelist
- title: Cursos - title: Cursos
@ -95,9 +98,6 @@ main_menu:
view_name: eventos_calendario view_name: eventos_calendario
- title: Alocação de equipe - title: Alocação de equipe
view_name: eventos_alocacaoequipe view_name: eventos_alocacaoequipe
- title: Solicitações de eventos
view_name: ocorrencias_painel
querystr: tipo_categoria=E&status=1&status=2
- title: Servidores - title: Servidores
icon: account_circle icon: account_circle
children: children:

Loading…
Cancel
Save