From f1e9f30f39317296996b2330c3627bcbc31c5a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ses=C3=B3stris=20Vieira?= Date: Mon, 2 Oct 2023 11:18:42 -0300 Subject: [PATCH] =?UTF-8?q?Aprimora=20sincroniza=C3=A7=C3=A3o=20de=20inscr?= =?UTF-8?q?itos=20e=20aprovados=20SIGI=20x=20Saberes.=20Gertiq=20#163655?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sigi/apps/eventos/admin.py | 57 ++++----- ...eres_evento_data_sincronizacao_and_more.py | 50 ++++++++ sigi/apps/eventos/models.py | 114 ++++++++++++++++-- sigi/settings.py | 6 + 4 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 sigi/apps/eventos/migrations/0049_evento_aprovados_saberes_evento_data_sincronizacao_and_more.py diff --git a/sigi/apps/eventos/admin.py b/sigi/apps/eventos/admin.py index 28d1191..d07a2db 100644 --- a/sigi/apps/eventos/admin.py +++ b/sigi/apps/eventos/admin.py @@ -89,8 +89,6 @@ class SolicitacaoResource(LabeledResourse): class EventoResource(ValueLabeledResource): - # categoria_evento = Field(column_name="tipo_evento__categoria") - # status = Field(column_name="status") class Meta: model = Evento fields = ( @@ -119,6 +117,9 @@ class EventoResource(ValueLabeledResource): "observacao", "publico_alvo", "total_participantes", + "inscritos_saberes", + "aprovados_saberes", + "data_sincronizacao", "status", "data_cancelamento", "motivo_cancelamento", @@ -594,11 +595,14 @@ class EventoAdmin(CartExportMixin, admin.ModelAdmin): }, ), ( - _("Status"), + _("Status/participação"), { "fields": ( "status", "total_participantes", + "inscritos_saberes", + "aprovados_saberes", + "data_sincronizacao", "data_cancelamento", "motivo_cancelamento", ) @@ -666,6 +670,11 @@ class EventoAdmin(CartExportMixin, admin.ModelAdmin): "solicitante", "num_processo", ) + readonly_fields = ( + "inscritos_saberes", + "aprovados_saberes", + "data_sincronizacao", + ) inlines = ( EquipeInline, ConviteInline, @@ -1146,41 +1155,33 @@ class EventoAdmin(CartExportMixin, admin.ModelAdmin): ) return redirect(change_url) - api_url = f"{settings.MOODLE_BASE_URL}/webservice/rest/server.php" - mws = Moodle(api_url, settings.MOODLE_API_TOKEN) try: - inscritos = mws.post( - "core_enrol_get_enrolled_users", - courseid=evento.moodle_courseid, - ) - except Exception as e: + evento.sincroniza_saberes() + except Evento.SaberesSyncException as e: self.message_user( request, - _( - "Ocorreu um erro ao acessar o curso no Saberes com " - f"a mensagem {e.message}" - ), + _(f"Erro ao sincronizar dados do Saberes: '{e.message}'"), level=messages.ERROR, ) return redirect(change_url) - evento.total_participantes = len( - list( - filter( - lambda u: any( - r["roleid"] in settings.MOODLE_STUDENT_ROLES - for r in u["roles"] - ), - inscritos, - ) - ) - ) - evento.save() + self.message_user( request, _( - f"Foram encontrados {evento.total_participantes} alunos " - "no Saberes" + f"Foram encontrados {evento.inscritos_saberes} alunos " + f"no Saberes. Destes, {evento.aprovados_saberes} concluíram." ), level=messages.SUCCESS, ) + if evento.total_participantes != evento.inscritos_saberes: + self.message_user( + request, + _( + "O total de participantes ficou em " + f"{evento.total_participantes} alunos, pois o campo " + "já estava preenchido." + ), + level=messages.WARNING, + ) + return redirect(change_url) diff --git a/sigi/apps/eventos/migrations/0049_evento_aprovados_saberes_evento_data_sincronizacao_and_more.py b/sigi/apps/eventos/migrations/0049_evento_aprovados_saberes_evento_data_sincronizacao_and_more.py new file mode 100644 index 0000000..969f6e2 --- /dev/null +++ b/sigi/apps/eventos/migrations/0049_evento_aprovados_saberes_evento_data_sincronizacao_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.2.4 on 2023-10-02 11:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("eventos", "0048_evento_set_defaults"), + ] + + operations = [ + migrations.AddField( + model_name="evento", + name="aprovados_saberes", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="Número de pessoas que concluíram o curso no Saberes. Computado via integração SIGI x Saberes.", + verbose_name="aprovados no Saberes", + ), + ), + migrations.AddField( + model_name="evento", + name="data_sincronizacao", + field=models.DateTimeField( + editable=False, + null=True, + verbose_name="data da última sincronização com Saberes", + ), + ), + migrations.AddField( + model_name="evento", + name="inscritos_saberes", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="Número de pessoas que se inscreveram no evento no Saberes. Computado via integração SIGI x Saberes.", + verbose_name="inscritos no Saberes", + ), + ), + migrations.AlterField( + model_name="evento", + name="total_participantes", + field=models.PositiveIntegerField( + default=0, + help_text="Se informar quantidade de participantes na aba de convites, este campo será ajustado com a somatória dos participantes naquela aba. Senão, será igual ao número de inscritos no Saberes.", + verbose_name="total de participantes", + ), + ), + ] diff --git a/sigi/apps/eventos/models.py b/sigi/apps/eventos/models.py index 5b73ff4..85f5b4c 100644 --- a/sigi/apps/eventos/models.py +++ b/sigi/apps/eventos/models.py @@ -1,7 +1,9 @@ from collections.abc import Iterable import datetime import re +from moodle import Moodle from tinymce.models import HTMLField +from django.conf import settings from django.contrib import admin from django.core.validators import RegexValidator from django.core.exceptions import ValidationError @@ -306,6 +308,11 @@ class AnexoSolicitacao(models.Model): class Evento(models.Model): + class SaberesSyncException(Exception): + @property + def message(self): + return str(self) + STATUS_PLANEJAMENTO = "E" STATUS_AGUARDANDOSIGAD = "G" STATUS_PREVISAO = "P" @@ -404,13 +411,37 @@ class Evento(models.Model): observacao = models.TextField(_("Observações e anotações"), blank=True) publico_alvo = models.TextField(_("Público alvo"), blank=True) total_participantes = models.PositiveIntegerField( - _("Total de participantes"), + _("total de participantes"), default=0, help_text=_( "Se informar quantidade de participantes na aba de " "convites, este campo será ajustado com a somatória " - "dos participantes naquela aba." + "dos participantes naquela aba. Senão, será igual ao número de " + "inscritos no Saberes." + ), + ) + inscritos_saberes = models.PositiveIntegerField( + _("inscritos no Saberes"), + default=0, + help_text=_( + "Número de pessoas que se inscreveram no evento no Saberes. " + "Computado via integração SIGI x Saberes." + ), + editable=False, + ) + aprovados_saberes = models.PositiveIntegerField( + _("aprovados no Saberes"), + default=0, + help_text=_( + "Número de pessoas que concluíram o curso no Saberes. " + "Computado via integração SIGI x Saberes." ), + editable=False, + ) + data_sincronizacao = models.DateTimeField( + _("data da última sincronização com Saberes"), + null=True, + editable=False, ) status = models.CharField( _("Status"), max_length=1, choices=STATUS_CHOICES @@ -543,6 +574,69 @@ class Evento(models.Model): + f"/course/view.php?id={self.moodle_courseid}" ) + def sincroniza_saberes(self): + if self.moodle_courseid is None: + raise Evento.SaberesSyncException( + _("Este evento não tem curso associado no Saberes"), + ) + + api_url = f"{settings.MOODLE_BASE_URL}/webservice/rest/server.php" + mws = Moodle(api_url, settings.MOODLE_API_TOKEN) + try: + inscritos = mws.post( + "core_enrol_get_enrolled_users", + courseid=self.moodle_courseid, + ) + except Exception as e: + raise Evento.SaberesSyncException( + _( + "Ocorreu um erro ao acessar o curso no Saberes com " + f"a mensagem {e.message}" + ), + ) + participantes = list( + filter( + lambda u: any( + r["roleid"] in settings.MOODLE_STUDENT_ROLES + for r in u["roles"] + ), + inscritos, + ) + ) + + aprovados = 0 + for participante in participantes: + try: + completion_data = mws.post( + "core_completion_get_course_completion_status", + courseid=self.moodle_courseid, + userid=participante["id"], + ) + except Exception: + completion_data = None + + if completion_data and ( + completion_data["completionstatus"]["completed"] + or any( + filter( + lambda c: c["type"] + == settings.MOODLE_COMPLETE_CRITERIA_TYPE + and c["complete"], + completion_data["completionstatus"]["completions"], + ) + ) + ): + aprovados += 1 + + self.inscritos_saberes = len(participantes) + self.aprovados_saberes = aprovados + self.data_sincronizacao = timezone.localtime() + + if self.total_participantes == 0: + self.total_participantes = self.inscritos_saberes + + self.save() + def save(self, *args, **kwargs): if self.status != Evento.STATUS_CANCELADO: self.data_cancelamento = None @@ -579,13 +673,15 @@ class Evento(models.Model): # É preciso salvar para poder usar o relacionamento com convites # super().save(*args, **kwargs) - total = self.convite_set.aggregate(total=Sum("qtde_participantes"))[ - "total" - ] - if total and total > 0 and total != self.total_participantes: - self.total_participantes = total - # Salva de novo se o total de participantes mudou # - super().save(*args, **kwargs) + + if self.total_participantes == 0: + total = self.convite_set.aggregate( + total=Sum("qtde_participantes") + )["total"] + if total and total > 0 and total != self.total_participantes: + self.total_participantes = total + # Salva de novo se o total de participantes mudou # + super().save(*args, **kwargs) if self.status in [ Evento.STATUS_PLANEJAMENTO, diff --git a/sigi/settings.py b/sigi/settings.py index 0e45116..9578d89 100644 --- a/sigi/settings.py +++ b/sigi/settings.py @@ -280,3 +280,9 @@ REGISTRO_PATH = Path(env("REGISTRO_PATH", default="/tmp/DNS/")) MOODLE_BASE_URL = env("MOODLE_BASE_URL", default=None) MOODLE_API_TOKEN = env("MOODLE_API_TOKEN", default=None) MOODLE_STUDENT_ROLES = env("MOODLE_STUDENT_ROLES", eval, default=(5, 9)) +# See [webroot]/completion/criteria/completion_criteria.php moodle code +# Search for COMPLETION_CRITERIA_TYPE_GRADE const definition +# define('COMPLETION_CRITERIA_TYPE_GRADE', 6); +MOODLE_COMPLETE_CRITERIA_TYPE = env( + "MOODLE_COMPLETE_CRITERIA_TYPE", int, default=6 # Type Grade +)