From 883f9f6a0c4c65fa2566e2744ee1efa7e454c3d3 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Tue, 18 Mar 2025 10:00:51 +0100 Subject: [PATCH 01/12] views : add create view for Creator model (WIP) --- datacite/forms.py | 2 +- datacite/models.py | 49 ++++++++++--------- datacite/templates/datacite/creator_form.html | 15 ++++++ datacite/urls.py | 2 + datacite/views.py | 10 +++- 5 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 datacite/templates/datacite/creator_form.html diff --git a/datacite/forms.py b/datacite/forms.py index 15b9a6b..f2ee2e3 100644 --- a/datacite/forms.py +++ b/datacite/forms.py @@ -58,7 +58,7 @@ def get_div_for_2_inline_inputs(field1: str, field2: str) -> Div: class CreatorModelForm(DalimaModelForm): class Meta: model = Creator - fields = ["name", "family_name", "name_type"] + fields = ["name", "given_name", "family_name", "name_type", "lang"] class IdentifierModelForm(DalimaModelForm): diff --git a/datacite/models.py b/datacite/models.py index 64c5e80..c8e0487 100644 --- a/datacite/models.py +++ b/datacite/models.py @@ -42,6 +42,31 @@ class Publisher(models.Model): return f"{self.name}" +class CreatorTypes(models.TextChoices): + DEFAULT = "", "" + PERSONAL = "Personal", "Personal" + ORGANIZATION = "Organizational", "Organizational" + + +class Creator(models.Model): + """https://datacite-metadata-schema.readthedocs.io/en/4/properties/creator/""" + + name = models.CharField(max_length=255) + given_name = models.CharField(max_length=255, blank=True, default="") + family_name = models.CharField(max_length=255) + name_type = models.CharField(max_length=255, choices=CreatorTypes) + lang = models.CharField(max_length=255, blank=True, default="") + + objects = DataciteManager() + + # TODO(@garciacp): https://gricad-gitlab.univ-grenoble-alpes.fr/OSUG/RESIF/dalima + # /-/issues/37 + # complete affiliations and name identifier + + def __str__(self) -> str: + return f"{self.family_name}({self.name})" + + class MetadataState(models.TextChoices): DRAFT = "draft", "Draft" REGISTERED = "registered", "Registered" @@ -71,6 +96,7 @@ class Metadata(models.Model): state = models.CharField( max_length=255, choices=MetadataState, blank=True, default=MetadataState.DRAFT ) + creators = models.ManyToManyField(Creator) types = models.ForeignKey(ResourceType, on_delete=models.PROTECT) publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT) @@ -115,29 +141,6 @@ class Identifier(models.Model): return f"{self.identifier}" -class CreatorTypes(models.TextChoices): - DEFAULT = "", "" - PERSONAL = "Personal", "Personal" - ORGANIZATION = "Organizational", "Organizational" - - -class Creator(models.Model): - """https://datacite-metadata-schema.readthedocs.io/en/4/properties/creator/""" - - name = models.CharField(max_length=255) - given_name = models.CharField(max_length=255, blank=True) - family_name = models.CharField(max_length=255) - name_type = models.CharField(max_length=255, choices=CreatorTypes) - lang = models.CharField(max_length=255, blank=True) - - # TODO(@garciacp): https://gricad-gitlab.univ-grenoble-alpes.fr/OSUG/RESIF/dalima - # /-/issues/37 - # complete affiliations and name identifier - - def __str__(self) -> str: - return f"{self.family_name}({self.name})" - - class Subject(models.Model): """https://datacite-metadata-schema.readthedocs.io/en/4/properties/subject/""" diff --git a/datacite/templates/datacite/creator_form.html b/datacite/templates/datacite/creator_form.html new file mode 100644 index 0000000..03b2dc3 --- /dev/null +++ b/datacite/templates/datacite/creator_form.html @@ -0,0 +1,15 @@ +{% extends "./metadata_base.html" %} +{% load crispy_forms_tags %} + +{% block links %} + <a href="{% url 'datacite:metadata-list' %}">List</a> +{% endblock links %} + +{% block content %} + <h1>Create Creator</h1> + <form action="" method="post"> + {% csrf_token %} + {% crispy form %} + <input type="submit" value="Submit" class="btn btn-primary"> + </form> +{% endblock content %} diff --git a/datacite/urls.py b/datacite/urls.py index e819148..4e496f9 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import ( + CreatorCreate, MetadataCreate, MetadataDetail, MetadataList, @@ -14,4 +15,5 @@ urlpatterns = [ path("metadata/create/", MetadataCreate.as_view(), name="metadata-create"), path("metadata/<int:pk>/update/", MetadataUpdate.as_view(), name="metadata-update"), path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), + path("creator/create/", CreatorCreate.as_view(), name="creator-create"), ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index a15c2ff..2175bec 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -2,6 +2,7 @@ from typing import Any from django.contrib import messages from django.views.generic.detail import DetailView +from django.views.generic.edit import CreateView from django.views.generic.list import ListView from extra_views import ( # type: ignore[import-untyped] CreateWithInlinesView, @@ -13,12 +14,13 @@ from extra_views.contrib.mixins import ( # type: ignore[import-untyped] from datacite.datacite import DataciteRESTClient from datacite.forms import ( + CreatorModelForm, DalimaModelFormHelper, IdentifierInline, MetadataModelForm, TitleInline, ) -from datacite.models import Metadata +from datacite.models import Creator, Metadata from datacite.serializers import ( IdentifierSerializer, MetadataSerializer, @@ -82,6 +84,12 @@ class MetadataUpdate(SuccessMessageWithInlinesMixin, UpdateWithInlinesView): return context +class CreatorCreate(CreateView): + model = Creator + template_name = "datacite/creator_form.html" + form_class = CreatorModelForm + + def serialize_to_datacite(metadata_obj: Metadata) -> dict: metadata = MetadataSerializer(metadata_obj).data metadata["titles"] = TitleSerializer(metadata_obj.title_set.all(), many=True).data -- GitLab From a7555b12b540eeefbda6e125a001471580730df3 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Wed, 19 Mar 2025 10:08:12 +0100 Subject: [PATCH 02/12] views : add create/list view for creator items --- datacite/models.py | 5 ++-- datacite/templates/datacite/creator_list.html | 28 +++++++++++++++++++ datacite/urls.py | 2 ++ datacite/views.py | 5 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 datacite/templates/datacite/creator_list.html diff --git a/datacite/models.py b/datacite/models.py index c8e0487..9ab7d56 100644 --- a/datacite/models.py +++ b/datacite/models.py @@ -57,8 +57,6 @@ class Creator(models.Model): name_type = models.CharField(max_length=255, choices=CreatorTypes) lang = models.CharField(max_length=255, blank=True, default="") - objects = DataciteManager() - # TODO(@garciacp): https://gricad-gitlab.univ-grenoble-alpes.fr/OSUG/RESIF/dalima # /-/issues/37 # complete affiliations and name identifier @@ -66,6 +64,9 @@ class Creator(models.Model): def __str__(self) -> str: return f"{self.family_name}({self.name})" + def get_absolute_url(self) -> str: + return reverse("datacite:creator-list") + class MetadataState(models.TextChoices): DRAFT = "draft", "Draft" diff --git a/datacite/templates/datacite/creator_list.html b/datacite/templates/datacite/creator_list.html new file mode 100644 index 0000000..d57be77 --- /dev/null +++ b/datacite/templates/datacite/creator_list.html @@ -0,0 +1,28 @@ +{% extends "./metadata_base.html" %} + +{% block content %} + <a href="{% url 'datacite:creator-create' %}"> + <button>Create Creator</button> + </a> + <h1>Creators</h1> + <table class="table table-striped table-sm"> + <thead class="thead-light"> + <tr> + <th scope="col">name</th> + <th scope="col">family_name</th> + <th scope="col">name_type</th> + <th scope="col">lang</th> + </tr> + </thead> + <tbody> + {% for creator in creator_list %} + <tr> + <td>{{ creator.name }}</td> + <td>{{ creator.family_name }}</td> + <td>{{ creator.name_type }}</td> + <td>{{ creator.lang }}</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock content %} diff --git a/datacite/urls.py b/datacite/urls.py index 4e496f9..a80d4ea 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -2,6 +2,7 @@ from django.urls import path from .views import ( CreatorCreate, + CreatorList, MetadataCreate, MetadataDetail, MetadataList, @@ -15,5 +16,6 @@ urlpatterns = [ path("metadata/create/", MetadataCreate.as_view(), name="metadata-create"), path("metadata/<int:pk>/update/", MetadataUpdate.as_view(), name="metadata-update"), path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), + path("creator/", CreatorList.as_view(), name="creator-list"), path("creator/create/", CreatorCreate.as_view(), name="creator-create"), ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index 2175bec..ad29377 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -90,6 +90,11 @@ class CreatorCreate(CreateView): form_class = CreatorModelForm +class CreatorList(ListView): + model = Creator + template_name = "datacite/creator_list.html" + + def serialize_to_datacite(metadata_obj: Metadata) -> dict: metadata = MetadataSerializer(metadata_obj).data metadata["titles"] = TitleSerializer(metadata_obj.title_set.all(), many=True).data -- GitLab From b7d9d240ec878c95a4a1bb0406f0d7e6a1c22a93 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Wed, 19 Mar 2025 10:33:43 +0100 Subject: [PATCH 03/12] forms : add multichoice field for metadata creators --- datacite/forms.py | 5 ++++- datacite/templates/datacite/metadata_detail.html | 2 +- datacite/templates/datacite/metadata_form.html | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/datacite/forms.py b/datacite/forms.py index f2ee2e3..cf9e494 100644 --- a/datacite/forms.py +++ b/datacite/forms.py @@ -78,7 +78,10 @@ class ExampleFormSetHelper(FormHelper): class MetadataModelForm(DalimaModelForm): class Meta: model = Metadata - fields = ["doi", "url", "publisher", "types", "publication_year"] + fields = ["doi", "url", "publisher", "types", "publication_year", "creators"] + widgets = { + "creators": forms.CheckboxSelectMultiple, + } class TitleInline(InlineFormSetFactory): diff --git a/datacite/templates/datacite/metadata_detail.html b/datacite/templates/datacite/metadata_detail.html index 4a6afed..0f2f89b 100644 --- a/datacite/templates/datacite/metadata_detail.html +++ b/datacite/templates/datacite/metadata_detail.html @@ -28,7 +28,7 @@ </tr> </thead> <tbody> - {% for creator in object.creators %} + {% for creator in object.creators.all %} <tr> <td>{{ creator.name }}</td> <td>{{ creator.family_name }}</td> diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index 1982abe..7b6d713 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -24,4 +24,7 @@ {% endfor %} <input type="submit" value="Submit" class="btn btn-primary"> </form> + <a href="{% url 'datacite:creator-create' %}" target="_blank"> + <button>Add new Creator</button> + </a> {% endblock content %} -- GitLab From a839eda00124c964ada91fd2c9f0339937c86655 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Wed, 19 Mar 2025 14:53:15 +0100 Subject: [PATCH 04/12] templates : rendered unstyled form in a modal form with htmx --- datacite/static/datacite/dialog.js | 23 +++++++++++++++ datacite/static/datacite/toast.js | 10 +++++++ datacite/templates/datacite/creator_form.html | 24 +++++++-------- .../templates/datacite/metadata_base.html | 3 ++ .../templates/datacite/metadata_form.html | 29 +++++++++++++++++-- datacite/views.py | 26 +++++++++++++++++ 6 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 datacite/static/datacite/dialog.js create mode 100644 datacite/static/datacite/toast.js diff --git a/datacite/static/datacite/dialog.js b/datacite/static/datacite/dialog.js new file mode 100644 index 0000000..4bfbad4 --- /dev/null +++ b/datacite/static/datacite/dialog.js @@ -0,0 +1,23 @@ +;(function () { + const modal = new bootstrap.Modal(document.getElementById("modal")) + + htmx.on("htmx:afterSwap", (e) => { + // Response targeting #dialog => show the modal + if (e.detail.target.id == "dialog") { + modal.show() + } + }) + + htmx.on("htmx:beforeSwap", (e) => { + // Empty response targeting #dialog => hide the modal + if (e.detail.target.id == "dialog" && !e.detail.xhr.response) { + modal.hide() + e.detail.shouldSwap = false + } + }) + + // Remove dialog content after hiding + htmx.on("hidden.bs.modal", () => { + document.getElementById("dialog").innerHTML = "" + }) +})() diff --git a/datacite/static/datacite/toast.js b/datacite/static/datacite/toast.js new file mode 100644 index 0000000..0f634cf --- /dev/null +++ b/datacite/static/datacite/toast.js @@ -0,0 +1,10 @@ +;(function () { + const toastElement = document.getElementById("toast") + const toastBody = document.getElementById("toast-body") + const toast = new bootstrap.Toast(toastElement, { delay: 2000 }) + + htmx.on("showMessage", (e) => { + toastBody.innerText = e.detail.value + toast.show() + }) +})() diff --git a/datacite/templates/datacite/creator_form.html b/datacite/templates/datacite/creator_form.html index 03b2dc3..b89b899 100644 --- a/datacite/templates/datacite/creator_form.html +++ b/datacite/templates/datacite/creator_form.html @@ -1,15 +1,13 @@ -{% extends "./metadata_base.html" %} -{% load crispy_forms_tags %} - -{% block links %} - <a href="{% url 'datacite:metadata-list' %}">List</a> -{% endblock links %} - -{% block content %} - <h1>Create Creator</h1> - <form action="" method="post"> +{% with WIDGET_ERROR_CLASS="is-invalid" %} + <form hx-post="{{ request.path }}" class="modal-content"> {% csrf_token %} - {% crispy form %} - <input type="submit" value="Submit" class="btn btn-primary"> + <div class="modal-header"> + <h5 class="modal-title">Edit Movie</h5> + </div> + <div class="modal-body">{{ creator_form.as_p }}</div> + <div class="modal-footer"> + <button type="button" data-bs-dismiss="modal">Cancel</button> + <button type="submit">Save</button> + </div> </form> -{% endblock content %} +{% endwith %} diff --git a/datacite/templates/datacite/metadata_base.html b/datacite/templates/datacite/metadata_base.html index 02455ab..4631c73 100644 --- a/datacite/templates/datacite/metadata_base.html +++ b/datacite/templates/datacite/metadata_base.html @@ -6,4 +6,7 @@ {% block navigation %} {% include "./navbar.html" %} + <div id="modal" class="modal fade"> + <div id="dialog" class="modal-dialog" hx-target="this"></div> + </div> {% endblock navigation %} diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index 7b6d713..15fcf70 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -1,5 +1,6 @@ {% extends "./metadata_base.html" %} {% load crispy_forms_tags %} +{% load static %} {% block links %} <a href="{% url 'datacite:metadata-list' %}">List</a> @@ -24,7 +25,29 @@ {% endfor %} <input type="submit" value="Submit" class="btn btn-primary"> </form> - <a href="{% url 'datacite:creator-create' %}" target="_blank"> - <button>Add new Creator</button> - </a> + <button hx-get="{% url 'datacite:creator-create' %}" + hx-target="#creator-form-content" + hx-trigger="click" + data-bs-toggle="modal" + data-bs-target="#creator-form-container" + class="btn primary">Open Modal</button> + <div id="creator-form-container" + class="modal modal-blur fade" + aria-hidden="false" + tabindex="-1"> + <div class="modal-dialog modal-lg modal-dialog-centered" role="document"> + <div id="creator-form-content" class="modal-content"></div> + </div> + </div> {% endblock content %} + +{% block extrajs %} + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" + integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" + crossorigin="anonymous"></script> + <script src="https://unpkg.com/htmx.org@2.0.4" + integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" + crossorigin="anonymous"></script> + <script src="{% static "datacite/dialog.js" %}"></script> + <script src="{% static "datacite/toast.js" %}"></script> +{% endblock extrajs %} diff --git a/datacite/views.py b/datacite/views.py index ad29377..2698d08 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -1,6 +1,9 @@ +import json from typing import Any from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView from django.views.generic.list import ListView @@ -81,6 +84,7 @@ class MetadataUpdate(SuccessMessageWithInlinesMixin, UpdateWithInlinesView): def get_context_data(self, **kwargs: Any) -> Any: context = super().get_context_data(**kwargs) context["formset_helper"] = DalimaModelFormHelper() + context["creator_form"] = CreatorModelForm() return context @@ -89,6 +93,28 @@ class CreatorCreate(CreateView): template_name = "datacite/creator_form.html" form_class = CreatorModelForm + def form_valid(self, form): + super().form_valid(form) + return HttpResponse( + status=204, + headers={ + "HX-Trigger": json.dumps( + {"movieListChanged": None, "showMessage": "Creator added."} + ) + }, + ) + + def form_invalid(self, form): + return render( + self.request, self.template_name, self.get_context_data(form=form) + ) + + def get_context_data(self, **kwargs: Any) -> Any: + context = super().get_context_data(**kwargs) + context["formset_helper"] = DalimaModelFormHelper() + context["creator_form"] = CreatorModelForm(kwargs.get("form")) + return context + class CreatorList(ListView): model = Creator -- GitLab From fb7908e9dfc5878d1282243f0702129299b177da Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Wed, 19 Mar 2025 16:05:09 +0100 Subject: [PATCH 05/12] templates : add creator form in modal --- dalima/settings.py | 1 + datacite/admin.py | 3 +- datacite/static/datacite/dialog.js | 11 ++++---- datacite/static/datacite/toast.js | 3 +- datacite/templates/datacite/creator_form.html | 28 +++++++++++-------- .../templates/datacite/metadata_form.html | 20 +++++++++++-- pyproject.toml | 1 + uv.lock | 11 ++++++++ 8 files changed, 55 insertions(+), 23 deletions(-) diff --git a/dalima/settings.py b/dalima/settings.py index 0bd21d5..ac35c7a 100644 --- a/dalima/settings.py +++ b/dalima/settings.py @@ -61,6 +61,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "datacite", "extra_views", + "widget_tweaks", ] MIDDLEWARE = [ diff --git a/datacite/admin.py b/datacite/admin.py index 1d657fa..e033ba3 100644 --- a/datacite/admin.py +++ b/datacite/admin.py @@ -1,10 +1,11 @@ # Register your models here. from django.contrib import admin -from .models import Identifier, Metadata, Publisher, ResourceType, Title +from .models import Creator, Identifier, Metadata, Publisher, ResourceType, Title admin.site.register(Metadata) admin.site.register(Identifier) admin.site.register(ResourceType) admin.site.register(Title) admin.site.register(Publisher) +admin.site.register(Creator) diff --git a/datacite/static/datacite/dialog.js b/datacite/static/datacite/dialog.js index 4bfbad4..c28ba19 100644 --- a/datacite/static/datacite/dialog.js +++ b/datacite/static/datacite/dialog.js @@ -1,16 +1,16 @@ -;(function () { - const modal = new bootstrap.Modal(document.getElementById("modal")) + + const modal = new bootstrap.Modal(document.getElementById("creator-form-container")) htmx.on("htmx:afterSwap", (e) => { // Response targeting #dialog => show the modal - if (e.detail.target.id == "dialog") { + if (e.detail.target.id == "creator-form-container") { modal.show() } }) htmx.on("htmx:beforeSwap", (e) => { // Empty response targeting #dialog => hide the modal - if (e.detail.target.id == "dialog" && !e.detail.xhr.response) { + if (e.detail.target.id == "creator-form-container" && !e.detail.xhr.response) { modal.hide() e.detail.shouldSwap = false } @@ -18,6 +18,5 @@ // Remove dialog content after hiding htmx.on("hidden.bs.modal", () => { - document.getElementById("dialog").innerHTML = "" + document.getElementById("creator-form-container").innerHTML = "" }) -})() diff --git a/datacite/static/datacite/toast.js b/datacite/static/datacite/toast.js index 0f634cf..59636d1 100644 --- a/datacite/static/datacite/toast.js +++ b/datacite/static/datacite/toast.js @@ -1,4 +1,4 @@ -;(function () { + const toastElement = document.getElementById("toast") const toastBody = document.getElementById("toast-body") const toast = new bootstrap.Toast(toastElement, { delay: 2000 }) @@ -7,4 +7,3 @@ toastBody.innerText = e.detail.value toast.show() }) -})() diff --git a/datacite/templates/datacite/creator_form.html b/datacite/templates/datacite/creator_form.html index b89b899..37307b2 100644 --- a/datacite/templates/datacite/creator_form.html +++ b/datacite/templates/datacite/creator_form.html @@ -1,13 +1,19 @@ +{% load widget_tweaks %} + {% with WIDGET_ERROR_CLASS="is-invalid" %} - <form hx-post="{{ request.path }}" class="modal-content"> - {% csrf_token %} - <div class="modal-header"> - <h5 class="modal-title">Edit Movie</h5> - </div> - <div class="modal-body">{{ creator_form.as_p }}</div> - <div class="modal-footer"> - <button type="button" data-bs-dismiss="modal">Cancel</button> - <button type="submit">Save</button> - </div> - </form> + <div class="modal-dialog modal-lg modal-dialog-centered" role="document"> + <form hx-post="{{ request.path }}" + class="modal-content" + hx-target="#creator-form-container"> + {% csrf_token %} + <div class="modal-header"> + <h5 class="modal-title">Edit Movie</h5> + </div> + <div class="modal-body">{{ creator_form.as_p }}</div> + <div class="modal-footer"> + <button type="button" data-bs-dismiss="modal">Cancel</button> + <button type="submit">Save</button> + </div> + </form> + </div> {% endwith %} diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index 15fcf70..5fdd40e 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -26,11 +26,10 @@ <input type="submit" value="Submit" class="btn btn-primary"> </form> <button hx-get="{% url 'datacite:creator-create' %}" - hx-target="#creator-form-content" + hx-target="#creator-form-container" hx-trigger="click" - data-bs-toggle="modal" data-bs-target="#creator-form-container" - class="btn primary">Open Modal</button> + class="btn btn-primary">Add creator</button> <div id="creator-form-container" class="modal modal-blur fade" aria-hidden="false" @@ -39,6 +38,21 @@ <div id="creator-form-content" class="modal-content"></div> </div> </div> + <div class="toast-container position-fixed top-0 end-0 p-3"> + <div id="toast" + class="toast align-items-center text-white bg-success border-0" + role="alert" + aria-live="assertive" + aria-atomic="true"> + <div class="d-flex"> + <div id="toast-body" class="toast-body"></div> + <button type="button" + class="btn-close btn-close-white me-2 m-auto" + data-bs-dismiss="toast" + aria-label="Close"></button> + </div> + </div> + </div> {% endblock content %} {% block extrajs %} diff --git a/pyproject.toml b/pyproject.toml index 23edc21..6d31d01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "django-environ>=0.12.0", "django-extensions>=3.2.3", "django-extra-views>=0.15.0", + "django-widget-tweaks>=1.5.0", "djangorestframework>=3.15.2", "gunicorn>=23.0.0", "psycopg[binary]>=3.2.4", diff --git a/uv.lock b/uv.lock index 68d7164..e33702f 100644 --- a/uv.lock +++ b/uv.lock @@ -137,6 +137,7 @@ dependencies = [ { name = "django-environ" }, { name = "django-extensions" }, { name = "django-extra-views" }, + { name = "django-widget-tweaks" }, { name = "djangorestframework" }, { name = "gunicorn" }, { name = "psycopg", extra = ["binary"] }, @@ -170,6 +171,7 @@ requires-dist = [ { name = "django-environ", specifier = ">=0.12.0" }, { name = "django-extensions", specifier = ">=3.2.3" }, { name = "django-extra-views", specifier = ">=0.15.0" }, + { name = "django-widget-tweaks", specifier = ">=1.5.0" }, { name = "djangorestframework", specifier = ">=3.15.2" }, { name = "gunicorn", specifier = ">=23.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.4" }, @@ -308,6 +310,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/52/50125afcf29382b7f9d88a992e44835108dd2f1694d6d17d6d3d6fe06c81/django_stubs_ext-5.1.3-py3-none-any.whl", hash = "sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd", size = 9034 }, ] +[[package]] +name = "django-widget-tweaks" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/fe/26eb92fba83844e71bbec0ced7fc2e843e5990020e3cc676925204031654/django-widget-tweaks-1.5.0.tar.gz", hash = "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", size = 14767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/6a/6cb6deb5c38b785c77c3ba66f53051eada49205979c407323eb666930915/django_widget_tweaks-1.5.0-py3-none-any.whl", hash = "sha256:a41b7b2f05bd44d673d11ebd6c09a96f1d013ee98121cb98c384fe84e33b881e", size = 8960 }, +] + [[package]] name = "djangorestframework" version = "3.15.2" -- GitLab From d7e9619551f9fbb43ae52326814b1e81ed6a549a Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 11:21:57 +0100 Subject: [PATCH 06/12] templates : autoupdate 'creator' tomselect options after 'creator' creation --- datacite/forms.py | 2 +- datacite/serializers.py | 12 ++++++- .../templates/datacite/metadata_form.html | 33 ++++++++++++++++--- .../datacite/metadata_form_content.html | 11 +++++++ datacite/urls.py | 2 ++ datacite/views.py | 15 ++++++++- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 datacite/templates/datacite/metadata_form_content.html diff --git a/datacite/forms.py b/datacite/forms.py index cf9e494..1d3a4d7 100644 --- a/datacite/forms.py +++ b/datacite/forms.py @@ -80,7 +80,7 @@ class MetadataModelForm(DalimaModelForm): model = Metadata fields = ["doi", "url", "publisher", "types", "publication_year", "creators"] widgets = { - "creators": forms.CheckboxSelectMultiple, + "creators": forms.SelectMultiple, } diff --git a/datacite/serializers.py b/datacite/serializers.py index e400362..2cd4968 100644 --- a/datacite/serializers.py +++ b/datacite/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from datacite.models import Identifier, Metadata, ResourceType, Title +from datacite.models import Creator, Identifier, Metadata, ResourceType, Title class ResourceTypeSerializer(serializers.ModelSerializer): @@ -36,3 +36,13 @@ class IdentifierSerializer(serializers.ModelSerializer): class Meta: model = Identifier fields = ["identifier", "identifier_type"] + + +class CreatorSerializer(serializers.ModelSerializer): + class Meta: + model = Creator + fields = ["name", "given_name", "family_name", "name_type", "lang"] + + +def creator_serializer2(data): + return [{"value": str(item.pk), "text": item.__str__()} for item in data] diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index 5fdd40e..3eeb8dc 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -33,11 +33,7 @@ <div id="creator-form-container" class="modal modal-blur fade" aria-hidden="false" - tabindex="-1"> - <div class="modal-dialog modal-lg modal-dialog-centered" role="document"> - <div id="creator-form-content" class="modal-content"></div> - </div> - </div> + tabindex="-1">{% include "datacite/creator_form.html" %}</div> <div class="toast-container position-fixed top-0 end-0 p-3"> <div id="toast" class="toast align-items-center text-white bg-success border-0" @@ -62,6 +58,33 @@ <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script> + <link href="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.css" + rel="stylesheet"> + <script src="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js"></script> <script src="{% static "datacite/dialog.js" %}"></script> <script src="{% static "datacite/toast.js" %}"></script> + <script> + var select = new TomSelect("#id_creators",{ + maxItems: 3, +}) + +document.addEventListener('DOMContentLoaded', (event) => { +<!-- document.body.addEventListener('htmx:load', function(evt) {--> +<!-- updateOptions(select, evt.detail.value);--> +<!-- });--> + document.body.addEventListener('creatorListChanged', function(evt) { + updateOptions(select, evt.detail.value); + }); + function updateOptions(item, array) { + console.log("updateOptions") + select.clearOptions() + select.addOptions(array) + + } +}) + +document.body.addEventListener('htmx:load', function(evt) { + console.log("onLoad"); +}); + </script> {% endblock extrajs %} diff --git a/datacite/templates/datacite/metadata_form_content.html b/datacite/templates/datacite/metadata_form_content.html new file mode 100644 index 0000000..a73e97c --- /dev/null +++ b/datacite/templates/datacite/metadata_form_content.html @@ -0,0 +1,11 @@ +{% load crispy_forms_tags %} + +{% csrf_token %} +{% crispy form %} +{% for formset in inlines %} + {{ formset.management_form|crispy }} + {% for form in formset %} + {% crispy form formset_helper %} + {% endfor %} +{% endfor %} +<input type="submit" value="Submit" class="btn btn-primary"> diff --git a/datacite/urls.py b/datacite/urls.py index a80d4ea..c303d87 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -3,6 +3,7 @@ from django.urls import path from .views import ( CreatorCreate, CreatorList, + CreatorRestList, MetadataCreate, MetadataDetail, MetadataList, @@ -18,4 +19,5 @@ urlpatterns = [ path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), path("creator/", CreatorList.as_view(), name="creator-list"), path("creator/create/", CreatorCreate.as_view(), name="creator-create"), + path("creator/list/", CreatorRestList.as_view(), name="creator-rest-list"), ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index 2698d08..3e03321 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -14,6 +14,7 @@ from extra_views import ( # type: ignore[import-untyped] from extra_views.contrib.mixins import ( # type: ignore[import-untyped] SuccessMessageWithInlinesMixin, ) +from rest_framework.generics import ListAPIView from datacite.datacite import DataciteRESTClient from datacite.forms import ( @@ -25,9 +26,11 @@ from datacite.forms import ( ) from datacite.models import Creator, Metadata from datacite.serializers import ( + CreatorSerializer, IdentifierSerializer, MetadataSerializer, TitleSerializer, + creator_serializer2, ) @@ -99,7 +102,12 @@ class CreatorCreate(CreateView): status=204, headers={ "HX-Trigger": json.dumps( - {"movieListChanged": None, "showMessage": "Creator added."} + { + "creatorListChanged": creator_serializer2( + Creator.objects.all() + ), + "showMessage": "Creator added.", + } ) }, ) @@ -129,3 +137,8 @@ def serialize_to_datacite(metadata_obj: Metadata) -> dict: ).data return metadata + + +class CreatorRestList(ListAPIView): + queryset = Creator.objects.all() + serializer_class = CreatorSerializer -- GitLab From fc5d16ca2deea86ec897d43ef26092a2414105e2 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 11:27:39 +0100 Subject: [PATCH 07/12] views : remove unused CreatorRestList view --- .../templates/datacite/metadata_form.html | 24 +++++-------------- .../datacite/metadata_form_content.html | 11 --------- datacite/urls.py | 2 -- datacite/views.py | 14 ----------- 4 files changed, 6 insertions(+), 45 deletions(-) delete mode 100644 datacite/templates/datacite/metadata_form_content.html diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index 3eeb8dc..f070882 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -64,27 +64,15 @@ <script src="{% static "datacite/dialog.js" %}"></script> <script src="{% static "datacite/toast.js" %}"></script> <script> - var select = new TomSelect("#id_creators",{ - maxItems: 3, -}) + let select = new TomSelect("#id_creators", {maxItems: 3}); -document.addEventListener('DOMContentLoaded', (event) => { -<!-- document.body.addEventListener('htmx:load', function(evt) {--> -<!-- updateOptions(select, evt.detail.value);--> -<!-- });--> - document.body.addEventListener('creatorListChanged', function(evt) { - updateOptions(select, evt.detail.value); - }); - function updateOptions(item, array) { - console.log("updateOptions") + function updateOptions(item, array) { select.clearOptions() select.addOptions(array) + } - } -}) - -document.body.addEventListener('htmx:load', function(evt) { - console.log("onLoad"); -}); + document.body.addEventListener('creatorListChanged', function(evt) { + updateOptions(select, evt.detail.value); + }); </script> {% endblock extrajs %} diff --git a/datacite/templates/datacite/metadata_form_content.html b/datacite/templates/datacite/metadata_form_content.html deleted file mode 100644 index a73e97c..0000000 --- a/datacite/templates/datacite/metadata_form_content.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load crispy_forms_tags %} - -{% csrf_token %} -{% crispy form %} -{% for formset in inlines %} - {{ formset.management_form|crispy }} - {% for form in formset %} - {% crispy form formset_helper %} - {% endfor %} -{% endfor %} -<input type="submit" value="Submit" class="btn btn-primary"> diff --git a/datacite/urls.py b/datacite/urls.py index c303d87..a80d4ea 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -3,7 +3,6 @@ from django.urls import path from .views import ( CreatorCreate, CreatorList, - CreatorRestList, MetadataCreate, MetadataDetail, MetadataList, @@ -19,5 +18,4 @@ urlpatterns = [ path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), path("creator/", CreatorList.as_view(), name="creator-list"), path("creator/create/", CreatorCreate.as_view(), name="creator-create"), - path("creator/list/", CreatorRestList.as_view(), name="creator-rest-list"), ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index 3e03321..eb581db 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -3,7 +3,6 @@ from typing import Any from django.contrib import messages from django.http import HttpResponse -from django.shortcuts import render from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView from django.views.generic.list import ListView @@ -14,7 +13,6 @@ from extra_views import ( # type: ignore[import-untyped] from extra_views.contrib.mixins import ( # type: ignore[import-untyped] SuccessMessageWithInlinesMixin, ) -from rest_framework.generics import ListAPIView from datacite.datacite import DataciteRESTClient from datacite.forms import ( @@ -26,7 +24,6 @@ from datacite.forms import ( ) from datacite.models import Creator, Metadata from datacite.serializers import ( - CreatorSerializer, IdentifierSerializer, MetadataSerializer, TitleSerializer, @@ -87,7 +84,6 @@ class MetadataUpdate(SuccessMessageWithInlinesMixin, UpdateWithInlinesView): def get_context_data(self, **kwargs: Any) -> Any: context = super().get_context_data(**kwargs) context["formset_helper"] = DalimaModelFormHelper() - context["creator_form"] = CreatorModelForm() return context @@ -112,11 +108,6 @@ class CreatorCreate(CreateView): }, ) - def form_invalid(self, form): - return render( - self.request, self.template_name, self.get_context_data(form=form) - ) - def get_context_data(self, **kwargs: Any) -> Any: context = super().get_context_data(**kwargs) context["formset_helper"] = DalimaModelFormHelper() @@ -137,8 +128,3 @@ def serialize_to_datacite(metadata_obj: Metadata) -> dict: ).data return metadata - - -class CreatorRestList(ListAPIView): - queryset = Creator.objects.all() - serializer_class = CreatorSerializer -- GitLab From d4d1528108345ecf9c717e7855c802fcd43bbdf9 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 11:56:30 +0100 Subject: [PATCH 08/12] typing : add typing to methods signature --- datacite/serializers.py | 3 ++- datacite/views.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/datacite/serializers.py b/datacite/serializers.py index 2cd4968..9aec1a4 100644 --- a/datacite/serializers.py +++ b/datacite/serializers.py @@ -1,3 +1,4 @@ +from django.db.models.query import QuerySet from rest_framework import serializers from datacite.models import Creator, Identifier, Metadata, ResourceType, Title @@ -44,5 +45,5 @@ class CreatorSerializer(serializers.ModelSerializer): fields = ["name", "given_name", "family_name", "name_type", "lang"] -def creator_serializer2(data): +def creator_serializer2(data: QuerySet) -> list: return [{"value": str(item.pk), "text": item.__str__()} for item in data] diff --git a/datacite/views.py b/datacite/views.py index eb581db..e0c66a6 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -92,7 +92,7 @@ class CreatorCreate(CreateView): template_name = "datacite/creator_form.html" form_class = CreatorModelForm - def form_valid(self, form): + def form_valid(self, form: CreatorModelForm) -> HttpResponse: super().form_valid(form) return HttpResponse( status=204, -- GitLab From 802f525f744f7d417eec1c34c1be9b9e9c96283b Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 13:57:07 +0100 Subject: [PATCH 09/12] tests : add tests for new views with messages --- dalima/test_settings.py | 6 +++ datacite/tests/views_tests.py | 97 +++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/dalima/test_settings.py b/dalima/test_settings.py index 98f629d..dee2752 100644 --- a/dalima/test_settings.py +++ b/dalima/test_settings.py @@ -6,3 +6,9 @@ DATABASES = { "NAME": "dalima", } } + +STORAGES = { + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} diff --git a/datacite/tests/views_tests.py b/datacite/tests/views_tests.py index 6b53534..5e5deb4 100644 --- a/datacite/tests/views_tests.py +++ b/datacite/tests/views_tests.py @@ -1,14 +1,20 @@ # ruff: noqa: S101, FIX002, TD003 +from http import HTTPStatus + import pytest from crispy_forms.helper import FormHelper +from django.contrib import messages +from django.contrib.messages.storage.base import Message from django.urls import reverse +from pytest_django.asserts import assertMessages -from datacite.models import Metadata, Publisher, ResourceType +from datacite.forms import CreatorModelForm +from datacite.models import Creator, Metadata, Publisher, ResourceType from datacite.tests.conftest import ( DOI_1A_2007, ) from datacite.tests.utils_tests import add_publisher_and_resource_type -from datacite.views import MetadataCreate, MetadataUpdate +from datacite.views import CreatorCreate, MetadataCreate, MetadataUpdate DUMMY_RESPONSE = "response rendered" NETWORK_DETAIL_GET_NETWORK = "datacite.views.DetailMetadataView.get_network" @@ -44,13 +50,39 @@ def setup_mocker(mocker, target, return_value): mocker.patch(target, mocker_value) -##################################################################### -### CreateNetwork View Tests -##################################################################### +@pytest.mark.parametrize( + ("datacite_return_value", "expected_message"), + [ + ( + {"id": "10.7914/resif.1A_2007"}, + [ + Message( + messages.SUCCESS, "Successfully published the metadata in Datacite" + ) + ], + ), + (None, [Message(messages.ERROR, "Could not publish the metadata in Datacite")]), + ], +) +@pytest.mark.django_db +def test_metadata_detail__post__valid_publish( + mocker, client, datacite_return_value, expected_message +): + """Publish metadata to datacite""" + metadata = create_metadata() + + setup_mocker( + mocker, "datacite.datacite.DataciteRESTClient.upsert_doi", datacite_return_value + ) + response = client.post(reverse("datacite:metadata-detail", args=[metadata.pk]), {}) + + assertMessages(response, expected_message) + + def test_metadata_create__get_context_data(rf, form_body): - post_request = rf.post(reverse("datacite:metadata-create"), form_body) + request = rf.post(reverse("datacite:metadata-create"), form_body) view = MetadataCreate() - view.setup(post_request) + view.setup(request) view.object = None context_data = view.get_context_data() @@ -59,11 +91,23 @@ def test_metadata_create__get_context_data(rf, form_body): assert isinstance(context_data["formset_helper"], FormHelper) -##################################################################### -### UpdateNetwork View Tests -##################################################################### @pytest.mark.django_db def test_metadata_update__get_context_data(rf, form_body): + metadata = create_metadata() + + pk = metadata.pk + request = rf.post(reverse("datacite:metadata-update", args=[pk]), form_body) + view = MetadataUpdate() + view.setup(request, pk=pk) + view.object = view.get_object() + + context_data = view.get_context_data() + + assert "formset_helper" in context_data + assert isinstance(context_data["formset_helper"], FormHelper) + + +def create_metadata(): add_publisher_and_resource_type() body = { "doi": DOI_1A_2007, @@ -72,15 +116,36 @@ def test_metadata_update__get_context_data(rf, form_body): "types": ResourceType.objects.first(), "publisher": Publisher.objects.first(), } - metadata = Metadata.objects.create(**body) + return Metadata.objects.create(**body) - pk = metadata.pk - post_request = rf.post(reverse("datacite:metadata-update", args=[pk]), form_body) - view = MetadataUpdate() - view.setup(post_request, pk=pk) - view.object = view.get_object() + +@pytest.mark.django_db +def test_creator_create__form_valid(rf): + request = rf.post( + reverse("datacite:creator-create"), + {"name": "Alfred", "family_name": "Trostin", "name_type": "Personal"}, + ) + view = CreatorCreate() + view.setup(request) + response = view.post(request) + + assert len(Creator.objects.all()) == 1 + assert response.status_code == HTTPStatus.NO_CONTENT + assert "HX-Trigger" in response.headers + + +def test_creator_create__get_context_data(rf): + request = rf.post( + reverse("datacite:creator-create"), + {"name": "Alfred", "family_name": "Trostin", "name_type": "Personal"}, + ) + view = CreatorCreate() + view.setup(request) + view.object = None context_data = view.get_context_data() assert "formset_helper" in context_data assert isinstance(context_data["formset_helper"], FormHelper) + assert "creator_form" in context_data + assert isinstance(context_data["creator_form"], CreatorModelForm) -- GitLab From 290eab9e51587a2e7bacd355ed2e39c6a709ecbc Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 14:15:11 +0100 Subject: [PATCH 10/12] views : remove unused views --- datacite/models.py | 3 --- datacite/serializers.py | 2 +- datacite/tests/views_tests.py | 6 ++++-- datacite/urls.py | 4 ---- datacite/views.py | 14 +++++++------- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/datacite/models.py b/datacite/models.py index 9ab7d56..ff7b3c1 100644 --- a/datacite/models.py +++ b/datacite/models.py @@ -64,9 +64,6 @@ class Creator(models.Model): def __str__(self) -> str: return f"{self.family_name}({self.name})" - def get_absolute_url(self) -> str: - return reverse("datacite:creator-list") - class MetadataState(models.TextChoices): DRAFT = "draft", "Draft" diff --git a/datacite/serializers.py b/datacite/serializers.py index 9aec1a4..9229cf0 100644 --- a/datacite/serializers.py +++ b/datacite/serializers.py @@ -45,5 +45,5 @@ class CreatorSerializer(serializers.ModelSerializer): fields = ["name", "given_name", "family_name", "name_type", "lang"] -def creator_serializer2(data: QuerySet) -> list: +def creator_tomselect_serializer(data: QuerySet) -> list: return [{"value": str(item.pk), "text": item.__str__()} for item in data] diff --git a/datacite/tests/views_tests.py b/datacite/tests/views_tests.py index 5e5deb4..fb070a5 100644 --- a/datacite/tests/views_tests.py +++ b/datacite/tests/views_tests.py @@ -122,10 +122,11 @@ def create_metadata(): @pytest.mark.django_db def test_creator_create__form_valid(rf): request = rf.post( - reverse("datacite:creator-create"), + "/", {"name": "Alfred", "family_name": "Trostin", "name_type": "Personal"}, ) view = CreatorCreate() + view.success_url = "/" view.setup(request) response = view.post(request) @@ -136,10 +137,11 @@ def test_creator_create__form_valid(rf): def test_creator_create__get_context_data(rf): request = rf.post( - reverse("datacite:creator-create"), + "/", {"name": "Alfred", "family_name": "Trostin", "name_type": "Personal"}, ) view = CreatorCreate() + view.success_url = "/" view.setup(request) view.object = None diff --git a/datacite/urls.py b/datacite/urls.py index a80d4ea..e819148 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -1,8 +1,6 @@ from django.urls import path from .views import ( - CreatorCreate, - CreatorList, MetadataCreate, MetadataDetail, MetadataList, @@ -16,6 +14,4 @@ urlpatterns = [ path("metadata/create/", MetadataCreate.as_view(), name="metadata-create"), path("metadata/<int:pk>/update/", MetadataUpdate.as_view(), name="metadata-update"), path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), - path("creator/", CreatorList.as_view(), name="creator-list"), - path("creator/create/", CreatorCreate.as_view(), name="creator-create"), ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index e0c66a6..a4d428f 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -24,10 +24,11 @@ from datacite.forms import ( ) from datacite.models import Creator, Metadata from datacite.serializers import ( + CreatorSerializer, IdentifierSerializer, MetadataSerializer, TitleSerializer, - creator_serializer2, + creator_tomselect_serializer, ) @@ -99,7 +100,7 @@ class CreatorCreate(CreateView): headers={ "HX-Trigger": json.dumps( { - "creatorListChanged": creator_serializer2( + "creatorListChanged": creator_tomselect_serializer( Creator.objects.all() ), "showMessage": "Creator added.", @@ -115,11 +116,6 @@ class CreatorCreate(CreateView): return context -class CreatorList(ListView): - model = Creator - template_name = "datacite/creator_list.html" - - def serialize_to_datacite(metadata_obj: Metadata) -> dict: metadata = MetadataSerializer(metadata_obj).data metadata["titles"] = TitleSerializer(metadata_obj.title_set.all(), many=True).data @@ -127,4 +123,8 @@ def serialize_to_datacite(metadata_obj: Metadata) -> dict: metadata_obj.identifier_set.all(), many=True ).data + metadata["creators"] = CreatorSerializer( + metadata_obj.creators.all(), many=True + ).data + return metadata -- GitLab From 725d588dd5f212776e0b546df9dd1ef20e60efe5 Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 16:21:30 +0100 Subject: [PATCH 11/12] views : fix merge-broken features --- datacite/tests/views_tests.py | 61 ++++++++++++++++++++++------------- datacite/urls.py | 3 ++ datacite/views.py | 3 +- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/datacite/tests/views_tests.py b/datacite/tests/views_tests.py index 618f66b..13b56ff 100644 --- a/datacite/tests/views_tests.py +++ b/datacite/tests/views_tests.py @@ -2,6 +2,7 @@ from http import HTTPStatus import pytest +import requests.exceptions from crispy_forms.helper import FormHelper from django.contrib import messages from django.contrib.messages.storage.base import Message @@ -10,7 +11,7 @@ from pytest_django.asserts import assertMessages from datacite.forms import CreatorModelForm from datacite.models import Creator, Metadata, Publisher, ResourceType -from datacite.tests.conftest import DOI_1A_2007, datacite_bad_response +from datacite.tests.conftest import DOI_1A_2007 from datacite.tests.utils_tests import add_publisher_and_resource_type from datacite.views import CreatorCreate, MetadataCreate, MetadataUpdate @@ -48,36 +49,50 @@ def setup_mocker(mocker, target, return_value): mocker.patch(target, mocker_value) -@pytest.mark.parametrize( - ("datacite_return_value", "expected_message"), - [ - ( - {"id": "10.7914/resif.1A_2007"}, - [ - Message( - messages.SUCCESS, "Successfully published the metadata in Datacite" - ) - ], - ), - ( - datacite_bad_response(), - [Message(messages.ERROR, "Could not publish the metadata in Datacite")], - ), - ], -) @pytest.mark.django_db -def test_metadata_detail__post__valid_publish( - mocker, client, datacite_return_value, expected_message -): +def test_metadata_detail__post__valid_publish(mocker, client): """Publish metadata to datacite""" metadata = create_metadata() setup_mocker( - mocker, "datacite.datacite.DataciteRESTClient.upsert_doi", datacite_return_value + mocker, + "datacite.datacite.DataciteRESTClient.upsert_doi", + {"id": "10.7914/resif.1A_2007"}, ) + expected_messages = [ + Message(messages.SUCCESS, "Successfully published the metadata in Datacite") + ] + + # with mocker.patch("datacite.datacite.DataciteRESTClient.upsert_doi", + # side_effect=requests.exceptions.HTTPError('mocked error')): response = client.post(reverse("datacite:metadata-detail", args=[metadata.pk]), {}) - assertMessages(response, expected_message) + assertMessages(response, expected_messages) + + +@pytest.mark.django_db +def test_metadata_detail__post__invalid_publish(mocker, client, datacite_bad_response): + """Publish metadata to datacite""" + metadata = create_metadata() + + expected_messages = [ + Message( + messages.ERROR, + 'Could not publish the metadata in Datacite 400 : { "key" : "a" }', + ) + ] + + with mocker.patch( + "datacite.datacite.DataciteRESTClient.upsert_doi", + side_effect=requests.exceptions.HTTPError( + "mocked error", response=datacite_bad_response + ), + ): + response = client.post( + reverse("datacite:metadata-detail", args=[metadata.pk]), {} + ) + + assertMessages(response, expected_messages) def test_metadata_create__get_context_data(rf, form_body): diff --git a/datacite/urls.py b/datacite/urls.py index e819148..4283b1a 100644 --- a/datacite/urls.py +++ b/datacite/urls.py @@ -1,6 +1,7 @@ from django.urls import path from .views import ( + CreatorCreate, MetadataCreate, MetadataDetail, MetadataList, @@ -14,4 +15,6 @@ urlpatterns = [ path("metadata/create/", MetadataCreate.as_view(), name="metadata-create"), path("metadata/<int:pk>/update/", MetadataUpdate.as_view(), name="metadata-update"), path("metadata/<int:pk>/detail/", MetadataDetail.as_view(), name="metadata-detail"), + path("creator/create/", CreatorCreate.as_view(), name="creator-create"), + ] # fmt: skip diff --git a/datacite/views.py b/datacite/views.py index af683e1..fe69f72 100644 --- a/datacite/views.py +++ b/datacite/views.py @@ -66,7 +66,7 @@ class MetadataDetail(DetailView): messages.error( self.request, "Could not publish the metadata in Datacite " - f"{err.response.status_code} :\n " + f"{err.response.status_code} : " f"{err.response.content.decode()}", ) @@ -103,6 +103,7 @@ class CreatorCreate(CreateView): model = Creator template_name = "datacite/creator_form.html" form_class = CreatorModelForm + success_url = "/" def form_valid(self, form: CreatorModelForm) -> HttpResponse: super().form_valid(form) -- GitLab From dd707cf2d1a4a52b3f09a6b80e26ea55444689fe Mon Sep 17 00:00:00 2001 From: Pablo Garcia Campos <pablo.garcia-campos@univ-grenoble-alpes.fr> Date: Thu, 20 Mar 2025 16:29:29 +0100 Subject: [PATCH 12/12] templates : fix format in js import --- datacite/static/datacite/toast.js | 2 +- datacite/templates/datacite/metadata_form.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/datacite/static/datacite/toast.js b/datacite/static/datacite/toast.js index 59636d1..83cef67 100644 --- a/datacite/static/datacite/toast.js +++ b/datacite/static/datacite/toast.js @@ -6,4 +6,4 @@ htmx.on("showMessage", (e) => { toastBody.innerText = e.detail.value toast.show() - }) + }) \ No newline at end of file diff --git a/datacite/templates/datacite/metadata_form.html b/datacite/templates/datacite/metadata_form.html index f070882..a91482a 100644 --- a/datacite/templates/datacite/metadata_form.html +++ b/datacite/templates/datacite/metadata_form.html @@ -61,8 +61,8 @@ <link href="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js"></script> - <script src="{% static "datacite/dialog.js" %}"></script> - <script src="{% static "datacite/toast.js" %}"></script> + <script src="{% static 'datacite/dialog.js' %}"></script> + <script src="{% static 'datacite/toast.js' %}"></script> <script> let select = new TomSelect("#id_creators", {maxItems: 3}); -- GitLab