mirror of https://github.com/interlegis/sigi.git
Sesóstris Vieira
1 year ago
19 changed files with 1001 additions and 2 deletions
@ -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( |
||||
|
"<path:object_id>/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, |
||||
|
) |
@ -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"), |
||||
|
] |
@ -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") |
@ -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",), |
||||
|
}, |
||||
|
), |
||||
|
] |
@ -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)] |
@ -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}") |
@ -0,0 +1,15 @@ |
|||||
|
{% extends "admin/change_form.html" %} |
||||
|
{% load i18n admin_urls %} |
||||
|
|
||||
|
{% block object-tools-items %} |
||||
|
{% if object_id %} |
||||
|
<li> |
||||
|
{% url opts|admin_urlname:'cancel' object_id|admin_urlquote as tool_url %} |
||||
|
<a href="{% add_preserved_filters tool_url %}"> |
||||
|
<i class="left material-icons" aria-hidden="true">cancel</i> |
||||
|
{% trans "Cancelar reserva" %} |
||||
|
</a> |
||||
|
</li> |
||||
|
{% endif %} |
||||
|
{{ block.super }} |
||||
|
{% endblock %} |
@ -0,0 +1,44 @@ |
|||||
|
{% extends "eventos/calendario.html" %} |
||||
|
{% load i18n static sigi_tags %} |
||||
|
|
||||
|
{% block extrastyle %} |
||||
|
{{ block.super }} |
||||
|
<style> |
||||
|
tr.linha-evento { |
||||
|
border-bottom: 1px solid var(--hairline-color); |
||||
|
} |
||||
|
tr td { |
||||
|
border-left: 1px solid var(--hairline-color); |
||||
|
} |
||||
|
</style> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="fixed-action-btn"> |
||||
|
<a class="btn-floating"> |
||||
|
<i class="large material-icons">mode_edit</i> |
||||
|
</a> |
||||
|
<ul> |
||||
|
<li> |
||||
|
<a class="btn-floating btn-small" href="?ano={{ ano_pesquisa|safe }}&mes={{ mes_pesquisa|safe }}&pdf=1" target="_blank"><i class="material-icons">picture_as_pdf</i></a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col s12"> |
||||
|
<ul class="tabs"> |
||||
|
{% for ano in meses %} |
||||
|
<li class="tab col"><a {% if ano == ano_pesquisa %}class="active"{% endif %} href="#tab-{{ ano|safe }}">{{ ano| safe }}</a></li> |
||||
|
{% endfor %} |
||||
|
</ul> |
||||
|
</div> |
||||
|
{% for ano, lista in meses.items %} |
||||
|
<div id="tab-{{ ano|safe }}" class="col s12"> |
||||
|
{% for mes, nome in lista.items %} |
||||
|
<a class="waves-effect waves-light btn-flat btn-small{% if ano == ano_pesquisa and mes == mes_pesquisa %} disabled{% endif %}" href="?ano={{ ano|safe }}&mes={{ mes|safe }}">{{ nome }}</a> |
||||
|
{% endfor %} |
||||
|
</div> |
||||
|
{% endfor %} |
||||
|
</div> |
||||
|
{% include "espacos/snippets/agenda_cal.html" %} |
||||
|
{% endblock %} |
@ -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 %} |
@ -0,0 +1,55 @@ |
|||||
|
{% load i18n static sigi_tags %} |
||||
|
|
||||
|
{% for semana in semanas %} |
||||
|
<div class="card-panel"> |
||||
|
<blockquote> |
||||
|
{% blocktranslate with start=semana.datas|first|date:"SHORT_DATE_FORMAT" end=semana.datas|last|date:"SHORT_DATE_FORMAT" %} |
||||
|
Semana de {{ start }} a {{ end }} |
||||
|
{% endblocktranslate %} |
||||
|
</blockquote> |
||||
|
<table class="calendar-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th rowspan="2">{% trans "Espaço" %}</th> |
||||
|
{% for name in day_names %} |
||||
|
<th>{{ name }}</th> |
||||
|
{% endfor %} |
||||
|
</tr> |
||||
|
<tr> |
||||
|
{% for dia in semana.datas %} |
||||
|
<th>{{ dia|date:"d/m"}}</th> |
||||
|
{% endfor %} |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{% for espaco, reservas in semana.reservas.items %} |
||||
|
<tr class="linha-evento"> |
||||
|
<th>{{ espaco.sigla }}</th> |
||||
|
{% setvar 0 as last_pos %} |
||||
|
{% for reserva, tupla in reservas %} |
||||
|
{% for x in ""|ljust:tupla.0|make_list %}<td></td>{% endfor %} |
||||
|
<td colspan="{{ tupla.1 }}" class="blue lighten-4"> |
||||
|
<p><strong>{{ reserva.proposito }}</strong></p> |
||||
|
<p>{{ reserva.inicio|interval:reserva.termino }}</p> |
||||
|
<p> |
||||
|
{% blocktranslate with solicitante=reserva.solicitante %} |
||||
|
solicitado por {{ solicitante }} |
||||
|
{% endblocktranslate %} |
||||
|
</p> |
||||
|
</td> |
||||
|
{% if forloop.last %} |
||||
|
{% for x in ""|ljust:tupla.2|make_list %}<td></td>{% endfor %} |
||||
|
{% endif %} |
||||
|
{% setvar last_pos|sum:tupla.1 as last_pos %} |
||||
|
{% empty %} |
||||
|
{% for x in "1234567"|make_list %}<td></td>{% endfor %} |
||||
|
{% endfor %} |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
{% if pdf and not forloop.last %} |
||||
|
<div class="new-page"></div> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
@ -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) |
@ -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 |
Loading…
Reference in new issue