feat: implement document management views, forms, templates, and URL routing
This commit is contained in:
parent
061fb6dc62
commit
ff4042b333
5 changed files with 145 additions and 2 deletions
|
|
@ -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}))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
<h2 class="mb-1">{{ document.title }}</h2>
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<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;
|
||||||
|
|
|
||||||
|
|
@ -808,4 +808,52 @@ class PublicAccessLinkTests(TestCase):
|
||||||
url = reverse("documents:folder_public", args=[self.folder_link.token])
|
url = reverse("documents:folder_public", args=[self.folder_link.token])
|
||||||
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"')
|
||||||
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue