Skip to content

Commit

Permalink
feat(partner): ajout des pages de creation / mise à jour depuis la pa…
Browse files Browse the repository at this point in the history
…ge "nos partenaires" (#780)

## Description

🎸 Permettre l'ajout et la mise à jour des `Partner` depuis la page
`partner:list`
🎸 Accès limité aux super-utilisateurs

## Type de changement

🎢 Nouvelle fonctionnalité (changement non cassant qui ajoute une
fonctionnalité).

### Points d'attention

🦺 factorisation de la méthode `wrap_iframe_in_div_tag`
🦺 mixin et template communs pour `PartnerCreateView` et
`PartnerUpdateView`


### Captures d'écran (optionnel)


![image](https://github.com/user-attachments/assets/9b2937cb-e1bd-454b-a5bc-cb85fddbae76)


![image](https://github.com/user-attachments/assets/08be764f-9aac-4c6e-aab1-e591a93e2197)
  • Loading branch information
vincentporte authored Sep 30, 2024
1 parent 732bd5c commit 4001074
Show file tree
Hide file tree
Showing 16 changed files with 312 additions and 40 deletions.
16 changes: 1 addition & 15 deletions lacommunaute/forum/forms.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import re

from django import forms
from django.conf import settings
from django.forms import CharField, CheckboxSelectMultiple, ModelMultipleChoiceField
from taggit.models import Tag

from lacommunaute.forum.models import Forum
from lacommunaute.partner.models import Partner


def wrap_iframe_in_div_tag(text):
# iframe tags must be wrapped in a div tag to be displayed correctly
# add div tag if not present

iframe_regex = r"((<div>)?<iframe.*?</iframe>(</div>)?)"

for match, starts_with, ends_with in re.findall(iframe_regex, text, re.DOTALL):
if not starts_with and not ends_with:
text = text.replace(match, f"<div>{match}</div>")

return text
from lacommunaute.utils.html import wrap_iframe_in_div_tag


class ForumForm(forms.ModelForm):
Expand Down
17 changes: 0 additions & 17 deletions lacommunaute/forum/tests/tests_forms.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import pytest # noqa

from lacommunaute.forum.forms import wrap_iframe_in_div_tag

from lacommunaute.forum.forms import ForumForm
from lacommunaute.forum.models import Forum


def test_wrap_iframe_in_div_tag():
inputs = [
"<iframe src='xxx'></iframe>",
"<div><iframe src='yyy'></iframe></div>",
"<div><iframe src='zzz'></iframe>",
"<iframe src='www'></iframe></div>",
]
outputs = [
"<div><iframe src='xxx'></iframe></div>",
"<div><iframe src='yyy'></iframe></div>",
"<div><iframe src='zzz'></iframe>",
"<iframe src='www'></iframe></div>",
]
assert wrap_iframe_in_div_tag(" ".join(inputs)) == " ".join(outputs)


def test_saved_forum_description(db):
form = ForumForm(
data={
Expand Down
26 changes: 26 additions & 0 deletions lacommunaute/partner/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django import forms
from django.conf import settings

from lacommunaute.partner.models import Partner
from lacommunaute.utils.html import wrap_iframe_in_div_tag


class PartnerForm(forms.ModelForm):
logo = forms.ImageField(
required=False,
label="Logo, format 200 x 200 pixels recommandé",
widget=forms.FileInput(attrs={"accept": settings.SUPPORTED_IMAGE_FILE_TYPES.keys()}),
)

def save(self, commit=True):
partner = super().save(commit=False)
partner.description = wrap_iframe_in_div_tag(self.cleaned_data.get("description"))

if commit:
partner.save()

return partner

class Meta:
model = Partner
fields = ("name", "short_description", "description", "logo", "url")
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
<h1 class="mb-0">Best Partner Ever</h1>
</div>

<a href="/admin/partner/partner/[PK of Partner]/change/"><small>Mettre à jour</small></a>
<a href="/partenaires/best-partner-ever-[PK of Partner]/update/"><small>Mettre à jour</small></a>

<h2 class="mt-3">short description for SEO</h2>
</div>
Expand Down
28 changes: 28 additions & 0 deletions lacommunaute/partner/tests/tests_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest # noqa

from lacommunaute.partner.forms import PartnerForm
from lacommunaute.partner.models import Partner


def test_saved_partner_description(db):
form = PartnerForm(
data={
"name": "test",
"short_description": "test",
"description": "Text\n<iframe src='xxx'></iframe>\ntext\n<div><iframe src='yyy'></iframe></div>\nbye",
}
)
assert form.is_valid()
partner = form.save()
assert partner.description.rendered == (
"<p>Text</p>\n\n<div><iframe src='xxx'></iframe></div>\n\n"
"<p>text</p>\n\n<div><iframe src='yyy'></iframe></div>\n\n<p>bye</p>"
)


def test_form_field():
form = PartnerForm()
assert form.Meta.model == Partner
assert form.Meta.fields == ("name", "short_description", "description", "logo", "url")
assert form.fields["name"].required
assert list(form.fields["logo"].widget.attrs["accept"]) == ["image/png", "image/jpeg", "image/jpg", "image/gif"]
51 changes: 51 additions & 0 deletions lacommunaute/partner/tests/tests_partner_createview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest # noqa

from django.urls import reverse

from lacommunaute.users.factories import UserFactory
from lacommunaute.partner.models import Partner


@pytest.fixture(name="url")
def url_fixture():
return reverse("partner:create")


@pytest.fixture(name="superuser")
def superuser_fixture():
return UserFactory(is_superuser=True)


@pytest.mark.parametrize(
"user,status_code", [(None, 302), (lambda: UserFactory(), 403), (lambda: UserFactory(is_superuser=True), 200)]
)
def test_user_passes_test_mixin(client, db, url, user, status_code):
if user:
client.force_login(user())
response = client.get(url)
assert response.status_code == status_code


def test_view(client, db, url, superuser):
client.force_login(superuser)
response = client.get(url)
assert response.status_code == 200
assert response.context["title"] == "Créer une nouvelle page partenaire"
assert response.context["back_url"] == reverse("partner:list")
assert list(response.context["form"].fields.keys()) == ["name", "short_description", "description", "logo", "url"]


def test_post_partner(client, db, url, superuser):
client.force_login(superuser)
data = {
"name": "Test",
"short_description": "Short description",
"description": "# Titre\n<iframe src='https://www.example.com'></iframe>",
"url": "https://www.example.com",
}
response = client.post(url, data)
assert response.status_code == 302

partner = Partner.objects.get()
assert response.url == reverse("partner:detail", kwargs={"pk": partner.pk, "slug": partner.slug})
assert partner.description.raw == "# Titre\n<div><iframe src='https://www.example.com'></iframe></div>"
26 changes: 22 additions & 4 deletions lacommunaute/partner/tests/tests_partner_listview.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,38 @@
from django.urls import reverse

from lacommunaute.partner.factories import PartnerFactory
from lacommunaute.users.factories import UserFactory
from lacommunaute.utils.testing import parse_response_to_soup


def test_listview(client, db, snapshot):
@pytest.fixture(name="url")
def url_fixture():
return reverse("partner:list")


def test_listview(client, db, snapshot, url):
partner = PartnerFactory(for_snapshot=True, with_logo=True)
response = client.get(reverse("partner:list"))
response = client.get(url)
assert response.status_code == 200
assert str(
parse_response_to_soup(response, selector="#partner-list", replace_img_src=True, replace_in_href=[partner])
) == snapshot(name="partner_listview")


def test_pagination(client, db, snapshot):
def test_pagination(client, db, snapshot, url):
PartnerFactory.create_batch(8 * 3 + 1)
response = client.get(reverse("partner:list"))
response = client.get(url)
assert response.status_code == 200
assert str(parse_response_to_soup(response, selector="ul.pagination")) == snapshot(name="partner_pagination")


@pytest.mark.parametrize(
"user,link_is_visible",
[(None, False), (lambda: UserFactory(), False), (lambda: UserFactory(is_superuser=True), True)],
)
def test_link_to_createview(client, db, url, user, link_is_visible):
if user:
client.force_login(user())
response = client.get(url)
assert response.status_code == 200
assert bool(reverse("partner:create") in response.content.decode()) == link_is_visible
56 changes: 56 additions & 0 deletions lacommunaute/partner/tests/tests_partner_updateview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pytest # noqa

from django.urls import reverse

from lacommunaute.partner.factories import PartnerFactory
from lacommunaute.users.factories import UserFactory


@pytest.fixture(name="partner")
def partner_fixture():
return PartnerFactory(for_snapshot=True)


@pytest.fixture(name="url")
def url_fixture(partner):
return reverse("partner:update", kwargs={"pk": partner.id, "slug": partner.slug})


@pytest.fixture(name="superuser")
def superuser_fixture():
return UserFactory(is_superuser=True)


@pytest.mark.parametrize(
"user,status_code", [(None, 302), (lambda: UserFactory(), 403), (lambda: UserFactory(is_superuser=True), 200)]
)
def test_user_passes_test_mixin(client, db, url, user, status_code):
if user:
client.force_login(user())
response = client.get(url)
assert response.status_code == status_code


def test_view(client, db, url, superuser, partner):
client.force_login(superuser)
response = client.get(url)
assert response.status_code == 200
assert response.context["title"] == f"Modifier la page {partner.name}"
assert response.context["back_url"] == reverse("partner:detail", kwargs={"pk": partner.pk, "slug": partner.slug})
assert list(response.context["form"].fields.keys()) == ["name", "short_description", "description", "logo", "url"]


def test_post_partner(client, db, url, superuser, partner):
client.force_login(superuser)
data = {
"name": "Test",
"short_description": "Short description",
"description": "# Titre\ndescription",
"url": "https://www.example.com",
}
response = client.post(url, data)
assert response.status_code == 302

partner.refresh_from_db()
assert response.url == reverse("partner:detail", kwargs={"pk": partner.pk, "slug": partner.slug})
assert partner.description.rendered == "<h1>Titre</h1>\n\n<p>description</p>"
4 changes: 3 additions & 1 deletion lacommunaute/partner/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.urls import path

from lacommunaute.partner.views import PartnerDetailView, PartnerListView
from lacommunaute.partner.views import PartnerCreateView, PartnerDetailView, PartnerListView, PartnerUpdateView


app_name = "partner"
Expand All @@ -9,4 +9,6 @@
urlpatterns = [
path("", PartnerListView.as_view(), name="list"),
path("<str:slug>-<int:pk>/", PartnerDetailView.as_view(), name="detail"),
path("<str:slug>-<int:pk>/update/", PartnerUpdateView.as_view(), name="update"),
path("create/", PartnerCreateView.as_view(), name="create"),
]
33 changes: 32 additions & 1 deletion lacommunaute/partner/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.views.generic import DetailView, ListView
from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse
from django.views.generic import CreateView, DetailView, ListView, UpdateView

from lacommunaute.forum.models import Forum
from lacommunaute.partner.forms import PartnerForm
from lacommunaute.partner.models import Partner
from lacommunaute.utils.perms import forum_visibility_content_tree_from_forums

Expand All @@ -23,3 +26,31 @@ def get_context_data(self, **kwargs):
self.request, Forum.objects.filter(partner=self.object)
)
return context


class PartnerCreateUpdateMixin(UserPassesTestMixin):
model = Partner
template_name = "partner/create_or_update.html"
form_class = PartnerForm

def test_func(self):
return self.request.user.is_superuser

def get_success_url(self):
return reverse("partner:detail", kwargs={"pk": self.object.pk, "slug": self.object.slug})


class PartnerCreateView(PartnerCreateUpdateMixin, CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Créer une nouvelle page partenaire"
context["back_url"] = reverse("partner:list")
return context


class PartnerUpdateView(PartnerCreateUpdateMixin, UpdateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = f"Modifier la page {self.object.name}"
context["back_url"] = reverse("partner:detail", kwargs={"pk": self.object.pk, "slug": self.object.slug})
return context
16 changes: 16 additions & 0 deletions lacommunaute/templates/partner/create_or_update.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "board_base.html" %}
{% block sub_title %}
{{ title }}
{% endblock sub_title %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card post-edit">
<div class="card-header">
<h3 class="m-0 h4 card-title">{{ title }}</h3>
</div>
<div class="card-body">{% include "partner/partials/partner_form.html" %}</div>
</div>
</div>
</div>
{% endblock content %}
2 changes: 1 addition & 1 deletion lacommunaute/templates/partner/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<h1 class="mb-0">{{ partner.name }}</h1>
</div>
{% if user.is_superuser %}
<a href="{% url 'admin:partner_partner_change' partner.pk %}"><small>Mettre à jour</small></a>
<a href="{% url 'partner:update' partner.slug partner.pk %}"><small>Mettre à jour</small></a>
{% endif %}
<h2 class="mt-3">{{ partner.short_description }}</h2>
</div>
Expand Down
11 changes: 11 additions & 0 deletions lacommunaute/templates/partner/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,15 @@ <h1 class="s-title-01__title h1">{% trans "Partners" %}</h1>
</div>
</div>
</section>
{% if user.is_superuser %}
<section class="s-section">
<div class="s-section__container container">
<div class="s-section__row row">
<div class="s-section__col col-12">
<a href="{% url 'partner:create' %}" aria-label="Ajouter un nouveau partenaire" role="button" class="btn btn-outline-primary">Ajouter un nouveau partenaire</a>
</div>
</div>
</div>
</section>
{% endif %}
{% endblock content %}
21 changes: 21 additions & 0 deletions lacommunaute/templates/partner/partials/partner_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load i18n %}
<form method="post" action="." class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{% for error in post_form.non_field_errors %}
<div class="alert alert-danger">
<i class="icon-exclamation-sign"></i> {{ error }}
</div>
{% endfor %}
{% include "partials/form_field.html" with field=form.name %}
{% include "partials/form_field.html" with field=form.short_description %}
{% include "partials/form_field.html" with field=form.description %}
{% include "partials/form_field.html" with field=form.logo %}
{% include "partials/form_field.html" with field=form.url %}
<hr class="mb-5">
<div class="form-actions form-row">
<div class="form-group col-auto">
<input type="submit" class="btn btn-primary" value="{% trans "Submit" %}" />
<a href="{{ back_url }}" class="btn btn-outline-warning">{% trans "Cancel" %}</a>
</div>
</div>
</form>
Loading

0 comments on commit 4001074

Please sign in to comment.