Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ajout d'un sitemap #1450

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sitemaps",
"django.contrib.sites",
"django.contrib.flatpages",
"django.contrib.gis",
Expand Down
13 changes: 13 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from django.conf import settings
from django.conf.urls.static import static
from django.urls import include, path

from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.contrib.sitemaps.sitemap_generator import Sitemap as WagtailSitemap
from wagtail.contrib.sitemaps.views import sitemap
from wagtail.documents import urls as wagtaildocs_urls
from wagtail_transfer import urls as wagtailtransfer_urls

from lemarche.utils.admin.admin_site import admin_site
from lemarche.www.pages.sitemaps import FlatPageSitemap, StaticPageSitemap


sitemaps = {
"flatpages": FlatPageSitemap,
"staticpages": StaticPageSitemap,
"wagtail": WagtailSitemap,
}


urlpatterns = [
Expand All @@ -29,6 +40,8 @@
# urls pages blog
path("", include("lemarche.www.pages.urls")),
path("", include(wagtail_urls)),
# sitemap
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="sitemap"),
]

if settings.DEBUG and "debug_toolbar" in settings.INSTALLED_APPS:
Expand Down
1 change: 1 addition & 0 deletions lemarche/templates/layouts/_footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ <h3 class="fr-footer__top-cat">En savoir plus</h3>
<li><a class="fr-footer__top-link" href="https://github.com/gip-inclusion/le-marche" target="_blank" title="Github (lien externe)">Code source</a></li>
<li><a class="fr-footer__top-link" href="{% url 'api:home' %}">API</a></li>
<li><a class="fr-footer__top-link" href="{% url 'pages:stats' %}">Statistiques</a></li>
<li><a class="fr-footer__top-link" href="{% url 'pages:plan-du-site' %}">Plan du site</a></li>
</ul>
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions lemarche/templates/pages/plan_du_site.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "layouts/base.html" %}
{% load dsfr_tags process_dict static %}

{% block page_title %}Plan du site{{ block.super }}{% endblock page_title %}

{% block content %}
<section class="fr-my-6v">
<div class="fr-container">
<h1>Plan du site</h1>
<ul>
{% for url in sitemap_urls %}
<li><a href="{{ url }}">{{ url }}</a></li>
{% endfor %}
</ul>
</div>
</section>
{% endblock content %}
74 changes: 74 additions & 0 deletions lemarche/www/pages/sitemaps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.contrib.sitemaps import Sitemap
from django.urls import get_resolver, reverse, URLPattern, URLResolver

from lemarche.pages.models import Page
from lemarche.www.pages.views import ContactView, PageView, SitemapView

EXCLUDED_STATIC_PAGES = [
"2021-10-06-le-marche-fait-peau-neuve",
]


class FlatPageSitemap(Sitemap):
changefreq = "monthly"
priority = 0.6

def items(self):
return Page.objects.all()

def lastmod(self, obj):
return obj.updated_at

def location(self, obj):
return obj.get_absolute_url()


class StaticPageSitemap(Sitemap):
changefreq = "yearly"
priority = 0.4

def _is_included_view(self, callback):
"""
Check if a view has to be included in the sitemap.

Args:
callback: The view callable.

Returns:
bool: True if the callback is a is a ContactView,
HomeView, PageView, or SitemapView, False otherwise.
"""
return hasattr(callback, "view_class") and issubclass(
callback.view_class, (ContactView, PageView, SitemapView)
)

def items(self):
"""
Return a list of URL names for static pages.

This method iterates over all URL patterns, checking if a view inherits
from certain class views. Excluded pages defined in EXCLUDED_STATIC_PAGES
are not included in the sitemap.
"""
urlconf = get_resolver().url_patterns
static_pages = []

for pattern in urlconf:
# Handle nested URLResolver patterns (e.g., 'pages' namespace)
if isinstance(pattern, URLResolver):
static_pages.extend(
f"pages:{sub_pattern.name}"
for sub_pattern in pattern.url_patterns
if isinstance(sub_pattern, URLPattern)
and self._is_included_view(sub_pattern.callback)
and sub_pattern.name not in EXCLUDED_STATIC_PAGES
)
# Handle top-level URLPatterns
elif isinstance(pattern, URLPattern) and pattern.name:
if self._is_included_view(pattern.callback) and pattern.name not in EXCLUDED_STATIC_PAGES:
static_pages.append(pattern.name)

return static_pages

def location(self, item):
return reverse(item)
88 changes: 88 additions & 0 deletions lemarche/www/pages/tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from lemarche.pages.models import Page as FlatPage
from lemarche.siaes.factories import SiaeFactory
from lemarche.users.factories import UserFactory
from lemarche.users.models import User

from wagtail.models import Page as WagtailPage
from wagtail.models import Site

import xml.etree.ElementTree as ET


class PagesHeaderLinkTest(TestCase):
fixtures = [
Expand Down Expand Up @@ -133,3 +140,84 @@ def test_buyer_user_home(self):

self.assertContains(response, "Déconnexion")
self.assertContains(response, reverse("auth:logout"))


class SitemapTest(TestCase):
@classmethod
def setUpTestData(cls):
# WagtailPage creation for tests
root = WagtailPage.get_first_root_node()
Site.objects.create(
hostname="testserver",
root_page=root,
is_default_site=True,
site_name="testserver",
)
cls.wagtail_page = root.add_child(
instance=WagtailPage(
title="Test WagtailPage",
slug="test-wagtail-page",
depth=2,
path="0002",
last_published_at=timezone.now(),
)
)

# FlatPage creation for tests
cls.flat_page = FlatPage.objects.create(
title="Test Flatpage",
url="/test-flatpage/",
created_at=timezone.now(),
updated_at="2024-10-02",
)

def test_sitemap(self):
"""Test sitemap exists"""
response = self.client.get(reverse("sitemap"))

self.assertEqual(response.status_code, 200)

def test_sitemap_is_not_empty(self):
"""Test sitemap is not empty and contains urls"""
response = self.client.get(reverse("sitemap"))

self.assertGreater(len(response.content), 0)

def test_sitemap_is_valid_xml(self):
"""Test sitemap is valid XML"""
response = self.client.get(reverse("sitemap"))

try:
ET.fromstring(response.content)
except ET.ParseError:
self.fail("Le sitemap n'est pas dans un format XML valide.")

def test_sitemap_contains_wagtail_page(self):
"""Test the sitemap contains the wagtail page URL."""
response = self.client.get(reverse("sitemap"))
wagtail_page_url = self.wagtail_page.full_url

self.assertEqual(response.status_code, 200)
self.assertContains(response, f"<loc>{wagtail_page_url}</loc>")

def test_wagtail_page_contains_lastmod_tag(self):
"""Test the sitemap contains the lastmod tag for the wagtail page."""
response = self.client.get(reverse("sitemap"))
lastmod = self.wagtail_page.last_published_at.strftime("%Y-%m-%d")

self.assertContains(response, f"<lastmod>{lastmod}</lastmod>")

def test_sitemap_contains_flatpage(self):
"""Test the sitemap contains the flat page URL."""
response = self.client.get(reverse("sitemap"))
flat_page_url = self.flat_page.get_absolute_url()

self.assertEqual(response.status_code, 200)
self.assertContains(response, f"<loc>http://example.com{flat_page_url}</loc>")

def test_flatpage_contains_lastmod_tag(self):
"""Test the sitemap contains the lastmod tag for the flatpage."""
response = self.client.get(reverse("sitemap"))
lastmod = self.flat_page.updated_at.strftime("%Y-%m-%d")

self.assertContains(response, f"<lastmod>{lastmod}</lastmod>")
4 changes: 2 additions & 2 deletions lemarche/www/pages/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from lemarche.www.pages.views import (
CompanyReferenceCalculatorView,
ContactView,
HomeView,
ImpactCalculatorView,
PageView,
SiaeGroupListView,
SitemapView,
SocialImpactBuyersCalculatorView,
StatsView,
TrackView,
Expand All @@ -19,7 +19,6 @@
app_name = "pages"

urlpatterns = [
path("ancien_accueil", HomeView.as_view(), name="home"),
path("contact/", ContactView.as_view(), name="contact"),
# Calculator endpoints
path("calibrer-achat-socialement-responsable/", ImpactCalculatorView.as_view(), name="impact_calculator"),
Expand Down Expand Up @@ -83,6 +82,7 @@
path("cgu/", PageView.as_view(), {"url": "/cgu/"}, name="cgu"),
path("cgu-api/", PageView.as_view(), {"url": "/cgu-api/"}, name="cgu-api"),
path("confidentialite/", PageView.as_view(), {"url": "/confidentialite/"}, name="confidentialite"),
path("plan-du-site/", SitemapView.as_view(), name="plan-du-site"),
# Error pages
path("403/", TemplateView.as_view(template_name="403.html"), name="403"),
path("404/", TemplateView.as_view(template_name="404.html"), name="404"),
Expand Down
30 changes: 30 additions & 0 deletions lemarche/www/pages/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
from lemarche.www.tenders.utils import create_tender_from_dict, get_or_create_user_from_anonymous_content
from lemarche.www.tenders.views import TenderCreateMultiStepView

from xml.etree import ElementTree

import requests


class HomeView(TemplateView):
template_name = "pages/home.html"
Expand Down Expand Up @@ -393,3 +397,29 @@ def trigger_error(request):
if request.POST:
raise Exception("%s error: %s" % (request.POST.get("status_code"), request.POST.get("error_message")))
print(1 / 0) # Should raise a ZeroDivisionError.


class SitemapView(View):
def get(self, request):
# Get sitemap.xml content
sitemap_url = request.build_absolute_uri("/sitemap.xml")
response = requests.get(sitemap_url)

urls = []

if response.status_code == 200:
try:
# Read XML and extract URLs
root = ElementTree.fromstring(response.content)

# Define namespace to find URLs
namespace = {"ns": "http://www.sitemaps.org/schemas/sitemap/0.9"}

for url in root.findall("ns:url", namespaces=namespace):
loc = url.find("ns:loc", namespaces=namespace)
if loc is not None:
urls.append(loc.text.strip())
except ElementTree.ParseError as e:
print("Erreur d'analyse XML:", e)

return render(request, "pages/plan_du_site.html", {"sitemap_urls": urls})
Loading