feat: implement document management views, forms, templates, and URL routing

This commit is contained in:
Karl Determe 2026-08-18 14:42:29 +02:00
parent 061fb6dc62
commit ff4042b333
5 changed files with 145 additions and 2 deletions

View file

@ -31,6 +31,21 @@ class TagField(forms.CharField):
return [item.strip() for item in value.split(",") if item.strip()] return [item.strip() for item in value.split(",") if item.strip()]
class DocumentUpdateForm(forms.ModelForm):
class Meta:
model = ManagedDocument
fields = ["title", "description"]
widgets = {
"title": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["title"].widget.attrs.setdefault("class", "form-control")
self.fields["description"].widget.attrs.setdefault("class", "form-control")
class ManagedDocumentForm(forms.ModelForm): class ManagedDocumentForm(forms.ModelForm):
file = forms.FileField(label=_("File")) file = forms.FileField(label=_("File"))
comment = forms.CharField(label=_("Comment"), required=False, widget=forms.Textarea(attrs={"rows": 2})) comment = forms.CharField(label=_("Comment"), required=False, widget=forms.Textarea(attrs={"rows": 2}))

View file

@ -19,10 +19,22 @@
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3"> <div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
<div> <div>
<div class="d-flex align-items-center gap-2">
<h2 class="mb-1">{{ document.title }}</h2> <h2 class="mb-1">{{ document.title }}</h2>
{% if can_edit_document %}
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" data-bs-toggle="modal" data-bs-target="#editDocumentModal" title="{% translate 'Modifier le titre et la description' %}">
<i class="bi bi-pencil"></i>
</button>
{% endif %}
</div>
<p class="text-muted mb-0">{{ document.description|default:_("No description provided") }}</p> <p class="text-muted mb-0">{{ document.description|default:_("No description provided") }}</p>
</div> </div>
<div class="btn-group"> <div class="btn-group">
{% if can_edit_document %}
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editDocumentModal">
<i class="bi bi-pencil"></i> {% translate "Modifier" %}
</button>
{% endif %}
{% if document.georeference_coords %} {% if document.georeference_coords %}
<a class="btn btn-outline-success" href="{% url 'documents:map_view' document.pk %}" target="_blank"> <a class="btn btn-outline-success" href="{% url 'documents:map_view' document.pk %}" target="_blank">
<i class="bi bi-map"></i> {% translate "Afficher sur la carte" %} <i class="bi bi-map"></i> {% translate "Afficher sur la carte" %}
@ -698,6 +710,41 @@ document.addEventListener('DOMContentLoaded', function () {
</div> </div>
</div> </div>
{% if can_edit_document %}
<!-- Modal de modification du document (titre et description) -->
<div class="modal fade" id="editDocumentModal" tabindex="-1" aria-labelledby="editDocumentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form method="post" action="{% url 'documents:edit' document.pk %}">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="editDocumentModalLabel">
<i class="bi bi-pencil-square text-primary"></i> {% translate "Modifier le document" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="id_doc_title" class="form-label fw-bold">{% translate "Titre" %} <span class="text-danger">*</span></label>
<input type="text" name="title" id="id_doc_title" class="form-control" value="{{ document.title }}" required maxlength="255">
</div>
<div class="mb-3">
<label for="id_doc_description" class="form-label fw-bold">{% translate "Description" %}</label>
<textarea name="description" id="id_doc_description" class="form-control" rows="4" placeholder="{% translate 'Description facultative...' %}">{{ document.description }}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> {% translate "Enregistrer" %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<style> <style>
.border-hover-primary:hover { .border-hover-primary:hover {
border-color: #0d6efd !important; border-color: #0d6efd !important;

View file

@ -809,3 +809,51 @@ class PublicAccessLinkTests(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Public Folder") self.assertContains(response, "Public Folder")
class DocumentUpdateViewTests(TestCase):
def setUp(self):
self.owner = User.objects.create_user(username="doc_owner", password="pwd")
UserConfig.objects.create(user=self.owner, is_intern=True)
self.other_user = User.objects.create_user(username="doc_other", password="pwd")
UserConfig.objects.create(user=self.other_user, is_intern=True)
self.document = ManagedDocument.objects.create(
title="Original Title",
description="Original Description",
created_by=self.owner,
)
def test_owner_can_update_title_and_description(self):
self.client.force_login(self.owner)
url = reverse("documents:edit", args=[self.document.pk])
response = self.client.post(url, {
"title": "Updated Title",
"description": "Updated Description with new details",
})
self.assertRedirects(response, self.document.get_absolute_url())
self.document.refresh_from_db()
self.assertEqual(self.document.title, "Updated Title")
self.assertEqual(self.document.description, "Updated Description with new details")
def test_unauthorized_user_cannot_update_document(self):
self.client.force_login(self.other_user)
url = reverse("documents:edit", args=[self.document.pk])
response = self.client.post(url, {
"title": "Hacked Title",
"description": "Hacked Description",
})
# Should raise 404/403 or PermissionDenied
self.assertIn(response.status_code, [403, 404])
self.document.refresh_from_db()
self.assertEqual(self.document.title, "Original Title")
self.assertEqual(self.document.description, "Original Description")
def test_detail_view_includes_edit_modal_for_authorized_user(self):
self.client.force_login(self.owner)
response = self.client.get(self.document.get_absolute_url())
self.assertEqual(response.status_code, 200)
self.assertContains(response, "editDocumentModal")
self.assertContains(response, 'value="Original Title"')

View file

@ -46,6 +46,7 @@ urlpatterns = [
name="folder_access_link_delete", name="folder_access_link_delete",
), ),
path("<int:pk>/", views.DocumentDetailView.as_view(), name="detail"), path("<int:pk>/", views.DocumentDetailView.as_view(), name="detail"),
path("<int:pk>/edit/", views.DocumentUpdateView.as_view(), name="edit"),
path("<int:pk>/add-version/", views.AddVersionView.as_view(), name="add_version"), path("<int:pk>/add-version/", views.AddVersionView.as_view(), name="add_version"),
path("<int:pk>/version/<int:version_id>/delete/", views.DeleteVersionView.as_view(), name="delete_version"), path("<int:pk>/version/<int:version_id>/delete/", views.DeleteVersionView.as_view(), name="delete_version"),
path("<int:pk>/share/", views.ShareDocumentView.as_view(), name="share"), path("<int:pk>/share/", views.ShareDocumentView.as_view(), name="share"),

View file

@ -26,6 +26,7 @@ from .forms import (
DocumentFolderForm, DocumentFolderForm,
DocumentFolderShareForm, DocumentFolderShareForm,
DocumentShareForm, DocumentShareForm,
DocumentUpdateForm,
DocumentValidationForm, DocumentValidationForm,
DocumentVersionForm, DocumentVersionForm,
ManagedDocumentForm, ManagedDocumentForm,
@ -832,6 +833,35 @@ class DocumentDeleteView(DocumentsAppAccessMixin, LoginRequiredMixin, View):
return redirect(success_url) return redirect(success_url)
class DocumentUpdateView(DocumentsAppAccessMixin, LoginRequiredMixin, View):
"""Permet de modifier le titre et la description d'un document."""
def get_document(self, pk, user):
document = get_object_or_404(ManagedDocument.objects.all(), pk=pk)
if not (
user_is_documents_admin(user)
or user_has_document_permission(
user, document, required_permission=ManagedDocument.PERMISSION_EDIT
)
):
raise PermissionDenied
return document
def post(self, request, pk):
document = self.get_document(pk, request.user)
form = DocumentUpdateForm(request.POST, instance=document)
if form.is_valid():
form.save()
messages.success(request, _("Document updated successfully."))
else:
messages.error(request, _("Error updating document."))
return redirect(document.get_absolute_url())
def get(self, request, pk):
document = self.get_document(pk, request.user)
return redirect(document.get_absolute_url())
class DocumentFolderShareView(DocumentsAppAccessMixin, LoginRequiredMixin, View): class DocumentFolderShareView(DocumentsAppAccessMixin, LoginRequiredMixin, View):
template_name = "documents/documentfolder_share.html" template_name = "documents/documentfolder_share.html"
@ -1081,6 +1111,8 @@ class DocumentDetailView(DocumentsAppAccessMixin, LoginRequiredMixin, DetailView
pk__in=existing_tag_ids pk__in=existing_tag_ids
).order_by("name_fr") ).order_by("name_fr")
context["edit_form"] = DocumentUpdateForm(instance=document)
return context return context