From 9a7a78cbd40308f014c4cd9df60957a269eed891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ses=C3=B3stris=20Vieira?= Date: Tue, 19 Sep 2023 11:19:09 -0300 Subject: [PATCH] Adiciona API rest para expor eventos. Gertiq #160534 --- requirements/requirements.txt | 3 + sigi/apps/eventos/admin.py | 56 ++++- sigi/apps/eventos/api_urls.py | 15 ++ sigi/apps/eventos/forms.py | 14 ++ ...ricao_evento_contato_inscricao_and_more.py | 77 ++++++ sigi/apps/eventos/models.py | 47 +++- sigi/apps/eventos/serializers.py | 67 ++++++ sigi/apps/eventos/views.py | 224 ++---------------- sigi/apps/utils/pagination.py | 7 + sigi/settings.py | 11 + sigi/urls.py | 11 + 11 files changed, 331 insertions(+), 201 deletions(-) create mode 100644 sigi/apps/eventos/api_urls.py create mode 100644 sigi/apps/eventos/migrations/0044_evento_chave_inscricao_evento_contato_inscricao_and_more.py create mode 100644 sigi/apps/eventos/serializers.py create mode 100644 sigi/apps/utils/pagination.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e45ff39..daed0a0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,8 +7,10 @@ moodlepy==0.23.10 pandas==2.0.2 Pillow==9.5.0 psycopg2-binary==2.9.6 +PyYAML==6.0.1 python-docx==0.8.11 requests==2.31.0 +uritemplate==4.1.1 weasyprint==58.0 XlsxWriter==3.1.2 Django==4.2.4 @@ -20,5 +22,6 @@ django-filter==23.2 django-import-export==3.2.0 django-localflavor==4.0 django-material-admin==1.8.6 +djangorestframework==3.14.0 django-tinymce==3.6.1 django-weasyprint==2.2.0 \ No newline at end of file diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py index a27860c..0d45698 100644 --- a/sigi/apps/eventos/admin.py +++ b/sigi/apps/eventos/admin.py @@ -547,7 +547,60 @@ class ModeloDeclaracaoAdmin(admin.ModelAdmin): class EventoAdmin(CartExportMixin, admin.ModelAdmin): form = EventoAdminForm resource_class = EventoResource - date_hierarchy = "data_inicio" + fieldsets = ( + ( + None, + { + "fields": ( + "tipo_evento", + "nome", + "turma", + "descricao", + "virtual", + "solicitante", + "num_processo", + "data_pedido", + "data_recebido_coperi", + "data_inicio", + "data_termino", + "carga_horaria", + "casa_anfitria", + "contato", + "telefone", + "observacao", + ) + }, + ), + ( + _("Status"), + { + "fields": ( + "status", + "total_participantes", + "data_cancelamento", + "motivo_cancelamento", + ) + }, + ), + ( + _("Portal/Saberes"), + { + "fields": ( + "publicar", + "publico_alvo", + "local", + "moodle_courseid", + "chave_inscricao", + "perfil_aluno", + "observacao_inscricao", + "contato_inscricao", + "telefone_inscricao", + "banner", + ) + }, + ), + ) + list_display = ( "get_banner", "publicar", @@ -578,6 +631,7 @@ class EventoAdmin(CartExportMixin, admin.ModelAdmin): "virtual", "solicitante", ) + date_hierarchy = "data_inicio" autocomplete_fields = ( "tipo_evento", "casa_anfitria", diff --git a/sigi/apps/eventos/api_urls.py b/sigi/apps/eventos/api_urls.py new file mode 100644 index 0000000..2c2a4cd --- /dev/null +++ b/sigi/apps/eventos/api_urls.py @@ -0,0 +1,15 @@ +from django.urls import path, include +from sigi.apps.eventos import views + +urlpatterns = [ + path( + "evento//", + views.ApiEventoRetrieve.as_view(), + name="api_eventos_evento_view", + ), + path( + "evento/", + views.ApiEventoList.as_view(), + name="api_eventos_evento_list", + ), +] diff --git a/sigi/apps/eventos/forms.py b/sigi/apps/eventos/forms.py index 5083e47..31eb822 100644 --- a/sigi/apps/eventos/forms.py +++ b/sigi/apps/eventos/forms.py @@ -30,6 +30,11 @@ class EventoAdminForm(forms.ModelForm): "status", "publicar", "moodle_courseid", + "chave_inscricao", + "perfil_aluno", + "observacao_inscricao", + "contato_inscricao", + "telefone_inscricao", "contato", "telefone", "banner", @@ -41,6 +46,7 @@ class EventoAdminForm(forms.ModelForm): cleaned_data = super(EventoAdminForm, self).clean() data_inicio = cleaned_data.get("data_inicio") data_termino = cleaned_data.get("data_termino") + publicar = cleaned_data.get("publicar") if data_inicio and data_termino and data_inicio > data_termino: raise forms.ValidationError( @@ -48,6 +54,14 @@ class EventoAdminForm(forms.ModelForm): code="invalid_period", ) + if publicar and (data_inicio is None or data_termino is None): + raise forms.ValidationError( + _( + "Para publicar no site é preciso ter data início e data término" + ), + code="cannot_publish", + ) + class SelecionaModeloForm(forms.Form): modelo = forms.ModelChoiceField( diff --git a/sigi/apps/eventos/migrations/0044_evento_chave_inscricao_evento_contato_inscricao_and_more.py b/sigi/apps/eventos/migrations/0044_evento_chave_inscricao_evento_contato_inscricao_and_more.py new file mode 100644 index 0000000..47664fe --- /dev/null +++ b/sigi/apps/eventos/migrations/0044_evento_chave_inscricao_evento_contato_inscricao_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 4.2.4 on 2023-09-19 11:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("eventos", "0043_alter_solicitacao_estimativa_casas_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="evento", + name="chave_inscricao", + field=models.CharField( + blank=True, max_length=100, verbose_name="chave de inscrição" + ), + ), + migrations.AddField( + model_name="evento", + name="contato_inscricao", + field=models.CharField( + blank=True, + help_text="pessoa ou setor responsável por dar suporte aos alunos no processo de inscrição", + max_length=100, + verbose_name="contato para inscrição", + ), + ), + migrations.AddField( + model_name="evento", + name="observacao_inscricao", + field=models.TextField( + blank=True, + help_text="Mais detalhes para ajudar o aluno a se inscrever no curso", + verbose_name="Observações para inscrição", + ), + ), + migrations.AddField( + model_name="evento", + name="perfil_aluno", + field=models.URLField( + blank=True, + help_text="Link completo da página de perfil do aluno deste curso no Saberes", + verbose_name="Link do perfil do aluno", + ), + ), + migrations.AddField( + model_name="evento", + name="telefone_inscricao", + field=models.CharField( + blank=True, + help_text="telefone da pessoa ou setor responsável por dar suporte aos alunos no processo de inscrição", + max_length=30, + verbose_name="telefone do contato", + ), + ), + migrations.AlterField( + model_name="evento", + name="contato", + field=models.CharField( + blank=True, + help_text="pessoa de contato na casa anfitriã", + max_length=100, + verbose_name="contato", + ), + ), + migrations.AlterField( + model_name="evento", + name="telefone", + field=models.CharField( + blank=True, + help_text="telefone da pessoa de contato na casa anfitriã", + max_length=30, + verbose_name="tefone de contato", + ), + ), + ] diff --git a/sigi/apps/eventos/models.py b/sigi/apps/eventos/models.py index 073c7e6..84d8a9f 100644 --- a/sigi/apps/eventos/models.py +++ b/sigi/apps/eventos/models.py @@ -411,9 +411,52 @@ class Evento(models.Model): "quando o curso é criado no Saberes." ), ) - contato = models.CharField(_("contato"), max_length=100, blank=True) + chave_inscricao = models.CharField( + _("chave de inscrição"), max_length=100, blank=True + ) + perfil_aluno = models.URLField( + _("Link do perfil do aluno"), + blank=True, + help_text=_( + "Link completo da página de perfil do aluno deste curso no Saberes" + ), + ) + observacao_inscricao = models.TextField( + _("Observações para inscrição"), + blank=True, + help_text=_( + "Mais detalhes para ajudar o aluno a se inscrever no curso" + ), + ) + contato_inscricao = models.CharField( + _("contato para inscrição"), + max_length=100, + blank=True, + help_text=_( + "pessoa ou setor responsável por dar suporte aos alunos no " + "processo de inscrição" + ), + ) + telefone_inscricao = models.CharField( + _("telefone do contato"), + max_length=30, + blank=True, + help_text=_( + "telefone da pessoa ou setor responsável por dar suporte aos " + "alunos no processo de inscrição" + ), + ) + contato = models.CharField( + _("contato"), + max_length=100, + blank=True, + help_text=_("pessoa de contato na casa anfitriã"), + ) telefone = models.CharField( - _("tefone de contato"), max_length=30, blank=True + _("tefone de contato"), + max_length=30, + blank=True, + help_text=_("telefone da pessoa de contato na casa anfitriã"), ) banner = models.ImageField(_("banner do evento"), blank=True, null=True) data_cancelamento = models.DateField( diff --git a/sigi/apps/eventos/serializers.py b/sigi/apps/eventos/serializers.py new file mode 100644 index 0000000..5557562 --- /dev/null +++ b/sigi/apps/eventos/serializers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers +from sigi.apps.eventos.models import Evento + + +class EventoSerializer(serializers.ModelSerializer): + casa_nome = serializers.SerializerMethodField("get_casa_nome") + casa_logradouro = serializers.SerializerMethodField("get_casa_logradouro") + casa_bairro = serializers.SerializerMethodField("get_casa_bairro") + casa_municipio = serializers.SerializerMethodField("get_casa_municipio") + casa_uf = serializers.SerializerMethodField("get_casa_uf") + casa_cep = serializers.SerializerMethodField("get_casa_cep") + + class Meta: + model = Evento + fields = [ + "id", + "nome", + "turma", + "publico_alvo", + "data_inicio", + "data_termino", + "carga_horaria", + "local", + "casa_nome", + "casa_logradouro", + "casa_bairro", + "casa_municipio", + "casa_uf", + "casa_cep", + "link_inscricao", + "chave_inscricao", + "perfil_aluno", + "observacao_inscricao", + "contato_inscricao", + "telefone_inscricao", + "banner", + ] + + def get_casa_nome(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.nome + return "" + + def get_casa_logradouro(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.logradouro + return "" + + def get_casa_bairro(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.bairro + return "" + + def get_casa_municipio(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.municipio.nome + return "" + + def get_casa_uf(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.municipio.uf.nome + return "" + + def get_casa_cep(self, obj): + if obj.casa_anfitria: + return obj.casa_anfitria.cep + return "" diff --git a/sigi/apps/eventos/views.py b/sigi/apps/eventos/views.py index e7dce1e..35e2889 100644 --- a/sigi/apps/eventos/views.py +++ b/sigi/apps/eventos/views.py @@ -2,6 +2,7 @@ import calendar import csv import locale from functools import reduce +from rest_framework import mixins, generics from typing import OrderedDict from django import forms from django.contrib import messages @@ -35,6 +36,7 @@ from sigi.apps.eventos.forms import ( FuncionarioForm, ParlamentarForm, ) +from sigi.apps.eventos.serializers import EventoSerializer from sigi.apps.parlamentares.models import Parlamentar from sigi.apps.servidores.models import Servidor @@ -362,201 +364,27 @@ def alocacao_equipe(request): return render(request, "eventos/alocacao_equipe.html", context) -# # Views e functions para carrinho de exportação - -# def query_ordena(qs, o): -# from sigi.apps.eventos.admin import EventoAdmin -# list_display = EventoAdmin.list_display -# order_fields = [] - -# for order_number in o.split('.'): -# order_number = int(order_number) -# order = '' -# if order_number != abs(order_number): -# order_number = abs(order_number) -# order = '-' -# order_fields.append(order + list_display[order_number - 1]) -# qs = qs.order_by(*order_fields) -# return qs - -# def get_for_qs(get, qs): -# kwargs = {} -# for k, v in get.iteritems(): -# if str(k) not in ('page', 'pop', 'q', '_popup', 'o', 'ot'): -# kwargs[str(k)] = v -# qs = qs.filter(**kwargs) -# if 'o' in get: -# qs = query_ordena(qs, get['o']) -# return qs - -# def carrinhoOrGet_for_qs(request): -# if 'carrinho_eventos' in request.session: -# ids = request.session['carrinho_eventos'] -# qs = Evento.objects.filter(pk__in=ids) -# else: -# qs = Evento.objects.all() -# if request.GET: -# qs = get_for_qs(request.GET, qs) -# return qs - -# def adicionar_eventos_carrinho(request, queryset=None, id=None): -# if request.method == 'POST': -# ids_selecionados = request.POST.getlist('_selected_action') -# if 'carrinho_eventos' not in request.session: -# request.session['carrinho_eventos'] = ids_selecionados -# else: -# lista = request.session['carrinho_eventos'] -# # Verifica se id já não está adicionado -# for id in ids_selecionados: -# if id not in lista: -# lista.append(id) -# request.session['carrinho_eventos'] = lista - -# @login_required -# def visualizar_carrinho(request): -# qs = carrinhoOrGet_for_qs(request) -# paginator = Paginator(qs, 100) - -# try: -# page = int(request.GET.get('page', '1')) -# except ValueError: -# page = 1 - -# try: -# paginas = paginator.page(page) -# except (EmptyPage, InvalidPage): -# paginas = paginator.page(paginator.num_pages) - -# carrinhoIsEmpty = not('carrinho_eventos' in request.session) - -# return render( -# request, -# 'eventos/carrinho.html', -# { -# 'carIsEmpty': carrinhoIsEmpty, -# 'paginas': paginas, -# 'query_str': '?' + request.META['QUERY_STRING'] -# } -# ) - -# @login_required -# def excluir_carrinho(request): -# if 'carrinho_eventos' in request.session: -# del request.session['carrinho_eventos'] -# messages.info(request, 'O carrinho foi esvaziado') -# return HttpResponseRedirect('../../') - -# @login_required -# def deleta_itens_carrinho(request): -# if request.method == 'POST': -# ids_selecionados = request.POST.getlist('_selected_action') -# removed = 0 -# if 'carrinho_eventos' in request.session: -# lista = request.session['carrinho_eventos'] -# for item in ids_selecionados: -# lista.remove(item) -# removed += 1 -# if lista: -# request.session['carrinho_eventos'] = lista -# else: -# del lista -# del request.session['carrinho_eventos'] -# messages.info(request, "{0} itens removidos do carrinho".format(removed)) -# return HttpResponseRedirect('.') - -# @login_required -# def export_csv(request): -# def rm_rows(lista,reg): -# for a in lista: -# if a in lista: -# reg.pop(a,None) -# else: -# pass - -# def serialize(r, field): -# value = (getattr(r, 'get_{0}_display'.format(field.name), None) or -# getattr(r, field.name, "")) -# if callable(value): -# value = value() -# if value is None: -# value = "" -# return unicode(value).encode('utf8') - -# eventos = carrinhoOrGet_for_qs(request) -# eventos.select_related('equipe', 'convite') - -# if not eventos: -# messages.info(request, _("Nenhum evento a exportar")) -# return HttpResponseRedirect('../') - -# max_equipe = max([e.equipe_set.count() for e in eventos]) - -# mun_casa = 'Município da Casa Anfitriã'.encode('utf8') -# uf_casa = 'UF da Casa Anfitriã'.encode('utf8') -# reg_casa = 'Região da Casa Anfitriã'.encode('utf8') - -# head = [f.verbose_name.encode('utf8') for f in Evento._meta.fields] -# head.extend([mun_casa, uf_casa, reg_casa]) -# head.extend([f.verbose_name.encode('utf8')+"_{0}".format(i+1) -# for i in range(max_equipe) for f in Equipe._meta.fields -# if f.name not in ('id', 'evento')]) -# head.extend([f.verbose_name.encode('utf8') for f in Convite._meta.fields -# if f.name not in ('id', 'evento')]) -# head.extend([f.verbose_name.encode('utf8') for f in Modulo._meta.fields -# if f.name not in ('id', 'evento')]) - -# response = HttpResponse(content_type='text/csv') -# response['Content-Disposition'] = 'attachment; filename=eventos.csv' -# rm_list = ['Descrição do evento', 'Local do evento', 'Público alvo', 'Motivo do cancelamento', 'Descrição do módulo'] - -# for a in head: -# if 'Observações_' in a: -# rm_list.append(a) - -# for a in rm_list: -# if a in head: -# head.remove(a) -# else: -# pass -# writer = csv.DictWriter(response, fieldnames=head) -# writer.writeheader() - -# for evento in eventos: -# reg = {f.verbose_name.encode('utf8'): serialize(evento, f) -# for f in Evento._meta.fields} -# if evento.casa_anfitria is None: -# reg[mun_casa] = "" -# reg[uf_casa] = "" -# reg[reg_casa] = "" -# else: -# reg[mun_casa] = evento.casa_anfitria.municipio.nome.encode('utf8') -# reg[uf_casa] = evento.casa_anfitria.municipio.uf.sigla.\ -# encode('utf8') -# reg[reg_casa] = evento.casa_anfitria.municipio.uf.\ -# get_regiao_display().encode('utf8') - -# idx = 1 -# for membro in evento.equipe_set.all(): -# reg.update( -# { -# "{0}_{1}".format(f.verbose_name.encode('utf8'), idx): -# serialize(membro, f) for f in Equipe._meta.fields -# if f.name not in ('id', 'evento') -# } -# ) -# idx += 1 -# for convite in evento.convite_set.all(): -# reg.update( -# {f.verbose_name.encode('utf8'): serialize(convite, f) -# for f in Convite._meta.fields -# if f.name not in ('id', 'evento')} -# ) -# rm_rows(rm_list,reg) -# writer.writerow(reg) - -# if evento.convite_set.count() == 0: -# rm_rows(rm_list,reg) - -# writer.writerow(reg) - -# return response +class ApiEventoAbstract: + queryset = ( + Evento.objects.filter(publicar=True) + .exclude(data_inicio=None) + .exclude(data_termino=None) + .order_by("-data_inicio") + ) + serializer_class = EventoSerializer + + +class ApiEventoList(ApiEventoAbstract, generics.ListAPIView): + """ + Lista de eventos, oficinas e cursos realizados pelo ILB / Interlegis + """ + + pass + + +class ApiEventoRetrieve(ApiEventoAbstract, generics.RetrieveAPIView): + """ + Recupera um evento pelo id + """ + + pass diff --git a/sigi/apps/utils/pagination.py b/sigi/apps/utils/pagination.py new file mode 100644 index 0000000..6bc3b98 --- /dev/null +++ b/sigi/apps/utils/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination +from rest_framework.settings import api_settings + + +class SigiPageNumberPagination(PageNumberPagination): + page_size_query_param = "page_size" + max_page_size = 100 diff --git a/sigi/settings.py b/sigi/settings.py index 70aec83..0e45116 100644 --- a/sigi/settings.py +++ b/sigi/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ "import_export", "tinymce", "django.forms", + "rest_framework", "material", "material.admin", "django.contrib.auth", @@ -258,6 +259,16 @@ TINYMCE_DEFAULT_CONFIG = { "toolbar3": "table tabledelete | tableprops tablerowprops tablecellprops | tableinsertrowbefore tableinsertrowafter tabledeleterow tablerowheader | tableinsertcolbefore tableinsertcolafter tabledeletecol tablecolheader | tablemergecells tablesplitcells | tablecellbackgroundcolor tablecellbordercolor tablecellborderwidth tablecellborderstyle", } +# Rest Framework settings + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PAGINATION_CLASS": "sigi.apps.utils.pagination.SigiPageNumberPagination", + "PAGE_SIZE": 100, +} + # SIGI specific settings MENU_FILE = BASE_DIR / "menu_conf.yaml" diff --git a/sigi/urls.py b/sigi/urls.py index 2fd593f..843dc04 100644 --- a/sigi/urls.py +++ b/sigi/urls.py @@ -13,6 +13,7 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from rest_framework.schemas import get_schema_view from django.contrib import admin from django.urls import path, include from django.conf import settings @@ -28,6 +29,16 @@ urlpatterns = [ path("admin/convenios/", include("sigi.apps.convenios.urls")), path("admin/ocorrencias/", include("sigi.apps.ocorrencias.admin_urls")), path("admin/", admin.site.urls), + path( + "api/", + get_schema_view( + title="SIGI Open API Schema", + description="API for SIGI opendata", + version="1.0.0", + ), + name="openapi-schema", + ), + path("api/eventos/", include("sigi.apps.eventos.api_urls")), path("tinymce/", include("tinymce.urls")), path("accounts/", include("sigi.apps.home.accounts_urls")), path("", include("sigi.apps.home.urls")),