Browse Source

Merge pull request #186 from moonshinerd/sigi-4.0

Refatoração dos Relatórios de Eventos para ReportListView
sigi-4.0
Sesostris Vieira 4 weeks ago
committed by GitHub
parent
commit
886c96ecf6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      sigi/apps/eventos/admin_urls.py
  2. 94
      sigi/apps/eventos/templates/eventos/calendario.html
  3. 585
      sigi/apps/eventos/views.py
  4. 0
      sigi/templates/admin/date_range_filter.html

9
sigi/apps/eventos/admin_urls.py

@ -1,12 +1,13 @@
from django.urls import path from django.urls import path
from sigi.apps.eventos import views from sigi.apps.eventos import views
from sigi.apps.eventos.views import EventosPorUfReportView, SolicitacoesPorPeriodoReportView, CalendarioReportView, AlocacaoEquipeReportView
urlpatterns = [ urlpatterns = [
path("calendario/", views.calendario, name="eventos_calendario"), path("calendario/", CalendarioReportView.as_view(), name="eventos_calendario"),
path( path(
"alocacaoequipe/", views.alocacao_equipe, name="eventos_alocacaoequipe" "alocacaoequipe/", AlocacaoEquipeReportView.as_view(), name="eventos_alocacaoequipe"
), ),
path("eventosporuf/", views.eventos_por_uf, name="eventos_eventosporuf"), path("eventosporuf/", EventosPorUfReportView.as_view(), name="eventos_eventosporuf"),
path( path(
"alunosporuf/", "alunosporuf/",
views.AlunosPorUfReportView.as_view(), views.AlunosPorUfReportView.as_view(),
@ -14,7 +15,7 @@ urlpatterns = [
), ),
path( path(
"solicitacoesporperiodo/", "solicitacoesporperiodo/",
views.solicitacoes_por_periodo, SolicitacoesPorPeriodoReportView.as_view(),
name="eventos_solicitacoesporperiodo", name="eventos_solicitacoesporperiodo",
), ),
path( path(

94
sigi/apps/eventos/templates/eventos/calendario.html

@ -22,17 +22,76 @@
<form> <form>
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<a class="icon-link" data-bs-toggle="collapse" href="#filterFormCollapse" aria-expanded="{% if eventos is None %}true{% else %}false{% endif %}" aria-controls="filterFormCollapse"> <a class="icon-link" data-bs-toggle="collapse" href="#filterFormCollapse"
aria-expanded="{% if eventos is None %}true{% else %}false{% endif %}"
aria-controls="filterFormCollapse">
{% icon "filter" %} {% translate "Filtros" %} {% icon "filter" %} {% translate "Filtros" %}
</a> </a>
</div> </div>
<div class="card-body collapse{% if eventos is None %} show{% endif %}" id="filterFormCollapse"> <div class="card-body collapse{% if eventos is None %} show{% endif %}" id="filterFormCollapse">
{{ form }}
{% for field in form %}
{% if field.name != "mes_ano" and field.name != "categorias" and field.name != "status" %}
{{ field }}
{% endif %}
{% endfor %}
<div class="mb-3">
<fieldset>
<legend>Categorias:</legend>
{{ form.categorias }}
</fieldset>
</div>
<div class="mb-3">
<fieldset>
<legend>Status:</legend>
{{ form.status }}
</fieldset>
</div> </div>
<div class="mb-3">
<fieldset>
<legend>Período:</legend>
<div class="mb-3">
<label for="id_year" class="form-label">Ano:</label>
<input type="number" id="id_year" class="form-control" placeholder="YYYY"
style="width: max-content;" required min="1000" max="9999">
</div>
<div class="mb-3">
<label for="id_month" class="form-label">Mês:</label>
<select id="id_month" class="form-select" style="width: max-content;" required>
<option value="">Selecione</option>
<option value="01">Janeiro</option>
<option value="02">Fevereiro</option>
<option value="03">Março</option>
<option value="04">Abril</option>
<option value="05">Maio</option>
<option value="06">Junho</option>
<option value="07">Julho</option>
<option value="08">Agosto</option>
<option value="09">Setembro</option>
<option value="10">Outubro</option>
<option value="11">Novembro</option>
<option value="12">Dezembro</option>
</select>
</div>
<input type="hidden" name="mes_ano" id="id_mes_ano" value="">
</fieldset>
</div>
</div>
<div class="card-footer d-flex justify-content-end"> <div class="card-footer d-flex justify-content-end">
<div> <div>
<button type="submit" class="btn btn-primary">{% trans 'Ver calendário' %}</button> <button type="submit" class="btn btn-primary">{% trans 'Ver calendário' %}</button>
<button type="submit" name="fmt" value="pdf" class="btn btn-primary" title="{% trans 'Exportar para PDF' %}"> <button type="submit" name="fmt" value="pdf" class="btn btn-primary"
title="{% trans 'Exportar para PDF' %}">
{% icon "pdf" %} {% icon "pdf" %}
{% trans 'Exportar para PDF' %} {% trans 'Exportar para PDF' %}
</button> </button>
@ -45,7 +104,8 @@
<div class="accordion" id="accordionReport"> <div class="accordion" id="accordionReport">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLegenda" aria-expanded="true" aria-controls="collapseLegenda"> <button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseLegenda" aria-expanded="true" aria-controls="collapseLegenda">
{% translate "Legenda" %} {% translate "Legenda" %}
</button> </button>
</h2> </h2>
@ -57,7 +117,8 @@
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseCalendario" aria-expanded="true" aria-controls="collapseCalendario"> <button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseCalendario" aria-expanded="true" aria-controls="collapseCalendario">
{% trans 'Calendário' %} {% trans 'Calendário' %}
</button> </button>
</h2> </h2>
@ -69,7 +130,8 @@
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLista" aria-expanded="false" aria-controls="collapseLista"> <button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseLista" aria-expanded="false" aria-controls="collapseLista">
{% trans 'Lista' %} {% trans 'Lista' %}
</button> </button>
</h2> </h2>
@ -83,4 +145,24 @@
{% include "eventos/snippets/calendario_modals.html" %} {% include "eventos/snippets/calendario_modals.html" %}
{% endif %} {% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const yearInput = document.getElementById('id_year');
const monthSelect = document.getElementById('id_month');
const hiddenMesAno = document.getElementById('id_mes_ano');
function updateHiddenField() {
const year = yearInput.value.trim();
const month = monthSelect.value;
if (year && month) {
hiddenMesAno.value = year + '-' + month;
} else {
hiddenMesAno.value = '';
}
}
yearInput.addEventListener('input', updateHiddenField);
monthSelect.addEventListener('change', updateHiddenField);
});
</script>
{% endblock %} {% endblock %}

585
sigi/apps/eventos/views.py

@ -190,31 +190,53 @@ class AlunosPorUfReportView(
return context return context
@login_required
@staff_member_required class CalendarioReportView(LoginRequiredMixin, UserPassesTestMixin, ReportListView):
def calendario(request): title = _("Calendário de eventos")
fmt = request.GET.get("fmt", "html") filter_form = CalendarioForm
if "mes_ano" in request.GET: template_name = "eventos/calendario.html"
form = CalendarioForm(request.GET) template_name_pdf = "eventos/calendario_pdf.html"
else:
form = CalendarioForm(
initial={ list_fields = []
list_labels = []
def get_list_labels(self):
return []
def test_func(self):
return self.request.user.is_staff
def get_initial(self):
return {
"mes_ano": timezone.localdate().replace(day=1), "mes_ano": timezone.localdate().replace(day=1),
"categorias": [c[0] for c in TipoEvento.CATEGORIA_CHOICES], "categorias": [c[0] for c in TipoEvento.CATEGORIA_CHOICES],
"status": [s[0] for s in Evento.STATUS_CHOICES], "status": [s[0] for s in Evento.STATUS_CHOICES],
} }
)
context = {"form": form} def get_queryset(self):
return Evento.objects.none()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if "mes_ano" in self.request.GET:
form = CalendarioForm(self.request.GET)
else:
form = CalendarioForm(initial=self.get_initial())
context["form"] = form
if not form.is_valid(): if not form.is_valid():
return render(request, "eventos/calendario.html", context) return context
mes_pesquisa = form.cleaned_data["mes_ano"].month mes_pesquisa = form.cleaned_data["mes_ano"].month
ano_pesquisa = form.cleaned_data["mes_ano"].year ano_pesquisa = form.cleaned_data["mes_ano"].year
sel_categorias = form.cleaned_data["categorias"] sel_categorias = form.cleaned_data["categorias"]
sel_status = form.cleaned_data["status"] sel_status = form.cleaned_data["status"]
lang = to_locale(get_language()) + ".UTF-8" lang = to_locale(get_language()) + ".UTF-8"
locale.setlocale(locale.LC_ALL, lang) locale.setlocale(locale.LC_ALL, lang)
@ -232,32 +254,25 @@ def calendario(request):
semanas = [ semanas = [
{"datas": s, "eventos": []} {"datas": s, "eventos": []}
for s in calendar.Calendar().monthdatescalendar( for s in calendar.Calendar().monthdatescalendar(ano_pesquisa, mes_pesquisa)
ano_pesquisa, mes_pesquisa
)
] ]
for e in eventos: for e in eventos:
for s in semanas: for s in semanas:
if not (
(e.data_termino < s["datas"][0]) if not (e.data_termino < s["datas"][0] or e.data_inicio > s["datas"][-1]):
or (e.data_inicio > s["datas"][-1])
):
start = max(s["datas"][0], e.data_inicio) start = max(s["datas"][0], e.data_inicio)
end = min(s["datas"][-1], e.data_termino) end = min(s["datas"][-1], e.data_termino)
s["eventos"].append( s["eventos"].append((
(
e, e,
( (
start.weekday(), start.weekday(),
end.weekday() - start.weekday() + 1, end.weekday() - start.weekday() + 1,
6 - end.weekday(), 6 - end.weekday(),
), ),
) ))
)
context.update( context.update({
{
"ano_pesquisa": ano_pesquisa, "ano_pesquisa": ano_pesquisa,
"mes_pesquisa": mes_pesquisa, "mes_pesquisa": mes_pesquisa,
"sel_categorias": sel_categorias, "sel_categorias": sel_categorias,
@ -267,21 +282,22 @@ def calendario(request):
"status": Evento.STATUS_CHOICES, "status": Evento.STATUS_CHOICES,
"eventos": eventos, "eventos": eventos,
"semanas": semanas, "semanas": semanas,
} })
) return context
def render_to_response(self, context, **response_kwargs):
fmt = self.request.GET.get("fmt", "html")
if fmt == "pdf": if fmt == "pdf":
context["title"] = _("Calendário de eventos") context["title"] = _("Calendário de eventos")
context["pdf"] = True context["pdf"] = True
return WeasyTemplateResponse( return WeasyTemplateResponse(
filename=f"calendario_{ano_pesquisa:04}{mes_pesquisa:02}.pdf", filename=f"calendario_{context.get('ano_pesquisa'):04}{context.get('mes_pesquisa'):02}.pdf",
request=request, request=self.request,
template="eventos/calendario_pdf.html", template=self.template_name_pdf,
context=context, context=context,
content_type="application/pdf", content_type="application/pdf",
) )
return render(request, "eventos/calendario.html", context) return super().render_to_response(context, **response_kwargs)
class EventoListView(ListView): class EventoListView(ListView):
model = Evento model = Evento
@ -303,107 +319,145 @@ class EventoListView(ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class AlocacaoEquipeReportView(LoginRequiredMixin, UserPassesTestMixin, ReportListView):
title = _("Alocação de equipe")
template_name = "eventos/alocacao_equipe.html"
template_name_pdf = "eventos/alocacao_equipe_pdf.html"
list_fields = []
list_labels = []
def test_func(self):
"""Restringe o acesso a usuários staff."""
return self.request.user.is_staff
def get_list_labels(self):
"""Sobrescreve para não exigir listagem de campos."""
return []
def get_queryset(self):
"""Retorna um queryset vazio, pois toda a lógica está em get_context_data."""
return Evento.objects.none()
def get_context_data(self, **kwargs):
"""Reproduz a lógica original da FBV, populando o contexto com dados."""
context = super().get_context_data(**kwargs)
ano_pesquisa = int(self.request.GET.get("ano", timezone.localdate().year))
mes_pesquisa = int(self.request.GET.get("mes", 0))
semana_pesquisa = int(self.request.GET.get("semana", 0))
formato = self.request.GET.get("fmt", "html")
@login_required
@staff_member_required
def alocacao_equipe(request):
ano_pesquisa = int(request.GET.get("ano", timezone.localdate().year))
mes_pesquisa = int(request.GET.get("mes", 0))
semana_pesquisa = int(request.GET.get("semana", 0))
formato = request.GET.get("fmt", "html")
lang = to_locale(get_language()) + ".UTF-8" lang = to_locale(get_language()) + ".UTF-8"
locale.setlocale(locale.LC_ALL, lang) locale.setlocale(locale.LC_ALL, lang)
eventos = Evento.objects.exclude(
eventos = (
Evento.objects.exclude(
status__in=(Evento.STATUS_CANCELADO, Evento.STATUS_SOBRESTADO) status__in=(Evento.STATUS_CANCELADO, Evento.STATUS_SOBRESTADO)
).prefetch_related("equipe_set") )
.prefetch_related("equipe_set")
)
num_cols = 12 num_cols = 12
if mes_pesquisa > 0: if mes_pesquisa > 0:
semanas = [ semanas = [
[s[0], s[-1]] [s[0], s[-1]]
for s in calendar.Calendar().monthdatescalendar( for s in calendar.Calendar().monthdatescalendar(ano_pesquisa, mes_pesquisa)
ano_pesquisa, mes_pesquisa
)
] ]
num_cols = len(semanas) num_cols = len(semanas)
if semana_pesquisa > 0: if semana_pesquisa > 0:
dias = calendar.Calendar().monthdatescalendar(
ano_pesquisa, mes_pesquisa dias = calendar.Calendar().monthdatescalendar(ano_pesquisa, mes_pesquisa)[semana_pesquisa - 1]
)[semana_pesquisa - 1]
num_cols = len(dias) num_cols = len(dias)
eventos = eventos.filter( eventos = eventos.filter(
data_inicio__gte=dias[0], data_inicio__lte=dias[-1] data_inicio__gte=dias[0], data_inicio__lte=dias[-1]
) )
else: else:
eventos = eventos.filter( eventos = eventos.filter(
data_inicio__gte=semanas[0][0], data_inicio__gte=semanas[0][0],
data_inicio__lte=semanas[-1][-1], data_inicio__lte=semanas[-1][-1],
) )
else: else:
eventos = eventos.filter(data_inicio__year=ano_pesquisa) eventos = eventos.filter(data_inicio__year=ano_pesquisa)
dados = [] dados = []
for evento in eventos: for evento in eventos:
for p in evento.equipe_set.all(): for equipe in evento.equipe_set.all():
registro = None registro = None
for r in dados: for r in dados:
if r[0] == p.membro.pk: if r[0] == equipe.membro.pk:
registro = r registro = r
break break
if not registro: if not registro:
if semana_pesquisa > 0: if semana_pesquisa > 0:
registro = [ registro = [
p.membro.pk, equipe.membro.pk,
p.membro.get_apelido(), equipe.membro.get_apelido(),
OrderedDict([(dia, []) for dia in dias]), OrderedDict([(dia, []) for dia in dias]),
] ]
else: else:
registro = [ registro = [
p.membro.pk, equipe.membro.pk,
p.membro.get_apelido(), equipe.membro.get_apelido(),
[{"dias": 0, "eventos": 0} for __ in range(num_cols)], [{"dias": 0, "eventos": 0} for __ in range(num_cols)],
] ]
dados.append(registro) dados.append(registro)
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 evento.data_inicio <= dia <= evento.data_termino: if evento.data_inicio <= dia <= evento.data_termino:
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 <= fim: if inicio <= evento.data_inicio <= fim:
registro[2][idx]["dias"] += ( registro[2][idx]["dias"] += (
min(fim, evento.data_termino) min(fim, evento.data_termino) - evento.data_inicio
- evento.data_inicio
).days + 1 ).days + 1
registro[2][idx]["eventos"] += 1 registro[2][idx]["eventos"] += 1
elif inicio <= evento.data_termino <= fim: elif inicio <= evento.data_termino <= fim:
registro[2][idx]["dias"] += ( registro[2][idx]["dias"] += (
min(fim, evento.data_termino) min(fim, evento.data_termino) - evento.data_inicio
- evento.data_inicio
).days + 1 ).days + 1
registro[2][idx]["eventos"] += 1 registro[2][idx]["eventos"] += 1
else: else:
registro[2][evento.data_inicio.month - 1]["dias"] += ( registro[2][evento.data_inicio.month - 1]["dias"] += (
evento.data_termino - evento.data_inicio evento.data_termino - evento.data_inicio
).days + 1 ).days + 1
registro[2][evento.data_inicio.month - 1]["eventos"] += 1 registro[2][evento.data_inicio.month - 1]["eventos"] += 1
dados.sort(key=lambda x: x[1]) dados.sort(key=lambda x: x[1])
meses = list(calendar.month_abbr)[1:] meses = list(calendar.month_abbr)[1:]
linhas = [] linhas = []
if semana_pesquisa: if semana_pesquisa:
linhas = [ linhas = [
[registro[1]] + list(registro[2].values()) for registro in dados [registro[1]] + list(registro[2].values())
for registro in dados
] ]
else: else:
for r in dados: for r in dados:
r[2].append( r[2].append(
reduce( reduce(
lambda x, y: { lambda x, y: {
@ -413,14 +467,13 @@ def alocacao_equipe(request):
r[2], r[2],
) )
) )
linhas.append( row = [r[1]]
[r[1]] for d in r[2]:
+ [ if d["dias"] > 0 or d["eventos"] > 0:
(
texto = (
_( _(
ngettext( ngettext("%(dias)s dia", "%(dias)s dias", d["dias"])
"%(dias)s dia", "%(dias)s dias", d["dias"]
)
+ " em " + " em "
+ ngettext( + ngettext(
"%(eventos)s evento", "%(eventos)s evento",
@ -429,34 +482,44 @@ def alocacao_equipe(request):
) )
) )
% d % d
if d["dias"] > 0 or d["eventos"] > 0
else ""
)
for d in r[2]
]
) )
row.append(texto)
else:
row.append("")
linhas.append(row)
context = { context.update({
"anos": Evento.objects.exclude(data_inicio=None) "anos": (
Evento.objects.exclude(data_inicio=None)
.order_by("data_inicio__year") .order_by("data_inicio__year")
.distinct("data_inicio__year") .distinct("data_inicio__year")
.values_list("data_inicio__year", flat=True), .values_list("data_inicio__year", flat=True)
),
"ano_pesquisa": ano_pesquisa, "ano_pesquisa": ano_pesquisa,
"linhas": linhas, "linhas": linhas,
"meses": meses, "meses": meses,
} })
if mes_pesquisa > 0: if mes_pesquisa > 0:
context["mes_pesquisa"] = mes_pesquisa context["mes_pesquisa"] = mes_pesquisa
semanas = [
[s[0], s[-1]]
for s in calendar.Calendar().monthdatescalendar(ano_pesquisa, mes_pesquisa)
]
context["semanas"] = [ context["semanas"] = [
_(f"de {inicio:%d/%m} a {fim:%d/%m}") for inicio, fim in semanas _(f"de {inicio:%d/%m} a {fim:%d/%m}")
for inicio, fim in semanas
] ]
if semana_pesquisa > 0: if semana_pesquisa > 0:
cabecalho = [_("Servidor")] + dias
context["semana_pesquisa"] = semana_pesquisa context["semana_pesquisa"] = semana_pesquisa
context["eventos"] = eventos context["eventos"] = eventos
cabecalho = [_("Servidor")] + list(dias)
else: else:
cabecalho = ( cabecalho = (
[_("Servidor")] [_("Servidor")]
+ [ + [
@ -466,38 +529,59 @@ def alocacao_equipe(request):
+ ["total"] + ["total"]
) )
else: else:
cabecalho = [_("Servidor")] + meses + ["total"] cabecalho = [_("Servidor")] + meses + ["total"]
context["cabecalho"] = cabecalho context["cabecalho"] = cabecalho
context["formato"] = formato
if formato == "pdf": return context
context["title"] = _("Alocação de equipe")
def render_to_response(self, context, **response_kwargs):
"""Decide se retorna HTML, PDF ou CSV."""
fmt = context.get("formato", "html")
if fmt == "pdf":
context["pdf"] = True context["pdf"] = True
context["title"] = self.title
ano_pesquisa = context.get("ano_pesquisa", timezone.localdate().year)
return WeasyTemplateResponse( return WeasyTemplateResponse(
filename=f"alocacao_equipe_{ano_pesquisa}.pdf", filename=f"alocacao_equipe_{ano_pesquisa}.pdf",
request=request, request=self.request,
template="eventos/alocacao_equipe_pdf.html", template=self.template_name_pdf,
context=context, context=context,
content_type="application/pdf", content_type="application/pdf",
) )
elif formato == "csv": elif fmt == "csv":
ano_pesquisa = context.get("ano_pesquisa", timezone.localdate().year)
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = ( response["Content-Disposition"] = f'attachment; filename="alocacao_equipe_{ano_pesquisa}.csv"'
f'attachment; filename="alocacao_equipe_{ano_pesquisa}.csv"'
)
writer = csv.writer(response) writer = csv.writer(response)
writer.writerow(cabecalho) writer.writerow(context["cabecalho"])
writer.writerows(linhas) writer.writerows(context["linhas"])
return response return response
return render(request, "eventos/alocacao_equipe.html", context) return super().render_to_response(context, **response_kwargs)
class EventosPorUfReportView(LoginRequiredMixin, UserPassesTestMixin, ReportListView):
title = _("Eventos por UF")
filter_form = EventosPorUfForm
template_name = "eventos/eventos_por_uf.html"
template_name_pdf = "eventos/eventos_por_uf_pdf.html"
@login_required list_fields = []
@staff_member_required list_labels = []
def eventos_por_uf(request):
formato = request.GET.get("fmt", "html") def get_list_labels(self):
initials = {
return []
def test_func(self):
return self.request.user.is_staff
def get_initial(self):
return {
"data_inicio": datetime.date.today().replace(day=1), "data_inicio": datetime.date.today().replace(day=1),
"data_fim": datetime.date.today().replace( "data_fim": datetime.date.today().replace(
day=calendar.monthrange( day=calendar.monthrange(
@ -507,20 +591,27 @@ def eventos_por_uf(request):
"categoria": [c[0] for c in TipoEvento.CATEGORIA_CHOICES], "categoria": [c[0] for c in TipoEvento.CATEGORIA_CHOICES],
"virtual": [m[0] for m in EventosPorUfForm.MODO_CHOICES], "virtual": [m[0] for m in EventosPorUfForm.MODO_CHOICES],
} }
if "data_inicio" in request.GET or "data_fim" in request.GET:
form = EventosPorUfForm(request.GET) def get_queryset(self):
else:
form = EventosPorUfForm(initial=initials) return UnidadeFederativa.objects.none()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = self.get_filter_form_instance()
context["form"] = form
if not form.is_valid(): if not form.is_valid():
return render( return context
request, "eventos/eventos_por_uf.html", context={"form": form}
)
data_inicio = form.cleaned_data.get("data_inicio") data_inicio = form.cleaned_data.get("data_inicio")
data_fim = form.cleaned_data.get("data_fim") data_fim = form.cleaned_data.get("data_fim")
categorias = form.cleaned_data.get("categoria", initials["categoria"]) initial = self.get_initial()
virtual = form.cleaned_data.get("virtual", initials["virtual"]) categorias = form.cleaned_data.get("categoria", initial["categoria"])
annotates = dict() virtual = form.cleaned_data.get("virtual", initial["virtual"])
aggfuncs = dict()
annotates = {}
aggfuncs = {}
if "P" in virtual: if "P" in virtual:
annotates["eventos_presenciais"] = Count( annotates["eventos_presenciais"] = Count(
"municipio__orgao__evento__id", "municipio__orgao__evento__id",
@ -545,13 +636,11 @@ def eventos_por_uf(request):
) )
aggfuncs["nº eventos virtuais"] = sum aggfuncs["nº eventos virtuais"] = sum
aggfuncs["participantes virtuais"] = sum aggfuncs["participantes virtuais"] = sum
eventos = ( eventos = (
UnidadeFederativa.objects.filter( UnidadeFederativa.objects.filter(
municipio__orgao__evento__status=Evento.STATUS_REALIZADO, municipio__orgao__evento__status=Evento.STATUS_REALIZADO,
municipio__orgao__evento__data_inicio__range=( municipio__orgao__evento__data_inicio__range=(data_inicio, data_fim),
data_inicio,
data_fim,
),
municipio__orgao__evento__tipo_evento__categoria__in=categorias, municipio__orgao__evento__tipo_evento__categoria__in=categorias,
) )
.order_by("regiao", "nome") .order_by("regiao", "nome")
@ -564,15 +653,13 @@ def eventos_por_uf(request):
) )
df = pd.DataFrame(eventos) df = pd.DataFrame(eventos)
if df.empty: if df.empty:
messages.add_message( messages.error(
request, self.request,
messages.ERROR, _("Nenhum evento foi realizado no período solicitado")
_("Nenhum evento foi realizado no período solicitado"),
) )
return render( return context
request, "eventos/eventos_por_uf.html", context={"form": form}
)
# Renomeia colunas
df.rename( df.rename(
columns={ columns={
"municipio__orgao__evento__tipo_evento__categoria": "categoria", "municipio__orgao__evento__tipo_evento__categoria": "categoria",
@ -583,13 +670,11 @@ def eventos_por_uf(request):
}, },
inplace=True, inplace=True,
) )
# Troca a sigla pelo nome da região
for sigla, nome in UnidadeFederativa.REGIAO_CHOICES: for sigla, nome in UnidadeFederativa.REGIAO_CHOICES:
df["regiao"].replace(sigla, nome, inplace=True) df["regiao"].replace(sigla, nome, inplace=True)
# Troca o código pelo nome da categoria de eventos
for cod, nome in TipoEvento.CATEGORIA_CHOICES: for cod, nome in TipoEvento.CATEGORIA_CHOICES:
df["categoria"].replace(cod, nome, inplace=True) df["categoria"].replace(cod, nome, inplace=True)
# Cria tabela pivot das UFs
pivo_uf = df.pivot_table( pivo_uf = df.pivot_table(
index=["regiao", "nome"], index=["regiao", "nome"],
columns="categoria", columns="categoria",
@ -597,103 +682,55 @@ def eventos_por_uf(request):
fill_value=0, fill_value=0,
) )
if len(categorias) > 1: if len(categorias) > 1:
# calcula os totais de eventos e de participantes para as UFs ix_eventos_presenciais = [i for i in pivo_uf.columns if i[0] == "nº eventos presenciais"]
ix_eventos_presenciais = [ ix_eventos_virtuais = [i for i in pivo_uf.columns if i[0] == "nº eventos virtuais"]
i for i in pivo_uf.columns if i[0] == "nº eventos presenciais" ix_participantes_presenciais = [i for i in pivo_uf.columns if i[0] == "participantes presenciais"]
] ix_participantes_virtuais = [i for i in pivo_uf.columns if i[0] == "participantes virtuais"]
ix_eventos_virtuais = [
i for i in pivo_uf.columns if i[0] == "nº eventos virtuais"
]
ix_participantes_presenciais = [
i for i in pivo_uf.columns if i[0] == "participantes presenciais"
]
ix_participantes_virtuais = [
i for i in pivo_uf.columns if i[0] == "participantes virtuais"
]
if ix_eventos_presenciais: if ix_eventos_presenciais:
pivo_uf[("nº eventos presenciais", "total")] = pivo_uf[ pivo_uf[("nº eventos presenciais", "total")] = pivo_uf[ix_eventos_presenciais].sum(axis=1)
ix_eventos_presenciais
].sum(axis=1)
ix_eventos_presenciais.append(("nº eventos presenciais", "total")) ix_eventos_presenciais.append(("nº eventos presenciais", "total"))
if ix_eventos_virtuais: if ix_eventos_virtuais:
pivo_uf[("nº eventos virtuais", "total")] = pivo_uf[ pivo_uf[("nº eventos virtuais", "total")] = pivo_uf[ix_eventos_virtuais].sum(axis=1)
ix_eventos_virtuais
].sum(axis=1)
ix_eventos_virtuais.append(("nº eventos virtuais", "total")) ix_eventos_virtuais.append(("nº eventos virtuais", "total"))
if ix_participantes_presenciais: if ix_participantes_presenciais:
pivo_uf[("participantes presenciais", "total")] = pivo_uf[ pivo_uf[("participantes presenciais", "total")] = pivo_uf[ix_participantes_presenciais].sum(axis=1)
ix_participantes_presenciais ix_participantes_presenciais.append(("participantes presenciais", "total"))
].sum(axis=1)
ix_participantes_presenciais.append(
("participantes presenciais", "total")
)
if ix_participantes_virtuais: if ix_participantes_virtuais:
pivo_uf[("participantes virtuais", "total")] = pivo_uf[ pivo_uf[("participantes virtuais", "total")] = pivo_uf[ix_participantes_virtuais].sum(axis=1)
ix_participantes_virtuais ix_participantes_virtuais.append(("participantes virtuais", "total"))
].sum(axis=1)
ix_participantes_virtuais.append(
("participantes virtuais", "total")
)
pivo_uf = pivo_uf[ pivo_uf = pivo_uf[
ix_eventos_presenciais ix_eventos_presenciais + ix_eventos_virtuais +
+ ix_eventos_virtuais ix_participantes_presenciais + ix_participantes_virtuais
+ ix_participantes_presenciais
+ ix_participantes_virtuais
] ]
# Cria tabela pivot das regiões
pivo_regiao = df.pivot_table( pivo_regiao = df.pivot_table(
index="regiao", index="regiao",
columns="categoria", columns="categoria",
aggfunc=aggfuncs, aggfunc=aggfuncs,
fill_value=0, fill_value=0,
) )
# Calcula os totais de eventos e participantes para as regiões
if len(categorias) > 1: if len(categorias) > 1:
ix_eventos_presenciais = [ ix_eventos_presenciais = [i for i in pivo_regiao.columns if i[0] == "nº eventos presenciais"]
i for i in pivo_regiao.columns if i[0] == "nº eventos presenciais" ix_eventos_virtuais = [i for i in pivo_regiao.columns if i[0] == "nº eventos virtuais"]
] ix_participantes_presenciais = [i for i in pivo_regiao.columns if i[0] == "participantes presenciais"]
ix_eventos_virtuais = [ ix_participantes_virtuais = [i for i in pivo_regiao.columns if i[0] == "participantes virtuais"]
i for i in pivo_regiao.columns if i[0] == "nº eventos virtuais"
]
ix_participantes_presenciais = [
i
for i in pivo_regiao.columns
if i[0] == "participantes presenciais"
]
ix_participantes_virtuais = [
i for i in pivo_regiao.columns if i[0] == "participantes virtuais"
]
if ix_eventos_presenciais: if ix_eventos_presenciais:
pivo_regiao[("nº eventos presenciais", "total")] = pivo_regiao[ pivo_regiao[("nº eventos presenciais", "total")] = pivo_regiao[ix_eventos_presenciais].sum(axis=1)
ix_eventos_presenciais
].sum(axis=1)
ix_eventos_presenciais.append(("nº eventos presenciais", "total")) ix_eventos_presenciais.append(("nº eventos presenciais", "total"))
if ix_eventos_virtuais: if ix_eventos_virtuais:
pivo_regiao[("nº eventos virtuais", "total")] = pivo_regiao[ pivo_regiao[("nº eventos virtuais", "total")] = pivo_regiao[ix_eventos_virtuais].sum(axis=1)
ix_eventos_virtuais
].sum(axis=1)
ix_eventos_virtuais.append(("nº eventos virtuais", "total")) ix_eventos_virtuais.append(("nº eventos virtuais", "total"))
if ix_participantes_presenciais: if ix_participantes_presenciais:
pivo_regiao[("participantes presenciais", "total")] = pivo_regiao[ pivo_regiao[("participantes presenciais", "total")] = pivo_regiao[ix_participantes_presenciais].sum(axis=1)
ix_participantes_presenciais ix_participantes_presenciais.append(("participantes presenciais", "total"))
].sum(axis=1)
ix_participantes_presenciais.append(
("participantes presenciais", "total")
)
if ix_participantes_virtuais: if ix_participantes_virtuais:
pivo_regiao[("participantes virtuais", "total")] = pivo_regiao[ pivo_regiao[("participantes virtuais", "total")] = pivo_regiao[ix_participantes_virtuais].sum(axis=1)
ix_participantes_virtuais ix_participantes_virtuais.append(("participantes virtuais", "total"))
].sum(axis=1)
ix_participantes_virtuais.append(
("participantes virtuais", "total")
)
pivo_regiao = pivo_regiao[ pivo_regiao = pivo_regiao[
ix_eventos_presenciais ix_eventos_presenciais + ix_eventos_virtuais +
+ ix_eventos_virtuais ix_participantes_presenciais + ix_participantes_virtuais
+ ix_participantes_presenciais
+ ix_participantes_virtuais
] ]
# Cabeçalhos para impressão
cabecalho_uf = [ cabecalho_uf = [
(k, [i[1] for i in v]) (k, [i[1] for i in v])
for k, v in groupby(pivo_uf.columns, lambda x: x[0]) for k, v in groupby(pivo_uf.columns, lambda x: x[0])
@ -702,96 +739,111 @@ def eventos_por_uf(request):
(k, [i[1] for i in v]) (k, [i[1] for i in v])
for k, v in groupby(pivo_regiao.columns, lambda x: x[0]) for k, v in groupby(pivo_regiao.columns, lambda x: x[0])
] ]
# Fixar tudo em int
pivo_uf = pivo_uf.astype(int) pivo_uf = pivo_uf.astype(int)
pivo_regiao = pivo_regiao.astype(int) pivo_regiao = pivo_regiao.astype(int)
# Imprimir
context = { context.update({
"form": form,
"data_inicio": data_inicio, "data_inicio": data_inicio,
"data_fim": data_fim, "data_fim": data_fim,
"categorias": [ "categorias": [c[1] for c in TipoEvento.CATEGORIA_CHOICES if c[0] in categorias],
c[1] for c in TipoEvento.CATEGORIA_CHOICES if c[0] in categorias "virtual": [m[1] for m in EventosPorUfForm.MODO_CHOICES if m[0] in virtual],
],
"virtual": [
m[1] for m in EventosPorUfForm.MODO_CHOICES if m[0] in virtual
],
"pivo_uf": pivo_uf, "pivo_uf": pivo_uf,
"pivo_regiao": pivo_regiao, "pivo_regiao": pivo_regiao,
"cabecalho_uf": cabecalho_uf, "cabecalho_uf": cabecalho_uf,
"cabecalho_regiao": cabecalho_regiao, "cabecalho_regiao": cabecalho_regiao,
"total_uf": pivo_uf.sum(), "total_uf": pivo_uf.sum(),
"total_regiao": pivo_regiao.sum(), "total_regiao": pivo_regiao.sum(),
} })
if formato == "pdf": return context
def render_to_response(self, context, **response_kwargs):
fmt = self.request.GET.get("fmt", "html")
if fmt == "pdf":
context["title"] = _("Eventos por Unidade da Federação") context["title"] = _("Eventos por Unidade da Federação")
context["pdf"] = True context["pdf"] = True
return WeasyTemplateResponse( return WeasyTemplateResponse(
# filename=f"eventos_por_uf-{data_inicio}-{data_fim}.pdf", request=self.request,
request=request, template=self.template_name_pdf,
template="eventos/eventos_por_uf_pdf.html",
context=context, context=context,
content_type="application/pdf", content_type="application/pdf",
) )
elif formato == "csv": elif fmt == "csv":
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
data_inicio = context.get("data_inicio")
data_fim = context.get("data_fim")
response["Content-Disposition"] = ( response["Content-Disposition"] = (
f'attachment; filename="eventos_por_uf-{data_inicio}-{data_fim}.csv"' f'attachment; filename="eventos_por_uf-{data_inicio}-{data_fim}.csv"'
) )
pivo_uf = context.get("pivo_uf")
pivo_uf.to_csv(response) pivo_uf.to_csv(response)
return response return response
return render(request, "eventos/eventos_por_uf.html", context=context) return super().render_to_response(context, **response_kwargs)
class SolicitacoesPorPeriodoReportView(LoginRequiredMixin, UserPassesTestMixin, ReportListView):
title = _("Solicitações por período")
filter_form = SolicitacoesPorPeriodoForm
template_name = "eventos/solicitacoes_por_periodo.html"
template_name_pdf = "eventos/solicitacoes_por_periodo_pdf.html"
list_fields = []
list_labels = []
def get_list_labels(self):
return []
@login_required def test_func(self):
@staff_member_required return self.request.user.is_staff
def solicitacoes_por_periodo(request):
formato = request.GET.get("fmt", "html") def get_initial(self):
initials = { return {
"data_inicio": datetime.date.today().replace(day=1), "data_inicio": datetime.date.today().replace(day=1),
"data_fim": datetime.date.today().replace( "data_fim": datetime.date.today().replace(
day=calendar.monthrange( day=calendar.monthrange(datetime.date.today().year, datetime.date.today().month)[1]
datetime.date.today().year, datetime.date.today().month
)[1]
), ),
"tipos_evento": TipoEvento.objects.all(), "tipos_evento": TipoEvento.objects.all(),
"virtual": [m[0] for m in SolicitacoesPorPeriodoForm.MODO_CHOICES], "virtual": [m[0] for m in SolicitacoesPorPeriodoForm.MODO_CHOICES],
"status": [s[0] for s in Solicitacao.STATUS_CHOICES], "status": [s[0] for s in Solicitacao.STATUS_CHOICES],
} }
if "data_inicio" in request.GET or "data_fim" in request.GET:
form = SolicitacoesPorPeriodoForm(request.GET) def get_queryset(self):
else:
form = SolicitacoesPorPeriodoForm(initial=initials) return Solicitacao.objects.none()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = self.get_filter_form_instance()
context["form"] = form
if not form.is_valid(): if not form.is_valid():
return render( return context
request,
"eventos/solicitacoes_por_periodo.html",
context={"form": form},
)
data_inicio = form.cleaned_data.get("data_inicio") data_inicio = form.cleaned_data.get("data_inicio")
data_fim = form.cleaned_data.get("data_fim") data_fim = form.cleaned_data.get("data_fim")
tipos_evento = form.cleaned_data.get( initial = self.get_initial()
"tipos_evento", initials["tipos_evento"] tipos_evento = form.cleaned_data.get("tipos_evento", initial["tipos_evento"])
) virtual = form.cleaned_data.get("virtual", initial["virtual"])
virtual = form.cleaned_data.get("virtual", initials["virtual"]) status = form.cleaned_data.get("status", initial["status"])
status = form.cleaned_data.get("status", initials["status"])
sq_equipe = ( sq_equipe = (
Equipe.objects.order_by() Equipe.objects.order_by()
.annotate( .annotate(
tot=Sum( tot=Sum(F("qtde_diarias") * F("valor_diaria") + F("total_passagens"))
F("qtde_diarias") * F("valor_diaria") + F("total_passagens")
)
) )
.values("tot") .values("tot")
) )
sq_equipe.query.group_by = [] sq_equipe.query.group_by = []
solicitacoes = Solicitacao.objects.order_by().filter( solicitacoes = Solicitacao.objects.order_by().filter(
data_pedido__range=(data_inicio, data_fim), data_pedido__range=(data_inicio, data_fim),
itemsolicitado__tipo_evento__in=tipos_evento, itemsolicitado__tipo_evento__in=tipos_evento,
itemsolicitado__virtual__in=virtual, itemsolicitado__virtual__in=virtual,
status__in=status, status__in=status,
) )
legenda_oficinas = ( legenda_oficinas = (
solicitacoes.order_by("itemsolicitado__tipo_evento__sigla") solicitacoes.order_by("itemsolicitado__tipo_evento__sigla")
.values_list( .values_list(
@ -800,6 +852,7 @@ def solicitacoes_por_periodo(request):
) )
.distinct() .distinct()
) )
solicitacoes = ( solicitacoes = (
solicitacoes.order_by( solicitacoes.order_by(
"casa__municipio__uf__regiao", "casa__municipio__uf__regiao",
@ -811,21 +864,15 @@ def solicitacoes_por_periodo(request):
qtde_solicitadas=Count("itemsolicitado__id"), qtde_solicitadas=Count("itemsolicitado__id"),
qtde_atendidas=Count( qtde_atendidas=Count(
"itemsolicitado__id", "itemsolicitado__id",
filter=Q( filter=Q(itemsolicitado__status=ItemSolicitado.STATUS_AUTORIZADO)
itemsolicitado__status=ItemSolicitado.STATUS_AUTORIZADO
),
), ),
qtde_rejeitadas=Count( qtde_rejeitadas=Count(
"itemsolicitado__id", "itemsolicitado__id",
filter=Q( filter=Q(itemsolicitado__status=ItemSolicitado.STATUS_REJEITADO)
itemsolicitado__status=ItemSolicitado.STATUS_REJEITADO
),
), ),
participantes=Sum("itemsolicitado__evento__total_participantes"), participantes=Sum("itemsolicitado__evento__total_participantes"),
custo_total=Subquery( custo_total=Subquery(
sq_equipe.filter( sq_equipe.filter(evento__itemsolicitado__solicitacao=OuterRef("pk"))[:1]
evento__itemsolicitado__solicitacao=OuterRef("pk")
)[:1]
), ),
) )
.select_related( .select_related(
@ -836,6 +883,7 @@ def solicitacoes_por_periodo(request):
) )
.prefetch_related("itemsolicitado_set") .prefetch_related("itemsolicitado_set")
) )
sumario = solicitacoes.aggregate( sumario = solicitacoes.aggregate(
Sum("qtde_solicitadas"), Sum("qtde_solicitadas"),
Sum("qtde_atendidas"), Sum("qtde_atendidas"),
@ -843,6 +891,7 @@ def solicitacoes_por_periodo(request):
Sum("participantes"), Sum("participantes"),
Sum("custo_total"), Sum("custo_total"),
).values() ).values()
resumo_uf = pd.DataFrame( resumo_uf = pd.DataFrame(
solicitacoes.order_by( solicitacoes.order_by(
"casa__municipio__uf__regiao", "casa__municipio__uf__regiao",
@ -886,18 +935,15 @@ def solicitacoes_por_periodo(request):
].sum() ].sum()
resumo_uf.replace([0], [None], inplace=True) resumo_uf.replace([0], [None], inplace=True)
resumo_regiao.replace([0], [None], inplace=True) resumo_regiao.replace([0], [None], inplace=True)
resumo_tipo_evento = pd.DataFrame( resumo_tipo_evento = pd.DataFrame(
ItemSolicitado.objects.filter(solicitacao__in=solicitacoes) ItemSolicitado.objects.filter(solicitacao__in=solicitacoes)
.order_by("tipo_evento__sigla", "tipo_evento__nome") .order_by("tipo_evento__sigla", "tipo_evento__nome")
.values("tipo_evento__sigla", "tipo_evento__nome") .values("tipo_evento__sigla", "tipo_evento__nome")
.annotate( .annotate(
qtde_solicitadas=Count("id"), qtde_solicitadas=Count("id"),
qtde_atendidas=Count( qtde_atendidas=Count("id", filter=Q(status=ItemSolicitado.STATUS_AUTORIZADO)),
"id", filter=Q(status=ItemSolicitado.STATUS_AUTORIZADO) qtde_rejeitadas=Count("id", filter=Q(status=ItemSolicitado.STATUS_REJEITADO)),
),
qtde_rejeitadas=Count(
"id", filter=Q(status=ItemSolicitado.STATUS_REJEITADO)
),
participantes=Sum("evento__total_participantes"), participantes=Sum("evento__total_participantes"),
custo_total=Subquery( custo_total=Subquery(
sq_equipe.filter(evento__itemsolicitado=OuterRef("pk"))[:1] sq_equipe.filter(evento__itemsolicitado=OuterRef("pk"))[:1]
@ -916,41 +962,39 @@ def solicitacoes_por_periodo(request):
.sum() .sum()
.fillna(0) .fillna(0)
) )
resumo_tipo_evento["participantes"] = resumo_tipo_evento[ resumo_tipo_evento["participantes"] = resumo_tipo_evento["participantes"].astype("int")
"participantes"
].astype("int")
resumo_tipo_evento.replace([0], [None], inplace=True) resumo_tipo_evento.replace([0], [None], inplace=True)
# Imprimir
context = { context.update({
"form": form,
"data_inicio": data_inicio, "data_inicio": data_inicio,
"data_fim": data_fim, "data_fim": data_fim,
"status_choices": ItemSolicitado.STATUS_CHOICES, "status_choices": ItemSolicitado.STATUS_CHOICES,
"legenda_oficinas": legenda_oficinas, "legenda_oficinas": legenda_oficinas,
"tipos_evento": tipos_evento, "tipos_evento": tipos_evento,
"virtual": [ "virtual": [m[1] for m in SolicitacoesPorPeriodoForm.MODO_CHOICES if m[0] in virtual],
m[1]
for m in SolicitacoesPorPeriodoForm.MODO_CHOICES
if m[0] in virtual
],
"solicitacoes": solicitacoes, "solicitacoes": solicitacoes,
"sumario": sumario, "sumario": sumario,
"resumo_uf": resumo_uf, "resumo_uf": resumo_uf,
"resumo_regiao": resumo_regiao, "resumo_regiao": resumo_regiao,
"resumo_tipo_evento": resumo_tipo_evento, "resumo_tipo_evento": resumo_tipo_evento,
} })
return context
def render_to_response(self, context, **response_kwargs):
formato = self.request.GET.get("fmt", "html")
if formato == "pdf": if formato == "pdf":
context["title"] = _("Solicitações por período")
context["pdf"] = True context["pdf"] = True
return WeasyTemplateResponse( return WeasyTemplateResponse(
filename=f"solicitacoes_por_periodo-{data_inicio}-{data_fim}.pdf", filename=f"solicitacoes_por_periodo-{context.get('data_inicio')}-{context.get('data_fim')}.pdf",
request=request, request=self.request,
template="eventos/solicitacoes_por_periodo_pdf.html", template=self.template_name_pdf,
context=context, context=context,
content_type="application/pdf", content_type="application/pdf",
) )
elif formato == "csv": elif formato == "csv":
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
data_inicio = context.get("data_inicio")
data_fim = context.get("data_fim")
response["Content-Disposition"] = ( response["Content-Disposition"] = (
f'attachment; filename="solicitacoes_por_periodo-{data_inicio}-{data_fim}.csv"' f'attachment; filename="solicitacoes_por_periodo-{data_inicio}-{data_fim}.csv"'
) )
@ -970,12 +1014,9 @@ def solicitacoes_por_periodo(request):
] ]
writer = csv.DictWriter(response, fieldnames) writer = csv.DictWriter(response, fieldnames)
writer.writeheader() writer.writeheader()
writer.writerows(solicitacoes.values(*fieldnames)) writer.writerows(context["solicitacoes"].values(*fieldnames))
return response return response
return render( return super().render_to_response(context, **response_kwargs)
request, "eventos/solicitacoes_por_periodo.html", context=context
)
class ApiEventoAbstract: class ApiEventoAbstract:
queryset = ( queryset = (

0
sigi/templates/admin/date_range_filter.html

Loading…
Cancel
Save