Skip to content
Merged

Dev #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
# Presente!
# <img src="static/img/presente-icon.svg" alt="presente!" width="48" align="top"> presente!

O **Presente!** é um sistema open source para gerenciamento de frequência em atividades extra-classe no IFRN.
Professores e gestores podem cadastrar atividades ou eventos, e os participantes registram sua presença por meio de QR Codes ou códigos em texto.
Sistema open source para gerenciamento de presença em atividades extra-classe do IFRN. Professores e gestores cadastram atividades, e os participantes registram presença através de QR Codes dinâmicos.

O sistema utiliza o **SUAP** para autenticação e identificação automática das turmas, permitindo a geração de relatórios de frequência de forma integrada.
Utiliza o **SUAP** para autenticação institucional e identificação dos participantes, com geração de relatórios integrada.

---

## Funcionalidades

* Registro de atividades extra-classe autorizadas pelo professor.
* Registro de presença pelos próprios participantes, por meio de QR-Code.
* Validação automática ou manual da presença pelo administrador.
* Mecanismos para prevenção de fraudes:
* QR-Code/código de verificação dinâmico com tempo de atualização configurável;
* Registro de IP e *timestamp* no momento do registro;
* Possibilidade de restringir o acesso à rede local do campus.
* Autenticação via SUAP, com identificação automática dos participantes.
* Geração de relatórios de frequência e participação com filtros por curso, turma, turno, etc.
* Interface simples e objetiva.
### Atividades
* CRUD completo com suporte a múltiplos responsáveis
* Controle automático de status (não iniciada, ativa, encerrada)
* Página pública com QR Code para registro de presença
* Sincronização de tempo com o servidor
* Recarregamento automático da interface ao término

### Presença
* Registro via QR Code dinâmico ou link direto
* QR Codes com renovação configurável (padrão: 30 segundos)
* Tokens com validação de expiração
* Histórico completo por atividade e por usuário
* Contadores em tempo real

### Segurança
* Restrição por endereço IP ou faixa de rede (CIDR)
* Gerenciamento de redes permitidas (superusuários)
* Registro de IP e timestamp em cada marcação
* Sincronização de relógio cliente-servidor para prevenir manipulação

### Relatórios
* Filtros por nome, tipo de usuário, curso e período
* Seleção de colunas para impressão
* Ordenação customizável
* Formato otimizado para impressão física

### Interface
* Design responsivo (Bootstrap/AdminLTE)
* Atualizações via HTMX (sem reload completo)
* Contadores regressivos em tempo real
* Feedback visual de erros e sucessos

### Administração
* Painel para superusuários
* Gerenciamento de usuários e redes
* Visualização de todas as atividades do sistema

---

Expand Down
2 changes: 2 additions & 0 deletions presente/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ class AttendancePrintConfigForm(forms.Form):
("-type", _("Tipo (Z-A)")),
("curso", _("Curso (A-Z)")),
("-curso", _("Curso (Z-A)")),
("periodo", _("Período (A-Z)")),
("-periodo", _("Período (Z-A)")),
]

columns = forms.MultipleChoiceField(
Expand Down
17 changes: 11 additions & 6 deletions presente/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@
name="activity_attendances",
),
path(
"activity/<int:pk>/attendances/print/config/",
views.ActivityAttendancePrintConfigView.as_view(),
name="activity_attendance_print_config",
"activity/<int:pk>/attendances/export/config/",
views.ActivityAttendanceExportConfigView.as_view(),
name="activity_attendance_export_config",
),
path(
"activity/<int:pk>/attendances/print/",
views.ActivityAttendancePrintView.as_view(),
name="activity_attendance_print",
"activity/<int:pk>/attendances/pdf/",
views.ActivityAttendancePDFView.as_view(),
name="activity_attendance_pdf",
),
path(
"activity/<int:pk>/attendances/csv/",
views.ActivityAttendanceCSVExportView.as_view(),
name="activity_attendance_csv",
),
path(
"activity/<int:activity_pk>/attendance/<int:pk>/delete/",
Expand Down
137 changes: 133 additions & 4 deletions presente/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.http import Http404
from django.urls import reverse, reverse_lazy
from django_filters.views import FilterView
from django_weasyprint import WeasyTemplateResponseMixin
from core.mixins import PageTitleMixin, SuperuserRequiredMixin
from core.views import (
CoreListView,
Expand All @@ -20,6 +21,10 @@
import qrcode.image.svg
from io import BytesIO
import base64
import os
import csv
from django.conf import settings
from django.http import HttpResponse
from .models import Activity, Attendance, Network
from .tables import (
ActivityTable,
Expand Down Expand Up @@ -363,19 +368,23 @@ def get_success_url(self):
)


class ActivityAttendancePrintConfigView(
class ActivityAttendanceExportConfigView(
ActivityOwnerMixin,
LoginRequiredMixin,
TemplateView,
):
template_name = "presente/includes/attendance_print_config_modal.html"
template_name = "presente/includes/attendance_export_config_modal.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
activity = self.get_activity()
context["activity"] = activity
context["form"] = AttendancePrintConfigForm()

# Get current sort setting from page
current_sort = self.request.GET.get("sort_by", "name")
context["current_sort"] = current_sort

# Convert GET params to dict for easier template iteration
filter_params = {}
for key, value in self.request.GET.items():
Expand All @@ -386,15 +395,24 @@ def get_context_data(self, **kwargs):
return context


class ActivityAttendancePrintView(
class ActivityAttendancePDFView(
WeasyTemplateResponseMixin,
ActivityOwnerMixin,
LoginRequiredMixin,
FilterView,
):
model = Attendance
filterset_class = ActivityAttendanceFilter
template_name = "presente/attendance_print.html"
template_name = "presente/attendance_pdf.html"
context_object_name = "attendances"
pdf_filename = "relatorio_presencas.pdf"

def get_pdf_filename(self):
activity = self.get_activity()
safe_title = "".join(
c if c.isalnum() or c in (" ", "-", "_") else "" for c in activity.title
).replace(" ", "_")
return f"presencas_{safe_title}.pdf"

def get_queryset(self):
activity = self.get_activity()
Expand All @@ -413,6 +431,8 @@ def get_queryset(self):
"-type": "-user__type",
"curso": "user__curso",
"-curso": "-user__curso",
"periodo": "user__periodo_referencia",
"-periodo": "-user__periodo_referencia",
"checked_in_at": "checked_in_at",
"-checked_in_at": "-checked_in_at",
}
Expand Down Expand Up @@ -458,9 +478,118 @@ def get_context_data(self, **kwargs):
context["filter_info"] = filter_info
context["generated_at"] = timezone.now()

# Add absolute path to logo for WeasyPrint
logo_path = os.path.join(
settings.BASE_DIR, "static", "img", "presente-icon.svg"
)
context["logo_path"] = logo_path

return context


class ActivityAttendanceCSVExportView(
ActivityOwnerMixin,
LoginRequiredMixin,
FilterView,
):
model = Attendance
filterset_class = ActivityAttendanceFilter

def get_queryset(self):
activity = self.get_activity()
qs = Attendance.objects.filter(activity=activity).select_related("user")

# Apply sorting
sort_by = self.request.GET.get("sort_by", "name")
sort_mapping = {
"name": "user__full_name",
"-name": "-user__full_name",
"type": "user__type",
"-type": "-user__type",
"curso": "user__curso",
"-curso": "-user__curso",
"periodo": "user__periodo_referencia",
"-periodo": "-user__periodo_referencia",
"checked_in_at": "checked_in_at",
"-checked_in_at": "-checked_in_at",
}
sort_field = sort_mapping.get(sort_by, "user__full_name")
qs = qs.order_by(sort_field)

return qs

def get(self, request, *args, **kwargs):
activity = self.get_activity()
filterset = self.filterset_class(request.GET, queryset=self.get_queryset())
queryset = filterset.qs

# Get column configuration
columns = request.GET.getlist("columns")
if not columns:
columns = ["number", "name", "matricula", "checked_in_at"]

# Generate filename
safe_title = "".join(
c if c.isalnum() or c in (" ", "-", "_") else "" for c in activity.title
).replace(" ", "_")
filename = f"presencas_{safe_title}.csv"

# Create CSV response
response = HttpResponse(content_type="text/csv; charset=utf-8-sig")
response["Content-Disposition"] = f'attachment; filename="{filename}"'

writer = csv.writer(response)

# Write header row
header = []
if "number" in columns:
header.append("#")
if "name" in columns:
header.append("Nome")
if "email" in columns:
header.append("Email")
if "matricula" in columns:
header.append("Matrícula")
if "type" in columns:
header.append("Tipo")
if "curso" in columns:
header.append("Curso")
if "periodo" in columns:
header.append("Período")
if "checked_in_at" in columns:
header.append("Registrado em")
if "ip_address" in columns:
header.append("Rede")

writer.writerow(header)

# Write data rows
for idx, attendance in enumerate(queryset, 1):
row = []
if "number" in columns:
row.append(idx)
if "name" in columns:
row.append(attendance.user.get_full_name())
if "email" in columns:
row.append(attendance.user.email or "-")
if "matricula" in columns:
row.append(attendance.user.matricula or "-")
if "type" in columns:
row.append(attendance.user.get_type_display())
if "curso" in columns:
row.append(attendance.user.curso or "-")
if "periodo" in columns:
row.append(attendance.user.periodo_referencia or "-")
if "checked_in_at" in columns:
row.append(attendance.checked_in_at.strftime("%d/%m/%Y %H:%M:%S"))
if "ip_address" in columns:
row.append(attendance.get_network_name())

writer.writerow(row)

return response


# Network CRUD Views


Expand Down
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
asgiref==3.9.2
brotli==1.2.0
certifi==2025.8.3
cffi==2.0.0
cfgv==3.4.0
charset-normalizer==3.4.3
crispy-bootstrap5==2025.6
cryptography==46.0.2
cssselect2==0.8.0
distlib==0.4.0
Django==5.2.7
django-allauth==65.11.2
Expand All @@ -16,7 +18,9 @@ django-simple-menu==2.1.4
django-tables2==2.7.5
django-taggit==6.1.0
django-tinymce==4.1.0
django-weasyprint==2.4.0
filelock==3.19.1
fonttools==4.61.0
identify==2.6.15
idna==3.10
nodeenv==1.9.1
Expand All @@ -25,12 +29,19 @@ pillow==12.0.0
platformdirs==4.4.0
pre_commit==4.3.0
pycparser==2.23
pydyf==0.12.1
PyJWT==2.10.1
pyphen==0.17.2
python-dotenv==1.1.1
PyYAML==6.0.3
qrcode==8.2
requests==2.32.5
ruff==0.13.3
sqlparse==0.5.3
tinycss2==1.5.1
tinyhtml5==2.0.0
urllib3==2.5.0
virtualenv==20.34.0
weasyprint==67.0
webencodings==0.5.1
zopfli==0.4.0
Loading