Browse Source

Relatório de alunos por UF e região. Gertiq #160538

dependabot/pip/requirements/djangorestframework-3.15.2
Sesóstris Vieira 7 months ago
parent
commit
b2715fb359
  1. 12
      sigi/apps/eventos/admin.py
  2. 5
      sigi/apps/eventos/admin_urls.py
  3. 5
      sigi/apps/eventos/forms.py
  4. 69
      sigi/apps/eventos/migrations/0063_participantesevento_and_more.py
  5. 25
      sigi/apps/eventos/migrations/0064_participantes_evento_carga_inicial.py
  6. 58
      sigi/apps/eventos/models.py
  7. 36
      sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/dataset_snippet.html
  8. 35
      sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/report.html
  9. 83
      sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/report_pdf.html
  10. 144
      sigi/apps/eventos/views.py
  11. 9
      sigi/menu_conf.yaml
  12. 1
      sigi/settings.py

12
sigi/apps/eventos/admin.py

@ -3,7 +3,6 @@ import pandas as pd
import time import time
from admin_auto_filters.filters import AutocompleteFilter from admin_auto_filters.filters import AutocompleteFilter
from moodle import Moodle from moodle import Moodle
from typing import Any
from django.db import models from django.db import models
from django.db.models import ( from django.db.models import (
F, F,
@ -23,7 +22,6 @@ from django.db.models.functions import ExtractDay, Cast
from django.conf import settings from django.conf import settings
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin.utils import get_deleted_objects from django.contrib.admin.utils import get_deleted_objects
from django.core.exceptions import ValidationError
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.template import Template, Context from django.template import Template, Context
@ -53,6 +51,7 @@ from sigi.apps.eventos.models import (
Equipe, Equipe,
Convite, Convite,
Anexo, Anexo,
ParticipantesEvento,
) )
from sigi.apps.eventos.forms import EventoAdminForm, SelecionaModeloForm from sigi.apps.eventos.forms import EventoAdminForm, SelecionaModeloForm
from sigi.apps.servidores.models import Servidor from sigi.apps.servidores.models import Servidor
@ -360,6 +359,12 @@ class CronogramaInline(admin.StackedInline):
extra = 0 extra = 0
class ParticipantesEventoInline(admin.TabularInline):
model = ParticipantesEvento
fields = ("uf", "inscritos", "aprovados")
autocomplete_fields = ["uf"]
class ItemSolicitadoInline(admin.StackedInline): class ItemSolicitadoInline(admin.StackedInline):
model = ItemSolicitado model = ItemSolicitado
fields = ( fields = (
@ -993,6 +998,7 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin):
inlines = ( inlines = (
EquipeInline, EquipeInline,
ConviteInline, ConviteInline,
ParticipantesEventoInline,
ModuloInline, ModuloInline,
AnexoInline, AnexoInline,
CronogramaInline, CronogramaInline,
@ -1819,7 +1825,7 @@ class EventoAdmin(AsciifyQParameter, CartExportReportMixin, admin.ModelAdmin):
"startdate": inicio, "startdate": inicio,
"enddate": fim, "enddate": fim,
} }
res = mws.core.course.update_courses([changes]) mws.core.course.update_courses([changes])
except Exception as e: except Exception as e:
erros.append( erros.append(
_( _(

5
sigi/apps/eventos/admin_urls.py

@ -7,6 +7,11 @@ urlpatterns = [
"alocacaoequipe/", views.alocacao_equipe, name="eventos_alocacaoequipe" "alocacaoequipe/", views.alocacao_equipe, name="eventos_alocacaoequipe"
), ),
path("eventosporuf/", views.eventos_por_uf, name="eventos_eventosporuf"), path("eventosporuf/", views.eventos_por_uf, name="eventos_eventosporuf"),
path(
"alunosporuf/",
views.AlunosPorUfReportView.as_view(),
name="eventos_alunosporuf",
),
path( path(
"solicitacoesporperiodo/", "solicitacoesporperiodo/",
views.solicitacoes_por_periodo, views.solicitacoes_por_periodo,

5
sigi/apps/eventos/forms.py

@ -1,9 +1,4 @@
from collections.abc import Mapping
from typing import Any
from django import forms from django import forms
from django.core.files.base import File
from django.db.models.base import Model
from django.forms.utils import ErrorList
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from material.admin.widgets import ( from material.admin.widgets import (
MaterialAdminTextareaWidget, MaterialAdminTextareaWidget,

69
sigi/apps/eventos/migrations/0063_participantesevento_and_more.py

@ -0,0 +1,69 @@
# Generated by Django 5.0.4 on 2024-05-28 13:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contatos", "0007_alter_mesorregiao_options"),
("eventos", "0062_create_viw_eventos"),
]
operations = [
migrations.CreateModel(
name="ParticipantesEvento",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"inscritos",
models.PositiveIntegerField(
default=0, verbose_name="total de inscritos"
),
),
(
"aprovados",
models.PositiveIntegerField(
default=0, verbose_name="total de aprovados"
),
),
(
"evento",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="eventos.evento",
verbose_name="evento",
),
),
(
"uf",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contatos.unidadefederativa",
verbose_name="uf",
),
),
],
options={
"verbose_name": "Participante por UF",
"verbose_name_plural": "Participantes por UF",
},
),
migrations.AddConstraint(
model_name="participantesevento",
constraint=models.UniqueConstraint(
models.F("evento"), models.F("uf"), name="unique_evento_uf"
),
),
]

25
sigi/apps/eventos/migrations/0064_participantes_evento_carga_inicial.py

@ -0,0 +1,25 @@
# Generated by Django 5.0.4 on 2024-05-28 13:41
from django.db import migrations
from django.utils import timezone
from sigi.apps.eventos.models import Evento
def carga(apps, schema_editor):
for evento in Evento.objects.exclude(moodle_courseid=None).filter(
data_termino__lt=timezone.localtime()
):
try:
evento.sincroniza_saberes()
print(f"\t{evento.nome} sincronizado.")
except Evento.SaberesSyncException as err:
print(f"\tERRO: {evento.nome}: {err.message}")
class Migration(migrations.Migration):
dependencies = [
("eventos", "0063_participantesevento_and_more"),
]
operations = [migrations.RunPython(carga, migrations.RunPython.noop)]

58
sigi/apps/eventos/models.py

@ -14,9 +14,8 @@ from django.utils import timezone
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from sigi.apps.casas.models import Orgao, Servidor from sigi.apps.casas.models import Orgao, Servidor
from sigi.apps.contatos.models import Municipio from sigi.apps.contatos.models import UnidadeFederativa
from sigi.apps.espacos.models import Reserva from sigi.apps.espacos.models import Reserva
from sigi.apps.servidores.models import Servidor
class TipoEvento(models.Model): class TipoEvento(models.Model):
@ -577,7 +576,25 @@ class Evento(models.Model):
) )
aprovados = 0 aprovados = 0
self.participantesevento_set.update(inscritos=0, aprovados=0)
for participante in participantes: 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
part_uf, created = ParticipantesEvento.objects.get_or_create(
evento=self, uf=uf
)
part_uf.inscritos += 1
try: try:
completion_data = mws.post( completion_data = mws.post(
"core_completion_get_course_completion_status", "core_completion_get_course_completion_status",
@ -599,6 +616,8 @@ class Evento(models.Model):
) )
): ):
aprovados += 1 aprovados += 1
part_uf.aprovados += 1
part_uf.save()
self.inscritos_saberes = len(participantes) self.inscritos_saberes = len(participantes)
self.aprovados_saberes = aprovados self.aprovados_saberes = aprovados
@ -937,6 +956,41 @@ class Modulo(models.Model):
return _(f"{self.nome} ({self.get_tipo_display()})") return _(f"{self.nome} ({self.get_tipo_display()})")
class ParticipantesEvento(models.Model):
evento = models.ForeignKey(
Evento, verbose_name=_("evento"), on_delete=models.CASCADE
)
uf = models.ForeignKey(
UnidadeFederativa,
verbose_name=_("uf"),
on_delete=models.CASCADE,
blank=True,
null=True,
)
inscritos = models.PositiveIntegerField(_("total de inscritos"), default=0)
aprovados = models.PositiveIntegerField(_("total de aprovados"), default=0)
class Meta:
constraints = [
models.UniqueConstraint("evento", "uf", name="unique_evento_uf")
]
verbose_name = _("Participante por UF")
verbose_name_plural = _("Participantes por UF")
def __str__(self):
if self.uf is None:
return _(
f"{self.inscritos} pessoas se inscreveram no "
f"evento {self.evento.nome}, tendo {self.aprovados} "
"pessoas aprovadas, mas não informaram a UF onde residem."
)
return _(
f"{self.inscritos} pessoas de {self.uf.nome} se inscreveram no "
f"evento {self.evento.nome}, tendo {self.aprovados} "
"pessoas aprovadas"
)
class ModeloDeclaracao(models.Model): class ModeloDeclaracao(models.Model):
FORMATO_CHOICES = ( FORMATO_CHOICES = (
("A4 portrait", _("A4 retrato")), ("A4 portrait", _("A4 retrato")),

36
sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/dataset_snippet.html

@ -0,0 +1,36 @@
{% load i18n %}
<div class="row">
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title">{{ data_title }}</span>
<table class="striped">
<thead>
<tr>
<th>{% translate "Evento" %}</th>
{% for label in dataset.columns %}
<th class="right-align">{{ label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row_data in dataset.itertuples %}
<tr {% if forloop.last %}class="total-row"{% endif %}>
{% for value in row_data %}
{% if forloop.first %}
<td class="left-align">{{ value.0 }} - <strong>{{ value.1 }}</strong></td>
{% else %}
<td class="right-align{% if forloop.last %} total-col{% endif %}">
{{ value|default:"" }}
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<blockquote>* {% translate "Alunos que não informaram a UF de residência no Saberes" %}</blockquote>
</div>
</div>
</div>
</div>

35
sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/report.html

@ -0,0 +1,35 @@
{% extends 'utils/report/report.html' %}
{% load i18n %}
{% block extrastyle %}
{{ block.super }}
<style type="text/css">
tr.total-row td, td.total-col {
background: var(--darkened-bg);
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
color: var(--body-quiet-color);
font-size: 0.6875rem;
font-weight: 600;
line-height: normal;
padding: 5px 10px;
text-transform: uppercase;
}
</style>
{% endblock %}
{% block data %}
{% if not inscritos_uf is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=inscritos_uf data_title=_("Total de alunos inscritos por UF") %}
{% endif %}
{% if not aprovados_uf is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=aprovados_uf data_title=_("Total de alunos aprovados por UF") %}
{% endif %}
{% if not inscritos_regiao is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=inscritos_regiao data_title=_("Total de alunos inscritos por região") %}
{% endif %}
{% if not aprovados_regiao is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=aprovados_regiao data_title=_("Total de alunos aprovados por região") %}
{% endif %}
{% endblock data %}

83
sigi/apps/eventos/templates/eventos/report/alunos_por_uf_report_view/report_pdf.html

@ -0,0 +1,83 @@
{% extends 'utils/report/report_pdf.html' %}
{% load i18n %}
{% block extra_style %}
{{ block.super }}
tr.total-row td, td.total-col {
background-color: #007433;
border-top: 1px solid var(--hairline-color);
border-bottom: 1px solid var(--hairline-color);
color: white;
font-weight: 600;
padding: 5px 10px;
text-transform: uppercase;
}
.card-title {
font-weight: 600;
font-size: 1.5em;
margin: 20px 0;
}
table.applied-filter caption {
font-size: 1.2em;
font-weight: 600;
text-transform: uppercase;
margin-left: 4px;
}
table.applied-filter {
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 5px;
}
table.applied-filter tr {
background-color: white !important;
font-weight: 600;
}
table.applied-filter tr th {
text-align: left;
padding-right: 48px;
width: 20%;
background-color: unset !important;
}
table.applied-filter tr td {
width: 80%;
background-color: unset !important;
padding: 0 24px;
border-bottom: 1px solid #e0e0e0;
}
{% endblock %}
{% block main_content %}
<table class="applied-filter">
<caption>{% translate "Filtros aplicados" %}</caption>
{% for field in form %}
<tr>
<th>{{ field.label }}</th>
<td>
{% for choice in field.field.choices %}
{% if choice.0 in field.value %}{{ choice.1 }}, {% endif %}
{% empty %}
{{ field.value|default:"" }}
{% endfor %}
</td>
</tr>
{% endfor %}
</table>
<div style="height: 24px; width: 100%;"></div>
{% if not inscritos_uf is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=inscritos_uf data_title=_("Total de alunos inscritos por UF") %}
<div class="new-page">
{% endif %}
{% if not aprovados_uf is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=aprovados_uf data_title=_("Total de alunos aprovados por UF") %}
<div class="new-page">
{% endif %}
{% if not inscritos_regiao is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=inscritos_regiao data_title=_("Total de alunos inscritos por região") %}
<div class="new-page">
{% endif %}
{% if not aprovados_regiao is None %}
{% include 'eventos/report/alunos_por_uf_report_view/dataset_snippet.html' with dataset=aprovados_regiao data_title=_("Total de alunos aprovados por região") %}
{% endif %}
{% endblock %}

144
sigi/apps/eventos/views.py

@ -10,7 +10,18 @@ from typing import OrderedDict
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Count, Sum, Q, F, OuterRef, Subquery from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import (
Count,
Sum,
Q,
F,
OuterRef,
Subquery,
Case,
When,
Value,
)
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils import timezone
@ -30,6 +41,7 @@ from sigi.apps.eventos.models import (
Equipe, Equipe,
Solicitacao, Solicitacao,
ItemSolicitado, ItemSolicitado,
ParticipantesEvento,
) )
from sigi.apps.eventos.forms import ( from sigi.apps.eventos.forms import (
EventosPorUfForm, EventosPorUfForm,
@ -39,6 +51,136 @@ from sigi.apps.eventos.serializers import (
EventoSerializer, EventoSerializer,
EventoListSerializer, EventoListSerializer,
) )
from sigi.apps.utils.views import ReportListView
class AlunosPorUfReportView(
LoginRequiredMixin, UserPassesTestMixin, ReportListView
):
title = _("Alunos por UF")
empty_message = _("Nenhum evento para os parâmetros solicitados")
filter_form = EventosPorUfForm
list_fields = ["evento__nome", "uf__sigla", "inscritos", "aprovados"]
list_labels = [""]
def test_func(self):
return self.request.user.is_staff
def get_queryset(self):
form = self.get_filter_form_instance()
queryset = ParticipantesEvento.objects.none()
if form.is_valid():
data_inicio = form.cleaned_data.get("data_inicio")
data_fim = form.cleaned_data.get("data_fim")
categorias = form.cleaned_data.get(
"categoria", [c[0] for c in TipoEvento.CATEGORIA_CHOICES]
)
modo = form.cleaned_data.get("virtual", ["V", "P"])
queryset = ParticipantesEvento.objects.filter(
evento__status=Evento.STATUS_REALIZADO,
evento__data_inicio__gte=data_inicio,
evento__data_termino__lte=data_fim,
evento__tipo_evento__categoria__in=categorias,
)
if len(modo) == 1:
if "V" in modo:
queryset = queryset.filter(evento__virtual=True)
else:
queryset = queryset.filter(evento__virtual=False)
return queryset
def get_dataset(self):
queryset = self.get_queryset()
fieldnames = [
"evento__nome",
"evento__virtual",
"uf__nome",
"uf__sigla",
"uf__regiao",
"inscritos",
"aprovados",
]
queryset = queryset.values(*fieldnames)
return queryset, fieldnames
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
queryset = self.get_queryset()
if queryset:
uf_sigla = Case(
When(uf__sigla=None, then=Value("*")), default="uf__sigla"
)
uf_regiao = Case(
*[
When(uf__regiao=r[0], then=Value(r[1]))
for r in UnidadeFederativa.REGIAO_CHOICES
],
default=Value("*"),
)
modo = Case(
When(evento__virtual=True, then=Value("Virtual")),
default=Value("Presencial"),
)
df = pd.DataFrame(
queryset.order_by("evento", "uf").values(
"evento__nome",
"inscritos",
"aprovados",
uf_sigla=uf_sigla,
modo=modo,
)
)
context["inscritos_uf"] = df.pivot_table(
values="inscritos",
index=["evento__nome", "modo"],
columns="uf_sigla",
aggfunc="sum",
margins=True,
margins_name="Total",
sort=True,
fill_value=0,
).astype(pd.Int64Dtype())
context["aprovados_uf"] = df.pivot_table(
values="aprovados",
index=["evento__nome", "modo"],
columns="uf_sigla",
aggfunc="sum",
margins=True,
margins_name="Total",
sort=True,
fill_value=0,
).astype(pd.Int64Dtype())
df = pd.DataFrame(
queryset.order_by("evento", "uf__regiao").values(
"evento__nome",
"inscritos",
"aprovados",
uf_regiao=uf_regiao,
modo=modo,
)
)
context["inscritos_regiao"] = df.pivot_table(
values="inscritos",
index=["evento__nome", "modo"],
columns="uf_regiao",
aggfunc="sum",
margins=True,
margins_name="Total",
sort=True,
fill_value=0,
).astype(pd.Int64Dtype())
context["aprovados_regiao"] = df.pivot_table(
values="aprovados",
index=["evento__nome", "modo"],
columns="uf_regiao",
aggfunc="sum",
margins=True,
margins_name="Total",
sort=True,
fill_value=0,
).astype(pd.Int64Dtype())
return context
@login_required @login_required

9
sigi/menu_conf.yaml

@ -29,14 +29,17 @@ main_menu:
- title: Relatórios - title: Relatórios
icon: print icon: print
children: children:
- title: Erros importação Gescon - title: VALIDAÇÃO - Erros importação Gescon
view_name: convenios-report_erros_gescon view_name: convenios-report_erros_gescon
- title: Órgãos com CNPJ duplicado - title: VALIDAÇÃO - Órgãos com CNPJ duplicado
view_name: casas_cnpj_duplicado view_name: casas_cnpj_duplicado
- title: Órgãos com CNPJ errado - title: VALIDAÇÃO - Órgãos com CNPJ errado
view_name: casas_cnpj_errado view_name: casas_cnpj_errado
separator: [after,]
- title: Eventos por UF - title: Eventos por UF
view_name: eventos_eventosporuf view_name: eventos_eventosporuf
- title: Alunos por UF
view_name: eventos_alunosporuf
- title: Solicitações de eventos por período - title: Solicitações de eventos por período
view_name: eventos_solicitacoesporperiodo view_name: eventos_solicitacoesporperiodo
- title: Calendário de eventos - title: Calendário de eventos

1
sigi/settings.py

@ -298,6 +298,7 @@ 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
) )
MOODLE_UF_CUSTOMFIELD = env("MOODLE_UF_CUSTOMFIELD", str, default="estado")
# Integração com reserva de salas # Integração com reserva de salas

Loading…
Cancel
Save