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