Source code for geonode.maps.views
#########################################################################
#
# Copyright (C) 2016 OSGeo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#########################################################################
import json
import logging
import traceback
import warnings
from urllib.parse import urljoin
from deprecated import deprecated
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect, HttpResponseServerError
from django.shortcuts import render
from django.urls import reverse
from django.views.decorators.clickjacking import xframe_options_exempt
from geonode import geoserver
from geonode.base import register_event
from geonode.base.auth import get_or_create_token
from geonode.base.forms import CategoryForm, ThesaurusAvailableForm, TKeywordForm
from geonode.base.models import ExtraMetadata, Thesaurus, TopicCategory
from geonode.base.views import batch_modify
from geonode.client.hooks import hookset
from geonode.resource.manager import resource_manager
from geonode.decorators import check_keyword_write_perms
from geonode.groups.models import GroupProfile
from geonode.layers.models import Dataset
from geonode.maps.contants import _PERMISSION_MSG_DELETE # noqa: used by mapstore
from geonode.maps.contants import _PERMISSION_MSG_SAVE # noqa: used by mapstore
from geonode.maps.contants import (
_PERMISSION_MSG_GENERIC,
_PERMISSION_MSG_VIEW,
MSG_NOT_ALLOWED,
MSG_NOT_FOUND,
)
from geonode.maps.forms import MapForm
from geonode.maps.models import Map, MapLayer
from geonode.monitoring.models import EventType
from geonode.people.forms import ProfileForm
from geonode.security.utils import get_user_visible_groups, AdvancedSecurityWorkflowManager
from geonode.utils import check_ogc_backend, http_client, resolve_object
if check_ogc_backend(geoserver.BACKEND_PACKAGE):
# FIXME: The post service providing the map_status object
# should be moved to geonode.geoserver.
from geonode.geoserver.helpers import ogc_server_settings
[docs]
def _resolve_map(request, id, permission="base.change_resourcebase", msg=_PERMISSION_MSG_GENERIC, **kwargs):
"""
Resolve the Map by the provided typename and check the optional permission.
"""
key = "urlsuffix" if Map.objects.filter(urlsuffix=id).exists() else "pk"
map_obj = resolve_object(request, Map, {key: id}, permission=permission, permission_msg=msg, **kwargs)
return map_obj
@login_required
@check_keyword_write_perms
[docs]
def map_metadata(
request,
mapid,
template="maps/map_metadata.html",
ajax=True,
panel_template="layouts/map_panels.html",
custom_metadata=None,
):
try:
map_obj = _resolve_map(request, mapid, "base.change_resourcebase_metadata", _PERMISSION_MSG_VIEW)
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
# Add metadata_author or poc if missing
map_obj.add_missing_metadata_author_or_poc()
current_keywords = [keyword.name for keyword in map_obj.keywords.all()]
topic_thesaurus = map_obj.tkeywords.all()
topic_category = map_obj.category
if request.method == "POST":
map_form = MapForm(request.POST, instance=map_obj, prefix="resource", user=request.user)
category_form = CategoryForm(
request.POST,
prefix="category_choice_field",
initial=(
int(request.POST["category_choice_field"])
if "category_choice_field" in request.POST and request.POST["category_choice_field"]
else None
),
)
if hasattr(settings, "THESAURUS"):
tkeywords_form = TKeywordForm(request.POST)
else:
tkeywords_form = ThesaurusAvailableForm(request.POST, prefix="tkeywords")
else:
map_form = MapForm(instance=map_obj, prefix="resource", user=request.user)
map_form.disable_keywords_widget_for_non_superuser(request.user)
category_form = CategoryForm(
prefix="category_choice_field", initial=topic_category.id if topic_category else None
)
# Keywords from THESAURUS management
map_tkeywords = map_obj.tkeywords.all()
tkeywords_list = ""
# Create THESAURUS widgets
lang = "en"
if hasattr(settings, "THESAURUS") and settings.THESAURUS:
warnings.warn(
"The settings for Thesaurus has been moved to Model, \
this feature will be removed in next releases",
DeprecationWarning,
)
tkeywords_list = ""
if map_tkeywords and len(map_tkeywords) > 0:
tkeywords_ids = map_tkeywords.values_list("id", flat=True)
if hasattr(settings, "THESAURUS") and settings.THESAURUS:
el = settings.THESAURUS
thesaurus_name = el["name"]
try:
t = Thesaurus.objects.get(identifier=thesaurus_name)
for tk in t.thesaurus.filter(pk__in=tkeywords_ids):
tkl = tk.keyword.filter(lang=lang)
if len(tkl) > 0:
tkl_ids = ",".join(map(str, tkl.values_list("id", flat=True)))
tkeywords_list += f",{tkl_ids}" if len(tkeywords_list) > 0 else tkl_ids
except Exception:
tb = traceback.format_exc()
logger.error(tb)
tkeywords_form = TKeywordForm(instance=map_obj)
else:
tkeywords_form = ThesaurusAvailableForm(prefix="tkeywords")
# set initial values for thesaurus form
for tid in tkeywords_form.fields:
values = []
values = [keyword.id for keyword in topic_thesaurus if int(tid) == keyword.thesaurus.id]
tkeywords_form.fields[tid].initial = values
if request.method == "POST" and map_form.is_valid() and category_form.is_valid() and tkeywords_form.is_valid():
new_keywords = current_keywords if request.keyword_readonly else map_form.cleaned_data["keywords"]
new_regions = map_form.cleaned_data["regions"]
new_title = map_form.cleaned_data["title"]
new_abstract = map_form.cleaned_data["abstract"]
new_category = None
if (
category_form
and "category_choice_field" in category_form.cleaned_data
and category_form.cleaned_data["category_choice_field"]
):
new_category = TopicCategory.objects.get(id=int(category_form.cleaned_data["category_choice_field"]))
# update contact roles
map_obj.set_contact_roles_from_metadata_edit(map_form)
map_obj.save()
map_obj.title = new_title
map_obj.abstract = new_abstract
map_obj.keywords.clear()
map_obj.keywords.add(*new_keywords)
map_obj.regions.clear()
map_obj.regions.add(*new_regions)
map_obj.category = new_category
# clearing old metadata from the resource
map_obj.metadata.all().delete()
# creating new metadata for the resource
for _m in json.loads(map_form.cleaned_data["extra_metadata"]):
new_m = ExtraMetadata.objects.create(resource=map_obj, metadata=_m)
map_obj.metadata.add(new_m)
map_form.save_linked_resources()
register_event(request, EventType.EVENT_CHANGE_METADATA, map_obj)
if not ajax:
return HttpResponseRedirect(hookset.map_detail_url(map_obj))
message = map_obj.id
try:
# Keywords from THESAURUS management
# Rewritten to work with updated autocomplete
if not tkeywords_form.is_valid():
return HttpResponse(json.dumps({"message": "Invalid thesaurus keywords"}, status_code=400))
thesaurus_setting = getattr(settings, "THESAURUS", None)
if thesaurus_setting:
tkeywords_data = tkeywords_form.cleaned_data["tkeywords"]
tkeywords_data = tkeywords_data.filter(thesaurus__identifier=thesaurus_setting["name"])
map_obj.tkeywords.set(tkeywords_data)
elif Thesaurus.objects.all().exists():
fields = tkeywords_form.cleaned_data
map_obj.tkeywords.set(tkeywords_form.cleanx(fields))
except Exception:
tb = traceback.format_exc()
logger.error(tb)
vals = {}
if "group" in map_form.changed_data:
vals["group"] = map_form.cleaned_data.get("group")
if any([x in map_form.changed_data for x in ["is_approved", "is_published"]]):
vals["is_approved"] = map_form.cleaned_data.get("is_approved", map_obj.is_approved)
vals["is_published"] = map_form.cleaned_data.get("is_published", map_obj.is_published)
resource_manager.update(
map_obj.uuid,
instance=map_obj,
notify=True,
vals=vals,
extra_metadata=json.loads(map_form.cleaned_data["extra_metadata"]),
)
return HttpResponse(json.dumps({"message": message}))
elif request.method == "POST" and (
not map_form.is_valid() or not category_form.is_valid() or not tkeywords_form.is_valid()
):
errors_list = {**map_form.errors.as_data(), **category_form.errors.as_data(), **tkeywords_form.errors.as_data()}
logger.error(f"GeoApp Metadata form is not valid: {errors_list}")
out = {"success": False, "errors": [f"{x}: {y[0].messages[0]}" for x, y in errors_list.items()]}
return HttpResponse(json.dumps(out), content_type="application/json", status=400)
# - POST Request Ends here -
# Request.GET
# define contact role forms
contact_role_forms_context = {}
for role in map_obj.get_multivalue_role_property_names():
map_form.fields[role].initial = [p.username for p in map_obj.__getattribute__(role)]
role_form = ProfileForm(prefix=role)
role_form.hidden = True
contact_role_forms_context[f"{role}_form"] = role_form
layers = MapLayer.objects.filter(map=map_obj.id)
metadata_author_groups = get_user_visible_groups(request.user)
if not AdvancedSecurityWorkflowManager.is_allowed_to_publish(request.user, map_obj):
map_form.fields["is_published"].widget.attrs.update({"disabled": "true"})
if not AdvancedSecurityWorkflowManager.is_allowed_to_approve(request.user, map_obj):
map_form.fields["is_approved"].widget.attrs.update({"disabled": "true"})
register_event(request, EventType.EVENT_VIEW_METADATA, map_obj)
return render(
request,
template,
context={
"resource": map_obj,
"map": map_obj,
"config": json.dumps(map_obj.blob),
"panel_template": panel_template,
"custom_metadata": custom_metadata,
"map_form": map_form,
"category_form": category_form,
"tkeywords_form": tkeywords_form,
"layers": layers,
"preview": getattr(settings, "GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY", "mapstore"),
"crs": getattr(settings, "DEFAULT_MAP_CRS", "EPSG:3857"),
"metadata_author_groups": metadata_author_groups,
"TOPICCATEGORY_MANDATORY": getattr(settings, "TOPICCATEGORY_MANDATORY", False),
"GROUP_MANDATORY_RESOURCES": getattr(settings, "GROUP_MANDATORY_RESOURCES", False),
"UI_MANDATORY_FIELDS": list(
set(getattr(settings, "UI_DEFAULT_MANDATORY_FIELDS", []))
| set(getattr(settings, "UI_REQUIRED_FIELDS", []))
),
**contact_role_forms_context,
"UI_ROLES_IN_TOGGLE_VIEW": map_obj.get_ui_toggled_role_property_names(),
},
)
@login_required
[docs]
def map_metadata_advanced(request, mapid):
return map_metadata(request, mapid, template="maps/map_metadata_advanced.html")
@xframe_options_exempt
[docs]
def map_embed(request, mapid=None, template="maps/map_embed.html"):
try:
map_obj = _resolve_map(request, mapid, "base.view_resourcebase", _PERMISSION_MSG_VIEW)
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
access_token = None
if request and request.user:
access_token = get_or_create_token(request.user)
if access_token and not access_token.is_expired():
access_token = access_token.token
else:
access_token = None
context_dict = {
"access_token": access_token,
"resource": map_obj,
}
register_event(request, EventType.EVENT_VIEW, map_obj)
return render(request, template, context=context_dict)
# NEW MAPS #
[docs]
def clean_config(conf):
if isinstance(conf, str):
config = json.loads(conf)
config_extras = [
"tools",
"rest",
"homeUrl",
"localGeoServerBaseUrl",
"localCSWBaseUrl",
"csrfToken",
"db_datastore",
"authorizedRoles",
]
for config_item in config_extras:
if config_item in config:
del config[config_item]
if config_item in config["map"]:
del config["map"][config_item]
return json.dumps(config)
else:
return conf
# MAPS DOWNLOAD #
[docs]
def map_download(request, mapid, template="maps/map_download.html"):
"""
Download all the layers of a map as a batch
XXX To do, remove layer status once progress id done
This should be fix because
"""
try:
map_obj = _resolve_map(request, mapid, "base.download_resourcebase", _PERMISSION_MSG_VIEW)
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
map_status = dict()
if request.method == "POST":
def perm_filter(layer):
return request.user.has_perm("base.view_resourcebase", obj=layer.get_self_resource())
mapJson = map_obj.json(perm_filter)
# we need to remove duplicate layers
j_map = json.loads(mapJson)
j_datasets = j_map["layers"]
for j_dataset in j_datasets:
if j_dataset["service"] is None:
j_datasets.remove(j_dataset)
continue
if (len([_l for _l in j_datasets if _l == j_dataset])) > 1:
j_datasets.remove(j_dataset)
mapJson = json.dumps(j_map)
# the path to geoserver backend continue here
url = urljoin(settings.SITEURL, reverse("download-map", kwargs={"mapid": mapid}))
resp, content = http_client.request(url, "POST", data=mapJson)
status = int(resp.status_code)
if status == 200:
map_status = json.loads(content)
request.session["map_status"] = map_status
else:
raise Exception(f"Could not start the download of {map_obj.title}. Error was: {content}")
locked_datasets = []
remote_datasets = []
downloadable_datasets = []
for lyr in map_obj.maplayers.iterator():
if lyr.group != "background":
if not lyr.local:
remote_datasets.append(lyr)
else:
ownable_dataset = Dataset.objects.get(alternate=lyr.name)
if not request.user.has_perm("download_resourcebase", obj=ownable_dataset.get_self_resource()):
locked_datasets.append(lyr)
else:
# we need to add the layer only once
if len([_l for _l in downloadable_datasets if _l.name == lyr.name]) == 0:
downloadable_datasets.append(lyr)
site_url = settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL
register_event(request, EventType.EVENT_DOWNLOAD, map_obj)
return render(
request,
template,
context={
"geoserver": ogc_server_settings.PUBLIC_LOCATION,
"map_status": map_status,
"map": map_obj,
"locked_datasets": locked_datasets,
"remote_datasets": remote_datasets,
"downloadable_datasets": downloadable_datasets,
"site": site_url,
},
)
[docs]
def map_wmc(request, mapid, template="maps/wmc.xml"):
"""Serialize an OGC Web Map Context Document (WMC) 1.1"""
try:
map_obj = _resolve_map(request, mapid, "base.view_resourcebase", _PERMISSION_MSG_VIEW)
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
site_url = settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL
return render(
request,
template,
context={
"map": map_obj,
"maplayers": map_obj.maplayers.all(),
"siteurl": site_url,
},
content_type="text/xml",
)
@deprecated(version="2.10.1", reason="APIs have been changed on geospatial service")
[docs]
def map_wms(request, mapid):
"""
Publish local map layers as group layer in local OWS.
/maps/:id/wms
GET: return endpoint information for group layer,
PUT: update existing or create new group layer.
"""
try:
map_obj = _resolve_map(request, mapid, "base.view_resourcebase", _PERMISSION_MSG_VIEW)
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
if request.method == "PUT":
try:
layerGroupName = map_obj.publish_dataset_group()
response = dict(
layerGroupName=layerGroupName,
ows=getattr(ogc_server_settings, "ows", ""),
)
register_event(request, EventType.EVENT_PUBLISH, map_obj)
return HttpResponse(json.dumps(response), content_type="application/json")
except Exception:
return HttpResponseServerError()
if request.method == "GET":
response = dict(
layerGroupName=getattr(map_obj.dataset_group, "name", ""),
ows=getattr(ogc_server_settings, "ows", ""),
)
return HttpResponse(json.dumps(response), content_type="application/json")
return HttpResponseNotAllowed(["PUT", "GET"])
[docs]
def mapdataset_attributes(request, layername):
# Return custom layer attribute labels/order in JSON format
layer = Dataset.objects.get(alternate=layername)
return HttpResponse(json.dumps(layer.attribute_config()), content_type="application/json")
[docs]
def get_suffix_if_custom(map):
if map.use_custom_template:
if map.featuredurl:
return map.featuredurl
elif map.urlsuffix:
return map.urlsuffix
else:
return None
else:
return None
[docs]
def ajax_url_lookup(request):
if request.method != "POST":
return HttpResponse(content="ajax user lookup requires HTTP POST", status=405, content_type="text/plain")
elif "query" not in request.POST:
return HttpResponse(
content='use a field named "query" to specify a prefix to filter urls', content_type="text/plain"
)
if request.POST["query"] != "":
maps = Map.objects.filter(urlsuffix__startswith=request.POST["query"])
if request.POST["mapid"] != "":
maps = maps.exclude(id=request.POST["mapid"])
json_dict = {
"urls": [({"url": m.urlsuffix}) for m in maps],
"count": maps.count(),
}
else:
json_dict = {
"urls": [],
"count": 0,
}
return HttpResponse(content=json.dumps(json_dict), content_type="text/plain")
[docs]
def map_metadata_detail(request, mapid, template="maps/map_metadata_detail.html", custom_metadata=None):
try:
map_obj = _resolve_map(request, mapid, "view_resourcebase")
except PermissionDenied:
return HttpResponse(MSG_NOT_ALLOWED, status=403)
except Exception:
raise Http404(MSG_NOT_FOUND)
if not map_obj:
raise Http404(MSG_NOT_FOUND)
group = None
if map_obj.group:
try:
group = GroupProfile.objects.get(slug=map_obj.group.name)
except GroupProfile.DoesNotExist:
group = None
site_url = settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL
register_event(request, EventType.EVENT_VIEW_METADATA, map_obj)
return render(
request,
template,
context={"resource": map_obj, "group": group, "SITEURL": site_url, "custom_metadata": custom_metadata},
)
@login_required