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