-
Notifications
You must be signed in to change notification settings - Fork 10
feat: Cria endpoint para criar Article a partir de PidProviderXML #1445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7d8adf
62edbcb
64165c3
77df7fc
20960e3
82e16b0
c9f8426
c1e81fd
b5b8dd1
0427d5a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,17 @@ | ||
| import logging | ||
|
|
||
| from django.utils import timezone | ||
| from pid_provider.models import PidProviderXML | ||
| from rest_framework import status as rest_framework_status | ||
| from rest_framework import viewsets | ||
| from rest_framework.permissions import IsAuthenticated | ||
| from rest_framework.response import Response | ||
| from rest_framework.viewsets import GenericViewSet | ||
|
|
||
| from article import models | ||
| from article.sources.xmlsps import load_article | ||
|
|
||
| from .serializers import ArticleSerializer | ||
| from .serializers import ArticleSerializer, PublishArticleSerializer | ||
|
|
||
|
|
||
| class ArticleViewSet(viewsets.ModelViewSet): | ||
|
|
@@ -16,3 +25,161 @@ def get_queryset(self): | |
| if doi_prefix is not None: | ||
| queryset = queryset.filter(doi__value__startswith=doi_prefix) | ||
| return queryset | ||
|
|
||
|
|
||
| class PublishArticleViewSet(GenericViewSet): | ||
| """ | ||
| Registra a publicação de um artigo a partir de um PidProviderXML existente. | ||
|
|
||
| Usado após o upload do XML via ``pid_provider`` para criar ou atualizar o | ||
| ``Article`` no Core e marcá-lo como público. | ||
| """ | ||
|
|
||
| http_method_names = [ | ||
| "post", | ||
| ] | ||
| permission_classes = [IsAuthenticated] | ||
| serializer_class = PublishArticleSerializer | ||
|
|
||
| def create(self, request): | ||
| """ | ||
| Registra que um artigo identificado pelo PID Provider foi publicado ou | ||
| atualizado no site público. | ||
|
|
||
| Localiza o ``PidProviderXML`` pelo par ``pid_v3`` + ``sps_pkg_name``, | ||
| carrega os metadados do XML SPS versionado, cria ou atualiza o | ||
| ``Article`` e executa ``check_availability`` para expor o registro. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| pid_v3 : str, required | ||
| PID v3 do artigo (23 caracteres). | ||
| sps_pkg_name : str, required | ||
| Nome do pacote SPS associado ao XML (ex. ``2236-8906-hoehnea-49-e1082020``). | ||
|
|
||
| # solicita token | ||
| curl -X POST http://localhost:8000/api/v2/auth/token/ \ | ||
| -d 'username=scms-upload&password=secret' | ||
|
|
||
| # resposta | ||
| ``` | ||
| {"refresh":"eyJhbGciOi...","access":"eyJhbGciOi..."} | ||
| ``` | ||
|
|
||
| # registra publicação do artigo | ||
| curl -X POST http://localhost:8000/api/v1/publish_article/ \ | ||
| -H 'Authorization: Bearer eyJhbGciOi...' \ | ||
| -H 'Content-Type: application/json' \ | ||
| -d '{ | ||
| "pid_v3": "67CrZnsyZLpV7dyR7dgp6Vt", | ||
| "sps_pkg_name": "2236-8906-hoehnea-49-e1082020" | ||
| }' | ||
|
|
||
| Return | ||
| ------ | ||
| dict | ||
| Resposta de sucesso (``201 Created`` para criação, ``200 OK`` para atualização):: | ||
|
|
||
| { | ||
| "article_id": 123, | ||
| "pid_v3": "67CrZnsyZLpV7dyR7dgp6Vt", | ||
| "sps_pkg_name": "2236-8906-hoehnea-49-e1082020", | ||
| "operation": "created", | ||
| "data_status": "PUBLIC", | ||
| "is_public": true, | ||
| "timestamp": "2026-06-23T15:00:00+00:00" | ||
| } | ||
|
|
||
| Errors | ||
| ------ | ||
| - ``400 Bad Request``: ``pid_v3`` ou ``sps_pkg_name`` ausente, vazio ou inválido. | ||
| - ``500 Internal Server Error``: falha inesperada ao converter o XML em ``Article``. | ||
| - ``401 Unauthorized``: token JWT ausente, expirado ou inválido. | ||
| - ``404 Not Found``: nenhum ``PidProviderXML`` para o par informado. | ||
| """ | ||
| serializer = self.get_serializer(data=request.data) | ||
| serializer.is_valid(raise_exception=True) | ||
| identifiers = serializer.validated_data | ||
|
|
||
| try: | ||
| pp_xml = self.get_pid_provider_xml(identifiers) | ||
| except PidProviderXML.DoesNotExist: | ||
| return Response( | ||
| { | ||
| "error": "PidProviderXML not found", | ||
| "pid_v3": identifiers["pid_v3"], | ||
| "sps_pkg_name": identifiers["sps_pkg_name"], | ||
| }, | ||
| status=rest_framework_status.HTTP_404_NOT_FOUND, | ||
| ) | ||
|
|
||
| try: | ||
| result = self.publish_article_from_pid_provider_xml(request.user, pp_xml) | ||
| except Exception as e: | ||
| logging.error( | ||
| f"Erro ao registrar artigo. Identificadores: {identifiers}. Exceção: {type(e).__name__}: {e}", | ||
| exc_info=True, | ||
| ) | ||
| return Response( | ||
| { | ||
| "error_type": str(type(e)), | ||
| "error_message": str(e), | ||
| }, | ||
| status=rest_framework_status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
| ) | ||
|
|
||
| timestamp = timezone.now().isoformat() | ||
| logging.info( | ||
| f"Publish article operation={result['operation']} " | ||
| f"pid_v3={identifiers['pid_v3']} " | ||
| f"sps_pkg_name={identifiers['sps_pkg_name']} " | ||
| f"article_id={result['article_id']} " | ||
| f"user={request.user.username} timestamp={timestamp}" | ||
| ) | ||
| return Response( | ||
| data=self.build_response_data(result, timestamp), | ||
| status=self.get_response_status(result), | ||
| ) | ||
|
|
||
| def get_pid_provider_xml(self, identifiers): | ||
| return PidProviderXML.objects.select_related("current_version").get( | ||
| v3=identifiers["pid_v3"], | ||
| pkg_name=identifiers["sps_pkg_name"], | ||
| ) | ||
|
|
||
| def build_response(self, data, status=rest_framework_status.HTTP_400_BAD_REQUEST): | ||
| return Response(data, status=status) | ||
|
|
||
| def get_response_status(self, result): | ||
| if result["operation"] == "created": | ||
| return rest_framework_status.HTTP_201_CREATED | ||
| return rest_framework_status.HTTP_200_OK | ||
|
|
||
| def build_response_data(self, result, timestamp): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelveigarangel faça teste de build_response_data pois acho que algo errado na construção da resposta. Não retorna nada |
||
| return {key: value for key, value in result.items() if key != "article"} | { | ||
| "timestamp": timestamp, | ||
| } | ||
|
Comment on lines
+158
to
+161
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samuelveigarangel response = {"timestamp": timestamp}
response.update(result)
try:
response.pop("article")
except KeyError:
pass
return response |
||
|
|
||
| def publish_article_from_pid_provider_xml(self, user, pp_xml): | ||
| operation = ( | ||
| "updated" | ||
| if models.Article.get_by_pid_v3_or_by_sps_pkg_name( | ||
| pid_v3=pp_xml.v3, | ||
| sps_pkg_name=pp_xml.pkg_name, | ||
| ).exists() | ||
| else "created" | ||
| ) | ||
| article = load_article(user, pp_xml=pp_xml) | ||
| pp_xml.collections.set(article.collections) | ||
|
|
||
| article.check_availability(user) | ||
|
|
||
| return { | ||
| "article": article, | ||
| "article_id": article.id, | ||
| "pid_v3": article.pid_v3, | ||
| "sps_pkg_name": article.sps_pkg_name, | ||
| "operation": operation, | ||
| "data_status": article.data_status, | ||
| "is_public": article.is_public, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| from unittest.mock import patch | ||
|
|
||
| from django.contrib.auth import get_user_model | ||
| from rest_framework import status | ||
| from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate | ||
|
|
||
| from article.api.v1.views import PublishArticleViewSet | ||
| from pid_provider.models import PidProviderXML | ||
|
|
||
|
|
||
| User = get_user_model() | ||
|
|
||
|
|
||
| class PublishArticleViewSetTest(APITestCase): | ||
| url = "/api/v1/publish_article/" | ||
| pid_v3 = "12345678901234567890123" | ||
| sps_pkg_name = "1234-5678-journal-10-01-a01" | ||
|
|
||
| def setUp(self): | ||
| self.user = User.objects.create_user( | ||
| username="scms-upload", | ||
| password="test-password", | ||
| ) | ||
| self.factory = APIRequestFactory() | ||
| self.view = PublishArticleViewSet.as_view({"post": "create"}) | ||
| self.authenticated = False | ||
| PidProviderXML.objects.filter( | ||
| v3=self.pid_v3, | ||
| pkg_name=self.sps_pkg_name, | ||
| ).delete() | ||
|
|
||
| def authenticate(self): | ||
| self.authenticated = True | ||
|
|
||
| def create_pid_provider_xml(self): | ||
| return PidProviderXML.objects.create( | ||
| v3=self.pid_v3, | ||
| pkg_name=self.sps_pkg_name, | ||
| ) | ||
|
|
||
| def post(self, data=None, url=None): | ||
| request = self.factory.post( | ||
| url or self.url, | ||
| data or {"pid_v3": self.pid_v3, "sps_pkg_name": self.sps_pkg_name}, | ||
| format="json", | ||
| ) | ||
| if self.authenticated: | ||
| force_authenticate(request, user=self.user) | ||
| return self.view(request) | ||
|
|
||
| def registration_result(self, operation="created"): | ||
| return { | ||
| "article": object(), | ||
| "article_id": 123, | ||
| "pid_v3": self.pid_v3, | ||
| "sps_pkg_name": self.sps_pkg_name, | ||
| "operation": operation, | ||
| "data_status": "PUBLIC", | ||
| "is_public": True, | ||
| } | ||
|
|
||
| def test_requires_authentication(self): | ||
| response = self.post() | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) | ||
|
|
||
| def test_returns_400_when_required_params_are_missing(self): | ||
| self.authenticate() | ||
|
|
||
| response = self.post({"pid_v3": self.pid_v3}) | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
| self.assertIn("sps_pkg_name", response.data) | ||
|
|
||
| def test_returns_400_when_pid_v3_is_invalid(self): | ||
| self.authenticate() | ||
|
|
||
| response = self.post({"pid_v3": "invalid", "sps_pkg_name": self.sps_pkg_name}) | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) | ||
| self.assertIn("pid_v3", response.data) | ||
|
|
||
| def test_returns_404_when_pid_provider_xml_does_not_exist(self): | ||
| self.authenticate() | ||
|
|
||
| response = self.post() | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) | ||
| self.assertEqual(response.data["error"], "PidProviderXML not found") | ||
| self.assertEqual(response.data["pid_v3"], self.pid_v3) | ||
| self.assertEqual(response.data["sps_pkg_name"], self.sps_pkg_name) | ||
|
|
||
| @patch( | ||
| "article.api.v1.views.PublishArticleViewSet.publish_article_from_pid_provider_xml" | ||
| ) | ||
| def test_returns_201_when_article_is_created(self, mocked_register): | ||
| self.authenticate() | ||
| pp_xml = self.create_pid_provider_xml() | ||
| mocked_register.return_value = self.registration_result(operation="created") | ||
|
|
||
| response = self.post() | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_201_CREATED) | ||
| self.assertEqual(response.data["article_id"], 123) | ||
| self.assertEqual(response.data["pid_v3"], self.pid_v3) | ||
| self.assertEqual(response.data["sps_pkg_name"], self.sps_pkg_name) | ||
| self.assertEqual(response.data["operation"], "created") | ||
| self.assertEqual(response.data["data_status"], "PUBLIC") | ||
| self.assertTrue(response.data["is_public"]) | ||
| self.assertIn("timestamp", response.data) | ||
| self.assertNotIn("article", response.data) | ||
| mocked_register.assert_called_once_with(self.user, pp_xml) | ||
|
|
||
| @patch( | ||
| "article.api.v1.views.PublishArticleViewSet.publish_article_from_pid_provider_xml" | ||
| ) | ||
| def test_returns_200_when_article_is_updated(self, mocked_register): | ||
| self.authenticate() | ||
| pp_xml = self.create_pid_provider_xml() | ||
| mocked_register.return_value = self.registration_result(operation="updated") | ||
|
|
||
| response = self.post() | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_200_OK) | ||
| self.assertEqual(response.data["article_id"], 123) | ||
| self.assertEqual(response.data["operation"], "updated") | ||
| mocked_register.assert_called_once_with(self.user, pp_xml) | ||
|
|
||
| @patch( | ||
| "article.api.v1.views.PublishArticleViewSet.publish_article_from_pid_provider_xml" | ||
| ) | ||
| def test_returns_500_when_registration_fails(self, mocked_register): | ||
| self.authenticate() | ||
| self.create_pid_provider_xml() | ||
| mocked_register.side_effect = RuntimeError("registration failed") | ||
|
|
||
| response = self.post() | ||
|
|
||
| self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) | ||
| self.assertEqual(response.data["error_type"], "<class 'RuntimeError'>") | ||
| self.assertEqual(response.data["error_message"], "registration failed") |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samuelveigarangel não está retornando 404 e não sei a causa. Está retornando 302 e nenhuma resposta. Veja