feat: Create an oEmbed endpoint for maps

Fix #162
This commit is contained in:
David Larlet 2024-01-11 14:50:02 -05:00
parent b75e3bedd7
commit df76ffd80e
No known key found for this signature in database
GPG key ID: 3E2953A359E7E7BD
5 changed files with 130 additions and 8 deletions

View file

@ -12,6 +12,9 @@
{% endcompress %}
{% umap_js locale=locale %}
{% if object.share_status != object.PUBLIC %}<meta name="robots" content="noindex">{% endif %}
<link rel="alternate" type="application/json+oembed"
href="{{ oembed_absolute_uri }}?url={{ absolute_uri|urlencode }}&format=json"
title="{{ map.name }} oEmbed URL" />
{% endblock extra_head %}
{% block content %}
{% block map_init %}

View file

@ -275,7 +275,7 @@ def test_owner_cannot_access_map_with_share_status_blocked(client, map):
assert response.status_code == 403
def test_non_editor_cannot_access_map_if_share_status_private(client, map, user): # noqa
def test_non_editor_cannot_access_map_if_share_status_private(client, map, user):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = map.PRIVATE
map.save()
@ -346,14 +346,14 @@ def test_anonymous_create(cookieclient, post_data):
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_update_without_cookie_fails(client, anonymap, post_data): # noqa
def test_anonymous_update_without_cookie_fails(client, anonymap, post_data):
url = reverse("map_update", kwargs={"map_id": anonymap.pk})
response = client.post(url, post_data)
assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data): # noqa
def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data):
url = reverse("map_update", kwargs={"map_id": anonymap.pk})
# POST only mendatory fields
name = "new map name"
@ -420,7 +420,7 @@ def test_bad_anonymous_edit_url_should_return_403(cookieclient, anonymap):
@pytest.mark.usefixtures("allow_anonymous")
def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(
client, anonymap, user
): # noqa
):
assert Map.objects.count() == 1
url = reverse("map_clone", kwargs={"map_id": anonymap.pk})
anonymap.edit_status = anonymap.OWNER
@ -434,7 +434,7 @@ def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(
@pytest.mark.usefixtures("allow_anonymous")
def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap): # noqa
def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap):
assert Map.objects.count() == 1
url = reverse("map_clone", kwargs={"map_id": anonymap.pk})
anonymap.edit_status = anonymap.ANONYMOUS
@ -675,3 +675,63 @@ def test_download_my_map(client, map, datalayer):
# Test response is a json
j = json.loads(response.content.decode())
assert j["type"] == "umap"
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED, Map.OPEN])
def test_oembed_shared_status_map(client, map, datalayer, share_status):
map.share_status = share_status
map.save()
url = f"{reverse('map_oembed')}?url=http://testserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 403
def test_oembed_no_url_map(client, map, datalayer):
url = reverse("map_oembed")
response = client.get(url)
assert response.status_code == 404
def test_oembed_wrong_format_map(client, map, datalayer):
url = (
f"{reverse('map_oembed')}"
f"?url=http://testserver{map.get_absolute_url()}&format=xml"
)
response = client.get(url)
assert response.status_code == 501
def test_oembed_wrong_domain_map(client, map, datalayer):
url = f"{reverse('map_oembed')}?url=http://BADserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 404
def test_oembed_map(client, map, datalayer):
url = f"{reverse('map_oembed')}?url=http://testserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 200
j = json.loads(response.content.decode())
assert j["type"] == "rich"
assert j["version"] == "1.0"
assert j["width"] == 800
assert j["height"] == 300
assert j["html"] == (
'<iframe width="100%" height="300px" frameborder="0" allowfullscreen '
f'allow="geolocation" src="//testserver/en/map/test-map_{map.id}"></iframe>'
f'<p><a href="//testserver/en/map/test-map_{map.id}">See full screen</a></p>'
)
def test_oembed_link(client, map, datalayer):
response = client.get(map.get_absolute_url())
assert response.status_code == 200
assert (
'<link rel="alternate" type="application/json+oembed"'
in response.content.decode()
)
assert (
'href="http://testserver/map/oembed/'
f'?url=http%3A//testserver/en/map/test-map_{map.id}&format=json"'
) in response.content.decode()
assert 'title="test map oEmbed URL" />' in response.content.decode()

View file

@ -1,6 +1,6 @@
import json
import socket
from datetime import date, datetime, timedelta
from datetime import datetime, timedelta
import pytest
from django.conf import settings

View file

@ -41,6 +41,7 @@ urlpatterns = [
),
re_path(r"^i18n/", include("django.conf.urls.i18n")),
re_path(r"^agnocomplete/", include("agnocomplete.urls")),
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
re_path(
r"^map/(?P<map_id>\d+)/download/",
can_view_map(views.MapDownload.as_view()),

View file

@ -18,21 +18,23 @@ from django.contrib.auth import logout as do_logout
from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchVector
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.core.signing import BadSignature, Signer
from django.core.validators import URLValidator, ValidationError
from django.db.models import Q
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
HttpResponseServerError,
)
from django.middleware.gzip import re_accepts_gzip
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.urls import resolve, reverse, reverse_lazy
from django.utils.encoding import smart_bytes
from django.utils.http import http_date
from django.utils.timezone import make_aware
@ -526,6 +528,16 @@ class PermissionsMixin:
class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["oembed_absolute_uri"] = self.request.build_absolute_uri(
reverse("map_oembed")
)
context["absolute_uri"] = self.request.build_absolute_uri(
self.object.get_absolute_url()
)
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
canonical = self.get_canonical_url()
@ -607,6 +619,52 @@ class MapDownload(DetailView):
return response
class MapOEmbed(View):
def get(self, request, *args, **kwargs):
data = {"type": "rich", "version": "1.0"}
format_ = request.GET.get("format", "json")
if format_ != "json":
response = HttpResponseServerError("Only `json` format is implemented.")
response.status_code = 501
return response
url = request.GET.get("url")
if not url:
raise Http404("Missing `url` parameter.")
parsed_url = urlparse(url)
netloc = parsed_url.netloc
allowed_hosts = settings.ALLOWED_HOSTS
if parsed_url.hostname not in allowed_hosts and allowed_hosts != ["*"]:
raise Http404("Host not allowed.")
url_path = parsed_url.path
view, args, kwargs = resolve(url_path)
if "slug" not in kwargs or "map_id" not in kwargs:
raise Http404("Invalid URL path.")
map_ = Map.objects.get(id=kwargs["map_id"], slug=kwargs["slug"])
if map_.share_status != Map.PUBLIC:
raise PermissionDenied("This map is not public.")
map_url = map_.get_absolute_url()
label = _("See full screen")
height = 300
data["height"] = height
width = 800
data["width"] = width
# TODISCUSS: do we keep width=100% by default for the iframe?
html = (
f'<iframe width="100%" height="{height}px" '
f'frameborder="0" allowfullscreen allow="geolocation" '
f'src="//{netloc}{map_url}"></iframe>'
f'<p><a href="//{netloc}{map_url}">{label}</a></p>'
)
data["html"] = html
return simple_json_response(**data)
class MapViewGeoJSON(MapView):
def get_canonical_url(self):
return reverse("map_geojson", args=(self.object.pk,))