diff --git a/README.md b/README.md index c02d361..7a2b974 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ python -m venv .venv .\.venv\Scripts\activate ``` ```sh -pip install -r .\requiments-dev.txt +pip install -r .\requirements-dev.txt ``` ### Varáveis de ambiente diff --git a/src/tupan/alertas/admin.py b/src/tupan/alertas/admin.py index 96574b1..8a2a3ca 100644 --- a/src/tupan/alertas/admin.py +++ b/src/tupan/alertas/admin.py @@ -17,6 +17,10 @@ class AlertaAdmin(admin.ModelAdmin): "ativo", "condicao", ] + readonly_fields = [ + "criado", + "modificado" + ] @admin.register(HistoricoAlerta) diff --git a/src/tupan/alertas/models.py b/src/tupan/alertas/models.py index a5cafcf..e5445dd 100644 --- a/src/tupan/alertas/models.py +++ b/src/tupan/alertas/models.py @@ -11,8 +11,8 @@ class Meta: class Alerta(Base): - nome = models.CharField(help_text="nome do alerta", max_length=127, unique=True) - condicao = models.CharField(help_text="condição para o alerta acontecer", max_length=4) + nome = models.CharField(help_text="Nome do alerta", max_length=127, unique=True) + condicao = models.CharField(help_text="Condição para o alerta acontecer", max_length=4) ativo = models.BooleanField(default=True) # falta a chave estrangeira da estacao_parametro @@ -25,9 +25,9 @@ def __str__(self): class HistoricoAlerta(Base): - timestamp = models.BigIntegerField(blank=False) + timestamp = models.BigIntegerField(help_text="Data/hora do alerta em timestamp" ,blank=False) alerta = models.ForeignKey(Alerta, related_name="historico_alertas", on_delete=models.CASCADE) - timestamp_convertido = models.DateTimeField(blank=True, null=True) + timestamp_convertido = models.DateTimeField(help_text="Data/hora do alerta em datetime", blank=True, null=True) class Meta: verbose_name = "Histórico de Alerta" @@ -39,9 +39,9 @@ def save(self, *args, **kwargs): class Medicao(Base): - timestamp = models.BigIntegerField(blank=False) - timestamp_convertido = models.DateTimeField(blank=True, null=True) - dados = models.CharField(max_length=63, blank=False, null=False) + timestamp = models.BigIntegerField(help_text="Data/hora da medição em timestamp", blank=False) + timestamp_convertido = models.DateTimeField(help_text="Data/hora da medição em datetime", blank=True, null=True) + dados = models.CharField(help_text="Valor dos dados da medição", max_length=63, blank=False, null=False) # Falta a chave estrangeira estacao_parametro class Meta: diff --git a/src/tupan/alertas/tests.py b/src/tupan/alertas/tests.py index 7ce503c..51eb514 100644 --- a/src/tupan/alertas/tests.py +++ b/src/tupan/alertas/tests.py @@ -1,3 +1,73 @@ -from django.test import TestCase +import pytest +import json +from django.urls import reverse +from .models import Alerta, HistoricoAlerta -# Create your tests here. +@pytest.fixture +def alerta_auxiliar(): + alerta = { + "nome": "Alerta1", + "condicao": "<2", + } + return alerta + + +class TestAlerta: + @pytest.mark.django_db + def teste_criar_alerta(self, alerta_auxiliar): + Alerta.objects.create(**alerta_auxiliar) + + assert Alerta.objects.count() == 1 + alerta_no_banco = Alerta.objects.first() + + assert alerta_no_banco.pk == 1 + assert alerta_no_banco.nome == alerta_auxiliar['nome'] + assert alerta_no_banco.condicao == alerta_auxiliar['condicao'] + assert alerta_no_banco.ativo == True + + @pytest.mark.django_db + def teste_url_listar_alertas(self, client, alerta_auxiliar): + Alerta.objects.create(**alerta_auxiliar) + + url = reverse("alertas") + response = client.get(url) + + json_data = response.json() + + assert response.status_code == 200 + assert len(json_data) == 1 + assert json_data[0]['nome'] == "Alerta1" + assert json_data[0]['condicao'] == "<2" + + @pytest.mark.django_db + def teste_url_cadastrar_alerta(self, client, alerta_auxiliar): + + url = reverse("alertas") + response = client.post(url, data=json.dumps(alerta_auxiliar), content_type="application/json") + + json_data = response.json() + assert response.status_code == 201 + assert json_data['nome'] == 'Alerta1' + assert json_data['ativo'] == True + + +class TestHistoricoAlerta: + @pytest.fixture + def historico_alerta_auxiliar(self, alerta_auxiliar): + alerta = Alerta.objects.create(**alerta_auxiliar) + hist = { + "timestamp": 1726067292, + "alerta": alerta + } + return hist + + @pytest.mark.django_db + def teste_criar_historico(self, historico_alerta_auxiliar): + HistoricoAlerta.objects.create(**historico_alerta_auxiliar) + + assert HistoricoAlerta.objects.count() == 1 + historico_no_banco = HistoricoAlerta.objects.first() + + assert historico_no_banco.pk == 1 + assert historico_no_banco.timestamp == historico_alerta_auxiliar['timestamp'] + assert historico_no_banco.alerta == historico_alerta_auxiliar['alerta'] diff --git a/src/tupan/alertas/urls.py b/src/tupan/alertas/urls.py new file mode 100644 index 0000000..1ad748b --- /dev/null +++ b/src/tupan/alertas/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import AlertasView, AlertasDetalhesView, HistoricoAlertaView, MedicaoView, MedicaoDetalhesView + +urlpatterns = [ + path('', AlertasView.as_view(), name='alertas'), + path('', AlertasDetalhesView.as_view(), name='alertas-detalhes'), + path('historicos', HistoricoAlertaView.as_view(), name='historico-alertas'), + path('medicoes', MedicaoView.as_view(), name='medicoes'), + path('medicoes/', MedicaoDetalhesView.as_view(), name='medicoes-detalhes') +] \ No newline at end of file diff --git a/src/tupan/alertas/views.py b/src/tupan/alertas/views.py index 91ea44a..7419391 100644 --- a/src/tupan/alertas/views.py +++ b/src/tupan/alertas/views.py @@ -1,3 +1,159 @@ -from django.shortcuts import render +from rest_framework.views import APIView +from django.http import JsonResponse +import json +from .models import Alerta, HistoricoAlerta, Medicao +from django.core.exceptions import ValidationError -# Create your views here. + +class AlertasView(APIView): + def get(self, request, *args, **kwargs): + try: + # Obtém o valor do parâmetro 'ativo' da requisição, se disponível + ativo_param = request.GET.get('ativo', None) + + if ativo_param is not None: + ativo_param = ativo_param.strip().lower() + if ativo_param == 'true': + ativo = True + alertas = Alerta.objects.filter(ativo=ativo).values() + elif ativo_param == 'false': + ativo = False + alertas = Alerta.objects.filter(ativo=ativo).values() + else: + raise ValidationError('Parâmetro inválido para "ativo".') + else: + alertas = Alerta.objects.all().values() + + return JsonResponse(list(alertas), safe=False) + + except ValidationError as ve: + return JsonResponse({'error': str(ve)}, status=400) + + except Exception as e: + return JsonResponse({ + 'error': 'Erro ao buscar dados, tente novamente', + 'data': f"{e}" + }, status=500) + + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body) + nome = data.get('nome') + condicao = data.get('condicao') + + if not nome or not condicao: + return JsonResponse({'error': 'Campos obrigatórios: nome, condicao'}, status=400) + + alerta = Alerta(nome=nome, condicao=condicao) + alerta.save() + + return JsonResponse({ + 'id': alerta.pk, + 'nome': alerta.nome, + 'condicao': alerta.condicao, + 'ativo': alerta.ativo + }, status=201) + + except json.JSONDecodeError: + return JsonResponse({'error': 'Dados inválidos'}, status=400) + + +class AlertasDetalhesView(APIView): + def get(self, request, id, *args, **kwargs): + try: + alerta = Alerta.objects.filter(id=id, ativo=True).values().first() + if alerta: + return JsonResponse(alerta, safe=False) + else: + return JsonResponse({'error': 'Alerta não encontrado ou inativo'}, status=404) + except: + return JsonResponse({'error': 'Erro ao buscar dados, tente novamente'}, status=500) + + def put(self, request, id, *args, **kwargs): + try: + data = json.loads(request.body) + alerta = Alerta.objects.filter(id=id, ativo=True).first() + if alerta: + alerta.nome = data.get('nome', alerta.nome) + alerta.condicao = data.get('condicao', alerta.condicao) + alerta.save() + return JsonResponse({ + 'id': alerta.pk, + 'nome': alerta.nome, + 'condicao': alerta.condicao, + 'ativo': alerta.ativo + }, status=200) + else: + return JsonResponse({'error': 'Alerta não encontrado ou inativo'}, status=404) + except json.JSONDecodeError: + return JsonResponse({'error': 'Dados inválidos'}, status=400) + except: + return JsonResponse({'error': 'Erro ao atualizar alerta, tente novamente'}, status=500) + + def delete(self, request, id, *args, **kwargs): + try: + alerta = Alerta.objects.filter(id=id).first() + if alerta: + alerta.ativo = False + alerta.save() + return JsonResponse({'message': 'Alerta desativado com sucesso'}, status=200) + else: + return JsonResponse({'error': 'Alerta não encontrado'}, status=404) + except: + return JsonResponse({'error': 'Erro ao desativar alerta, tente novamente'}, status=500) + + +class HistoricoAlertaView(APIView): + def get(self, request, *args, **kwargs): + try: + historico = HistoricoAlerta.objects.get() + return JsonResponse(list(historico), safe=False) + except: + return JsonResponse({'error': 'Erro ao buscar dados, tente novamente'}, status=500) + + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body) + timestamp = data.get('timestamp') + alerta_id = data.get('alerta') + + if not timestamp or not alerta_id: + return JsonResponse({'error': 'Campos obrigatórios: timestamp, alerta'}, status=400) + + alerta = Alerta.objects.get(pk=alerta_id) + hist = HistoricoAlerta(timestamp=timestamp, alerta=alerta) + hist.save() + + return JsonResponse({ + 'id': hist.pk, + 'timestamp': hist.timestamp, + 'alerta': hist.alerta.pk, + 'timestamp_convertido': hist.timestamp_convertido + }, status=201) + except Alerta.DoesNotExist: + return JsonResponse({'error': 'Alerta não encontrado'}, status=404) + except json.JSONDecodeError: + return JsonResponse({'error': 'Dados inválidos'}, status=400) + except Exception as e: + return JsonResponse({'error': f'Erro ao salvar histórico: {str(e)}'}, status=500) + + +class MedicaoView(APIView): + def get(self, request, *args, **kwargs): + try: + medicoes = Medicao.objects.get() + return JsonResponse(list(medicoes), safe=False) + except: + return JsonResponse({'error': 'Erro ao buscar dados, tente novamente'}, status=500) + + +class MedicaoDetalhesView(APIView): + def get(self, request, id, *args, **kwargs): + try: + alerta = Medicao.objects.filter(id=id, ativo=True).values().first() + if alerta: + return JsonResponse(alerta, safe=False) + else: + return JsonResponse({'error': 'Medição não encontrada ou inativa'}, status=404) + except: + return JsonResponse({'error': 'Erro ao buscar dados, tente novamente'}, status=500) diff --git a/src/tupan/tupan/settings.py b/src/tupan/tupan/settings.py index 6de1858..98b3668 100644 --- a/src/tupan/tupan/settings.py +++ b/src/tupan/tupan/settings.py @@ -150,13 +150,13 @@ # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'pt-br' -TIME_ZONE = 'UTC' +TIME_ZONE = 'America/Sao_Paulo' USE_I18N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) diff --git a/src/tupan/tupan/urls.py b/src/tupan/tupan/urls.py index ae7f5c8..89c60c8 100644 --- a/src/tupan/tupan/urls.py +++ b/src/tupan/tupan/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ path('admin/', admin.site.urls), + path('alertas', include('alertas.urls')), path('', include('estacoes.urls')), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),