Browse Source

Integra SIGI com sistema de reserva de salas do Prodasen

pull/170/head
Sesóstris Vieira 10 months ago
parent
commit
d6059f40dd
  1. 33
      sigi/apps/espacos/admin.py
  2. 0
      sigi/apps/espacos/jobs/__init__.py
  3. 0
      sigi/apps/espacos/jobs/daily/__init__.py
  4. 0
      sigi/apps/espacos/jobs/hourly/__init__.py
  5. 605
      sigi/apps/espacos/jobs/hourly/sincroniza_reservas.py
  6. 0
      sigi/apps/espacos/jobs/monthly/__init__.py
  7. 0
      sigi/apps/espacos/jobs/weekly/__init__.py
  8. 0
      sigi/apps/espacos/jobs/yearly/__init__.py
  9. 129
      sigi/apps/espacos/migrations/0005_alter_reserva_options_and_more.py
  10. 36
      sigi/apps/espacos/migrations/0006_separa_hora_da_data.py
  11. 23
      sigi/apps/espacos/migrations/0007_alter_reserva_data_inicio_alter_reserva_data_termino.py
  12. 34
      sigi/apps/espacos/migrations/0008_carga_inicial_reserva_salas.py
  13. 73
      sigi/apps/espacos/models.py
  14. 52
      sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html
  15. 7
      sigi/apps/espacos/templates/espacos/agenda.html
  16. 3
      sigi/apps/espacos/templates/espacos/agenda_pdf.html
  17. 40
      sigi/apps/espacos/templates/espacos/snippets/agenda_cal.html
  18. 99
      sigi/apps/espacos/views.py
  19. 16
      sigi/apps/eventos/admin.py
  20. 12
      sigi/apps/eventos/forms.py
  21. 30
      sigi/apps/eventos/migrations/0058_evento_hora_inicio_evento_hora_termino.py
  22. 46
      sigi/apps/eventos/migrations/0059_separa_hora_da_data.py
  23. 48
      sigi/apps/eventos/migrations/0060_drop_viw_eventos.py
  24. 27
      sigi/apps/eventos/migrations/0061_alter_evento_data_inicio_alter_evento_data_termino.py
  25. 48
      sigi/apps/eventos/migrations/0062_create_viw_eventos.py
  26. 26
      sigi/apps/eventos/models.py
  27. 16
      sigi/apps/eventos/serializers.py
  28. 38
      sigi/apps/eventos/views.py
  29. 6
      sigi/settings.py

33
sigi/apps/espacos/admin.py

@ -71,26 +71,33 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin):
form = ReservaAdminForm form = ReservaAdminForm
resource_classes = [ReservaResource] resource_classes = [ReservaResource]
list_display = [ list_display = [
"get_status", "status",
"proposito", "proposito",
"get_link_sigad", "get_link_sigad",
"get_espaco", "get_espaco",
"inicio", "data_inicio",
"termino", "data_termino",
"hora_inicio",
"hora_termino",
"virtual", "virtual",
"solicitante", "solicitante",
"contato", "contato",
"telefone_contato", "telefone_contato",
] ]
list_display_links = ["get_status", "proposito"] list_display_links = ["status", "proposito"]
list_filter = ["status", "espaco", "virtual"] list_filter = [
"espaco",
"virtual",
"status",
("id_reserva", admin.EmptyFieldListFilter),
]
search_fields = [ search_fields = [
"proposito", "proposito",
"espaco__nome", "espaco__nome",
"espaco__sigla", "espaco__sigla",
"num_processo", "num_processo",
] ]
date_hierarchy = "inicio" date_hierarchy = "data_inicio"
fieldsets = [ fieldsets = [
(None, {"fields": ("status",)}), (None, {"fields": ("status",)}),
( (
@ -111,8 +118,8 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin):
_("Detalhes"), _("Detalhes"),
{ {
"fields": ( "fields": (
"inicio", ("data_inicio", "hora_inicio"),
"termino", ("data_termino", "hora_termino"),
"informacoes", "informacoes",
) )
}, },
@ -121,9 +128,13 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin):
_("Contato"), _("Contato"),
{"fields": ("solicitante", "contato", "telefone_contato")}, {"fields": ("solicitante", "contato", "telefone_contato")},
), ),
(
_("Integração com sistema de reservas"),
{"fields": ("id_reserva", "data_ult_atualizacao")},
),
] ]
autocomplete_fields = ["espaco"] autocomplete_fields = ["espaco"]
readonly_fields = ("evento",) readonly_fields = ("evento", "id_reserva", "data_ult_atualizacao")
inlines = [RecursoSolicitadoInline] inlines = [RecursoSolicitadoInline]
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
@ -142,10 +153,6 @@ class ReservaAdmin(CartExportMixin, admin.ModelAdmin):
return self.get_fields(request) return self.get_fields(request)
return super().get_readonly_fields(request, obj) return super().get_readonly_fields(request, obj)
@admin.display(description=_("Status"), ordering="status", boolean=True)
def get_status(self, obj):
return obj.status == Reserva.STATUS_ATIVO
@admin.display(description=_("Espaço"), ordering="espaco") @admin.display(description=_("Espaço"), ordering="espaco")
def get_espaco(self, obj): def get_espaco(self, obj):
return obj.espaco.sigla return obj.espaco.sigla

0
sigi/apps/espacos/jobs/__init__.py

0
sigi/apps/espacos/jobs/daily/__init__.py

0
sigi/apps/espacos/jobs/hourly/__init__.py

605
sigi/apps/espacos/jobs/hourly/sincroniza_reservas.py

@ -0,0 +1,605 @@
import requests
import datetime
from django.conf import settings
from django.db.models import Q
from django.utils import timezone
from django.utils.formats import localize
from django.utils.translation import gettext as _
from django_extensions.management.jobs import HourlyJob
from sigi.apps.utils.management.jobs import JobReportMixin
from sigi.apps.espacos.models import (
Espaco,
Recurso,
Reserva,
)
DEPARA_SITUACAO = {
"Aguardando análise": Reserva.STATUS_ATIVO,
"Excluída": None, # Será tratado como caso especial
"Reserva aprovada": Reserva.STATUS_ATIVO,
"Reserva cancelada": Reserva.STATUS_CANCELADO,
"Reserva rejeitada": Reserva.STATUS_CANCELADO,
}
class Job(JobReportMixin, HourlyJob):
help = "Sincroniza dados do sistema de reserva de salas"
report_data = []
resumo = []
infos = []
erros = []
@property
def auth_data(self):
return (
settings.RESERVA_SALA_API_USER,
settings.RESERVA_SALA_API_PASSWORD,
)
def do_job(self):
self.resumo = []
self.infos = []
self.erros = []
if (
settings.RESERVA_SALA_BASE_URL is None
or settings.RESERVA_SALA_API_USER is None
or settings.RESERVA_SALA_API_PASSWORD is None
):
# Acesso ao sistema não configurado. Não fazer nada
return
self.carrega_salas()
self.carrega_recursos()
self.carrega_reservas()
def carrega_salas(self):
tit = ["", "\t*Carga de salas*", ""]
self.infos.extend(tit)
self.erros.extend(tit)
self.resumo.extend(tit)
tot_novas = 0
tot_erros = 0
tot_atualizadas = 0
req = requests.get(
settings.RESERVA_SALA_BASE_URL + "salas", auth=self.auth_data
)
if not req.ok:
self.erros.append(
_(
"\t* Erro de autenticação na API do sistema de reserva "
f"de salas, com a mensagem *{req.reason}*"
)
)
return
for sala in req.json():
try:
espaco = Espaco.objects.get(id_sala=sala["id"])
except Espaco.DoesNotExist:
# Criar espaço
espaco = Espaco(
nome=sala["nome"],
sigla=sala["nome"][:20],
descricao=sala["nome"],
local=sala["local"],
capacidade=sala["capacidade"],
id_sala=sala["id"],
)
espaco.save()
self.infos.append(
_(
f"\t* Criado espaço *{espaco.id}* para a "
f"sala *{sala['id']} - {sala['nome']}*"
)
)
tot_novas += 1
continue
except Espaco.MultipleObjectsReturned:
self.erros.append(
_(
"\t* Existe mais de um espaço com o mesmo ID de sala. "
"Isso deve ser corrigido manualmente no SIGI. "
f"id_sala={sala['id']}"
)
)
tot_erros += 1
continue
# verifica se precisa atualizar os dados do espaço
jespaco = {
"id": espaco.id_sala,
"nome": espaco.nome,
"local": espaco.local,
"capacidade": espaco.capacidade,
}
if sorted(sala) != sorted(jespaco):
# Atualizar o espaço
espaco.sala_id = sala["id"]
espaco.nome = sala["nome"]
espaco.local = sala["local"]
espaco.capacidade = sala["capacidade"]
espaco.save()
self.infos.append(
_(
f"\t* Espaço *{espaco.id}* atualizado com novos dados "
f"da sala *{sala['id']}*"
)
)
tot_atualizadas += 1
self.resumo.append(
_(f"\t* Total de salas processadas: {len(req.json())}")
)
self.resumo.append(_(f"\t* Novos espaços criados: {tot_novas}"))
self.resumo.append(_(f"\t* Espaços atualizados: {tot_atualizadas}"))
self.resumo.append(_(f"\t* Erros encontrados nas salas: {tot_erros}"))
def carrega_recursos(self):
tit = ["", "\t*Carga de recursos*", ""]
self.infos.extend(tit)
self.erros.extend(tit)
self.resumo.extend(tit)
tot_novas = 0
tot_erros = 0
tot_atualizadas = 0
req = requests.get(
settings.RESERVA_SALA_BASE_URL + "equipamentos",
auth=self.auth_data,
)
if not req.ok:
self.erros.append(
_(
"\t* Erro na API do sistema de reserva ao ler "
f"equipamentos, com a mensagem *{req.reason}*"
)
)
return
for equipamento in req.json():
if equipamento["status"] == "Não":
# Não importar
continue
try:
recurso = Recurso.objects.get(id_equipamento=equipamento["id"])
except Recurso.DoesNotExist:
recurso = Recurso(
nome=equipamento["nome"],
sigla=equipamento["nome"][:20],
descricao=equipamento["nome"],
id_equipamento=equipamento["id"],
)
recurso.save()
self.infos.append(
f"\t* Recurso *{recurso}* criado a partir do equipamento *{equipamento['id']} - {equipamento['nome']}*"
)
tot_novas += 1
continue
except Recurso.MultipleObjectsReturned:
lista = ", ".join(
[
str(r)
for r in Recurso.objects.filter(
id_equipamento=equipamento["id"]
)
]
)
self.erros.append(
f"\t* O equipamento *{equipamento['id']} - "
f"{equipamento['nome']}* possui os seguintes recursos "
f"com mesmo ID no SIGI: *{lista}*"
)
tot_erros += 1
continue
if equipamento["nome"] != recurso.nome:
recurso.nome = equipamento["nome"]
recurso.save()
self.infos.append(
f"\t* Recurso *{str(recurso)}* atualizado com as alterações "
f"do equipamento *{equipamento['id']}*"
)
tot_atualizadas += 1
self.resumo.append(
_(f"\t* Total de equipamentos processados: {len(req.json())}")
)
self.resumo.append(_(f"\t* Novos recursos criados: {tot_novas}"))
self.resumo.append(_(f"\t* Recursos atualizados: {tot_atualizadas}"))
self.resumo.append(
_(f"\t* Erros encontrados nos equipamentos: {tot_erros}")
)
def carrega_reservas(
self,
ontem=(timezone.localdate() - timezone.timedelta(days=1)).isoformat(),
):
tit = ["", "\t*Carga de reservas*", ""]
self.infos.extend(tit)
self.erros.extend(tit)
self.resumo.extend(tit)
tot_processadas = 0
tot_novas = 0
tot_excluidas = 0
tot_erros = 0
tot_atualizadas = 0
for espaco in Espaco.objects.exclude(id_sala=None):
req = requests.get(
settings.RESERVA_SALA_BASE_URL
+ f"salas/{espaco.id_sala}/reservas/datas?dataInicio={ontem}",
auth=self.auth_data,
)
if not req.ok:
self.erros.append(
_(
"\t* Erro na API do sistema de reserva ao ler "
f"reservas da sala *{espaco.id_sala}*, com data de "
f"início maior que *{ontem}*, "
f"com a mensagem *{req.reason}*"
)
)
continue
tot_processadas += len(req.json())
for reserva in req.json():
# Hack sujo para campos igual a None
if reserva["horaInicio"] is None:
reserva["horaInicio"] = "00:00:00"
if reserva["horaFim"] is None:
reserva["horaFim"] = "23:59:59"
if reserva["descricao"] is None:
reserva["descricao"] = ""
if reserva["informacao"] is None:
reserva["informacao"] = ""
if reserva["ramal"] is None:
reserva["ramal"] = ""
# Hack sujo para strings muito grandes
reserva["evento"] = reserva["evento"][:100]
reserva["coordenador"] = reserva["coordenador"][:100]
reserva["ramal"] = reserva["ramal"][:100]
data_inicio = datetime.date.fromisoformat(
reserva["dataInicio"]
)
hora_inicio = datetime.time.fromisoformat(
reserva["horaInicio"]
)
data_termino = datetime.date.fromisoformat(reserva["dataFim"])
hora_termino = datetime.time.fromisoformat(reserva["horaFim"])
status = DEPARA_SITUACAO[reserva["situacao"]]
# Tratar reservas excluídas no sistema de reservas
if reserva["situacao"] == "Excluída":
res = Reserva.objects.filter(
id_reserva=reserva["id"]
).delete()[1]
if "espacos.Reserva" in res:
tot_excluidas += res["espacos.Reserva"]
continue
# Tratar os demais casos
try:
reserva_sigi = Reserva.objects.get(
id_reserva=reserva["id"]
)
except Reserva.DoesNotExist:
conflitos = self.verifica_conflito(
espaco,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
)
if conflitos:
# Verificar se existe um conflitante com as mesmas
# datas/horas e coordenador.
reserva_sigi = Reserva.objects.filter(
espaco=espaco,
id_reserva=None,
data_inicio=data_inicio,
data_termino=data_termino,
hora_inicio=hora_inicio,
hora_termino=hora_termino,
contato=reserva["coordenador"],
).first()
if reserva_sigi:
# Se existe, então é a mesma, bastando vincular
reserva_sigi.id_reserva = reserva["id"]
reserva_sigi.save()
# Deixa seguir para atualizar outros campos
else:
# Criar a reserva conflitante
if status != Reserva.STATUS_CANCELADO:
status = Reserva.STATUS_CONFLITO
reserva_sigi = self.cria_reserva(
reserva,
espaco,
status,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
)
if reserva_sigi.status == Reserva.STATUS_CONFLITO:
# Reportar como erro se a reserva é conflitante
lista = ", ".join([str(c) for c in conflitos])
self.erros.append(
f"\t* A reserva *{reserva['id']} - "
f"{reserva['evento']}"
"* do sistema de reservas conflita com "
"a(s) seguinte(s) reserva(s) do SIGI: "
f"*{lista}*. e foi copiada para o SIGI "
"como conflitante."
)
tot_erros += 1
else:
# Reportar como nova se o status for cancelada
self.infos.append(
f"\t* Reserva *{str(reserva_sigi)}* "
"criada no SIGI a partir da reserva "
f"*{reserva['descricao']}* "
"do sistema de reservas"
)
tot_novas += 1
continue
else: # Não há conflitos, basta criar a reserva
reserva_sigi = self.cria_reserva(
reserva,
espaco,
status,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
)
self.infos.append(
f"\t* Reserva *{str(reserva_sigi)}* criada no SIGI"
f" a partir da reserva *{reserva['descricao']}* "
"do sistema de reservas"
)
tot_novas += 1
continue
except Reserva.MultipleObjectsReturned:
# Esse erro nunca poderia acontecer, mas ...
self.erros.append(
_(
"\t* Existe mais de uma reserva no SIGI com o "
"mesmo ID de reserva do sistema de reservas. "
"Isso deve ser corrigido manualmente no SIGI. "
f"id_reserva=*{reserva['id']}*"
)
)
tot_erros += 1
continue
# Reserva foi encontrada no SIGI. Podemos atualizar
atualizou = False
if (
reserva_sigi.data_inicio != data_inicio
or reserva_sigi.data_termino != data_termino
or reserva_sigi.hora_inicio != hora_inicio
or reserva_sigi.hora_termino != hora_termino
):
# Se mudou de data/hora, pode ocorrer conflitos
conflitos = self.verifica_conflito(
espaco,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
reserva_sigi,
)
if not conflitos:
# Nenhum conflito, podemos alterar as datas de boa
reserva_sigi.data_inicio = data_inicio
reserva_sigi.data_termino = data_termino
reserva_sigi.hora_inicio = hora_inicio
reserva_sigi.hora_termino = hora_termino
self.infos.append(
f"\t* *{str(reserva_sigi)}* mudou para o período "
f"de *{localize(data_inicio)} "
f"{localize(hora_inicio)}* a "
f"*{localize(data_termino)} "
f"{localize(hora_termino)}*"
)
atualizou = True
else:
lista = ", ".join([str(c) for c in conflitos])
self.erros.append(
f"\t* A reserva *{reserva['evento']}* no sistema "
"de reservas mudou de data, mas esta mudança não "
"pode ser aplicada no SIGI pois gera conflito "
"com a(s) seguinte(s) outra(s) reserva(s): "
f"*{lista}*"
)
tot_erros += 1
continue
# Verificar outras atualizações
if reserva_sigi.status != status:
reserva_sigi.status = status
self.infos.append(
f"\t* A reserva SIGI *{str(reserva_sigi)}* mudou de "
f"status para *{reserva_sigi.get_status_display()}*"
)
atualizou = True
rr = (
reserva["evento"],
reserva["quantidadeAlunos"],
"\n".join(
[reserva["descricao"], str(reserva["informacao"])]
),
reserva["coordenador"],
reserva["coordenador"],
reserva["ramal"],
)
rs = (
reserva_sigi.proposito,
reserva_sigi.total_participantes,
reserva_sigi.informacoes,
reserva_sigi.solicitante,
reserva_sigi.contato,
reserva_sigi.telefone_contato,
)
if rr != rs:
# Campos descritivos foram alterados
reserva_sigi.proposito = reserva["evento"]
reserva_sigi.total_participantes = reserva[
"quantidadeAlunos"
]
reserva_sigi.informacoes = "\n".join(
[reserva["descricao"], str(reserva["informacao"])]
)
reserva_sigi.solicitante = reserva["coordenador"]
reserva_sigi.contato = reserva["coordenador"]
reserva_sigi.telefone_contato = reserva["ramal"]
reserva_sigi.save()
self.infos.append(
f"\t* A reserva SIGI *{str(reserva_sigi)}* foi "
"atualizada com as alterações da "
f"reserva *{reserva['id']}*"
)
atualizou = True
if self.recursos_solicitados(
reserva_sigi, reserva["equipamentos"]
):
self.infos.append(
"\t* Os recursos solicitados da reserva SIGI "
f"*{str(reserva_sigi)}* foram atualizados"
)
atualizou = True
if atualizou:
tot_atualizadas += 1
self.resumo.append(
_(f"\t* Total de reservas processadas: {tot_processadas}")
)
self.resumo.append(_(f"\t* Novas reservas criadas: {tot_novas}"))
self.resumo.append(_(f"\t* Reservas atualizados: {tot_atualizadas}"))
self.resumo.append(_(f"\t* Reservas excluídas: {tot_excluidas}"))
self.resumo.append(
_(f"\t* Erros encontrados nas reservas: {tot_erros}")
)
def verifica_conflito(
self,
espaco,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
reserva_sigi=None,
):
# Verifica se existe alguma reserva do espaço que conflita com o
# período desejado
reservas_conflitantes = Reserva.objects.exclude(
status=Reserva.STATUS_CANCELADO
).filter(
espaco=espaco,
data_inicio__lte=data_termino,
data_termino__gte=data_inicio,
hora_inicio__lte=hora_termino,
hora_termino__gte=hora_inicio,
)
if reserva_sigi:
reservas_conflitantes = reservas_conflitantes.exclude(
id=reserva_sigi.id
)
if not reservas_conflitantes.exists():
return None
else:
return reservas_conflitantes.all()
def cria_reserva(
self,
reserva,
espaco,
status,
data_inicio,
data_termino,
hora_inicio,
hora_termino,
):
reserva_sigi = Reserva(
status=status,
espaco=espaco,
proposito=reserva["evento"],
virtual=False,
total_participantes=reserva["quantidadeAlunos"],
data_pedido=timezone.localdate(),
data_inicio=data_inicio,
data_termino=data_termino,
hora_inicio=hora_inicio,
hora_termino=hora_termino,
informacoes="\n".join(
[
reserva["descricao"],
str(reserva["informacao"]),
]
),
solicitante=reserva["coordenador"],
contato=reserva["coordenador"],
telefone_contato=reserva["ramal"],
id_reserva=reserva["id"],
data_ult_atualizacao=timezone.localtime(),
)
reserva_sigi.save()
self.recursos_solicitados(reserva_sigi, reserva["equipamentos"])
return reserva_sigi
def recursos_solicitados(self, reserva_sigi, equipamentos_solicitados):
atualizou = False
for equipamento in equipamentos_solicitados:
recurso = Recurso.objects.filter(
id_equipamento=equipamento["id"]
).first()
if not recurso:
# Cria um novo recurso na tabela de recursos
recurso = Recurso(
nome=equipamento["nome"],
sigla=equipamento["nome"][:20],
descricao=equipamento["nome"],
id_equipamento=equipamento["id"],
)
recurso.save()
atualizou = True
continue
if not reserva_sigi.recursosolicitado_set.filter(
recurso=recurso
).exists():
reserva_sigi.recursosolicitado_set.create(
recurso=recurso, quantidade=1
)
atualizou = True
return atualizou
def report(self, start_time, end_time):
self.report_data = [
"",
"",
"RESUMO",
"------",
"",
"",
]
self.report_data.extend(self.resumo)
self.report_data.extend(
[
"",
"",
"ERROS ENCONTRADOS",
"-----------------",
"",
"",
]
)
self.report_data.extend(self.erros)
self.report_data.extend(
[
"",
"",
"MAIS INFORMAÇÕES",
"----------------",
"",
"",
]
)
self.report_data.extend(self.infos)
super().report(start_time, end_time)

0
sigi/apps/espacos/jobs/monthly/__init__.py

0
sigi/apps/espacos/jobs/weekly/__init__.py

0
sigi/apps/espacos/jobs/yearly/__init__.py

129
sigi/apps/espacos/migrations/0005_alter_reserva_options_and_more.py

@ -0,0 +1,129 @@
# Generated by Django 4.2.7 on 2024-03-12 12:36
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("espacos", "0004_alter_reserva_status"),
]
operations = [
migrations.AlterModelOptions(
name="reserva",
options={
"ordering": (
"data_inicio",
"hora_inicio",
"espaco",
"proposito",
),
"verbose_name": "reserva",
"verbose_name_plural": "reservas",
},
),
migrations.RenameField(
model_name="reserva",
old_name="inicio",
new_name="data_inicio",
),
migrations.RenameField(
model_name="reserva",
old_name="termino",
new_name="data_termino",
),
migrations.AddField(
model_name="espaco",
name="capacidade",
field=models.PositiveBigIntegerField(
default=0,
help_text="Número de acentos ou lotação máxima do espaço",
verbose_name="capacidade",
),
),
migrations.AddField(
model_name="espaco",
name="id_sala",
field=models.PositiveIntegerField(
blank=True,
help_text="ID da sala no sistema de reserva de salas do ILB",
null=True,
verbose_name="ID da sala",
),
),
migrations.AddField(
model_name="espaco",
name="reserva_eventos",
field=models.BooleanField(
default=False,
help_text="Pode ser reservado para eventos cadastrados no SIGI",
verbose_name="reserva para eventos",
),
),
migrations.AddField(
model_name="recurso",
name="id_equipamento",
field=models.PositiveBigIntegerField(
blank=True,
help_text="ID do equipamento no sistema de reserva de salas do ILB",
null=True,
unique=True,
verbose_name="ID do equipamento",
),
),
migrations.AddField(
model_name="reserva",
name="data_ult_atualizacao",
field=models.DateTimeField(
blank=True,
editable=False,
null=True,
verbose_name="data da última atualização",
),
),
migrations.AddField(
model_name="reserva",
name="hora_inicio",
field=models.TimeField(
default=django.utils.timezone.now,
verbose_name="hora início",
),
preserve_default=False,
),
migrations.AddField(
model_name="reserva",
name="hora_termino",
field=models.TimeField(
default=django.utils.timezone.now,
verbose_name="hora término",
),
preserve_default=False,
),
migrations.AddField(
model_name="reserva",
name="id_reserva",
field=models.PositiveBigIntegerField(
blank=True,
editable=False,
null=True,
unique=True,
verbose_name="ID da reserva",
),
),
migrations.AlterField(
model_name="reserva",
name="status",
field=models.CharField(
choices=[
("A", "Ativo"),
("C", "Cancelado"),
("O", "Conflito de datas"),
],
default="A",
max_length=1,
verbose_name="status",
),
),
]

36
sigi/apps/espacos/migrations/0006_separa_hora_da_data.py

@ -0,0 +1,36 @@
# Generated by Django 4.2.7 on 2024-03-12 12:39
from datetime import datetime
from django.db import migrations
from django.utils import timezone
def forward(apps, schema_editor):
Reserva = apps.get_model("espacos", "Reserva")
for reserva in Reserva.objects.all():
reserva.hora_inicio = timezone.localtime(reserva.data_inicio).time()
reserva.hora_termino = timezone.localtime(reserva.data_termino).time()
reserva.save()
def backward(apps, schema_editor):
Reserva = apps.get_model("espacos", "Reserva")
for reserva in Reserva.objects.all():
reserva.data_inicio = datetime.combine(
reserva.data_inicio, reserva.hora_inicio
).replace(tzinfo=timezone.get_current_timezone())
reserva.data_termino = datetime.combine(
reserva.data_termino, reserva.hora_termino
).replace(tzinfo=timezone.get_current_timezone())
reserva.save()
class Migration(migrations.Migration):
dependencies = [
("espacos", "0005_alter_reserva_options_and_more"),
]
operations = [
migrations.RunPython(forward, backward),
]

23
sigi/apps/espacos/migrations/0007_alter_reserva_data_inicio_alter_reserva_data_termino.py

@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-03-12 12:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("espacos", "0006_separa_hora_da_data"),
]
operations = [
migrations.AlterField(
model_name="reserva",
name="data_inicio",
field=models.DateField(verbose_name="data início"),
),
migrations.AlterField(
model_name="reserva",
name="data_termino",
field=models.DateField(verbose_name="data término"),
),
]

34
sigi/apps/espacos/migrations/0008_carga_inicial_reserva_salas.py

@ -0,0 +1,34 @@
# Generated by Django 4.2.7 on 2024-03-12 19:05
from datetime import datetime
from django.db import migrations
from django.db import migrations
from sigi.apps.espacos.jobs.hourly.sincroniza_reservas import Job
def forward(apps, schema_editor):
start = datetime.now()
Espaco = apps.get_model("espacos", "Espaco")
DEPARA_SALAS = [(5, 62), (4, 66), (3, 63)]
for espaco_id, id_sala in DEPARA_SALAS:
espaco = Espaco.objects.get(id=espaco_id)
espaco.id_sala = id_sala
espaco.save()
job = Job()
job.carrega_salas()
job.carrega_recursos()
job.carrega_reservas(ontem="2023-04-01")
job.report(start, datetime.now())
class Migration(migrations.Migration):
dependencies = [
(
"espacos",
"0007_alter_reserva_data_inicio_alter_reserva_data_termino",
),
]
operations = [migrations.RunPython(forward, migrations.RunPython.noop)]

73
sigi/apps/espacos/models.py

@ -17,6 +17,22 @@ class Espaco(models.Model):
"Indique o prédio/bloco/sala onde este espaço está localizado." "Indique o prédio/bloco/sala onde este espaço está localizado."
), ),
) )
capacidade = models.PositiveBigIntegerField(
_("capacidade"),
default=0,
help_text=_("Número de acentos ou lotação máxima do espaço"),
)
reserva_eventos = models.BooleanField(
_("reserva para eventos"),
default=False,
help_text=_("Pode ser reservado para eventos cadastrados no SIGI"),
)
id_sala = models.PositiveIntegerField(
_("ID da sala"),
blank=True,
null=True,
help_text=_("ID da sala no sistema de reserva de salas do ILB"),
)
class Meta: class Meta:
verbose_name = _("espaço") verbose_name = _("espaço")
@ -31,6 +47,13 @@ class Recurso(models.Model):
nome = models.CharField(_("nome"), max_length=100) nome = models.CharField(_("nome"), max_length=100)
sigla = models.CharField(_("sigla"), max_length=20) sigla = models.CharField(_("sigla"), max_length=20)
descricao = models.TextField(_("descrição"), blank=True) descricao = models.TextField(_("descrição"), blank=True)
id_equipamento = models.PositiveBigIntegerField(
_("ID do equipamento"),
blank=True,
null=True,
unique=True,
help_text=_("ID do equipamento no sistema de reserva de salas do ILB"),
)
class Meta: class Meta:
verbose_name = _("recurso") verbose_name = _("recurso")
@ -44,10 +67,12 @@ class Recurso(models.Model):
class Reserva(models.Model): class Reserva(models.Model):
STATUS_ATIVO = "A" STATUS_ATIVO = "A"
STATUS_CANCELADO = "C" STATUS_CANCELADO = "C"
STATUS_CONFLITO = "O"
STATUS_CHOICES = ( STATUS_CHOICES = (
(STATUS_ATIVO, _("Ativo")), (STATUS_ATIVO, _("Ativo")),
(STATUS_CANCELADO, _("Cancelado")), (STATUS_CANCELADO, _("Cancelado")),
(STATUS_CONFLITO, _("Conflito de datas")),
) )
status = models.CharField( status = models.CharField(
@ -72,8 +97,10 @@ class Reserva(models.Model):
_("total de participantes"), default=0 _("total de participantes"), default=0
) )
data_pedido = models.DateField(_("data do pedido"), blank=True, null=True) data_pedido = models.DateField(_("data do pedido"), blank=True, null=True)
inicio = models.DateTimeField(_("data/hora de início")) data_inicio = models.DateField(_("data início"))
termino = models.DateTimeField(_("data/hora de término")) data_termino = models.DateField(_("data término"))
hora_inicio = models.TimeField(_("hora início"))
hora_termino = models.TimeField(_("hora término"))
num_processo = models.CharField( num_processo = models.CharField(
_("número do processo SIGAD"), _("número do processo SIGAD"),
max_length=20, max_length=20,
@ -113,28 +140,37 @@ class Reserva(models.Model):
"Indique o telefone/ramal da pessoa responsável pela reserva." "Indique o telefone/ramal da pessoa responsável pela reserva."
), ),
) )
id_reserva = models.PositiveBigIntegerField(
_("ID da reserva"), blank=True, null=True, editable=False, unique=True
)
data_ult_atualizacao = models.DateTimeField(
_("data da última atualização"), blank=True, null=True, editable=False
)
class Meta: class Meta:
verbose_name = _("reserva") verbose_name = _("reserva")
verbose_name_plural = _("reservas") verbose_name_plural = _("reservas")
ordering = ("inicio", "espaco", "proposito") ordering = ("data_inicio", "hora_inicio", "espaco", "proposito")
def __str__(self): def __str__(self):
return _(f"{self.proposito} em {self.espaco.nome}") return _(f"{self.proposito} em {self.espaco.nome}")
def clean(self): def clean(self):
if self.inicio > self.termino: if self.data_inicio > self.data_termino:
raise ValidationError( raise ValidationError(
_("Data de início deve ser anterior à data de término") _("Data de início deve ser anterior à data de término")
) )
reservas_conflitantes = Reserva.objects.exclude(id=self.pk).filter( if self.hora_inicio > self.hora_termino:
espaco=self.espaco, raise ValidationError(
inicio__lte=self.termino, _("Hora de início deve ser anterior à hora de término")
termino__gte=self.inicio,
status=Reserva.STATUS_ATIVO,
) )
reservas_conflitantes = self.get_conflitantes()
if reservas_conflitantes.exists(): if reservas_conflitantes.exists():
if self.status == Reserva.STATUS_CONFLITO:
# Marco as conflitantes com status CONFLITO e deixo seguir
reservas_conflitantes.update(status=Reserva.STATUS_CONFLITO)
elif self.status == Reserva.STATUS_ATIVO:
# Não pode salvar assim. Lança exceção
link_list = ", ".join( link_list = ", ".join(
[ [
f"<a href='" f"<a href='"
@ -146,8 +182,8 @@ class Reserva(models.Model):
raise ValidationError( raise ValidationError(
mark_safe( mark_safe(
_( _(
"Existe(m) reserva(s) que conflita(m) com essas datas: " "Existe(m) reserva(s) que conflita(m) "
f"{ link_list }" f"com essas datas: { link_list }"
) )
) )
) )
@ -157,6 +193,19 @@ class Reserva(models.Model):
self.clean() self.clean()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def get_conflitantes(self):
return (
Reserva.objects.exclude(id=self.pk)
.exclude(status=Reserva.STATUS_CANCELADO)
.filter(
espaco=self.espaco,
data_inicio__lte=self.data_termino,
data_termino__gte=self.data_inicio,
hora_inicio__lte=self.hora_termino,
hora_termino__gte=self.hora_inicio,
)
)
def get_sigad_url(self): def get_sigad_url(self):
m = re.match( m = re.match(
"(?P<orgao>00100|00200)\.(?P<sequencial>\d{6})/(?P<ano>" "(?P<orgao>00100|00200)\.(?P<sequencial>\d{6})/(?P<ano>"

52
sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html

@ -0,0 +1,52 @@
{% extends 'admin/change_form.html' %}
{% load i18n admin_urls %}
{% block extrastyle %}
{{ block.super }}
<style>
.conflito {
color: var(--error-fg) !important;
border-color: var(--error-fg) !important;
}
</style>
{% endblock extrastyle %}
{% block after_related_objects %}
{% if original.get_conflitantes.exists %}
<fieldset class="module">
<h2 class="conflito">{% translate "Reservas conflitantes" %}</h2>
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th scope="row" class="column-status">{% translate "Status" %}</th>
<th scope="row" class="column-proposito">{% translate "Propósito" %}</th>
<th scope="row" class="column-inicio">{% translate "Data/hora início" %}</th>
<th scope="row" class="column-termino">{% translate "Data/hora término" %}</th>
<th scope="row" class="column-solicitante">{% translate "Senador/autoridade solicitante" %}</th>
<th scope="row" class="column-contato">{% translate "Pessoa de contato" %}</th>
<th scope="row" class="column-telefone">{% translate "Telefone de contato" %}</th>
</tr>
</thead>
<tbody>
{% for conf in original.get_conflitantes %}
<tr class="form-row {% cycle 'row1' 'row2' %}">
<td>
<a href="{% url opts|admin_urlname:'change' conf.pk %}">
<i class="material-icons small-icon" aria-hidden="true" title="Modificar">edit</i>
</a>
</td>
<td>{{ conf.get_status_display }}</td>
<td>{{ conf.proposito }}</td>
<td>{{ conf.data_inicio|date:"SHORT_DATE_FORMAT" }} {{ conf.hora_inicio|time }}</td>
<td>{{ conf.data_termino|date:"SHORT_DATE_FORMAT" }} {{ conf.hora_termino|time }}</td>
<td>{{ conf.solicitante }}</td>
<td>{{ conf.contato }}</td>
<td>{{ conf.telefone_contato }}</td>
</tr>
{% endfor %}
</table>
</fieldset>
{% endif %}
{% endblock %}

7
sigi/apps/espacos/templates/espacos/agenda.html

@ -7,9 +7,16 @@
tr.linha-evento { tr.linha-evento {
border-bottom: 1px solid var(--hairline-color); border-bottom: 1px solid var(--hairline-color);
} }
tr.linha-evento.last {
border-bottom: 1px solid var(--main-bg-color);
}
tr td { tr td {
border-left: 1px solid var(--hairline-color); border-left: 1px solid var(--hairline-color);
} }
.col-horario {
width: 5em;
text-aign: right;
}
</style> </style>
{% endblock %} {% endblock %}

3
sigi/apps/espacos/templates/espacos/agenda_pdf.html

@ -39,6 +39,9 @@
tr.linha-evento { tr.linha-evento {
border-bottom: 1px solid #d2d2d2; border-bottom: 1px solid #d2d2d2;
} }
tr.linha-evento.last {
border-bottom: 1px solid #007433;
}
span.numero-dia { span.numero-dia {
font-size: 1em; font-size: 1em;
} }

40
sigi/apps/espacos/templates/espacos/snippets/agenda_cal.html

@ -8,9 +8,14 @@
{% endblocktranslate %} {% endblocktranslate %}
</blockquote> </blockquote>
<table class="calendar-table"> <table class="calendar-table">
<colgroup>
<col class="col-sala"/>
<col class="col-horario"/>
</colgroup>
<thead> <thead>
<tr> <tr>
<th rowspan="2">{% trans "Espaço" %}</th> <th rowspan="2">{% trans "Espaço" %}</th>
<th rowspan="2" class="col-horario"></th>
{% for name in day_names %} {% for name in day_names %}
<th>{{ name }}</th> <th>{{ name }}</th>
{% endfor %} {% endfor %}
@ -22,34 +27,41 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for espaco, reservas in semana.reservas.items %} {% for espaco, linhas in semana.items %}
<tr class="linha-evento"> {% if espaco != "datas" %}
<th>{{ espaco.sigla }}</th> {% for linha in linhas %}
{% for reserva, tupla in reservas %} <tr class="linha-evento{% if forloop.last %} last{% endif %}">
{% for x in ""|ljust:tupla.0|make_list %}<td></td>{% endfor %} {% if forloop.first %}
<td colspan="{{ tupla.1 }}" class="blue lighten-4"> <th rowspan="{{ linhas|length}}">{{ espaco.sigla }}</th>
{% endif %}
<td class="col-horario">{{ linha.hora }}</td>
{% for coluna in linha.colunas %}
{% if "reserva" in coluna %}
<td colspan="{{ coluna.colspan}}" rowspan="{{ coluna.rowspan}}" class="blue lighten-4">
<p> <p>
<strong> <strong>
<a href="{% url "admin:espacos_reserva_change" reserva.id %}"> <a href="{% url "admin:espacos_reserva_change" coluna.reserva.id %}">
{{ reserva.proposito }} {{ coluna.reserva.proposito }}
</a> </a>
</strong> </strong>
</p> </p>
<p>{{ reserva.inicio|interval:reserva.termino }}</p> <p>{{ coluna.reserva.data_inicio|interval:coluna.reserva.data_termino }}</p>
<p> <p>
{% blocktranslate with solicitante=reserva.solicitante %} {% blocktranslate with solicitante=coluna.reserva.solicitante %}
solicitado por {{ solicitante }} solicitado por {{ solicitante }}
{% endblocktranslate %} {% endblocktranslate %}
</p> </p>
</td> </td>
{% if forloop.last %} {% else %}
{% for x in ""|ljust:tupla.2|make_list %}<td></td>{% endfor %} {% if not coluna is None %}
<td></td>
{% endif %}
{% endif %} {% endif %}
{% empty %}
{% for x in "1234567"|make_list %}<td></td>{% endfor %}
{% endfor %} {% endfor %}
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %}
{% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>

99
sigi/apps/espacos/views.py

@ -37,9 +37,11 @@ class Agenda(ReportViewMixin, StaffMemberRequiredMixin, TemplateView):
locale.setlocale(locale.LC_ALL, lang) locale.setlocale(locale.LC_ALL, lang)
for ano, mes in ( for ano, mes in (
Reserva.objects.values_list("inicio__year", "inicio__month") Reserva.objects.values_list(
.order_by("inicio__year", "inicio__month") "data_inicio__year", "data_inicio__month"
.distinct("inicio__year", "inicio__month") )
.order_by("data_inicio__year", "data_inicio__month")
.distinct("data_inicio__year", "data_inicio__month")
): ):
if ano in meses: if ano in meses:
meses[ano][mes] = calendar.month_name[mes] meses[ano][mes] = calendar.month_name[mes]
@ -49,50 +51,75 @@ class Agenda(ReportViewMixin, StaffMemberRequiredMixin, TemplateView):
espacos = list(Espaco.objects.all()) espacos = list(Espaco.objects.all())
semanas = [ semanas = [
{"datas": s, "reservas": {espaco: [] for espaco in espacos}} {"datas": s}
for s in calendar.Calendar().monthdatescalendar( for s in calendar.Calendar().monthdatescalendar(
ano_pesquisa, mes_pesquisa ano_pesquisa, mes_pesquisa
) )
] ]
primeiro_dia = timezone.make_aware( primeiro_dia = semanas[0]["datas"][0]
timezone.datetime(*semanas[0]["datas"][0].timetuple()[:6]) ultimo_dia = semanas[-1]["datas"][-1]
) shift = primeiro_dia.isocalendar().week
ultimo_dia = timezone.make_aware(
timezone.datetime(*semanas[-1]["datas"][-1].timetuple()[:6])
)
for reserva in Reserva.objects.exclude( for reserva in Reserva.objects.filter(
status=Reserva.STATUS_CANCELADO status=Reserva.STATUS_ATIVO
).filter( ).filter(
Q(inicio__range=[primeiro_dia, ultimo_dia]) Q(data_inicio__range=[primeiro_dia, ultimo_dia])
| Q(termino__range=[primeiro_dia, ultimo_dia]) | Q(data_termino__range=[primeiro_dia, ultimo_dia])
): ):
for semana in semanas: for ix in range(
if not ( reserva.data_inicio.isocalendar().week - shift,
(reserva.termino.date() < semana["datas"][0]) reserva.data_termino.isocalendar().week - shift + 1,
or (reserva.inicio.date() > semana["datas"][-1])
): ):
start = max(semana["datas"][0], reserva.inicio.date()) if ix < 0 or ix > len(semanas):
end = min(semana["datas"][-1], reserva.termino.date()) continue
semana["reservas"][reserva.espaco].append( semana = semanas[ix]
[ start = max(semana["datas"][0], reserva.data_inicio).weekday()
reserva, end = min(semana["datas"][-1], reserva.data_termino).weekday()
[ if reserva.espaco not in semana:
start.weekday(), semana[reserva.espaco] = []
end.weekday() - start.weekday() + 1, semana[reserva.espaco].append(
6 - end.weekday(), {
], "reserva": reserva,
] "col_start": start,
"col_end": end,
}
) )
for semana in semanas: for semana in semanas:
for espaco, reservas in semana["reservas"].items(): for espaco, reservas in semana.items():
last_pos = 0 if not isinstance(espaco, Espaco):
continue
horas = sorted(
{
h
for hh in [
(
r["reserva"].hora_inicio,
r["reserva"].hora_termino,
)
for r in reservas
]
for h in hh
}
)
semana[espaco] = [
{"hora": h, "colunas": [False] * 7} for h in horas
]
for reserva in reservas: for reserva in reservas:
if last_pos > 0: row_start = horas.index(reserva["reserva"].hora_inicio)
reserva[1][0] -= last_pos row_end = horas.index(reserva["reserva"].hora_termino)
last_pos += reserva[1][0] + reserva[1][1] col_start = reserva["col_start"]
col_end = reserva["col_end"]
semana[espaco][row_start]["colunas"][col_start] = {
"reserva": reserva["reserva"],
"colspan": col_end - col_start + 1,
"rowspan": row_end - row_start + 1,
}
for rx in range(row_start, row_end + 1):
for cx in range(col_start, col_end + 1):
if rx != row_start or cx != col_start:
semana[espaco][rx]["colunas"][cx] = None
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["mes_pesquisa"] = mes_pesquisa context["mes_pesquisa"] = mes_pesquisa
@ -153,7 +180,9 @@ class UsoEspacos(ReportViewMixin, StaffMemberRequiredMixin, TemplateView):
if agrupar_espacos: if agrupar_espacos:
espacos = ( espacos = (
sel_espacos.filter(q_virtual, reserva__status=Reserva.STATUS_ATIVO) sel_espacos.filter(
q_virtual, reserva__status=Reserva.STATUS_ATIVO
)
.filter( .filter(
Q(reserva__inicio__range=(data_inicio, data_fim)) Q(reserva__inicio__range=(data_inicio, data_fim))
| Q(reserva__termino__range=(data_inicio, data_fim)) | Q(reserva__termino__range=(data_inicio, data_fim))

16
sigi/apps/eventos/admin.py

@ -884,8 +884,8 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin):
"num_processo", "num_processo",
"data_pedido", "data_pedido",
"data_recebido_coperi", "data_recebido_coperi",
"data_inicio", ("data_inicio", "hora_inicio"),
"data_termino", ("data_termino", "hora_termino"),
"carga_horaria", "carga_horaria",
"casa_anfitria", "casa_anfitria",
"espaco", "espaco",
@ -939,6 +939,8 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin):
"get_link_sigad", "get_link_sigad",
"data_inicio", "data_inicio",
"data_termino", "data_termino",
"hora_inicio",
"hora_termino",
"get_municipio", "get_municipio",
"get_uf", "get_uf",
"get_regiao", "get_regiao",
@ -1781,8 +1783,14 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin):
mws = Moodle(api_url, settings.MOODLE_API_TOKEN) mws = Moodle(api_url, settings.MOODLE_API_TOKEN)
fullname = f"{evento.tipo_evento.nome} - {evento.casa_anfitria.municipio.nome}/{evento.casa_anfitria.municipio.uf.sigla} - {evento.tipo_evento.prefixo_turma}{evento.turma}" fullname = f"{evento.tipo_evento.nome} - {evento.casa_anfitria.municipio.nome}/{evento.casa_anfitria.municipio.uf.sigla} - {evento.tipo_evento.prefixo_turma}{evento.turma}"
shortname = f"{abreviatura(evento.tipo_evento.nome)} - {evento.tipo_evento.prefixo_turma}{evento.turma}" shortname = f"{abreviatura(evento.tipo_evento.nome)} - {evento.tipo_evento.prefixo_turma}{evento.turma}"
inicio = int(time.mktime(evento.data_inicio.astimezone().timetuple())) dt_inicio = datetime.datetime.combine(
fim = int(time.mktime(evento.data_termino.astimezone().timetuple())) evento.data_inicio, evento.hora_inicio
).replace(tzinfo=timezone.get_current_timezone())
dt_termino = datetime.datetime.combine(
evento.data_termino, evento.hora_termino
).replace(tzinfo=timezone.get_current_timezone())
inicio = int(time.mktime(dt_inicio.astimezone().timetuple()))
fim = int(time.mktime(dt_termino.astimezone().timetuple()))
erros = [] erros = []
try: # Criar novo curso a partir do template try: # Criar novo curso a partir do template
novo_curso = mws.core.course.duplicate_course( novo_curso = mws.core.course.duplicate_course(

12
sigi/apps/eventos/forms.py

@ -16,7 +16,7 @@ class EventoAdminForm(forms.ModelForm):
espaco = forms.ModelChoiceField( espaco = forms.ModelChoiceField(
label=_("Reservar espaço"), label=_("Reservar espaço"),
required=False, required=False,
queryset=Espaco.objects.all(), queryset=Espaco.objects.filter(reserva_eventos=True),
) )
class Meta: class Meta:
@ -33,6 +33,8 @@ class EventoAdminForm(forms.ModelForm):
"data_recebido_coperi", "data_recebido_coperi",
"data_inicio", "data_inicio",
"data_termino", "data_termino",
"hora_inicio",
"hora_termino",
"carga_horaria", "carga_horaria",
"casa_anfitria", "casa_anfitria",
"espaco", "espaco",
@ -64,6 +66,8 @@ class EventoAdminForm(forms.ModelForm):
cleaned_data = super().clean() cleaned_data = super().clean()
data_inicio = cleaned_data.get("data_inicio") data_inicio = cleaned_data.get("data_inicio")
data_termino = cleaned_data.get("data_termino") data_termino = cleaned_data.get("data_termino")
hora_inicio = cleaned_data.get("hora_inicio")
hora_termino = cleaned_data.get("hora_termino")
publicar = cleaned_data.get("publicar") publicar = cleaned_data.get("publicar")
if data_inicio and data_termino and data_inicio > data_termino: if data_inicio and data_termino and data_inicio > data_termino:
@ -72,6 +76,12 @@ class EventoAdminForm(forms.ModelForm):
code="invalid_period", code="invalid_period",
) )
if hora_inicio and hora_termino and hora_inicio > hora_termino:
raise forms.ValidationError(
_("Hora término deve ser posterior à hora inicio"),
code="invalid_period",
)
if publicar and (data_inicio is None or data_termino is None): if publicar and (data_inicio is None or data_termino is None):
raise forms.ValidationError( raise forms.ValidationError(
_( _(

30
sigi/apps/eventos/migrations/0058_evento_hora_inicio_evento_hora_termino.py

@ -0,0 +1,30 @@
# Generated by Django 4.2.7 on 2024-03-12 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"eventos",
"0057_alter_equipe_qtde_diarias_alter_equipe_valor_diaria",
),
]
operations = [
migrations.AddField(
model_name="evento",
name="hora_inicio",
field=models.TimeField(
blank=True, null=True, verbose_name="hora início"
),
),
migrations.AddField(
model_name="evento",
name="hora_termino",
field=models.TimeField(
blank=True, null=True, verbose_name="hora término"
),
),
]

46
sigi/apps/eventos/migrations/0059_separa_hora_da_data.py

@ -0,0 +1,46 @@
# Generated by Django 4.2.7 on 2024-03-12 14:15
from datetime import datetime
from django.db import migrations
from django.utils import timezone
def forward(apps, schema_editor):
Evento = apps.get_model("eventos", "Evento")
for evento in Evento.objects.all():
if evento.data_inicio is not None:
evento.hora_inicio = timezone.localtime(evento.data_inicio).time()
if evento.data_termino is not None:
evento.hora_termino = timezone.localtime(
evento.data_termino
).time()
evento.save()
def backward(apps, schema_editor):
Evento = apps.get_model("eventos", "Evento")
for evento in Evento.objects.all():
if evento.data_inicio is not None and evento.hora_inicio is not None:
evento.data_inicio = datetime.combine(
evento.data_inicio, evento.hora_inicio
).replace(tzinfo=timezone.get_current_timezone())
elif evento.data_inicio is not None:
evento.data_inicio.replace(tzinfo=timezone.get_current_timezone())
if evento.data_termino is not None and evento.hora_termino is not None:
evento.data_termino = datetime.combine(
evento.data_termino, evento.hora_termino
).replace(tzinfo=timezone.get_current_timezone())
elif evento.data_termino is not None:
evento.data_termino.replace(tzinfo=timezone.get_current_timezone())
evento.save()
class Migration(migrations.Migration):
dependencies = [
("eventos", "0058_evento_hora_inicio_evento_hora_termino"),
]
operations = [
migrations.RunPython(forward, backward),
]

48
sigi/apps/eventos/migrations/0060_drop_viw_eventos.py

@ -0,0 +1,48 @@
# Generated by Django 4.2.7 on 2024-03-12 14:27
from django.db import migrations
SQL_STMT = "DROP VIEW viw_eventos;"
SQL_REVERSE_STMT = """
create view viw_eventos as
select e.id, e.nome, e.descricao, e.solicitante, e.data_inicio, e.data_termino,
e.local, e.publico_alvo,
(case
when e.status = 'P' then 'Previsto'
when e.status = 'O' then 'Autorizado'
when e.status = 'R' then 'Realizado'
when e.status = 'C' then 'Cancelado'
when e.status = 'Q' then 'Sobrestado'
else e.status -- Fallback retorna a sigla do status
end) as status,
e.data_cancelamento, e.motivo_cancelamento,
e.casa_anfitria_id, o.nome as casa_anfitria,
o.municipio_id, m.nome as municipio, uf.sigla as uf_sigla,
uf.nome as uf_nome, t.nome as tipo_evento,
(case
when t.categoria='C' then 'Curso'
when t.categoria='E' then 'Encontro'
when t.categoria='O' then 'Oficina'
when t.categoria='S' then 'Seminário'
when t.categoria='V' then 'Visita'
end) as categoria,
e.virtual, e.total_participantes,
e.data_pedido, e.num_processo, e.observacao, e.turma
from eventos_evento e
inner join casas_orgao o on o.id = e.casa_anfitria_id
inner join contatos_municipio m on m.codigo_ibge = o.municipio_id
inner join contatos_unidadefederativa uf on uf.codigo_ibge = m.uf_id
inner join eventos_tipoevento t on t.id = e.tipo_evento_id;
grant select on viw_eventos to sigi_qs;
"""
class Migration(migrations.Migration):
dependencies = [
("eventos", "0059_separa_hora_da_data"),
]
operations = [
migrations.RunSQL(sql=SQL_STMT, reverse_sql=SQL_REVERSE_STMT)
]

27
sigi/apps/eventos/migrations/0061_alter_evento_data_inicio_alter_evento_data_termino.py

@ -0,0 +1,27 @@
# Generated by Django 4.2.7 on 2024-03-12 14:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("eventos", "0060_drop_viw_eventos"),
]
operations = [
migrations.AlterField(
model_name="evento",
name="data_inicio",
field=models.DateField(
blank=True, null=True, verbose_name="data início"
),
),
migrations.AlterField(
model_name="evento",
name="data_termino",
field=models.DateField(
blank=True, null=True, verbose_name="data término"
),
),
]

48
sigi/apps/eventos/migrations/0062_create_viw_eventos.py

@ -0,0 +1,48 @@
# Generated by Django 4.2.7 on 2024-03-12 14:50
from django.db import migrations
SQL_STMT = """
create view viw_eventos as
select e.id, e.nome, e.descricao, e.solicitante, (e.data_inicio + e.hora_inicio) as data_inicio,
(e.data_termino + e.hora_termino) as data_termino, e.local, e.publico_alvo,
(case
when e.status = 'P' then 'Previsto'
when e.status = 'O' then 'Autorizado'
when e.status = 'R' then 'Realizado'
when e.status = 'C' then 'Cancelado'
when e.status = 'Q' then 'Sobrestado'
else e.status -- Fallback retorna a sigla do status
end) as status,
e.data_cancelamento, e.motivo_cancelamento,
e.casa_anfitria_id, o.nome as casa_anfitria,
o.municipio_id, m.nome as municipio, uf.sigla as uf_sigla,
uf.nome as uf_nome, t.nome as tipo_evento,
(case
when t.categoria='C' then 'Curso'
when t.categoria='E' then 'Encontro'
when t.categoria='O' then 'Oficina'
when t.categoria='S' then 'Seminário'
when t.categoria='V' then 'Visita'
end) as categoria,
e.virtual, e.total_participantes,
e.data_pedido, e.num_processo, e.observacao, e.turma
from eventos_evento e
inner join casas_orgao o on o.id = e.casa_anfitria_id
inner join contatos_municipio m on m.codigo_ibge = o.municipio_id
inner join contatos_unidadefederativa uf on uf.codigo_ibge = m.uf_id
inner join eventos_tipoevento t on t.id = e.tipo_evento_id;
grant select on viw_eventos to sigi_qs;
"""
SQL_REVERSE_STMT = "DROP VIEW viw_eventos;"
class Migration(migrations.Migration):
dependencies = [
("eventos", "0061_alter_evento_data_inicio_alter_evento_data_termino"),
]
operations = [
migrations.RunSQL(sql=SQL_STMT, reverse_sql=SQL_REVERSE_STMT)
]

26
sigi/apps/eventos/models.py

@ -361,12 +361,10 @@ class Evento(models.Model):
verbose_name=_("Solicitação de origem"), verbose_name=_("Solicitação de origem"),
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
data_inicio = models.DateTimeField( data_inicio = models.DateField(_("data início"), null=True, blank=True)
_("Data/hora do Início"), null=True, blank=True data_termino = models.DateField(_("data término"), null=True, blank=True)
) hora_inicio = models.TimeField(_("hora início"), null=True, blank=True)
data_termino = models.DateTimeField( hora_termino = models.TimeField(_("hora término"), null=True, blank=True)
_("Data/hora do Termino"), null=True, blank=True
)
carga_horaria = models.PositiveIntegerField(_("carga horária"), default=0) carga_horaria = models.PositiveIntegerField(_("carga horária"), default=0)
casa_anfitria = models.ForeignKey( casa_anfitria = models.ForeignKey(
Orgao, Orgao,
@ -624,6 +622,14 @@ class Evento(models.Model):
raise ValidationError( raise ValidationError(
_("Data de término deve ser posterior à data de início") _("Data de término deve ser posterior à data de início")
) )
if (
self.hora_inicio
and self.hora_termino
and self.hora_inicio > self.hora_termino
):
raise ValidationError(
_("Hora de término deve ser posterior à hora de início")
)
if self.reserva: if self.reserva:
self.update_reserva() self.update_reserva()
self.reserva.clean() self.reserva.clean()
@ -635,8 +641,10 @@ class Evento(models.Model):
self.reserva.proposito = self.nome self.reserva.proposito = self.nome
self.reserva.virtual = self.virtual self.reserva.virtual = self.virtual
self.reserva.data_pedido = self.data_pedido self.reserva.data_pedido = self.data_pedido
self.reserva.inicio = self.data_inicio self.reserva.data_inicio = self.data_inicio
self.reserva.termino = self.data_termino self.reserva.data_termino = self.data_termino
self.reserva.hora_inicio = self.hora_inicio
self.reserva.hora_termino = self.hora_termino
self.reserva.num_processo = self.num_processo self.reserva.num_processo = self.num_processo
self.reserva.informacoes = self.observacao self.reserva.informacoes = self.observacao
self.reserva.solicitante = self.solicitante self.reserva.solicitante = self.solicitante
@ -778,7 +786,7 @@ class Evento(models.Model):
== 0 == 0
] ]
for item in leafs: for item in leafs:
ajusta_data(item, self.data_termino.date()) ajusta_data(item, self.data_termino)
class Funcao(models.Model): class Funcao(models.Model):

16
sigi/apps/eventos/serializers.py

@ -1,6 +1,8 @@
import base64 import base64
import magic import magic
from datetime import datetime
from rest_framework import serializers from rest_framework import serializers
from django.utils import timezone
from sigi.apps.eventos.models import Evento from sigi.apps.eventos.models import Evento
@ -12,6 +14,8 @@ class EventoSerializer(serializers.ModelSerializer):
casa_uf = serializers.SerializerMethodField("get_casa_uf") casa_uf = serializers.SerializerMethodField("get_casa_uf")
casa_cep = serializers.SerializerMethodField("get_casa_cep") casa_cep = serializers.SerializerMethodField("get_casa_cep")
banner_base64 = serializers.SerializerMethodField("get_banner_base64") banner_base64 = serializers.SerializerMethodField("get_banner_base64")
data_inicio = serializers.SerializerMethodField("get_data_inicio")
data_termino = serializers.SerializerMethodField("get_data_termino")
class Meta: class Meta:
model = Evento model = Evento
@ -78,6 +82,18 @@ class EventoSerializer(serializers.ModelSerializer):
return f"data:{mime_type};base64, {b64str}" return f"data:{mime_type};base64, {b64str}"
return None return None
def get_data_inicio(self, obj):
if obj.data_inicio and obj.hora_inicio:
return datetime.combine(obj.data_inicio, obj.hora_inicio)
else:
return obj.data_inicio
def get_data_termino(self, obj):
if obj.data_termino and obj.hora_termino:
return datetime.combine(obj.data_termino, obj.hora_termino)
else:
return obj.data_termino
class EventoListSerializer(EventoSerializer): class EventoListSerializer(EventoSerializer):
class Meta: class Meta:

38
sigi/apps/eventos/views.py

@ -127,11 +127,11 @@ def calendario(request):
for e in eventos: for e in eventos:
for s in semanas: for s in semanas:
if not ( if not (
(e.data_termino.date() < s["datas"][0]) (e.data_termino < s["datas"][0])
or (e.data_inicio.date() > s["datas"][-1]) or (e.data_inicio > s["datas"][-1])
): ):
start = max(s["datas"][0], e.data_inicio.date()) start = max(s["datas"][0], e.data_inicio)
end = min(s["datas"][-1], e.data_termino.date()) end = min(s["datas"][-1], e.data_termino)
s["eventos"].append( s["eventos"].append(
( (
e, e,
@ -246,24 +246,20 @@ def alocacao_equipe(request):
if mes_pesquisa > 0: if mes_pesquisa > 0:
if semana_pesquisa > 0: if semana_pesquisa > 0:
for dia in dias: for dia in dias:
if ( if evento.data_inicio <= dia <= evento.data_termino:
evento.data_inicio.date()
<= dia
<= evento.data_termino.date()
):
registro[2][dia].append(evento) registro[2][dia].append(evento)
else: else:
for idx, [inicio, fim] in enumerate(semanas): for idx, [inicio, fim] in enumerate(semanas):
if inicio <= evento.data_inicio.date() <= fim: if inicio <= evento.data_inicio <= fim:
registro[2][idx]["dias"] += ( registro[2][idx]["dias"] += (
min(fim, evento.data_termino.date()) min(fim, evento.data_termino)
- evento.data_inicio.date() - evento.data_inicio
).days + 1 ).days + 1
registro[2][idx]["eventos"] += 1 registro[2][idx]["eventos"] += 1
elif inicio <= evento.data_termino.date() <= fim: elif inicio <= evento.data_termino <= fim:
registro[2][idx]["dias"] += ( registro[2][idx]["dias"] += (
min(fim, evento.data_termino.date()) min(fim, evento.data_termino)
- evento.data_inicio.date() - evento.data_inicio
).days + 1 ).days + 1
registro[2][idx]["eventos"] += 1 registro[2][idx]["eventos"] += 1
else: else:
@ -295,8 +291,11 @@ def alocacao_equipe(request):
linhas.append( linhas.append(
[r[1]] [r[1]]
+ [ + [
(
_( _(
ngettext("%(dias)s dia", "%(dias)s dias", d["dias"]) ngettext(
"%(dias)s dia", "%(dias)s dias", d["dias"]
)
+ " em " + " em "
+ ngettext( + ngettext(
"%(eventos)s evento", "%(eventos)s evento",
@ -307,6 +306,7 @@ def alocacao_equipe(request):
% d % d
if d["dias"] > 0 or d["eventos"] > 0 if d["dias"] > 0 or d["eventos"] > 0
else "" else ""
)
for d in r[2] for d in r[2]
] ]
) )
@ -353,9 +353,9 @@ def alocacao_equipe(request):
) )
elif formato == "csv": elif formato == "csv":
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
response[ response["Content-Disposition"] = (
"Content-Disposition" 'attachment; filename="alocacao_equipe_%s.csv"' % (ano_pesquisa,)
] = 'attachment; filename="alocacao_equipe_%s.csv"' % (ano_pesquisa,) )
writer = csv.writer(response) writer = csv.writer(response)
writer.writerow(cabecalho) writer.writerow(cabecalho)
writer.writerows(linhas) writer.writerows(linhas)

6
sigi/settings.py

@ -296,3 +296,9 @@ MOODLE_STUDENT_ROLES = env("MOODLE_STUDENT_ROLES", eval, default=(5, 9))
MOODLE_COMPLETE_CRITERIA_TYPE = env( MOODLE_COMPLETE_CRITERIA_TYPE = env(
"MOODLE_COMPLETE_CRITERIA_TYPE", int, default=6 # Type Grade "MOODLE_COMPLETE_CRITERIA_TYPE", int, default=6 # Type Grade
) )
# Integração com reserva de salas
RESERVA_SALA_BASE_URL = env("RESERVA_SALA_BASE_URL", default=None)
RESERVA_SALA_API_USER = env("RESERVA_SALA_API_USER", default=None)
RESERVA_SALA_API_PASSWORD = env("RESERVA_SALA_API_PASSWORD", default=None)

Loading…
Cancel
Save