umap/umap/views.py

958 lines
30 KiB
Python
Raw Normal View History

import json
2023-02-27 05:04:09 -06:00
import mimetypes
2018-05-19 04:12:19 -05:00
import os
2017-05-10 04:12:10 -05:00
import re
2014-04-19 10:54:51 -05:00
import socket
from datetime import date, timedelta
from http.client import InvalidURL
2023-02-27 05:04:09 -06:00
from pathlib import Path
from urllib.error import URLError
2014-04-19 10:54:51 -05:00
2018-05-19 04:12:19 -05:00
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import logout as do_logout
from django.contrib.auth import get_user_model
from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchVector
from django.core.mail import send_mail
2018-05-19 04:12:19 -05:00
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
2023-02-27 04:38:59 -06:00
from django.http import (
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)
2018-05-19 04:12:19 -05:00
from django.middleware.gzip import re_accepts_gzip
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
2023-03-01 12:13:45 -06:00
from django.utils.encoding import smart_bytes
2018-05-19 04:12:19 -05:00
from django.utils.http import http_date
from django.utils.translation import gettext as _
2018-05-19 04:12:19 -05:00
from django.utils.translation import to_locale
from django.views.generic import DetailView, TemplateView, View
from django.views.generic.base import RedirectView
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
2018-05-19 04:12:19 -05:00
from django.views.generic.list import ListView
from . import VERSION
2023-02-27 04:38:59 -06:00
from .forms import (
DEFAULT_LATITUDE,
DEFAULT_LONGITUDE,
DEFAULT_CENTER,
AnonymousMapPermissionsForm,
DataLayerForm,
FlatErrorList,
MapSettingsForm,
SendLinkForm,
2023-02-27 04:38:59 -06:00
UpdateMapPermissionsForm,
)
from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer
from .utils import get_uri_template, gzip_file, is_ajax
2018-05-19 04:12:19 -05:00
try:
# python3
from urllib.parse import urlparse
from urllib.request import Request, build_opener
from urllib.error import HTTPError
except ImportError:
from urlparse import urlparse
from urllib2 import Request, HTTPError, build_opener
2012-11-20 03:47:19 -06:00
2014-06-19 07:39:45 -05:00
User = get_user_model()
2012-11-20 03:47:19 -06:00
2023-02-27 04:00:33 -06:00
PRIVATE_IP = re.compile(
r"((^127\.)|(^10\.)"
r"|(^172\.1[6-9]\.)"
r"|(^172\.2[0-9]\.)"
r"|(^172\.3[0-1]\.)"
r"|(^192\.168\.))"
)
2018-05-19 04:12:19 -05:00
ANONYMOUS_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 # One month
2017-05-10 04:12:10 -05:00
class PaginatorMixin:
per_page = 5
def paginate(self, qs, per_page=None):
paginator = Paginator(qs, per_page or self.per_page)
2023-02-27 04:00:33 -06:00
page = self.request.GET.get("p")
try:
qs = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
qs = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of
# results.
qs = paginator.page(paginator.num_pages)
return qs
def get_context_data(self, **kwargs):
kwargs.update({"is_ajax": is_ajax(self.request)})
return super().get_context_data(**kwargs)
def get_template_names(self):
"""
Dispatch template according to the kind of request: ajax or normal.
"""
if is_ajax(self.request):
return [self.list_template_name]
return super().get_template_names()
class PublicMapsMixin(object):
def get_public_maps(self):
qs = Map.public
2023-02-27 04:00:33 -06:00
if (
settings.UMAP_EXCLUDE_DEFAULT_MAPS
and "spatialite" not in settings.DATABASES["default"]["ENGINE"]
):
# Unsupported query type for sqlite.
qs = qs.filter(center__distance_gt=(DEFAULT_CENTER, D(km=1)))
maps = qs.order_by("-modified_at")
return maps
class Home(PaginatorMixin, TemplateView, PublicMapsMixin):
template_name = "umap/home.html"
list_template_name = "umap/map_list.html"
def get_context_data(self, **kwargs):
maps = self.get_public_maps()
demo_map = None
if hasattr(settings, "UMAP_DEMO_PK"):
try:
demo_map = Map.public.get(pk=settings.UMAP_DEMO_PK)
except Map.DoesNotExist:
pass
else:
maps = maps.exclude(id=demo_map.pk)
showcase_map = None
if hasattr(settings, "UMAP_SHOWCASE_PK"):
try:
showcase_map = Map.public.get(pk=settings.UMAP_SHOWCASE_PK)
except Map.DoesNotExist:
pass
else:
maps = maps.exclude(id=showcase_map.pk)
maps = self.paginate(maps, settings.UMAP_MAPS_PER_PAGE)
2012-11-20 03:47:19 -06:00
return {
"maps": maps,
"demo_map": demo_map,
"showcase_map": showcase_map,
2012-11-20 03:47:19 -06:00
}
2023-02-27 04:00:33 -06:00
2012-11-20 03:47:19 -06:00
home = Home.as_view()
2012-12-11 13:04:03 -06:00
class About(Home):
template_name = "umap/about.html"
2023-02-27 04:00:33 -06:00
about = About.as_view()
class UserMaps(PaginatorMixin, DetailView):
2012-12-11 13:04:03 -06:00
model = User
slug_url_kwarg = "identifier"
slug_field = settings.USER_URL_FIELD
2018-05-19 04:12:19 -05:00
list_template_name = "umap/map_list.html"
2012-12-11 13:04:03 -06:00
context_object_name = "current_user"
2023-05-15 07:50:18 -05:00
def is_owner(self):
return self.request.user == self.object
@property
def per_page(self):
if self.is_owner():
return settings.UMAP_MAPS_PER_PAGE_OWNER
return settings.UMAP_MAPS_PER_PAGE
def get_map_queryset(self):
return Map.objects if self.is_owner() else Map.public
def get_maps(self):
qs = self.get_map_queryset()
qs = qs.filter(Q(owner=self.object) | Q(editors=self.object))
return qs.distinct().order_by("-modified_at")
2012-12-11 13:04:03 -06:00
def get_context_data(self, **kwargs):
2023-05-15 07:50:18 -05:00
kwargs.update({"maps": self.paginate(self.get_maps(), self.per_page)})
return super().get_context_data(**kwargs)
2012-12-11 13:04:03 -06:00
2023-02-27 04:00:33 -06:00
2012-12-11 13:04:03 -06:00
user_maps = UserMaps.as_view()
2012-12-16 08:10:00 -06:00
class UserStars(UserMaps):
template_name = "auth/user_stars.html"
2023-05-15 07:50:18 -05:00
def get_maps(self):
qs = self.get_map_queryset()
stars = Star.objects.filter(by=self.object).values("map")
2023-05-15 07:50:18 -05:00
qs = qs.filter(pk__in=stars)
return qs.order_by("-modified_at")
user_stars = UserStars.as_view()
2023-07-04 09:09:12 -05:00
class SearchMixin:
def get_search_queryset(self, **kwargs):
2023-02-27 04:00:33 -06:00
q = self.request.GET.get("q")
if q:
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
query = SearchQuery(
q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch"
)
2023-07-04 09:09:12 -05:00
return Map.objects.annotate(search=vector).filter(search=query)
class Search(PaginatorMixin, TemplateView, PublicMapsMixin, SearchMixin):
2023-07-04 09:09:12 -05:00
template_name = "umap/search.html"
list_template_name = "umap/map_list.html"
def get_context_data(self, **kwargs):
qs = self.get_search_queryset()
qs_count = 0
results = []
if qs is not None:
qs = qs.filter(share_status=Map.PUBLIC).order_by("-modified_at")
qs_count = qs.count()
results = self.paginate(qs)
else:
2023-07-04 09:09:12 -05:00
results = self.get_public_maps()[: settings.UMAP_MAPS_PER_SEARCH]
kwargs.update({"maps": results, "count": qs_count})
2012-12-16 08:10:00 -06:00
return kwargs
@property
def per_page(self):
return settings.UMAP_MAPS_PER_SEARCH
2023-02-27 04:00:33 -06:00
2012-12-16 08:10:00 -06:00
search = Search.as_view()
class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
2023-07-04 09:09:12 -05:00
model = User
template_name = "umap/user_dashboard.html"
list_template_name = "umap/map_table.html"
2023-07-04 09:09:12 -05:00
def get_object(self):
return self.get_queryset().get(pk=self.request.user.pk)
def get_maps(self):
qs = self.get_search_queryset() or Map.objects.all()
qs = qs.filter(Q(owner=self.object) | Q(editors=self.object))
return qs.order_by("-modified_at")
def get_context_data(self, **kwargs):
kwargs.update(
{"maps": self.paginate(self.get_maps(), settings.UMAP_MAPS_PER_PAGE_OWNER)}
)
return super().get_context_data(**kwargs)
user_dashboard = UserDashboard.as_view()
class MapsShowCase(View):
2014-04-19 04:48:54 -05:00
def get(self, *args, **kwargs):
maps = Map.public.filter(center__distance_gt=(DEFAULT_CENTER, D(km=1)))
2023-02-27 04:00:33 -06:00
maps = maps.order_by("-modified_at")[:2500]
def make(m):
description = m.description or ""
if m.owner:
2023-02-27 04:00:33 -06:00
description = "{description}\n{by} [[{url}|{name}]]".format(
description=description,
by=_("by"),
url=m.owner.get_url(),
name=m.owner,
)
2023-02-27 04:00:33 -06:00
description = "{}\n[[{}|{}]]".format(
description, m.get_absolute_url(), _("View the map")
)
geometry = m.settings.get("geometry", json.loads(m.center.geojson))
return {
"type": "Feature",
"geometry": geometry,
2023-02-27 04:00:33 -06:00
"properties": {"name": m.name, "description": description},
}
2023-02-27 04:00:33 -06:00
geojson = {"type": "FeatureCollection", "features": [make(m) for m in maps]}
2015-10-17 06:09:58 -05:00
return HttpResponse(smart_bytes(json.dumps(geojson)))
2023-02-27 04:00:33 -06:00
showcase = MapsShowCase.as_view()
2014-04-19 04:48:54 -05:00
2014-04-19 10:54:51 -05:00
def validate_url(request):
assert request.method == "GET"
assert is_ajax(request)
2023-02-27 04:00:33 -06:00
url = request.GET.get("url")
2014-04-19 10:54:51 -05:00
assert url
try:
URLValidator(url)
except ValidationError:
raise AssertionError()
2023-02-27 04:00:33 -06:00
assert "HTTP_REFERER" in request.META
referer = urlparse(request.META.get("HTTP_REFERER"))
2014-04-19 10:54:51 -05:00
toproxy = urlparse(url)
local = urlparse(settings.SITE_URL)
assert toproxy.hostname
assert referer.hostname == local.hostname
assert toproxy.hostname != "localhost"
assert toproxy.netloc != local.netloc
2014-07-18 16:26:16 -05:00
try:
# clean this when in python 3.4
ipaddress = socket.gethostbyname(toproxy.hostname)
except:
raise AssertionError()
2017-05-10 04:12:10 -05:00
assert not PRIVATE_IP.match(ipaddress)
2014-04-19 10:54:51 -05:00
return url
2014-04-19 04:48:54 -05:00
class AjaxProxy(View):
def get(self, *args, **kwargs):
# You should not use this in production (use Nginx or so)
2014-04-19 10:54:51 -05:00
try:
url = validate_url(self.request)
except AssertionError:
2014-04-19 10:54:51 -05:00
return HttpResponseBadRequest()
2023-02-27 04:00:33 -06:00
headers = {"User-Agent": "uMapProxy +http://wiki.openstreetmap.org/wiki/UMap"}
request = Request(url, headers=headers)
opener = build_opener()
2014-04-19 04:48:54 -05:00
try:
2014-04-19 12:44:56 -05:00
proxied_request = opener.open(request)
except HTTPError as e:
2023-02-27 04:00:33 -06:00
return HttpResponse(e.msg, status=e.code, content_type="text/plain")
except URLError:
return HttpResponseBadRequest("URL error")
except InvalidURL:
return HttpResponseBadRequest("Invalid URL")
2014-04-19 04:48:54 -05:00
else:
2014-04-19 12:44:56 -05:00
status_code = proxied_request.code
2023-02-27 04:00:33 -06:00
mimetype = proxied_request.headers.get(
"Content-Type"
) or mimetypes.guess_type(
url
) # noqa
2014-04-19 12:44:56 -05:00
content = proxied_request.read()
# Quick hack to prevent Django from adding a Vary: Cookie header
self.request.session.accessed = False
2023-02-27 04:00:33 -06:00
response = HttpResponse(content, status=status_code, content_type=mimetype)
try:
2023-02-27 04:00:33 -06:00
ttl = int(self.request.GET.get("ttl"))
except (TypeError, ValueError):
pass
else:
2023-02-27 04:00:33 -06:00
response["X-Accel-Expires"] = ttl
return response
2023-02-27 04:00:33 -06:00
2014-04-19 04:48:54 -05:00
ajax_proxy = AjaxProxy.as_view()
2018-05-19 04:12:19 -05:00
# ############## #
# Utils #
# ############## #
2023-02-27 04:00:33 -06:00
2018-05-19 04:12:19 -05:00
def _urls_for_js(urls=None):
"""
Return templated URLs prepared for javascript.
"""
if urls is None:
# prevent circular import
from .urls import urlpatterns, i18n_urls
2023-02-27 04:00:33 -06:00
urls = [
url.name for url in urlpatterns + i18n_urls if getattr(url, "name", None)
]
2018-05-19 04:12:19 -05:00
urls = dict(zip(urls, [get_uri_template(url) for url in urls]))
2023-02-27 04:00:33 -06:00
urls.update(getattr(settings, "UMAP_EXTRA_URLS", {}))
2018-05-19 04:12:19 -05:00
return urls
def simple_json_response(**kwargs):
return HttpResponse(json.dumps(kwargs))
# ############## #
# Map #
# ############## #
class FormLessEditMixin:
2023-02-27 04:00:33 -06:00
http_method_names = [
"post",
]
2018-05-19 04:12:19 -05:00
def form_invalid(self, form):
2023-02-27 04:00:33 -06:00
return simple_json_response(errors=form.errors, error=str(form.errors))
2018-05-19 04:12:19 -05:00
2018-06-15 16:25:38 -05:00
def get_form(self, form_class=None):
2018-05-19 04:12:19 -05:00
kwargs = self.get_form_kwargs()
2023-02-27 04:00:33 -06:00
kwargs["error_class"] = FlatErrorList
2018-05-19 04:12:19 -05:00
return self.get_form_class()(**kwargs)
class MapDetailMixin:
2018-05-19 04:12:19 -05:00
model = Map
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2018-05-19 04:12:19 -05:00
properties = {
2023-02-27 04:00:33 -06:00
"urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(),
"allowEdit": self.is_edit_allowed(),
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa
"umap_id": self.get_umap_id(),
"starred": self.is_starred(),
2023-02-27 04:00:33 -06:00
"licences": dict((l.name, l.json) for l in Licence.objects.all()),
"edit_statuses": [(i, str(label)) for i, label in Map.EDIT_STATUS],
"share_statuses": [
(i, str(label)) for i, label in Map.SHARE_STATUS if i != Map.BLOCKED
],
"anonymous_edit_statuses": [
(i, str(label)) for i, label in AnonymousMapPermissionsForm.STATUS
],
"umap_version": VERSION,
2018-05-19 04:12:19 -05:00
}
if self.get_short_url():
2023-02-27 04:00:33 -06:00
properties["shortUrl"] = self.get_short_url()
2018-05-19 04:12:19 -05:00
if settings.USE_I18N:
lang = settings.LANGUAGE_CODE
2018-05-19 04:12:19 -05:00
# Check attr in case the middleware is not active
if hasattr(self.request, "LANGUAGE_CODE"):
lang = self.request.LANGUAGE_CODE
properties["lang"] = lang
locale = to_locale(lang)
2023-02-27 04:00:33 -06:00
properties["locale"] = locale
context["locale"] = locale
user = self.request.user
if not user.is_anonymous:
2023-02-27 04:00:33 -06:00
properties["user"] = {
"id": user.pk,
"name": str(user),
2023-07-04 09:09:12 -05:00
"url": reverse("user_dashboard"),
}
2018-05-19 04:12:19 -05:00
map_settings = self.get_geojson()
if "properties" not in map_settings:
2023-02-27 04:00:33 -06:00
map_settings["properties"] = {}
map_settings["properties"].update(properties)
map_settings["properties"]["datalayers"] = self.get_datalayers()
context["map_settings"] = json.dumps(map_settings, indent=settings.DEBUG)
2018-05-19 04:12:19 -05:00
return context
def get_datalayers(self):
return []
def is_edit_allowed(self):
return True
def get_umap_id(self):
2018-05-19 04:12:19 -05:00
return None
def is_starred(self):
return False
2018-05-19 04:12:19 -05:00
def get_geojson(self):
return {
"geometry": {
"coordinates": [DEFAULT_LONGITUDE, DEFAULT_LATITUDE],
2023-02-27 04:00:33 -06:00
"type": "Point",
2018-05-19 04:12:19 -05:00
},
"properties": {
2023-02-27 04:00:33 -06:00
"zoom": getattr(settings, "LEAFLET_ZOOM", 6),
2018-05-19 04:12:19 -05:00
"datalayers": [],
2023-02-27 04:00:33 -06:00
},
2018-05-19 04:12:19 -05:00
}
def get_short_url(self):
return None
class PermissionsMixin:
def get_permissions(self):
permissions = {}
2023-02-27 04:00:33 -06:00
permissions["edit_status"] = self.object.edit_status
permissions["share_status"] = self.object.share_status
if self.object.owner:
2023-02-27 04:00:33 -06:00
permissions["owner"] = {
"id": self.object.owner.pk,
"name": str(self.object.owner),
"url": self.object.owner.get_url(),
}
2023-02-27 04:00:33 -06:00
permissions["editors"] = [
{"id": editor.pk, "name": str(editor)}
2023-02-27 04:00:33 -06:00
for editor in self.object.editors.all()
]
if not self.object.owner and self.object.is_anonymous_owner(self.request):
permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url()
return permissions
class MapView(MapDetailMixin, PermissionsMixin, DetailView):
2018-05-19 04:12:19 -05:00
def get(self, request, *args, **kwargs):
self.object = self.get_object()
canonical = self.get_canonical_url()
if not request.path == canonical:
2023-02-27 04:00:33 -06:00
if request.META.get("QUERY_STRING"):
canonical = "?".join([canonical, request.META["QUERY_STRING"]])
2018-05-19 04:12:19 -05:00
return HttpResponsePermanentRedirect(canonical)
if not self.object.can_view(request):
return HttpResponseForbidden()
2018-05-19 04:12:19 -05:00
return super(MapView, self).get(request, *args, **kwargs)
def get_canonical_url(self):
return self.object.get_absolute_url()
def get_datalayers(self):
datalayers = DataLayer.objects.filter(map=self.object)
return [l.metadata for l in datalayers]
def is_edit_allowed(self):
return self.object.can_edit(self.request.user, self.request)
def get_umap_id(self):
2018-05-19 04:12:19 -05:00
return self.object.pk
def get_short_url(self):
shortUrl = None
2023-02-27 04:00:33 -06:00
if hasattr(settings, "SHORT_SITE_URL"):
short_path = reverse_lazy("map_short_url", kwargs={"pk": self.object.pk})
2018-05-19 04:12:19 -05:00
shortUrl = "%s%s" % (settings.SHORT_SITE_URL, short_path)
return shortUrl
def get_geojson(self):
map_settings = self.object.settings
if "properties" not in map_settings:
2023-02-27 04:00:33 -06:00
map_settings["properties"] = {}
map_settings["properties"]["name"] = self.object.name
map_settings["properties"]["permissions"] = self.get_permissions()
2018-05-19 04:12:19 -05:00
return map_settings
def is_starred(self):
user = self.request.user
if not user.is_authenticated:
return False
return Star.objects.filter(by=user, map=self.object).exists()
2018-05-19 04:12:19 -05:00
class MapViewGeoJSON(MapView):
def get_canonical_url(self):
2023-02-27 04:00:33 -06:00
return reverse("map_geojson", args=(self.object.pk,))
2018-05-19 04:12:19 -05:00
def render_to_response(self, context, *args, **kwargs):
2023-02-27 04:00:33 -06:00
return HttpResponse(context["map_settings"], content_type="application/json")
2018-05-19 04:12:19 -05:00
class MapNew(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html"
class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
2018-05-19 04:12:19 -05:00
model = Map
form_class = MapSettingsForm
def form_valid(self, form):
if self.request.user.is_authenticated:
form.instance.owner = self.request.user
self.object = form.save()
permissions = self.get_permissions()
# User does not have the cookie yet.
if not self.object.owner:
anonymous_url = self.object.get_anonymous_edit_url()
permissions["anonymous_edit_url"] = anonymous_url
2018-05-19 04:12:19 -05:00
response = simple_json_response(
id=self.object.pk,
url=self.object.get_absolute_url(),
permissions=permissions,
2018-05-19 04:12:19 -05:00
)
if not self.request.user.is_authenticated:
key, value = self.object.signed_cookie_elements
response.set_signed_cookie(
2023-02-27 04:00:33 -06:00
key=key, value=value, max_age=ANONYMOUS_COOKIE_MAX_AGE
2018-05-19 04:12:19 -05:00
)
return response
class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView):
2018-05-19 04:12:19 -05:00
model = Map
form_class = MapSettingsForm
2023-02-27 04:00:33 -06:00
pk_url_kwarg = "map_id"
2018-05-19 04:12:19 -05:00
def form_valid(self, form):
self.object.settings = form.cleaned_data["settings"]
self.object.save()
return simple_json_response(
id=self.object.pk,
url=self.object.get_absolute_url(),
permissions=self.get_permissions(),
info=_("Map has been updated!"),
2018-05-19 04:12:19 -05:00
)
2018-06-15 16:25:38 -05:00
class UpdateMapPermissions(FormLessEditMixin, UpdateView):
2018-05-19 04:12:19 -05:00
model = Map
2023-02-27 04:00:33 -06:00
pk_url_kwarg = "map_id"
2018-05-19 04:12:19 -05:00
def get_form_class(self):
if self.object.owner:
return UpdateMapPermissionsForm
else:
return AnonymousMapPermissionsForm
def get_form(self, form_class=None):
2018-06-15 16:25:38 -05:00
form = super().get_form(form_class)
2018-05-19 04:12:19 -05:00
user = self.request.user
if self.object.owner and not user == self.object.owner:
2023-02-27 04:00:33 -06:00
del form.fields["edit_status"]
del form.fields["share_status"]
del form.fields["owner"]
2018-05-19 04:12:19 -05:00
return form
def form_valid(self, form):
self.object = form.save()
2023-02-27 04:00:33 -06:00
return simple_json_response(info=_("Map editors updated with success!"))
2018-05-19 04:12:19 -05:00
class AttachAnonymousMap(View):
def post(self, *args, **kwargs):
2023-02-27 04:00:33 -06:00
self.object = kwargs["map_inst"]
if (
self.object.owner
or not self.object.is_anonymous_owner(self.request)
or not self.object.can_edit(self.request.user, self.request)
or not self.request.user.is_authenticated
):
return HttpResponseForbidden()
self.object.owner = self.request.user
self.object.save()
return simple_json_response()
class SendEditLink(FormLessEditMixin, FormView):
form_class = SendLinkForm
def post(self, form, **kwargs):
self.object = kwargs["map_inst"]
if (
self.object.owner
or not self.object.is_anonymous_owner(self.request)
or not self.object.can_edit(self.request.user, self.request)
):
return HttpResponseForbidden()
form = self.get_form()
if form.is_valid():
email = form.cleaned_data["email"]
else:
return HttpResponseBadRequest("Invalid")
link = self.object.get_anonymous_edit_url()
send_mail(
_(
"The uMap edit link for your map: %(map_name)s"
% {"map_name": self.object.name}
),
_("Here is your secret edit link: %(link)s" % {"link": link}),
settings.FROM_EMAIL,
[email],
fail_silently=False,
)
return simple_json_response(
info=_("Email sent to %(email)s" % {"email": email})
)
2018-05-19 04:12:19 -05:00
class MapDelete(DeleteView):
model = Map
pk_url_kwarg = "map_id"
2023-02-22 08:19:38 -06:00
def form_valid(self, form):
2018-05-19 04:12:19 -05:00
self.object = self.get_object()
if self.object.owner and self.request.user != self.object.owner:
2023-02-27 04:00:33 -06:00
return HttpResponseForbidden(_("Only its owner can delete the map."))
if not self.object.owner and not self.object.is_anonymous_owner(self.request):
return HttpResponseForbidden()
2018-05-19 04:12:19 -05:00
self.object.delete()
return simple_json_response(redirect="/")
class MapClone(PermissionsMixin, View):
2018-05-19 04:12:19 -05:00
def post(self, *args, **kwargs):
2023-02-27 04:00:33 -06:00
if (
not getattr(settings, "UMAP_ALLOW_ANONYMOUS", False)
and not self.request.user.is_authenticated
):
return HttpResponseForbidden()
2018-05-19 04:12:19 -05:00
owner = self.request.user if self.request.user.is_authenticated else None
2023-02-27 04:00:33 -06:00
self.object = kwargs["map_inst"].clone(owner=owner)
2018-05-19 04:12:19 -05:00
response = simple_json_response(redirect=self.object.get_absolute_url())
if not self.request.user.is_authenticated:
key, value = self.object.signed_cookie_elements
response.set_signed_cookie(
2023-02-27 04:00:33 -06:00
key=key, value=value, max_age=ANONYMOUS_COOKIE_MAX_AGE
2018-05-19 04:12:19 -05:00
)
msg = _(
"Your map has been cloned! If you want to edit this map from "
"another computer, please use this link: %(anonymous_url)s"
% {"anonymous_url": self.object.get_anonymous_edit_url()}
2018-05-19 04:12:19 -05:00
)
else:
msg = _("Congratulations, your map has been cloned!")
messages.info(self.request, msg)
return response
class ToggleMapStarStatus(View):
def post(self, *args, **kwargs):
map_inst = get_object_or_404(Map, pk=kwargs["map_id"])
qs = Star.objects.filter(map=map_inst, by=self.request.user)
if qs.exists():
qs.delete()
status = False
else:
Star.objects.create(map=map_inst, by=self.request.user)
status = True
return simple_json_response(starred=status)
2018-05-19 04:12:19 -05:00
class MapShortUrl(RedirectView):
query_string = True
permanent = True
def get_redirect_url(self, **kwargs):
2023-02-27 04:00:33 -06:00
map_inst = get_object_or_404(Map, pk=kwargs["pk"])
2018-05-19 04:12:19 -05:00
url = map_inst.get_absolute_url()
if self.query_string:
2023-02-27 04:00:33 -06:00
args = self.request.META.get("QUERY_STRING", "")
2018-05-19 04:12:19 -05:00
if args:
url = "%s?%s" % (url, args)
return url
class MapAnonymousEditUrl(RedirectView):
permanent = False
def get(self, request, *args, **kwargs):
signer = Signer()
try:
2023-02-27 04:00:33 -06:00
pk = signer.unsign(self.kwargs["signature"])
2018-05-19 04:12:19 -05:00
except BadSignature:
2023-05-11 04:33:30 -05:00
signer = Signer(algorithm="sha1")
try:
pk = signer.unsign(self.kwargs["signature"])
except BadSignature:
return HttpResponseForbidden()
map_inst = get_object_or_404(Map, pk=pk)
url = map_inst.get_absolute_url()
response = HttpResponseRedirect(url)
if not map_inst.owner:
key, value = map_inst.signed_cookie_elements
response.set_signed_cookie(
key=key, value=value, max_age=ANONYMOUS_COOKIE_MAX_AGE
)
return response
2018-05-19 04:12:19 -05:00
# ############## #
# DataLayer #
# ############## #
class GZipMixin(object):
2023-02-27 04:38:59 -06:00
EXT = ".gz"
2018-05-19 04:12:19 -05:00
2023-02-27 05:04:09 -06:00
@property
2018-05-19 04:12:19 -05:00
def path(self):
2023-02-27 05:04:09 -06:00
return self.object.geojson.path
2018-05-19 04:12:19 -05:00
@property
def gzip_path(self):
return Path(f"{self.path}{self.EXT}")
@property
def last_modified(self):
# Prior to 1.3.0 we did not set gzip mtime as geojson mtime,
# but we switched from If-Match header to IF-Unmodified-Since
# and when users accepts gzip their last modified value is the gzip
# (when umap is served by nginx and X-Accel-Redirect)
# one, so we need to compare with that value in that case.
# cf https://github.com/umap-project/umap/issues/1212
path = (
self.gzip_path
if self.accepts_gzip and self.gzip_path.exists()
else self.path
)
stat = os.stat(path)
return http_date(stat.st_mtime)
2018-05-19 04:12:19 -05:00
@property
def accepts_gzip(self):
return settings.UMAP_GZIP and re_accepts_gzip.search(
self.request.META.get("HTTP_ACCEPT_ENCODING", "")
)
2018-05-19 04:12:19 -05:00
class DataLayerView(GZipMixin, BaseDetailView):
model = DataLayer
def render_to_response(self, context, **response_kwargs):
response = None
2023-02-27 05:04:09 -06:00
path = self.path
# Generate gzip if needed
if self.accepts_gzip:
if not self.gzip_path.exists():
gzip_file(path, self.gzip_path)
2018-05-19 04:12:19 -05:00
2023-02-27 04:38:59 -06:00
if getattr(settings, "UMAP_XSENDFILE_HEADER", None):
2018-05-19 04:12:19 -05:00
response = HttpResponse()
2023-02-27 04:00:33 -06:00
path = path.replace(settings.MEDIA_ROOT, "/internal")
response[settings.UMAP_XSENDFILE_HEADER] = path
2018-05-19 04:12:19 -05:00
else:
2023-02-27 05:04:09 -06:00
# Do not use in production
# (no gzip/cache-control/If-Modified-Since/If-None-Match)
2018-05-19 04:12:19 -05:00
statobj = os.stat(path)
2023-02-27 04:00:33 -06:00
with open(path, "rb") as f:
# Should not be used in production!
2023-02-27 05:04:09 -06:00
response = HttpResponse(f.read(), content_type="application/geo+json")
response["Last-Modified"] = self.last_modified
2023-02-27 04:38:59 -06:00
response["Content-Length"] = statobj.st_size
2018-05-19 04:12:19 -05:00
return response
class DataLayerVersion(DataLayerView):
2023-02-27 05:04:09 -06:00
@property
def path(self):
2023-02-27 04:00:33 -06:00
return "{root}/{path}".format(
2018-05-19 04:12:19 -05:00
root=settings.MEDIA_ROOT,
2023-02-27 04:00:33 -06:00
path=self.object.get_version_path(self.kwargs["name"]),
)
2018-05-19 04:12:19 -05:00
class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
model = DataLayer
form_class = DataLayerForm
def form_valid(self, form):
2023-02-27 04:38:59 -06:00
form.instance.map = self.kwargs["map_inst"]
2018-05-19 04:12:19 -05:00
self.object = form.save()
# Simple response with only metadatas (including new id)
2018-05-19 04:12:19 -05:00
response = simple_json_response(**self.object.metadata)
response["Last-Modified"] = self.last_modified
2018-05-19 04:12:19 -05:00
return response
class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
model = DataLayer
form_class = DataLayerForm
def form_valid(self, form):
self.object = form.save()
# Simple response with only metadatas (client should not reload all data
# on save)
2018-05-19 04:12:19 -05:00
response = simple_json_response(**self.object.metadata)
response["Last-Modified"] = self.last_modified
2018-05-19 04:12:19 -05:00
return response
def is_unmodified(self):
2018-05-19 04:12:19 -05:00
"""Optimistic concurrency control."""
modified = True
if_unmodified = self.request.META.get("HTTP_IF_UNMODIFIED_SINCE")
if if_unmodified:
if self.last_modified != if_unmodified:
modified = False
return modified
2018-05-19 04:12:19 -05:00
def post(self, request, *args, **kwargs):
self.object = self.get_object()
2023-02-27 04:00:33 -06:00
if self.object.map != self.kwargs["map_inst"]:
return HttpResponseForbidden()
if not self.is_unmodified():
2018-05-19 04:12:19 -05:00
return HttpResponse(status=412)
return super(DataLayerUpdate, self).post(request, *args, **kwargs)
class DataLayerDelete(DeleteView):
model = DataLayer
2023-02-22 08:19:38 -06:00
def form_valid(self, form):
2018-05-19 04:12:19 -05:00
self.object = self.get_object()
2023-02-27 04:00:33 -06:00
if self.object.map != self.kwargs["map_inst"]:
return HttpResponseForbidden()
2018-05-19 04:12:19 -05:00
self.object.delete()
return simple_json_response(info=_("Layer successfully deleted."))
class DataLayerVersions(BaseDetailView):
model = DataLayer
def render_to_response(self, context, **response_kwargs):
return simple_json_response(versions=self.object.versions)
# ############## #
# Picto #
# ############## #
2023-02-27 04:00:33 -06:00
2018-05-19 04:12:19 -05:00
class PictogramJSONList(ListView):
model = Pictogram
def render_to_response(self, context, **response_kwargs):
content = [p.json for p in Pictogram.objects.all()]
return simple_json_response(pictogram_list=content)
# ############## #
# Generic #
# ############## #
2023-02-27 04:00:33 -06:00
def stats(request):
last_week = date.today() - timedelta(days=7)
return simple_json_response(
**{
2023-06-20 08:14:28 -05:00
"version": VERSION,
"maps_count": Map.objects.count(),
"maps_active_last_week_count": Map.objects.filter(
modified_at__gt=last_week
).count(),
"users_count": User.objects.count(),
"users_active_last_week_count": User.objects.filter(
last_login__gt=last_week
).count(),
}
)
2018-05-19 04:12:19 -05:00
def logout(request):
do_logout(request)
return simple_json_response(redirect="/")
class LoginPopupEnd(TemplateView):
"""
End of a loggin process in popup.
Basically close the popup.
"""
2023-02-27 04:00:33 -06:00
2018-05-19 04:12:19 -05:00
template_name = "umap/login_popup_end.html"