diff --git a/sigi/apps/espacos/__init__.py b/sigi/apps/espacos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/espacos/admin.py b/sigi/apps/espacos/admin.py new file mode 100644 index 0000000..9f0844b --- /dev/null +++ b/sigi/apps/espacos/admin.py @@ -0,0 +1,158 @@ +from django.contrib import admin, messages +from django.urls import path, reverse +from django.utils.translation import gettext as _, ngettext +from django.shortcuts import get_object_or_404, redirect +from import_export.fields import Field +from sigi.apps.espacos.models import ( + Espaco, + Recurso, + Reserva, + RecursoSolicitado, +) +from sigi.apps.utils.mixins import CartExportMixin, LabeledResourse + + +class ReservaResource(LabeledResourse): + recursos_solicitados = Field(column_name="recursos solicitados") + + class Meta: + model = Reserva + fields = ( + "status", + "espaco__sigla", + "espaco__nome", + "proposito", + "inicio", + "termino", + "informacoes", + "solicitante", + "contato", + "telefone_contato", + ) + export_order = fields + + def dehydrate_status(self, obj): + return obj.get_status_display() + + def dehydrate_recursos_solicitados(self, obj): + return ", ".join( + [ + _(f"{r.quantidade} {r.recurso.nome}") + for r in obj.recursosolicitado_set.all() + ] + ) + + +class RecursoSolicitadoInline(admin.TabularInline): + model = RecursoSolicitado + autocomplete_fields = [ + "recurso", + ] + + +@admin.register(Espaco) +class EspacoAdmin(admin.ModelAdmin): + list_display = ["sigla", "nome"] + search_fields = ["sigla", "nome"] + + +@admin.register(Recurso) +class RecursoAdmin(admin.ModelAdmin): + list_display = ["sigla", "nome"] + search_fields = ["sigla", "nome"] + + +@admin.register(Reserva) +class ReservaAdmin(CartExportMixin, admin.ModelAdmin): + resource_classes = [ReservaResource] + list_display = [ + "get_status", + "proposito", + "get_espaco", + "inicio", + "termino", + "solicitante", + "contato", + "telefone_contato", + ] + list_display_links = ["get_status", "proposito"] + list_filter = ["status", "espaco"] + search_fields = ["proposito", "espaco__nome", "espaco__sigla"] + date_hierarchy = "inicio" + actions = ["cancelar_action", "reativar_action"] + fieldsets = [ + (None, {"fields": ("status",)}), + ( + _("Solicitação"), + { + "fields": ( + "espaco", + "proposito", + "inicio", + "termino", + "informacoes", + ) + }, + ), + ( + _("Contato"), + {"fields": ("solicitante", "contato", "telefone_contato")}, + ), + ] + autocomplete_fields = ["espaco"] + readonly_fields = ("status",) + inlines = [RecursoSolicitadoInline] + + def get_urls(self): + urls = super().get_urls() + model_info = self.get_model_info() + my_urls = [ + path( + "/cancel/", + self.admin_site.admin_view(self.cancelar_reserva), + name="%s_%s_cancel" % model_info, + ) + ] + return my_urls + urls + + @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") + def get_espaco(self, obj): + return obj.espaco.sigla + + def cancelar_reserva(self, request, object_id): + reserva = get_object_or_404(Reserva, id=object_id) + reserva.status = Reserva.STATUS_CANCELADO + reserva.save() + return redirect( + reverse( + "admin:%s_%s_change" % self.get_model_info(), args=[object_id] + ) + + "?" + + self.get_preserved_filters(request) + ) + + @admin.action(description=_("Cancelar as reservas selecionadas")) + def cancelar_action(self, request, queryset): + count = queryset.update(status=Reserva.STATUS_CANCELADO) + self.message_user( + request, + ngettext( + "Uma reserva cancelada", f"{count} reservas canceladas", count + ), + messages.SUCCESS, + ) + + @admin.action(description=_("Reativar as reservas selecionadas")) + def reativar_action(self, request, queryset): + count = queryset.update(status=Reserva.STATUS_ATIVO) + self.message_user( + request, + ngettext( + "Uma reserva reativada", f"{count} reservas reativadas", count + ), + messages.SUCCESS, + ) diff --git a/sigi/apps/espacos/admin_urls.py b/sigi/apps/espacos/admin_urls.py new file mode 100644 index 0000000..d997a6a --- /dev/null +++ b/sigi/apps/espacos/admin_urls.py @@ -0,0 +1,6 @@ +from django.urls import path, include +from sigi.apps.espacos import views + +urlpatterns = [ + path("agenda/", views.Agenda.as_view(), name="espacos_agenda"), +] diff --git a/sigi/apps/espacos/apps.py b/sigi/apps/espacos/apps.py new file mode 100644 index 0000000..15da7f9 --- /dev/null +++ b/sigi/apps/espacos/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class EspacosConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "espacos" + name = "sigi.apps.espacos" + verbose_name = _("Agenda de espaços") diff --git a/sigi/apps/espacos/migrations/0001_initial.py b/sigi/apps/espacos/migrations/0001_initial.py new file mode 100644 index 0000000..21e0b61 --- /dev/null +++ b/sigi/apps/espacos/migrations/0001_initial.py @@ -0,0 +1,213 @@ +# Generated by Django 4.2.4 on 2023-11-08 14:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Espaco", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nome", + models.CharField(max_length=100, verbose_name="nome"), + ), + ( + "sigla", + models.CharField(max_length=20, verbose_name="sigla"), + ), + ( + "descricao", + models.TextField(blank=True, verbose_name="descrição"), + ), + ( + "local", + models.CharField( + help_text="Indique o prédio/bloco/sala onde este espaço está localizado.", + max_length=100, + verbose_name="local", + ), + ), + ], + options={ + "verbose_name": "espaço", + "verbose_name_plural": "espaços", + "ordering": ("nome",), + }, + ), + migrations.CreateModel( + name="Recurso", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "nome", + models.CharField(max_length=100, verbose_name="nome"), + ), + ( + "sigla", + models.CharField(max_length=20, verbose_name="sigla"), + ), + ( + "descricao", + models.TextField(blank=True, verbose_name="descrição"), + ), + ], + options={ + "verbose_name": "recurso", + "verbose_name_plural": "recursos", + "ordering": ("nome",), + }, + ), + migrations.CreateModel( + name="Reserva", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "status", + models.CharField( + choices=[("A", "Ativo"), ("C", "Cancelado")], + default="A", + editable=False, + max_length=1, + verbose_name="Status", + ), + ), + ( + "proposito", + models.CharField( + help_text="Indique o propósito da reserva (nome do evento, indicativo da reunião, aula, apresentação, etc.)", + max_length=100, + verbose_name="propósito", + ), + ), + ( + "inicio", + models.DateTimeField(verbose_name="Data/hora de início"), + ), + ( + "termino", + models.DateTimeField(verbose_name="Data/hora de término"), + ), + ( + "informacoes", + models.TextField( + blank=True, + help_text="Utilize para anotar informações adicionais e demais detalhes sobre a reserva", + verbose_name="informações adicionais", + ), + ), + ( + "solicitante", + models.CharField( + help_text="indique o nome da pessoa ou setor solicitante da reserva", + max_length=100, + verbose_name="solicitante", + ), + ), + ( + "contato", + models.CharField( + blank=True, + help_text="Indique o nome da(s) pessoa(s) de contato para tratar assuntos da reserva.", + max_length=100, + verbose_name="pessoa de contato", + ), + ), + ( + "telefone_contato", + models.CharField( + blank=True, + help_text="Indique o telefone/ramal da pessoa responsável pela reserva.", + max_length=100, + verbose_name="telefone de contato", + ), + ), + ( + "espaco", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="espacos.espaco", + verbose_name="espaço", + ), + ), + ], + options={ + "verbose_name": "reserva", + "verbose_name_plural": "reservas", + "ordering": ("inicio", "espaco", "proposito"), + }, + ), + migrations.CreateModel( + name="RecursoSolicitado", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "quantidade", + models.FloatField(default=0.0, verbose_name="quantidade"), + ), + ( + "observacoes", + models.TextField(blank=True, verbose_name="observações"), + ), + ( + "recurso", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="espacos.recurso", + verbose_name="recurso", + ), + ), + ( + "reserva", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="espacos.reserva", + verbose_name="reserva", + ), + ), + ], + options={ + "verbose_name": "recurso solicitado", + "verbose_name_plural": "recursos solicitados", + "ordering": ("recurso",), + }, + ), + ] diff --git a/sigi/apps/espacos/migrations/0002_converte_eventos.py b/sigi/apps/espacos/migrations/0002_converte_eventos.py new file mode 100644 index 0000000..99fae52 --- /dev/null +++ b/sigi/apps/espacos/migrations/0002_converte_eventos.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.4 on 2023-11-08 13:53 + +from django.db import migrations + + +def forwards(apps, schema_editor): + TipoEvento = apps.get_model("eventos", "TipoEvento") + Evento = apps.get_model("eventos", "Evento") + Espaco = apps.get_model("espacos", "Espaco") + Reserva = apps.get_model("espacos", "Reserva") + + print("") + for tipo_evento in TipoEvento.objects.filter(categoria="C"): + print(f"\t Processando {tipo_evento.nome}...") + espaco = Espaco( + nome=tipo_evento.nome, sigla=tipo_evento.sigla, local="ILB" + ) + espaco.save() + + for evento in Evento.objects.filter(tipo_evento=tipo_evento): + print(f"\t\t Evento {evento.nome}... ", end="") + if evento.status == "C": # CANCELADO + status_reserva = "C" + else: + status_reserva = "A" + reserva = Reserva( + status=status_reserva, + espaco=espaco, + proposito=evento.nome, + inicio=evento.data_inicio, + termino=evento.data_termino, + informacoes=( + f"Processo SIGAD: {evento.num_processo}\n" + f"{evento.observacao}" + ), + solicitante=evento.solicitante, + contato=evento.descricao[:100], + ) + reserva.save() + print("reserva criada... ", end="") + evento.delete() + print("evento apagado!") + + tipo_evento.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("espacos", "0001_initial"), + ] + + operations = [migrations.RunPython(forwards, migrations.RunPython.noop)] diff --git a/sigi/apps/espacos/migrations/__init__.py b/sigi/apps/espacos/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/espacos/models.py b/sigi/apps/espacos/models.py new file mode 100644 index 0000000..9249205 --- /dev/null +++ b/sigi/apps/espacos/models.py @@ -0,0 +1,150 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext as _ + + +class Espaco(models.Model): + nome = models.CharField(_("nome"), max_length=100) + sigla = models.CharField(_("sigla"), max_length=20) + descricao = models.TextField(_("descrição"), blank=True) + local = models.CharField( + _("local"), + max_length=100, + help_text=_( + "Indique o prédio/bloco/sala onde este espaço está localizado." + ), + ) + + class Meta: + verbose_name = _("espaço") + verbose_name_plural = _("espaços") + ordering = ("nome",) + + def __str__(self): + return _(f"{self.sigla} - {self.nome}") + + +class Recurso(models.Model): + nome = models.CharField(_("nome"), max_length=100) + sigla = models.CharField(_("sigla"), max_length=20) + descricao = models.TextField(_("descrição"), blank=True) + + class Meta: + verbose_name = _("recurso") + verbose_name_plural = _("recursos") + ordering = ("nome",) + + def __str__(self): + return _(f"{self.sigla} - {self.nome}") + + +class Reserva(models.Model): + STATUS_ATIVO = "A" + STATUS_CANCELADO = "C" + + STATUS_CHOICES = ( + (STATUS_ATIVO, _("Ativo")), + (STATUS_CANCELADO, _("Cancelado")), + ) + + status = models.CharField( + _("Status"), + max_length=1, + choices=STATUS_CHOICES, + default=STATUS_ATIVO, + editable=False, + ) + espaco = models.ForeignKey( + Espaco, verbose_name=_("espaço"), on_delete=models.PROTECT + ) + proposito = models.CharField( + _("propósito"), + max_length=100, + help_text=_( + "Indique o propósito da reserva (nome do evento, indicativo da " + "reunião, aula, apresentação, etc.)" + ), + ) + inicio = models.DateTimeField(_("Data/hora de início")) + termino = models.DateTimeField(_("Data/hora de término")) + informacoes = models.TextField( + _("informações adicionais"), + blank=True, + help_text=_( + "Utilize para anotar informações adicionais e demais detalhes " + "sobre a reserva" + ), + ) + solicitante = models.CharField( + _("solicitante"), + max_length=100, + help_text=_( + "indique o nome da pessoa ou setor solicitante da reserva" + ), + ) + contato = models.CharField( + _("pessoa de contato"), + max_length=100, + blank=True, + help_text=_( + "Indique o nome da(s) pessoa(s) de contato para tratar " + "assuntos da reserva." + ), + ) + telefone_contato = models.CharField( + _("telefone de contato"), + max_length=100, + blank=True, + help_text=_( + "Indique o telefone/ramal da pessoa responsável pela reserva." + ), + ) + + class Meta: + verbose_name = _("reserva") + verbose_name_plural = _("reservas") + ordering = ("inicio", "espaco", "proposito") + + def __str__(self): + return _(f"{self.proposito} em {self.espaco.nome}") + + def clean(self): + if self.inicio > self.termino: + raise ValidationError( + _("Data de início deve ser anterior à data de término") + ) + if ( + Reserva.objects.exclude(id=self.pk) + .filter( + espaco=self.espaco, + inicio__lte=self.termino, + termino__gte=self.inicio, + ) + .exists() + ): + raise ValidationError( + _( + "Já existe um evento neste mesmo espaço que conflita com " + "as datas solicitadas" + ) + ) + return super().clean() + + +class RecursoSolicitado(models.Model): + reserva = models.ForeignKey( + Reserva, verbose_name=_("reserva"), on_delete=models.CASCADE + ) + recurso = models.ForeignKey( + Recurso, verbose_name=_("recurso"), on_delete=models.PROTECT + ) + quantidade = models.FloatField(_("quantidade"), default=0.0) + observacoes = models.TextField(_("observações"), blank=True) + + class Meta: + verbose_name = _("recurso solicitado") + verbose_name_plural = _("recursos solicitados") + ordering = ("recurso",) + + def __str__(self): + return _(f"{self.recurso} para {self.reserva}") diff --git a/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html b/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html new file mode 100644 index 0000000..ce08861 --- /dev/null +++ b/sigi/apps/espacos/templates/admin/espacos/reserva/change_form.html @@ -0,0 +1,15 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} + +{% block object-tools-items %} + {% if object_id %} +
  • + {% url opts|admin_urlname:'cancel' object_id|admin_urlquote as tool_url %} + + + {% trans "Cancelar reserva" %} + +
  • + {% endif %} + {{ block.super }} +{% endblock %} diff --git a/sigi/apps/espacos/templates/espacos/agenda.html b/sigi/apps/espacos/templates/espacos/agenda.html new file mode 100644 index 0000000..2fdf9fa --- /dev/null +++ b/sigi/apps/espacos/templates/espacos/agenda.html @@ -0,0 +1,44 @@ +{% extends "eventos/calendario.html" %} +{% load i18n static sigi_tags %} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + +{% block content %} +
    + + mode_edit + + +
    +
    +
    + +
    + {% for ano, lista in meses.items %} +
    + {% for mes, nome in lista.items %} + {{ nome }} + {% endfor %} +
    + {% endfor %} +
    + {% include "espacos/snippets/agenda_cal.html" %} +{% endblock %} \ No newline at end of file diff --git a/sigi/apps/espacos/templates/espacos/agenda_pdf.html b/sigi/apps/espacos/templates/espacos/agenda_pdf.html new file mode 100644 index 0000000..b9c661d --- /dev/null +++ b/sigi/apps/espacos/templates/espacos/agenda_pdf.html @@ -0,0 +1,108 @@ +{% extends 'pdf/base_report.html' %} +{% load static i18n sigi_tags %} + +{% block page_size %}A4 landscape{% endblock page_size %} + +{% block extra_style %} + {{ block.super }} + blockquote { + margin: 20px 0; + padding-left: 1.5rem; + border-left: 5px solid #ee6e73; + font-size: 1.1 rem; + font-weight: bold; + } + a { + color: black; + text-decoration: none; + } + table { + table-layout: fixed; + } + .calendar-table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #d2d2d2; + } + .calendar-table td+td { + border-left: 1px solid #d2d2d2 !important; + } + table td, + table td * { + vertical-align: top; + } + .calendar-table tr:nth-child(even) { + background-color: white !important; + } + tr.linha-dias { + background: #d2d2d2; + border-top: 1px solid #d2d2d2; + } + tr.linha-evento { + border-bottom: 1px solid #d2d2d2; + } + span.numero-dia { + font-size: 1em; + } + .card { + background-color: #fff; + padding: 15px; + margin: 10px 0; + } + .card .card-content .card-title { + display: block; + line-height: 32px; + margin-bottom: 8px; + font-weight: 300; + } + .card-title { + font-size: 20px !important; + margin-bottom: -6px !important; + } + .data-evento { + font-size: 1em; + display: block; + } + .tipo-evento { + font-size: 1em; + color: var(--body-quiet-color); + display: block; + margin-bottom: 8px; + } + .evento { + margin: 0; + padding: 5px 10px; + } + .cyan.lighten-4 { background-color: #b2ebf2!important; } + .red.lighten-4 { background-color: #ffcdd2!important; } + .purple.lighten-4 { background-color: #e1bee7!important; } + .blue.lighten-4 { background-color: #bbdefb!important; } + .orange.lighten-4 { background-color: #ffe0b2!important; } + .brown.lighten-4 { background-color: #d7ccc8 !important; } + @font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url('/static/material/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2') format('woff2'); + } + i.tiny { font-size: 1rem; } + .material-icons { + font-family: "Material Icons"; + font-weight: 400; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: "liga"; + -webkit-font-smoothing: antialiased; + } +{% endblock %} + +{% block main_content %} + {% include "espacos/snippets/agenda_cal.html" %} +{% endblock main_content %} \ No newline at end of file diff --git a/sigi/apps/espacos/templates/espacos/snippets/agenda_cal.html b/sigi/apps/espacos/templates/espacos/snippets/agenda_cal.html new file mode 100644 index 0000000..5418f46 --- /dev/null +++ b/sigi/apps/espacos/templates/espacos/snippets/agenda_cal.html @@ -0,0 +1,55 @@ +{% load i18n static sigi_tags %} + +{% for semana in semanas %} +
    +
    + {% blocktranslate with start=semana.datas|first|date:"SHORT_DATE_FORMAT" end=semana.datas|last|date:"SHORT_DATE_FORMAT" %} + Semana de {{ start }} a {{ end }} + {% endblocktranslate %} +
    + + + + + {% for name in day_names %} + + {% endfor %} + + + {% for dia in semana.datas %} + + {% endfor %} + + + + {% for espaco, reservas in semana.reservas.items %} + + + {% setvar 0 as last_pos %} + {% for reserva, tupla in reservas %} + {% for x in ""|ljust:tupla.0|make_list %}{% endfor %} + + {% if forloop.last %} + {% for x in ""|ljust:tupla.2|make_list %}{% endfor %} + {% endif %} + {% setvar last_pos|sum:tupla.1 as last_pos %} + {% empty %} + {% for x in "1234567"|make_list %}{% endfor %} + {% endfor %} + + {% endfor %} + +
    {% trans "Espaço" %}{{ name }}
    {{ dia|date:"d/m"}}
    {{ espaco.sigla }} +

    {{ reserva.proposito }}

    +

    {{ reserva.inicio|interval:reserva.termino }}

    +

    + {% blocktranslate with solicitante=reserva.solicitante %} + solicitado por {{ solicitante }} + {% endblocktranslate %} +

    +
    +
    + {% if pdf and not forloop.last %} +
    + {% endif %} +{% endfor %} \ No newline at end of file diff --git a/sigi/apps/espacos/tests.py b/sigi/apps/espacos/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/sigi/apps/espacos/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sigi/apps/espacos/views.py b/sigi/apps/espacos/views.py new file mode 100644 index 0000000..3849990 --- /dev/null +++ b/sigi/apps/espacos/views.py @@ -0,0 +1,126 @@ +import calendar +import locale +from typing import Any +from django import http +from django.db.models import Q +from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.translation import ( + to_locale, + get_language, + ngettext, + gettext as _, +) +from django.views.generic.base import TemplateView +from django_weasyprint.views import WeasyTemplateResponse +from sigi.apps.espacos.models import Espaco, Reserva + + +class Agenda(TemplateView): + def _is_pdf(self): + return bool(self.request.GET.get("pdf", 0)) + + def get_template_names(self): + if self._is_pdf(): + return ["espacos/agenda_pdf.html"] + else: + return ["espacos/agenda.html"] + + def get_context_data(self, **kwargs): + mes_pesquisa = int( + self.request.GET.get("mes", timezone.localdate().month) + ) + ano_pesquisa = int( + self.request.GET.get("ano", timezone.localdate().year) + ) + sel_espacos = self.request.GET.getlist( + "espaco", list(Espaco.objects.values_list("id", flat=True)) + ) + + meses = {} + lang = to_locale(get_language()) + ".UTF-8" + locale.setlocale(locale.LC_ALL, lang) + + for ano, mes in ( + Reserva.objects.values_list("inicio__year", "inicio__month") + .order_by("inicio__year", "inicio__month") + .distinct("inicio__year", "inicio__month") + ): + if ano in meses: + meses[ano][mes] = calendar.month_name[mes] + else: + meses[ano] = {mes: calendar.month_name[mes]} + + espacos = list(Espaco.objects.all()) + + semanas = [ + {"datas": s, "reservas": {espaco: [] for espaco in espacos}} + for s in calendar.Calendar().monthdatescalendar( + ano_pesquisa, mes_pesquisa + ) + ] + + primeiro_dia = timezone.make_aware( + timezone.datetime(*semanas[0]["datas"][0].timetuple()[:6]) + ) + ultimo_dia = timezone.make_aware( + timezone.datetime(*semanas[-1]["datas"][-1].timetuple()[:6]) + ) + + for reserva in Reserva.objects.exclude( + status=Reserva.STATUS_CANCELADO + ).filter( + Q(inicio__range=[primeiro_dia, ultimo_dia]) + | Q(termino__range=[primeiro_dia, ultimo_dia]) + ): + for semana in semanas: + if not ( + (reserva.termino.date() < semana["datas"][0]) + or (reserva.inicio.date() > semana["datas"][-1]) + ): + start = max(semana["datas"][0], reserva.inicio.date()) + end = min(semana["datas"][-1], reserva.termino.date()) + semana["reservas"][reserva.espaco].append( + [ + reserva, + [ + start.weekday(), + end.weekday() - start.weekday() + 1, + 6 - end.weekday(), + ], + ] + ) + + for semana in semanas: + for espaco, reservas in semana["reservas"].items(): + last_pos = 0 + for reserva in reservas: + if last_pos > 0: + reserva[1][0] -= last_pos + last_pos += reserva[1][0] + reserva[1][1] + + context = super().get_context_data(**kwargs) + context["mes_pesquisa"] = mes_pesquisa + context["ano_pesquisa"] = ano_pesquisa + context["sel_espacos"] = sel_espacos + context["meses"] = meses + context["espacos"] = Espaco.objects.all() + context["semanas"] = semanas + context["day_names"] = calendar.day_abbr + + if self._is_pdf(): + context["pdf"] = True + context["title"] = _("Reserva de espaços do ILB") + + return context + + def render_to_response(self, context, **response_kwargs): + self.response_class = TemplateResponse + self.content_type = None + if self._is_pdf(): + self.content_type = "application/pdf" + self.response_class = WeasyTemplateResponse + response_kwargs.setdefault( + "filename", f"agenda-{timezone.localdate()}.pdf" + ) + return super().render_to_response(context, **response_kwargs) diff --git a/sigi/apps/utils/templatetags/action_icons.py b/sigi/apps/utils/templatetags/action_icons.py index 29d5632..0001ffe 100644 --- a/sigi/apps/utils/templatetags/action_icons.py +++ b/sigi/apps/utils/templatetags/action_icons.py @@ -9,10 +9,18 @@ ACTION_LIST = { "calcular_data_uso": "functions", } +ACTION_PREFIXES = { + "cancelar": "cancel", + "ativar": "check_circle", +} + @register.simple_tag def action_icon(action_name): if action_name in ACTION_LIST: return ACTION_LIST[action_name] else: - return "play_arrow" + for prefix in ACTION_PREFIXES: + if prefix in action_name: + return ACTION_PREFIXES[prefix] + return "play_arrow" diff --git a/sigi/apps/utils/templatetags/sigi_tags.py b/sigi/apps/utils/templatetags/sigi_tags.py new file mode 100644 index 0000000..fb96f65 --- /dev/null +++ b/sigi/apps/utils/templatetags/sigi_tags.py @@ -0,0 +1,38 @@ +import datetime +from django import template +from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext as _ + +register = template.Library() + + +@register.filter +def interval(value, arg): + if not isinstance(value, datetime.datetime) or not isinstance( + arg, datetime.datetime + ): + return "" + value = timezone.localtime(value) + arg = timezone.localtime(arg) + if value.year != arg.year: + format_mask = "%d/%m/%Y às %H:%M" + elif value.month != arg.month: + format_mask = "%d/%m às %H:%M" + elif value.day != arg.day: + format_mask = "dia %d às %H:%M" + else: + format_mask = "%H:%M" + return _( + f"de {value.strftime(format_mask)} " f"a {arg.strftime(format_mask)}" + ) + + +@register.filter +def sum(value, arg): + return value + arg + + +@register.simple_tag +def setvar(val=None): + return val diff --git a/sigi/menu_conf.yaml b/sigi/menu_conf.yaml index e3721a3..8866a10 100644 --- a/sigi/menu_conf.yaml +++ b/sigi/menu_conf.yaml @@ -71,6 +71,14 @@ main_menu: - title: Painel de ocorrências view_name: ocorrencias_painel querystr: status=1&status=2 + - title: Reserva de espaços + icon: event + children: + - title: Reservas + view_name: admin:espacos_reserva_changelist + querystr: status=A + - title: Agenda de reservas + view_name: espacos_agenda - title: Eventos icon: school children: @@ -128,6 +136,10 @@ main_menu: view_name: admin:eventos_modelodeclaracao_changelist - title: Partidos políticos view_name: admin:parlamentares_partido_changelist + - title: Espaços para eventos + view_name: admin:espacos_espaco_changelist + - title: Recursos para eventos + view_name: admin:espacos_recurso_changelist contato_menu: - title: Casa legislativa diff --git a/sigi/settings.py b/sigi/settings.py index 6bf9165..0dfdf97 100644 --- a/sigi/settings.py +++ b/sigi/settings.py @@ -44,6 +44,7 @@ INSTALLED_APPS = [ "sigi.apps.casas", "sigi.apps.contatos", "sigi.apps.convenios", + "sigi.apps.espacos", "sigi.apps.eventos", "sigi.apps.home", "sigi.apps.inventario", @@ -132,7 +133,7 @@ FORM_RENDERER = "django.forms.renderers.TemplatesSetting" WSGI_APPLICATION = "sigi.wsgi.application" - +167808 # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ diff --git a/sigi/urls.py b/sigi/urls.py index 03b0500..7182899 100644 --- a/sigi/urls.py +++ b/sigi/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path("parlamentares/", include("sigi.apps.parlamentares.urls")), path("servicos/", include("sigi.apps.servicos.urls")), path("admin/casas/", include("sigi.apps.casas.admin_urls")), + path("admin/espacos/", include("sigi.apps.espacos.admin_urls")), path("admin/eventos/", include("sigi.apps.eventos.admin_urls")), path("admin/convenios/", include("sigi.apps.convenios.urls")), path("admin/ocorrencias/", include("sigi.apps.ocorrencias.admin_urls")),