Source code for geonode.api.api

#########################################################################
#
# 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 time

from django.apps import apps
from django.db.models import Q
from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.db.models import Count
from django.utils.translation import get_language

from avatar.templatetags.avatar_tags import avatar_url

from geonode import geoserver
from geonode.api.paginator import CrossSiteXHRPaginator
from geonode.api.authorization import (
    GeoNodeStyleAuthorization,
    ApiLockdownAuthorization,
    GroupAuthorization,
    GroupProfileAuthorization,
    GeoNodePeopleAuthorization,
)
from guardian.shortcuts import get_objects_for_user
from tastypie.bundle import Bundle

from geonode.base.models import ResourceBase, ThesaurusKeyword
from geonode.base.models import TopicCategory
from geonode.base.models import Region
from geonode.base.models import HierarchicalKeyword
from geonode.base.models import ThesaurusKeywordLabel
from geonode.layers.models import Dataset, Style
from geonode.people.utils import get_available_users
from geonode.maps.models import Map
from geonode.geoapps.models import GeoApp
from geonode.documents.models import Document
from geonode.groups.models import GroupProfile, GroupCategory
from django.core.serializers.json import DjangoJSONEncoder
from tastypie.serializers import Serializer
from tastypie import fields
from tastypie.resources import ModelResource
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.utils import trailing_slash

from geonode.utils import check_ogc_backend
from geonode.security.utils import get_visible_resources

[docs] FILTER_TYPES = {"dataset": Dataset, "map": Map, "document": Document, "geoapp": GeoApp}
[docs] class CountJSONSerializer(Serializer): """ Custom serializer to post process the api and add counts """
[docs] def get_resources_counts(self, options): if settings.SKIP_PERMS_FILTER: resources = ResourceBase.objects.all() else: resources = get_objects_for_user(options["user"], "base.view_resourcebase") resources = get_visible_resources( resources, options["user"], admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, unpublished_not_visible=settings.RESOURCE_PUBLISHING, private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES, ) subtypes = [] if resources and resources.exists(): if options["title_filter"]: resources = resources.filter(title__icontains=options["title_filter"]) if options["type_filter"]: _type_filter = options["type_filter"] for label, app in apps.app_configs.items(): if hasattr(app, "type") and app.type == "GEONODE_APP": if hasattr(app, "default_model"): _model = apps.get_model(label, app.default_model) if issubclass(_model, _type_filter): subtypes.append(resources.filter(polymorphic_ctype__model=_model.__name__.lower())) if not isinstance(_type_filter, str): _type_filter = _type_filter.__name__.lower() resources = resources.filter(polymorphic_ctype__model=_type_filter) counts = list() if subtypes: for subtype in subtypes: counts.extend(list(subtype.values(options["count_type"]).annotate(count=Count(options["count_type"])))) else: counts = list(resources.values(options["count_type"]).annotate(count=Count(options["count_type"]))) _counts = {} for c in counts: if c and c["count"] and options["count_type"]: if not _counts.get(c[options["count_type"]], None): _counts.update({c[options["count_type"]]: c["count"]}) else: _counts[c[options["count_type"]]] += c["count"] return _counts
[docs] def to_json(self, data, options=None): options = options or {} data = self.to_simple(data, options) counts = self.get_resources_counts(options) if "objects" in data: for item in data["objects"]: item["count"] = counts.get(item["id"], 0) # Add in the current time. data["requested_time"] = time.time() return json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True)
[docs] class TypeFilteredResource(ModelResource): """ Common resource used to apply faceting to categories, keywords, and regions based on the type passed as query parameter in the form type:dataset/map/document """
[docs] count = fields.IntegerField()
[docs] def build_filters(self, filters=None, ignore_bad_filters=False): if filters is None: filters = {} self.type_filter = None self.title_filter = None orm_filters = super().build_filters(filters) if "type" in filters and filters["type"] in FILTER_TYPES.keys(): self.type_filter = FILTER_TYPES[filters["type"]] else: self.type_filter = None if "title__icontains" in filters: self.title_filter = filters["title__icontains"] return orm_filters
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["title_filter"] = getattr(self, "title_filter", None) options["type_filter"] = getattr(self, "type_filter", None) options["user"] = request.user return super().serialize(request, data, format, options)
[docs] class TagResource(TypeFilteredResource): """ Tags api """
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["count_type"] = "keywords" return super().serialize(request, data, format, options)
[docs] class Meta:
[docs] queryset = HierarchicalKeyword.objects.all().order_by("name")
[docs] resource_name = "keywords"
[docs] allowed_methods = ["get"]
[docs] filtering = { "slug": ALL, }
[docs] serializer = CountJSONSerializer()
[docs] authorization = ApiLockdownAuthorization()
[docs] class ThesaurusKeywordResource(TypeFilteredResource): """ ThesaurusKeyword api """
[docs] thesaurus_identifier = fields.CharField(null=False)
[docs] label_id = fields.CharField(null=False)
[docs] def build_filters(self, filters={}, ignore_bad_filters=False): """ Adds filtering by current language """ _filters = filters.copy() id = _filters.pop("id", None) orm_filters = super().build_filters(_filters) if id is not None: orm_filters["id__in"] = id if "thesaurus" in _filters: orm_filters["thesaurus__identifier"] = _filters["thesaurus"] return orm_filters
[docs] def serialize(self, request, data, format, options={}): options["count_type"] = "tkeywords__id" return super().serialize(request, data, format, options)
[docs] def dehydrate_id(self, bundle): return bundle.obj.id
[docs] def dehydrate_label_id(self, bundle): return bundle.obj.id
[docs] def dehydrate_thesaurus_identifier(self, bundle): return bundle.obj.thesaurus.identifier
[docs] def dehydrate(self, bundle): lang = get_language() label = ThesaurusKeywordLabel.objects.filter(keyword=bundle.data["id"]).filter(lang=lang) if label.exists(): bundle.data["label_id"] = label.get().id bundle.data["label"] = label.get().label bundle.data["alt_label"] = label.get().label else: bundle.data["label"] = bundle.data["alt_label"] return bundle
[docs] class Meta:
[docs] queryset = ThesaurusKeyword.objects.all().order_by("alt_label").select_related("thesaurus")
[docs] resource_name = "thesaurus/keywords"
[docs] allowed_methods = ["get"]
[docs] filtering = { "id": ALL, "alt_label": ALL, "thesaurus": ALL, }
[docs] serializer = CountJSONSerializer()
[docs] authorization = ApiLockdownAuthorization()
[docs] class RegionResource(TypeFilteredResource): """ Regions api """
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["count_type"] = "regions" return super().serialize(request, data, format, options)
[docs] class Meta:
[docs] queryset = Region.objects.all().order_by("name")
[docs] resource_name = "regions"
[docs] allowed_methods = ["get"]
[docs] filtering = { "name": ALL, "code": ALL, }
if settings.API_INCLUDE_REGIONS_COUNT:
[docs] serializer = CountJSONSerializer()
[docs] authorization = ApiLockdownAuthorization()
[docs] class TopicCategoryResource(TypeFilteredResource): """ Category api """
[docs] layers_count = fields.IntegerField(default=0)
[docs] def dehydrate_datasets_count(self, bundle): request = bundle.request obj_with_perms = get_objects_for_user(request.user, "base.view_resourcebase").filter( polymorphic_ctype__model="dataset" ) filter_set = bundle.obj.resourcebase_set.filter(id__in=obj_with_perms.values("id")).filter(metadata_only=False) if not settings.SKIP_PERMS_FILTER: filter_set = get_visible_resources( filter_set, request.user if request else None, admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, unpublished_not_visible=settings.RESOURCE_PUBLISHING, private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES, ) return filter_set.distinct().count()
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["count_type"] = "category" return super().serialize(request, data, format, options)
[docs] class Meta:
[docs] queryset = TopicCategory.objects.all()
[docs] resource_name = "categories"
[docs] allowed_methods = ["get"]
[docs] filtering = { "identifier": ALL, }
[docs] serializer = CountJSONSerializer()
[docs] authorization = ApiLockdownAuthorization()
[docs] class GroupCategoryResource(TypeFilteredResource):
[docs] detail_url = fields.CharField()
[docs] member_count = fields.IntegerField()
[docs] resource_counts = fields.CharField()
[docs] class Meta:
[docs] queryset = GroupCategory.objects.all()
[docs] allowed_methods = ["get"]
[docs] include_resource_uri = False
[docs] filtering = {"slug": ALL, "name": ALL}
[docs] ordering = ["name"]
[docs] authorization = ApiLockdownAuthorization()
[docs] def apply_filters(self, request, applicable_filters): filtered = super().apply_filters(request, applicable_filters) return filtered
[docs] def dehydrate_detail_url(self, bundle): return bundle.obj.get_absolute_url()
[docs] def dehydrate_member_count(self, bundle): request = bundle.request user = request.user filtered = bundle.obj.groups.all() if not user.is_authenticated or user.is_anonymous: filtered = filtered.exclude(access="private") elif not user.is_superuser: categories_ids = user.group_list_all().values("categories") filtered = filtered.filter(Q(id__in=categories_ids) | ~Q(access="private")) return filtered.count()
[docs] def dehydrate(self, bundle): """Provide additional resource counts""" request = bundle.request counts = _get_resource_counts( request, resourcebase_filter_kwargs={"group__groupprofile__categories": bundle.obj} ) bundle.data.update(resource_counts=counts) return bundle
[docs] class GroupProfileResource(ModelResource):
[docs] categories = fields.ToManyField(GroupCategoryResource, "categories", full=True)
[docs] member_count = fields.CharField()
[docs] manager_count = fields.CharField()
[docs] logo_url = fields.CharField()
[docs] detail_url = fields.CharField()
[docs] class Meta:
[docs] queryset = GroupProfile.objects.all()
[docs] resource_name = "group_profile"
[docs] allowed_methods = ["get"]
[docs] filtering = { "title": ALL, "slug": ALL, "categories": ALL_WITH_RELATIONS, }
[docs] ordering = ["title", "last_modified"]
[docs] authorization = GroupProfileAuthorization()
[docs] def dehydrate_member_count(self, bundle): """ Provide relative URL to the geonode UI's page on the group """ return bundle.obj.member_queryset().count()
[docs] def dehydrate_manager_count(self, bundle): """ Provide relative URL to the geonode UI's page on the group """ return bundle.obj.get_managers().count()
[docs] def dehydrate_detail_url(self, bundle): """ Return relative URL to the geonode UI's page on the group """ if bundle.obj.slug: return reverse("group_detail", args=[bundle.obj.slug]) else: return None
[docs] def dehydrate_logo_url(self, bundle): return bundle.obj.logo_url
[docs] class GroupResource(ModelResource):
[docs] group_profile = fields.ToOneField(GroupProfileResource, "groupprofile", full=True, null=True, blank=True)
[docs] resource_counts = fields.CharField()
[docs] class Meta:
[docs] queryset = Group.objects.exclude(groupprofile=None)
[docs] resource_name = "groups"
[docs] allowed_methods = ["get"]
[docs] filtering = { "name": ALL, "title": ALL, "group_profile": ALL_WITH_RELATIONS, }
[docs] ordering = ["name", "last_modified"]
[docs] authorization = GroupAuthorization()
[docs] def dehydrate(self, bundle): """ Provide additional resource counts """ request = bundle.request counts = _get_resource_counts(request, resourcebase_filter_kwargs={"group": bundle.obj, "metadata_only": False}) bundle.data.update(resource_counts=counts) return bundle
[docs] def get_object_list(self, request): """ Overridden in order to exclude the ``anoymous`` group from the list """ qs = super().get_object_list(request) return qs.exclude(name="anonymous")
[docs] class ProfileResource(TypeFilteredResource): """ Profile api """
[docs] avatar_100 = fields.CharField(null=True)
[docs] profile_detail_url = fields.CharField()
[docs] email = fields.CharField(default="")
[docs] layers_count = fields.IntegerField(default=0)
[docs] maps_count = fields.IntegerField(default=0)
[docs] documents_count = fields.IntegerField(default=0)
[docs] current_user = fields.BooleanField(default=False)
[docs] activity_stream_url = fields.CharField(null=True)
[docs] def build_filters(self, filters=None, ignore_bad_filters=False): """ Adds filtering by group functionality """ if filters is None: filters = {} orm_filters = super().build_filters(filters) if "group" in filters: orm_filters["group"] = filters["group"] if "name__icontains" in filters: orm_filters["username__icontains"] = filters["name__icontains"] return orm_filters
[docs] def apply_filters(self, request, applicable_filters): """ Filter by group if applicable by group functionality """ group = applicable_filters.pop("group", None) name = applicable_filters.pop("name__icontains", None) semi_filtered = super().apply_filters(request, applicable_filters) if group is not None: semi_filtered = semi_filtered.filter(groupmember__group__slug=group) if name is not None: semi_filtered = semi_filtered.filter(profile__first_name__icontains=name) if request.user and not group and not request.user.is_superuser: semi_filtered = semi_filtered & get_available_users(request.user) return semi_filtered
[docs] def dehydrate_email(self, bundle): email = "" if bundle.request.user.is_superuser: email = bundle.obj.email return email
[docs] def dehydrate_datasets_count(self, bundle): obj_with_perms = get_objects_for_user(bundle.request.user, "base.view_resourcebase").filter( polymorphic_ctype__model="dataset" ) return ( bundle.obj.resourcebase_set.filter(id__in=obj_with_perms.values("id")) .filter(metadata_only=False) .distinct() .count() )
[docs] def dehydrate_maps_count(self, bundle): obj_with_perms = get_objects_for_user(bundle.request.user, "base.view_resourcebase").filter( polymorphic_ctype__model="map" ) return ( bundle.obj.resourcebase_set.filter(id__in=obj_with_perms.values("id")) .filter(metadata_only=False) .distinct() .count() )
[docs] def dehydrate_documents_count(self, bundle): obj_with_perms = get_objects_for_user(bundle.request.user, "base.view_resourcebase").filter( polymorphic_ctype__model="document" ) return ( bundle.obj.resourcebase_set.filter(id__in=obj_with_perms.values("id")) .filter(metadata_only=False) .distinct() .count() )
[docs] def dehydrate_avatar_100(self, bundle): return avatar_url(bundle.obj, 240)
[docs] def dehydrate_profile_detail_url(self, bundle): return bundle.obj.get_absolute_url()
[docs] def dehydrate_current_user(self, bundle): return bundle.request.user.username == bundle.obj.username
[docs] def dehydrate_activity_stream_url(self, bundle): return reverse( "actstream_actor", kwargs={"content_type_id": ContentType.objects.get_for_model(bundle.obj).pk, "object_id": bundle.obj.pk}, )
[docs] def dehydrate(self, bundle): """ Protects user's personal information from non staff """ is_owner = bundle.request.user == bundle.obj is_admin = bundle.request.user.is_staff or bundle.request.user.is_superuser if not (is_owner or is_admin): bundle.data = dict( id=bundle.data.get("id", ""), username=bundle.data.get("username", ""), first_name=bundle.data.get("first_name", ""), last_name=bundle.data.get("last_name", ""), avatar_100=bundle.data.get("avatar_100", ""), profile_detail_url=bundle.data.get("profile_detail_url", ""), documents_count=bundle.data.get("documents_count", 0), maps_count=bundle.data.get("maps_count", 0), layers_count=bundle.data.get("layers_count", 0), organization=bundle.data.get("organization", 0), ) return bundle
[docs] def prepend_urls(self): if settings.HAYSTACK_SEARCH: return [ url( r"^(?P<resource_name>{})/search{}$".format(self._meta.resource_name, trailing_slash()), self.wrap_view("get_search"), name="api_get_search", ), ] else: return []
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["count_type"] = "owner" return super().serialize(request, data, format, options)
[docs] class Meta:
[docs] queryset = get_user_model().objects.exclude(Q(username="AnonymousUser") | Q(is_active=False))
[docs] resource_name = "profiles"
[docs] allowed_methods = ["get"]
[docs] ordering = ["username", "date_joined"]
[docs] excludes = ["is_staff", "password", "is_superuser", "is_active", "last_login"]
[docs] filtering = { "username": ALL, }
[docs] serializer = CountJSONSerializer()
[docs] authorization = GeoNodePeopleAuthorization()
[docs] class OwnersResource(TypeFilteredResource): """ Owners api, lighter and faster version of the profiles api """
[docs] full_name = fields.CharField(null=True)
[docs] def apply_filters(self, request, applicable_filters): """ Filter by group if applicable by group functionality """ semi_filtered = super().apply_filters(request, applicable_filters) if request.user and not request.user.is_superuser: semi_filtered = get_available_users(request.user) return semi_filtered
[docs] def dehydrate_full_name(self, bundle): return bundle.obj.get_full_name() or bundle.obj.username
[docs] def dehydrate_email(self, bundle): email = "" if bundle.request.user.is_superuser: email = bundle.obj.email return email
[docs] def dehydrate(self, bundle): """ Protects user's personal information from non staff """ is_owner = bundle.request.user == bundle.obj is_admin = bundle.request.user.is_staff or bundle.request.user.is_superuser if not (is_owner or is_admin): bundle.data = dict(id=bundle.obj.id, username=bundle.obj) return bundle
[docs] def serialize(self, request, data, format, options=None): if options is None: options = {} options["count_type"] = "owner" return super().serialize(request, data, format, options)
[docs] class Meta:
[docs] queryset = get_user_model().objects.exclude(username="AnonymousUser")
[docs] resource_name = "owners"
[docs] allowed_methods = ["get"]
[docs] ordering = ["username", "date_joined"]
[docs] excludes = ["is_staff", "password", "is_superuser", "is_active", "last_login"]
[docs] filtering = { "username": ALL, }
[docs] serializer = CountJSONSerializer()
[docs] authorization = ApiLockdownAuthorization()
[docs] class GeoserverStyleResource(ModelResource): """ Styles API for Geoserver backend. """
[docs] body = fields.CharField(attribute="sld_body", use_in="detail")
[docs] name = fields.CharField(attribute="name")
[docs] title = fields.CharField(attribute="sld_title")
# dataset_default_style is polymorphic, so it will have many to many # relation
[docs] layer = fields.ManyToManyField( "geonode.api.resourcebase_api.LayerResource", attribute="dataset_default_style", null=True )
[docs] version = fields.CharField(attribute="sld_version", null=True, blank=True)
[docs] style_url = fields.CharField(attribute="sld_url")
[docs] workspace = fields.CharField(attribute="workspace", null=True)
[docs] type = fields.CharField(attribute="type")
[docs] class Meta:
[docs] paginator_class = CrossSiteXHRPaginator
[docs] queryset = Style.objects.all()
[docs] resource_name = "styles"
[docs] detail_uri_name = "id"
[docs] authorization = GeoNodeStyleAuthorization()
[docs] allowed_methods = ["get"]
[docs] filtering = {"id": ALL, "title": ALL, "name": ALL, "layer": ALL_WITH_RELATIONS}
[docs] def build_filters(self, filters=None, **kwargs): """ Apply custom filters for layer. """ filters = super().build_filters(filters, **kwargs) # Convert dataset__ filters into dataset_styles__dataset__ updated_filters = {} for key, value in filters.items(): key = key.replace("dataset__", "dataset_default_style__") updated_filters[key] = value return updated_filters
[docs] def populate_object(self, style): """ Populate results with necessary fields :param style: Style objects :type style: Style :return: """ style.type = "sld" return style
[docs] def build_bundle(self, obj=None, data=None, request=None, **kwargs): """ Override build_bundle method to add additional info. """ if obj is None and self._meta.object_class: obj = self._meta.object_class() elif obj: obj = self.populate_object(obj) return Bundle(obj=obj, data=data, request=request, **kwargs)
if check_ogc_backend(geoserver.BACKEND_PACKAGE):
[docs] class StyleResource(GeoserverStyleResource): """Wrapper for Generic Style Resource""" pass
[docs] def _get_resource_counts(request, resourcebase_filter_kwargs): """ Return a dict with counts of resources of various types The ``resourcebase_filter_kwargs`` argument should be a dict with a suitable queryset filter that can be applied to select only the relevant ``ResourceBase`` objects to use when retrieving counts. For example: Example usage: .. code-block:: python _get_resource_counts( request, { 'group__slug': 'my-group', } ) The above function call would result in only counting ``ResourceBase`` objects that belong to the group that has ``my-group`` as slug """ resources = get_visible_resources( ResourceBase.objects.filter(**resourcebase_filter_kwargs), request.user, request=request, admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, unpublished_not_visible=settings.RESOURCE_PUBLISHING, private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES, ) values = resources.values( "polymorphic_ctype__model", "is_approved", "is_published", ) qs = values.annotate(counts=Count("polymorphic_ctype__model")) types = ["dataset", "document", "map", "geoapp", "all"] subtypes = [] for label, app in apps.app_configs.items(): if hasattr(app, "type") and app.type == "GEONODE_APP": if hasattr(app, "default_model"): _model = apps.get_model(label, app.default_model) if issubclass(_model, GeoApp): types.append(_model.__name__.lower()) subtypes.append(_model.__name__.lower()) counts = {} for type_ in types: counts[type_] = { "total": 0, "visible": 0, "published": 0, "approved": 0, } for record in qs: resource_type = record["polymorphic_ctype__model"] if resource_type in subtypes: resource_type = "geoapp" is_visible = all((record["is_approved"], record["is_published"])) counts["all"]["total"] += record["counts"] counts["all"]["visible"] += record["counts"] if is_visible else 0 counts["all"]["published"] += record["counts"] if record["is_published"] else 0 counts["all"]["approved"] += record["counts"] if record["is_approved"] else 0 section = counts.get(resource_type) if section is not None: section["total"] += record["counts"] section["visible"] += record["counts"] if is_visible else 0 section["published"] += record["counts"] if record["is_published"] else 0 section["approved"] += record["counts"] if record["is_approved"] else 0 return counts