From f5544248e24a0ad6f1415dc2d4f687004ac4ae19 Mon Sep 17 00:00:00 2001 From: Alessio Fabiani Date: Thu, 10 Dec 2020 16:59:53 +0100 Subject: [PATCH] [Issue #6684] GNIP-78: GeoNode generic "Apps" model to include pluggable entities into the framework (#6713) * [Hardening] - Recenet Activity List for Documents error when actor is None * [Frontend] Monitoring: Bump "node-sass" to version 4.14.1 * [Frontend] Bump jquery to version 3.5.1 * [Fixes: #6519] Bump jquery to 3.5.1 (#6526) (cherry picked from commit e53281357af33cc1a8f0645e6437e8dfcfcb34fd) # Conflicts: # geonode/static/lib/css/assets.min.css # geonode/static/lib/css/bootstrap-select.css # geonode/static/lib/css/bootstrap-table.css # geonode/static/lib/js/assets.min.js # geonode/static/lib/js/bootstrap-select.js # geonode/static/lib/js/bootstrap-table.js # geonode/static/lib/js/leaflet-plugins.min.js # geonode/static/lib/js/leaflet.js # geonode/static/lib/js/moment-timezone-with-data.js # geonode/static/lib/js/underscore.js * Merge branch 'master' of https://github.com/GeoNode/geonode into rest_api_v2_proof_of_concept # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. * [Hardening] Re-create the map thumbnail only if it is missing * Fixes error with GDAL 3.0.4 due to a breaking change on GDAL (https://code.djangoproject.com/ticket/30645) * Fixes error with GDAL 3.0.4 due to a breaking change on GDAL (https://code.djangoproject.com/ticket/30645) * - Introducing the concept of "GeoNode App" Resource Base * [GeoApps] Add "Create new" Button to the apps list page * [GeoApps] Hooking Resources List pages * [GeoApps] Hooking GeoApp List page * [GeoApps] Hooking GeoApp rest v2 API serializers fixes * [GeoApps] Fix resourcebase_api polymorphic ctype filter * [GeoApps] REST API v2 "geostories" endpoints optimizations * [REST APIs V2] Make use of the new "bbox_polygon" field based on GeoDjango * [Fixes RemoteServices bbox parse] Merge branch 'search-by-extent' of https://github.com/mtnorthcott/geonode * [Fix migrations] Merge branch 'search-by-extent' of https://github.com/mtnorthcott/geonode * [Fix migrations] Merge branch 'search-by-extent' of https://github.com/mtnorthcott/geonode * [GeoApps] Adding "geoapp_edit" page * [GeoApps] Adding "geoapp_edit" page context * [GeoApps] Adding security info (access_token, user, ...) to the page context * [GeoApps] Adding client endpoints * [GeoApps] Missing "post_save" signal * [GeoApps] Finalize GeoApp resources management * Fix "bbox_to_projection" coords order * Fix 'bbox_to_projection' coords order * Fix "bbox_to_projection" coords order (cherry picked from commit 72d6c1e718ac974273a048d3e6c9f5b5bdc6a76e) * Fix "bbox_to_projection" coords order: check GDAL version >= 3.0.4 * Include missing 'mapstore2_adapter.geoapps' app to default INSTALLED_APPS * Include mapstore client branch dependencies into requirements * Revert security commit on branch * Minor review of the current advanced resource workflow implementation #6551 * Minor review of the current advanced resource workflow implementation #6551 * Fix tests on Travis * Fix tests on Travis * Fix tests on Travis * Fix tests on Travis * Fix tests on Travis (cherry picked from commit c7f651c9bbdb95d1ac33b8ee357b55d8612778a4) # Conflicts: # geonode/layers/tests.py * Fix logical errors on approval workflow * Fix logical errors on approval workflow (cherry picked from commit 7a3d5d0bfb540d14e4e2b8db68154678a42baa17) * Fix tests on Travis * Cleanup "app_embed" template * Advanced workflow: remove change_permissions to the owner if not a manager * Advanced workflow: remove change_permissions to the owner if not a manager (cherry picked from commit 9a1552ac8429f3b2a26cbc074eac580bf77046dc) * Fix app_embed template * Advanced workflow: remove change_permissions to the owner if not a manager * Advanced workflow: remove change_permissions to the owner if not a manager * Advanced workflow: remove change_permissions to the owner if not a manager (cherry picked from commit f23096c64282294a65b9354ecba9bde678a51633) * Advanced workflow: remove change_permissions to the owner if not a manager (cherry picked from commit bfe51a7f562b69f88bee99d5ea29a6129131668d) * Advanced workflow: remove change_permissions to the owner if not a manager * Advanced workflow: remove change_permissions to the owner if not a manager (cherry picked from commit d9ec56673c07114e51c527ace2946e934bd3226c) * Advanced workflow: filter actions stream returned to the users accordingly to their perms * Advanced workflow: filter actions stream returned to the users accordingly to their perms (cherry picked from commit 7f513467a6dfae62b41fdbbdd732b441e7f505bc) * Advanced workflow: filter actions stream returned to the users accordingly to their perms * Add new settings from django-allauth 0.43.0 * Advanced workflow: filter actions stream returned to the users accordingly to their perms (cherry picked from commit e2522fd10e84e4131d3a65f3868c0ffd673bf63f) * Add new settings from django-allauth 0.43.0 (cherry picked from commit 00f4be1183a7aaa6f727b589f0c19dbb967fa420) * Code styling alerts: remove unnecessary pass * Refreshing static libs * Refreshing static libs * Code styling alerts: remove unnecessary pass (cherry picked from commit 0676f6e720f74a87bf15a19508db8e45c2e11c79) * Refreshing static libs (cherry picked from commit f27d0dfce0477f48c6ca279898b90d136c83e712) * Refreshing static libs (cherry picked from commit 5b166bcecc8479b88788da1c77e8ced4f9de3547) * Advanced Workflow: Make sure the APIs counters are coherent with the visible resources * Advanced Workflow: Make sure the APIs counters are coherent with the visible resources (cherry picked from commit 1855d74e6fe6778d1ce19b81ac883b3594e04048) * fix english/italian translations (#6563) * fix english/italian translations (#6563) * Advanced Workflow: fix "request editing" action when published * Advanced Workflow: fix "request editing" action when published -> send messages to group managers too * Advanced Workflow: fix "request editing" action when published (cherry picked from commit 1041b1245c673c6351ccf7928b20a99d5fca8dad) * Advanced Workflow: fix "request editing" action when published -> send messages to group managers too (cherry picked from commit 5c93ef3570182261e3f58dab4e13c92eb206a43b) * Fix test on travis * fix english/italian translations (#6563) * fix english/italian translations (#6563) * fix english/italian translations (#6563) * Avoid override User settings on "set_attributes_from_geoserver" * - Docs links to 3.x branch * Improve Celery Async Tasks configuration (cherry picked from commit 50e208a9ecff32244f34c6593d97770cfb1b5b45) * Improve Celery Async Tasks configuration (cherry picked from commit d5150e8cb4dd40aa25adb9b664a64e21e6087703) * Improve Celery Async Tasks configuration (cherry picked from commit 50e208a9ecff32244f34c6593d97770cfb1b5b45) * - Replace build.geo-solutions.it with www.dropbox.com (cherry picked from commit 882e3e5368a009c74d178867bac9ddccf7b47176) (cherry picked from commit 7b970f847dceb33eb0ba562e15330d85648a47d4) * [Security] Hardening Advanced Workflow resources visibility (cherry picked from commit 2103f13f862674163ecd9b5f43df0d733aea77f8) (cherry picked from commit 025c82e2f2969ca7d07270124f670589f8f5725a) * [Hardening] Removing redundant and replacement of instance abstract from GeoServer * Bump drf-yasg from 1.17.1 to 1.20.0 * [Hardening] Fixes: db connection closed and worker hangs with celery 4.2+ https://github.com/celery/celery/issues/4878 * [Hardening] Optimizing celery tasks settings * [Hardening] Optimizing celery tasks settings * - Documents REST v2 APIs * [Fixes #6596] Incorrect Legend displayed in the layer detail page (cherry picked from commit 0aa690204b8787b1dd2255f62bb2586c98ad0e06) * [Fixes #6596] Incorrect Legend displayed in the layer detail page (cherry picked from commit 0aa690204b8787b1dd2255f62bb2586c98ad0e06) * - Update travis dist to '20.04 focal' * - Fix geolimits panel translations * - Filter Comments on Recent Activities accordingly to the user's perms * [Hardening] Remove wrong class initializer * [Hardening] LGTM warning fixes * [CI Optimizations] - Continuous integration builders: CircleCI config based on "spcgeonode" docker-compose (cherry picked from commit 7f091a71aa21619198cf22b1c41114493c33c12c) * - Enable "memcached" plugins for monitoring * - Extend "documents" to accept and render video, audio and more image formats - Add "attribution" field to ResourceBase model * - Generating documents thumbnails for video and audio mime types (cherry picked from commit d1f425119cbef03706e7bb0648eaad1dca09507e) (cherry picked from commit 5c89762f38638a1f141680f82113e38cc6a01b55) * - Merge with master branch * - Generating documents thumbnails for video and audio mime types (cherry picked from commit 197c7ab354580e1d3b9addf7060affc6e9932059) * - Fixing doc image thumn generation * - Updating translations * - expose documents 'href' from REST serializer API endpoint * [Hardening] - expose **secured** documents 'href' from REST serializer API endpoint * [Hardening] - generate **secured** thumbnail for uploaded images * - Restore missing list key on GXP_PTYPES enumeration (cherry picked from commit 2352613e23203204217fecb529c490a241c09fb7) * [FIX #6626] add tinymce editor to resource text areas (cherry picked from commit 45bb0dcee2f6f67890c30e6ee6aa3ba95f4bf921) * [FIX #6626] add tinymce editor to resource text areas (cherry picked from commit 45bb0dcee2f6f67890c30e6ee6aa3ba95f4bf921) * [Hardening] Correctly manage "_resolve_object" exception as Django error templates (cherry picked from commit 017d8852653059c9dff2ad3027bd36b4059ec3ac) # Conflicts: # geonode/views.py * - Remove wrong migration * [Hardening] Using "apply_async" instead of "delay" for async signals calls * [Hardening] Avoid exit prematurely from geoserver cascading delete signal * Fix travis tests * [Fixes #5779] Data edition permissions set in GeoNode for a layer are not applied on the WFS (cherry picked from commit 9e4e839cb9693cd380f441e9d429429b56f1b2d1) * - Cleaning up wrong migrations * [Performance] - Improve Style editing requests callbacks * [Performance] - Transform "geoserver_post_save_layers" to an asynchronous task * [Performance] - Improve Style editing requests callbacks * [Optimization] Improve 'navbar' content reposition script * [Performance] - Transform "geoserver_post_save_layers" to an asynchronous task * [Performance] - Improve Style editing requests callbacks * FIXES[#6653] Mail notifications for private datasets are public * - exclude query optimization * [Performance] Dinamically loading the list of users geo-limits (cherry picked from commit c54cc617337efdfb6319a93abf77beed4d016963) (cherry picked from commit 756c1aac7fe5659691e98f3c3ea10ca10528b2a3) * [Fixes: #6640] Style Tag outside of html (#6657) * [Minor Layout Issue] - Missing title on "map list" page (cherry picked from commit 971e65f99355dd49dc502e0882196fccaff20918) * added Document Creation Fallback, fixed exclude_user_ids.append() * - Correct "geoapps" notification types * - Fix remaining issues: 1. Layer create does no send "title" before sending notifications - 2. Doc created does not set "permissions" before sending notifications * Typo: _QUEUE_ALL_FLAG * Typo: _QUEUE_ALL_FLAG (cherry picked from commit 8d9118f84d28c6c7ef4577db30c3b6f155775275) * - Fix asynchronous notification engine task * - Fix asynchronous notification engine task (cherry picked from commit 79274ebb8f218c892917038d88364643056285ff) * - Do not send notifications if the resource has no title * - Do not send notifications if the resource has no title (cherry picked from commit c3d470e0c5819fce791c5a519761b0d59f5a207d) * - Asynchronous "probe" task for Remote Services * [FIXES #6653] Mail notifications for private datasets are public * - Fixes rating notifications * - Fixes rating notifications (cherry picked from commit b814692a837a0107c4cacd7a88e0731918247e02) * - Fixes "guardian.exceptions.ObjectNotPersisted: Object None needs to be persisted first" exception on "set_workflow_perms" calls * - Fixes "guardian.exceptions.ObjectNotPersisted: Object None needs to be persisted first" exception on "set_workflow_perms" calls (cherry picked from commit fe35d46f4a58bd28a4aefd5821e29ad8d9869115) * - Fixes "guardian.exceptions.ObjectNotPersisted: Object None needs to be persisted first" exception on "set_workflow_perms" calls * - Fixes "guardian.exceptions.ObjectNotPersisted: Object None needs to be persisted first" exception on "set_workflow_perms" calls (cherry picked from commit dee7de1d9e26eaad2a92dcdfd30ec625dd3261bd) * - Fix LGTM issues * - Fix LGTM issues (cherry picked from commit 08644a60cfdf04497b1b099e241f9dd64e66ea99) * - Fix LGTM issues * - Fix LGTM issues (cherry picked from commit df112c8cfa355e6c3d78f0c11cbb58996f696ba1) * no notifications for resource owner, except for comments. PEP 8 reformatting * resource owners will get notified on updates of their resources * [Fixes #6665] Improve WYSIWYG metadata editor to store formatted and plain texts * - Travis test-cases: "ensure owner won't be notified on upload" * [Hardening] Do not fail in case of datastore with multiple geometries * - Minor refactoring and clean out of the "geoserver_post_save_layers" task body * [Hardening] Make "set_attributes" method more resilient to "Attribute.MultipleObjectsReturned" exception * [Hardening] Make "helpers" methods more resilient to "Layer.MultipleObjectsReturned" and "Layer.DoesNotExist" exceptions * - Minor environmnet params improvements. Exposing DB connection timeouts to .env * - Explicit error codes along with description on Layer Upload form * [Transaltions] - Explicit error codes along with description on Layer Upload form * [Transaltions] - Explicit error codes along with description on Layer Upload form (cherry picked from commit 395089e36faa24973a81b8a69162545d0de910e5) # Conflicts: # geonode/static/geonode/js/upload/LayerInfo.js (cherry picked from commit f62b69a6a5c06c2d23c07cbfb03a239f59dc7338) * [Docker] Use local nginx build * Merge branch 'master' of https://github.com/GeoNode/geonode into rest_api_v2_geonode_apps * [Hardening] More resiliet to 'missing thumbnail' on filesystem issues * - GeoApp Test Cases * - Typo * - Update mapstore client and adapter versions * - Set local .sh files exec perms * - Bump pycsw to version 2.6.0 * - Bump pycsw to version 2.6.0 * - Bump pycsw to version 2.6.0 * - Align "setup.cfg" to "requirements.txt" * - Fix travis Co-authored-by: Toni Co-authored-by: Piotr Dankowski Co-authored-by: Florian Hoedt --- geonode/api/api.py | 41 +- geonode/api/resourcebase_api.py | 66 ++ geonode/api/urls.py | 1 + geonode/base/api/serializers.py | 8 + geonode/base/api/tests.py | 64 +- geonode/base/api/urls.py | 2 +- geonode/base/api/views.py | 69 ++- .../templates/base/_resourcebase_snippet.html | 1 + geonode/base/templatetags/base_tags.py | 52 +- geonode/client/hooksets.py | 25 + .../client/templatetags/client_lib_tags.py | 43 ++ geonode/context_processors.py | 6 +- geonode/documents/api/views.py | 13 +- geonode/geoapps/__init__.py | 38 ++ geonode/geoapps/admin.py | 50 ++ geonode/geoapps/api/__init__.py | 19 + geonode/geoapps/api/permissions.py | 59 ++ geonode/geoapps/api/serializers.py | 176 ++++++ geonode/geoapps/api/tests.py | 238 ++++++++ geonode/geoapps/api/urls.py | 26 + geonode/geoapps/api/views.py | 52 ++ geonode/geoapps/forms.py | 34 ++ geonode/geoapps/migrations/0001_initial.py | 48 ++ geonode/geoapps/migrations/__init__.py | 19 + geonode/geoapps/models.py | 150 +++++ geonode/geoapps/templates/apps/app_base.html | 11 + .../geoapps/templates/apps/app_detail.html | 270 ++++++++ .../geoapps/templates/apps/app_download.html | 20 + geonode/geoapps/templates/apps/app_edit.html | 20 + geonode/geoapps/templates/apps/app_embed.html | 14 + geonode/geoapps/templates/apps/app_list.html | 14 + .../templates/apps/app_list_default.html | 36 ++ .../geoapps/templates/apps/app_metadata.html | 84 +++ .../templates/apps/app_metadata_advanced.html | 118 ++++ .../templates/apps/app_metadata_detail.html | 1 + geonode/geoapps/templates/apps/app_new.html | 20 + .../geoapps/templates/apps/app_remove.html | 26 + .../geoapps/templates/apps/app_update.html | 20 + .../geoapps/templates/layouts/app_panels.html | 575 ++++++++++++++++++ geonode/geoapps/tests.py | 19 + geonode/geoapps/translation.py | 29 + geonode/geoapps/urls.py | 56 ++ geonode/geoapps/views.py | 528 ++++++++++++++++ geonode/settings.py | 8 + geonode/templates/500.html | 5 + geonode/templates/base.html | 12 + geonode/templates/metadata_detail.html | 2 + .../templates/search/_general_filters.html | 16 + geonode/templates/search/_search_content.html | 2 + .../search/indexes/geoapps/app_text.txt | 7 + geonode/urls.py | 3 + geonode/views.py | 27 +- requirements.txt | 6 +- setup.cfg | 6 +- start_django_async.sh | 0 test_api_v2.sh | 4 + 56 files changed, 3236 insertions(+), 23 deletions(-) create mode 100644 geonode/geoapps/__init__.py create mode 100644 geonode/geoapps/admin.py create mode 100644 geonode/geoapps/api/__init__.py create mode 100644 geonode/geoapps/api/permissions.py create mode 100644 geonode/geoapps/api/serializers.py create mode 100644 geonode/geoapps/api/tests.py create mode 100644 geonode/geoapps/api/urls.py create mode 100644 geonode/geoapps/api/views.py create mode 100644 geonode/geoapps/forms.py create mode 100644 geonode/geoapps/migrations/0001_initial.py create mode 100644 geonode/geoapps/migrations/__init__.py create mode 100644 geonode/geoapps/models.py create mode 100644 geonode/geoapps/templates/apps/app_base.html create mode 100644 geonode/geoapps/templates/apps/app_detail.html create mode 100644 geonode/geoapps/templates/apps/app_download.html create mode 100644 geonode/geoapps/templates/apps/app_edit.html create mode 100644 geonode/geoapps/templates/apps/app_embed.html create mode 100644 geonode/geoapps/templates/apps/app_list.html create mode 100644 geonode/geoapps/templates/apps/app_list_default.html create mode 100644 geonode/geoapps/templates/apps/app_metadata.html create mode 100644 geonode/geoapps/templates/apps/app_metadata_advanced.html create mode 100644 geonode/geoapps/templates/apps/app_metadata_detail.html create mode 100644 geonode/geoapps/templates/apps/app_new.html create mode 100644 geonode/geoapps/templates/apps/app_remove.html create mode 100644 geonode/geoapps/templates/apps/app_update.html create mode 100644 geonode/geoapps/templates/layouts/app_panels.html create mode 100644 geonode/geoapps/tests.py create mode 100644 geonode/geoapps/translation.py create mode 100644 geonode/geoapps/urls.py create mode 100644 geonode/geoapps/views.py create mode 100644 geonode/templates/search/indexes/geoapps/app_text.txt mode change 100644 => 100755 start_django_async.sh create mode 100755 test_api_v2.sh diff --git a/geonode/api/api.py b/geonode/api/api.py index d8814c678ef..75f8320462f 100644 --- a/geonode/api/api.py +++ b/geonode/api/api.py @@ -21,6 +21,7 @@ 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 @@ -52,6 +53,7 @@ from geonode.base.models import ThesaurusKeywordLabel from geonode.layers.models import Layer, Style 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 @@ -67,7 +69,8 @@ FILTER_TYPES = { 'layer': Layer, 'map': Map, - 'document': Document + 'document': Document, + 'geoapp': GeoApp } @@ -90,19 +93,37 @@ def get_resources_counts(self, options): unpublished_not_visible=settings.RESOURCE_PUBLISHING, private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES) + subtypes = [] if resources and resources.count() > 0: 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(resources.values(options['count_type']).annotate(count=Count(options['count_type']))) + counts = list() + if subtypes: + for subtype in subtypes: + counts.append( + subtype.values(options['count_type']).annotate(count=Count(options['count_type'])).first() + ) + else: + counts = list(resources.values(options['count_type']).annotate(count=Count(options['count_type']))) - return dict([(c[options['count_type']], c['count']) for c in counts]) + return dict( + [(c[options['count_type']], c['count']) for c in counts if c and c['count'] and options['count_type']]) def to_json(self, data, options=None): options = options or {} @@ -869,8 +890,18 @@ def _get_resource_counts(request, resourcebase_filter_kwargs): 'layer', '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_] = { @@ -881,6 +912,8 @@ def _get_resource_counts(request, resourcebase_filter_kwargs): } 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 diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py index 07a4e984b08..d05d0ed1f5f 100644 --- a/geonode/api/resourcebase_api.py +++ b/geonode/api/resourcebase_api.py @@ -49,6 +49,7 @@ from geonode import get_version, qgis_server, geoserver from geonode.layers.models import Layer from geonode.maps.models import Map +from geonode.geoapps.models import GeoApp from geonode.documents.models import Document from geonode.base.models import ResourceBase from geonode.base.models import HierarchicalKeyword @@ -165,6 +166,8 @@ def build_filters(self, filters=None, ignore_bad_filters=False, **kwargs): filters=filters, ignore_bad_filters=ignore_bad_filters, **kwargs) if 'type__in' in filters and filters['type__in'] in FILTER_TYPES.keys(): orm_filters.update({'type': filters.getlist('type__in')}) + if 'app_type__in' in filters: + orm_filters.update({'polymorphic_ctype__model': filters['app_type__in'].lower()}) if 'extent' in filters: orm_filters.update({'extent': filters['extent']}) orm_filters['f_method'] = filters['f_method'] if 'f_method' in filters else 'and' @@ -1016,6 +1019,69 @@ class Meta(CommonMetaApi): GeonodeApiKeyAuthentication()) +class GeoAppResource(CommonModelApi): + + """GeoApps API""" + + def format_objects(self, objects): + """ + Formats the objects and provides reference to list of layers in GeoApp + resources. + + :param objects: GeoApp objects + """ + formatted_objects = [] + for obj in objects: + # convert the object to a dict using the standard values. + formatted_obj = model_to_dict(obj, fields=self.VALUES) + username = obj.owner.get_username() + full_name = (obj.owner.get_full_name() or username) + formatted_obj['owner__username'] = username + formatted_obj['owner_name'] = full_name + if obj.category: + formatted_obj['category__gn_description'] = obj.category.gn_description + if obj.group: + formatted_obj['group'] = obj.group + try: + formatted_obj['group_name'] = GroupProfile.objects.get(slug=obj.group.name) + except GroupProfile.DoesNotExist: + formatted_obj['group_name'] = obj.group + + formatted_obj['keywords'] = [k.name for k in obj.keywords.all()] if obj.keywords else [] + formatted_obj['regions'] = [r.name for r in obj.regions.all()] if obj.regions else [] + + if 'site_url' not in formatted_obj or len(formatted_obj['site_url']) == 0: + formatted_obj['site_url'] = settings.SITEURL + + # Probe Remote Services + formatted_obj['store_type'] = 'geoapp' + formatted_obj['online'] = True + + # replace thumbnail_url with curated_thumbs + try: + if hasattr(obj, 'curatedthumbnail'): + if hasattr(obj.curatedthumbnail.img_thumbnail, 'url'): + formatted_obj['thumbnail_url'] = obj.curatedthumbnail.thumbnail_url + else: + formatted_obj['thumbnail_url'] = '' + except Exception as e: + formatted_obj['thumbnail_url'] = '' + logger.exception(e) + + formatted_objects.append(formatted_obj) + return formatted_objects + + class Meta(CommonMetaApi): + paginator_class = CrossSiteXHRPaginator + filtering = CommonMetaApi.filtering + filtering.update({'app_type': ALL}) + queryset = GeoApp.objects.distinct().order_by('-date') + resource_name = 'geoapps' + authentication = MultiAuthentication(SessionAuthentication(), + OAuthAuthentication(), + GeonodeApiKeyAuthentication()) + + class DocumentResource(CommonModelApi): """Documents API""" diff --git a/geonode/api/urls.py b/geonode/api/urls.py index e53deb032c2..9eac64c7e9f 100644 --- a/geonode/api/urls.py +++ b/geonode/api/urls.py @@ -46,6 +46,7 @@ api.register(resourcebase_resources.FeaturedResourceBaseResource()) api.register(resourcebase_resources.LayerResource()) api.register(resourcebase_resources.MapResource()) +api.register(resourcebase_resources.GeoAppResource()) api.register(resourcebase_resources.ResourceBaseResource()) router = routers.DynamicRouter() diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 65da593e55a..1c74ccb570d 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -44,6 +44,14 @@ logger = logging.getLogger(__name__) +class ResourceBaseTypesSerializer(DynamicEphemeralSerializer): + + class Meta: + name = 'resource-type' + + resource_types = serializers.ListField() + + class PermSpecSerialiazer(DynamicEphemeralSerializer): class Meta: diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index 59b3db24df2..4ead6cae7aa 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -197,7 +197,11 @@ def test_base_resources(self): # Pagination self.assertEqual(len(response.data['resources']), 17) - # Search + def test_search_resources(self): + """ + Ensure we can search across the Resource Base list. + """ + url = reverse('base-resources-list') # Admin self.assertTrue(self.client.login(username='admin', password='admin')) @@ -209,7 +213,11 @@ def test_base_resources(self): # Pagination self.assertEqual(len(response.data['resources']), 1) - # Filtering + def test_filter_resources(self): + """ + Ensure we can filter across the Resource Base list. + """ + url = reverse('base-resources-list') # Admin self.assertTrue(self.client.login(username='admin', password='admin')) @@ -297,7 +305,11 @@ def test_base_resources(self): # Pagination self.assertEqual(len(response.data['resources']), 12) - # Sorting + def test_sort_resources(self): + """ + Ensure we can sort the Resource Base list. + """ + url = reverse('base-resources-list') # Admin self.assertTrue(self.client.login(username='admin', password='admin')) @@ -330,7 +342,11 @@ def test_base_resources(self): reversed_resource_titles = sorted(resource_titles.copy()) self.assertNotEqual(resource_titles, reversed_resource_titles) - # Get & Set Permissions + def test_perms_resources(self): + """ + Ensure we can Get & Set Permissions across the Resource Base list. + """ + url = reverse('base-resources-list') # Admin self.assertTrue(self.client.login(username='admin', password='admin')) @@ -371,3 +387,43 @@ def test_base_resources(self): self.assertEqual(response.status_code, 200) resource_perm_spec = response.data self.assertFalse('norman' in resource_perm_spec['users']) + + def test_featured_and_published_resources(self): + """ + Ensure we can Get & Set Permissions across the Resource Base list. + """ + url = reverse('base-resources-list') + # Admin + self.assertTrue(self.client.login(username='admin', password='admin')) + + resources = ResourceBase.objects.filter(owner__username='bobby') + + url = urljoin(f"{reverse('base-resources-list')}/", 'featured/') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + self.assertEqual(response.data['total'], 0) + # Pagination + self.assertEqual(len(response.data['resources']), 0) + + resources.filter(resource_type='map').update(featured=True) + url = urljoin(f"{reverse('base-resources-list')}/", 'featured/') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + self.assertEqual(response.data['total'], 2) + # Pagination + self.assertEqual(len(response.data['resources']), 2) + + def test_resource_types(self): + """ + Ensure we can Get & Set Permissions across the Resource Base list. + """ + url = urljoin(f"{reverse('base-resources-list')}/", 'resource_types/') + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertTrue('resource_types' in response.data) + self.assertTrue('layer' in response.data['resource_types']) + self.assertTrue('map' in response.data['resource_types']) + self.assertTrue('document' in response.data['resource_types']) + self.assertTrue('service' in response.data['resource_types']) diff --git a/geonode/base/api/urls.py b/geonode/base/api/urls.py index 8658df7e3f4..72a53d34fda 100644 --- a/geonode/base/api/urls.py +++ b/geonode/base/api/urls.py @@ -23,6 +23,6 @@ router.register(r'users', views.UserViewSet, 'users') router.register(r'groups', views.GroupViewSet, 'group-profiles') -router.register(r'base_resources', views.ResourceBaseViewSet, 'base-resources') +router.register(r'resources', views.ResourceBaseViewSet, 'base-resources') urlpatterns = [] diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 790dd473713..6533742d24f 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -17,6 +17,7 @@ # along with this program. If not, see . # ######################################################################### +from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model @@ -45,7 +46,8 @@ UserSerializer, PermSpecSerialiazer, GroupProfileSerializer, - ResourceBaseSerializer + ResourceBaseSerializer, + ResourceBaseTypesSerializer ) from .pagination import GeoNodeApiPagination @@ -145,6 +147,71 @@ class ResourceBaseViewSet(DynamicModelViewSet): serializer_class = ResourceBaseSerializer pagination_class = GeoNodeApiPagination + def _filtered(self, request, filter): + paginator = GeoNodeApiPagination() + paginator.page_size = request.GET.get('page_size', 10) + resources = ResourceBase.objects.filter(**filter) + exclude = [] + for resource in resources: + if not request.user.is_superuser and \ + not request.user.has_perm('view_resourcebase', resource.get_self_resource()): + exclude.append(resource.id) + resources = resources.exclude(id__in=exclude) + result_page = paginator.paginate_queryset(resources, request) + serializer = ResourceBaseSerializer(result_page, embed=True, many=True) + return paginator.get_paginated_response({"resources": serializer.data}) + + @extend_schema(methods=['get'], responses={200: ResourceBaseSerializer(many=True)}, + description="API endpoint allowing to retrieve the approved Resources.") + @action(detail=False, methods=['get']) + def approved(self, request): + return self._filtered(request, {"is_approved": True}) + + @extend_schema(methods=['get'], responses={200: ResourceBaseSerializer(many=True)}, + description="API endpoint allowing to retrieve the published Resources.") + @action(detail=False, methods=['get']) + def published(self, request): + return self._filtered(request, {"is_published": True}) + + @extend_schema(methods=['get'], responses={200: ResourceBaseSerializer(many=True)}, + description="API endpoint allowing to retrieve the featured Resources.") + @action(detail=False, methods=['get']) + def featured(self, request): + return self._filtered(request, {"featured": True}) + + @extend_schema(methods=['get'], responses={200: ResourceBaseTypesSerializer()}, + description=""" + Returns the list of available ResourceBase polymorphic_ctypes. + + the mapping looks like: + ``` + { + "resource_types": [ + "layer", + "map", + "document", + "service" + ] + } + ``` + """) + @action(detail=False, methods=['get']) + def resource_types(self, request): + resource_types = [] + for _model in apps.get_models(): + if _model.__name__ == "ResourceBase": + resource_types = [_m.__name__.lower() for _m in _model.__subclasses__()] + if "geoapp" in resource_types: + from geonode.geoapps.models import GeoApp + resource_types.remove("geoapp") + 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): + resource_types.append(_model.__name__.lower()) + return Response({"resource_types": resource_types}) + @extend_schema(methods=['get'], responses={200: PermSpecSerialiazer()}, description=""" Gets an object's the permission levels based on the perm_spec JSON. diff --git a/geonode/base/templates/base/_resourcebase_snippet.html b/geonode/base/templates/base/_resourcebase_snippet.html index 7399a691ecc..762db4eed2f 100644 --- a/geonode/base/templates/base/_resourcebase_snippet.html +++ b/geonode/base/templates/base/_resourcebase_snippet.html @@ -33,6 +33,7 @@

+ {{ item.title }}

diff --git a/geonode/base/templatetags/base_tags.py b/geonode/base/templatetags/base_tags.py index 9beb8253648..a75303d018a 100644 --- a/geonode/base/templatetags/base_tags.py +++ b/geonode/base/templatetags/base_tags.py @@ -90,7 +90,55 @@ def facets(context): except Exception: pass - if facet_type == 'documents': + if facet_type == 'geoapps': + facets = {} + + from django.apps import apps + for label, app in apps.app_configs.items(): + if hasattr(app, 'type') and app.type == 'GEONODE_APP': + if hasattr(app, 'default_model'): + geoapps = get_visible_resources( + apps.get_model(label, app.default_model).objects.all(), + 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) + + if category_filter: + geoapps = geoapps.filter(category__identifier__in=category_filter) + if regions_filter: + geoapps = geoapps.filter(regions__name__in=regions_filter) + if owner_filter: + geoapps = geoapps.filter(owner__username__in=owner_filter) + if date_gte_filter: + geoapps = geoapps.filter(date__gte=date_gte_filter) + if date_lte_filter: + geoapps = geoapps.filter(date__lte=date_lte_filter) + if date_range_filter: + geoapps = geoapps.filter(date__range=date_range_filter.split(',')) + + if extent_filter: + geoapps = filter_bbox(geoapps, extent_filter) + + if keywords_filter: + treeqs = HierarchicalKeyword.objects.none() + for keyword in keywords_filter: + try: + kws = HierarchicalKeyword.objects.filter(name__iexact=keyword) + for kw in kws: + treeqs = treeqs | HierarchicalKeyword.get_tree(kw) + except Exception: + # Ignore keywords not actually used? + pass + + geoapps = geoapps.filter(Q(keywords__in=treeqs)) + + if not settings.SKIP_PERMS_FILTER: + geoapps = geoapps.filter(id__in=authorized) + + facets[app.default_model] = geoapps.count() + return facets + elif facet_type == 'documents': documents = Document.objects.filter(title__icontains=title_filter) if category_filter: documents = documents.filter(category__identifier__in=category_filter) @@ -290,7 +338,7 @@ def get_current_path(context): @register.simple_tag(takes_context=True) def get_context_resourcetype(context): c_path = get_current_path(context) - resource_types = ['layers', 'maps', 'documents', 'search', 'people', + resource_types = ['layers', 'maps', 'geoapps', 'documents', 'search', 'people', 'groups/categories', 'groups'] for resource_type in resource_types: if "/{0}/".format(resource_type) in c_path: diff --git a/geonode/client/hooksets.py b/geonode/client/hooksets.py index 9ac17072d9d..9d71f349b11 100644 --- a/geonode/client/hooksets.py +++ b/geonode/client/hooksets.py @@ -77,6 +77,31 @@ def map_embed_template(self, context=None): def map_download_template(self, context=None): return NotImplemented + # GeoApps + def geoapp_list_template(self, context=None): + return 'apps/app_list_default.html' + + def geoapp_detail_template(self, context=None): + return NotImplemented + + def geoapp_new_template(self, context=None): + return NotImplemented + + def geoapp_view_template(self, context=None): + return NotImplemented + + def geoapp_edit_template(self, context=None): + return NotImplemented + + def geoapp_update_template(self, context=None): + return NotImplemented + + def geoapp_embed_template(self, context=None): + return NotImplemented + + def geoapp_download_template(self, context=None): + return NotImplemented + # Map Persisting def viewer_json(self, conf, context=None): if not context: diff --git a/geonode/client/templatetags/client_lib_tags.py b/geonode/client/templatetags/client_lib_tags.py index be31671f7cb..3e3f431b9d3 100644 --- a/geonode/client/templatetags/client_lib_tags.py +++ b/geonode/client/templatetags/client_lib_tags.py @@ -158,6 +158,40 @@ def render(self, context): hookset.map_download_template( context=context)) + # GEONODE_APPS + if self.tag_name == 'get_geoapp_list': + t = context.template.engine.get_template( + hookset.geoapp_list_template( + context=context)) + elif self.tag_name == 'get_geoapp_detail': + t = context.template.engine.get_template( + hookset.geoapp_detail_template( + context=context)) + elif self.tag_name == 'get_geoapp_new': + t = context.template.engine.get_template( + hookset.geoapp_new_template( + context=context)) + elif self.tag_name == 'get_geoapp_view': + t = context.template.engine.get_template( + hookset.geoapp_view_template( + context=context)) + elif self.tag_name == 'get_geoapp_edit': + t = context.template.engine.get_template( + hookset.geoapp_edit_template( + context=context)) + elif self.tag_name == 'get_geoapp_update': + t = context.template.engine.get_template( + hookset.geoapp_update_template( + context=context)) + elif self.tag_name == 'get_geoapp_embed': + t = context.template.engine.get_template( + hookset.geoapp_embed_template( + context=context)) + elif self.tag_name == 'get_geoapp_download': + t = context.template.engine.get_template( + hookset.geoapp_download_template( + context=context)) + if t: return t.render(context) else: @@ -187,3 +221,12 @@ def do_get_client_library_template(parser, token): register.tag('get_map_update', do_get_client_library_template) register.tag('get_map_embed', do_get_client_library_template) register.tag('get_map_download', do_get_client_library_template) + +register.tag('get_geoapp_list', do_get_client_library_template) +register.tag('get_geoapp_detail', do_get_client_library_template) +register.tag('get_geoapp_new', do_get_client_library_template) +register.tag('get_geoapp_view', do_get_client_library_template) +register.tag('get_geoapp_edit', do_get_client_library_template) +register.tag('get_geoapp_update', do_get_client_library_template) +register.tag('get_geoapp_embed', do_get_client_library_template) +register.tag('get_geoapp_download', do_get_client_library_template) diff --git a/geonode/context_processors.py b/geonode/context_processors.py index 3b4ba3a082f..973a5bd97c3 100644 --- a/geonode/context_processors.py +++ b/geonode/context_processors.py @@ -181,7 +181,11 @@ def resource_urls(request): ), OGC_SERVER=getattr(settings, 'OGC_SERVER', None), DELAYED_SECURITY_SIGNALS=getattr(settings, 'DELAYED_SECURITY_SIGNALS', False), - READ_ONLY_MODE=getattr(Configuration.load(), 'read_only', False) + READ_ONLY_MODE=getattr(Configuration.load(), 'read_only', False), + # GeoNode Apps + GEONODE_APPS_ENABLE=getattr(settings, 'GEONODE_APPS_ENABLE', False), + GEONODE_APPS_NAME=getattr(settings, 'GEONODE_APPS_NAME', 'Apps'), + GEONODE_APPS_NAV_MENU_ENABLE=getattr(settings, 'GEONODE_APPS_NAV_MENU_ENABLE', False), ) return defaults diff --git a/geonode/documents/api/views.py b/geonode/documents/api/views.py index 2ce29d08d42..2492c92036a 100644 --- a/geonode/documents/api/views.py +++ b/geonode/documents/api/views.py @@ -23,7 +23,6 @@ from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter from rest_framework.decorators import action -from rest_framework.response import Response from rest_framework.permissions import IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly, DjangoModelPermissionsOrAnonReadOnly # noqa from rest_framework.authentication import SessionAuthentication, BasicAuthentication from oauth2_provider.contrib.rest_framework import OAuth2Authentication @@ -65,4 +64,14 @@ def linked_resources(self, request, pk=None): document = self.get_object() resources_id = document.links.all().values('object_id') resources = ResourceBase.objects.filter(id__in=resources_id) - return Response(ResourceBaseSerializer(embed=True, many=True).to_representation(resources)) + exclude = [] + for resource in resources: + if not request.user.is_superuser and \ + not request.user.has_perm('view_resourcebase', resource.get_self_resource()): + exclude.append(resource.id) + resources = resources.exclude(id__in=exclude) + paginator = GeoNodeApiPagination() + paginator.page_size = request.GET.get('page_size', 10) + result_page = paginator.paginate_queryset(resources, request) + serializer = ResourceBaseSerializer(result_page, embed=True, many=True) + return paginator.get_paginated_response({"resources": serializer.data}) diff --git a/geonode/geoapps/__init__.py b/geonode/geoapps/__init__.py new file mode 100644 index 00000000000..dc685246de8 --- /dev/null +++ b/geonode/geoapps/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from django.utils.translation import ugettext_noop as _ +from geonode.notifications_helper import NotificationsAppConfigBase + + +class GeoNodeAppsConfig(NotificationsAppConfigBase): + name = 'geonode.geoapps' + type = 'GEONODE_APP' + + NOTIFICATIONS = (("geoapp_created", _("App Created"), _("A App was created"),), + ("geoapp_updated", _("App Updated"), _("A App was updated"),), + ("geoapp_approved", _("App Approved"), _("A App was approved by a Manager"),), + ("geoapp_published", _("App Published"), _("A App was published"),), + ("geoapp_deleted", _("App Deleted"), _("A App was deleted"),), + ("geoapp_comment", _("Comment on App"), _("An App was commented on"),), + ("geoapp_rated", _("Rating for App"), _("A rating was given to an App"),), + ) + + +default_app_config = 'geonode.geoapps.GeoNodeAppsConfig' diff --git a/geonode/geoapps/admin.py b/geonode/geoapps/admin.py new file mode 100644 index 00000000000..988def4d103 --- /dev/null +++ b/geonode/geoapps/admin.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### + +from django.contrib import admin + +from modeltranslation.admin import TabbedTranslationAdmin + +from geonode.geoapps.models import GeoApp, GeoAppData +from geonode.base.admin import ResourceBaseAdminForm + + +class GeoAppDataInline(admin.TabularInline): + model = GeoAppData + + +class GeoAppAdminForm(ResourceBaseAdminForm): + + class Meta(ResourceBaseAdminForm.Meta): + model = GeoApp + fields = '__all__' + + +class GeoAppAdmin(TabbedTranslationAdmin): + inlines = [GeoAppDataInline, ] + list_display_links = ('title',) + list_display = ('id', 'title', 'type', 'owner', 'category', 'group', 'is_approved', 'is_published',) + list_editable = ('owner', 'category', 'group', 'is_approved', 'is_published',) + list_filter = ('title', 'owner', 'category', 'group', 'is_approved', 'is_published',) + search_fields = ('title', 'abstract', 'purpose', 'is_approved', 'is_published',) + form = GeoAppAdminForm + + +admin.site.register(GeoApp, GeoAppAdmin) diff --git a/geonode/geoapps/api/__init__.py b/geonode/geoapps/api/__init__.py new file mode 100644 index 00000000000..fe4e643c905 --- /dev/null +++ b/geonode/geoapps/api/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### diff --git a/geonode/geoapps/api/permissions.py b/geonode/geoapps/api/permissions.py new file mode 100644 index 00000000000..eb16decd377 --- /dev/null +++ b/geonode/geoapps/api/permissions.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from django.conf import settings +from rest_framework.filters import BaseFilterBackend + +from geonode.geoapps.models import GeoApp + + +class GeoAppPermissionsFilter(BaseFilterBackend): + """ + A filter backend that limits results to those where the requesting user + has read object level permissions. + """ + shortcut_kwargs = { + 'accept_global_perms': True, + } + + def filter_queryset(self, request, queryset, view): + # We want to defer this import until runtime, rather than import-time. + # See https://github.com/encode/django-rest-framework/issues/4608 + # (Also see #1624 for why we need to make this import explicitly) + from guardian.shortcuts import get_objects_for_user + from geonode.security.utils import get_visible_resources + + user = request.user + resources = get_objects_for_user( + user, + 'base.view_resourcebase', + **self.shortcut_kwargs + ) + + _allowed_ids = get_visible_resources( + resources, + user, + admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, + unpublished_not_visible=settings.RESOURCE_PUBLISHING, + private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES + ).values_list('id', flat=True) + + obj_with_perms = [_app.id for _app in GeoApp.objects.filter(id__in=_allowed_ids)] + + return queryset.filter(id__in=obj_with_perms) diff --git a/geonode/geoapps/api/serializers.py b/geonode/geoapps/api/serializers.py new file mode 100644 index 00000000000..693929c4cb0 --- /dev/null +++ b/geonode/geoapps/api/serializers.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +import six +import json + +from django.contrib.auth import get_user_model + +from rest_framework.serializers import ValidationError + +from dynamic_rest.serializers import DynamicModelSerializer +from dynamic_rest.fields.fields import DynamicRelationField + +from geonode.geoapps.models import GeoApp, GeoAppData +from geonode.base.api.serializers import ResourceBaseSerializer + +import logging + +logger = logging.getLogger(__name__) + + +class GeoAppDataField(DynamicRelationField): + + def value_to_string(self, obj): + value = self.value_from_object(obj) + return self.get_prep_value(value) + + +class GeoAppDataSerializer(DynamicModelSerializer): + + class Meta: + ref_name = 'GeoAppData' + model = GeoAppData + name = 'GeoAppData' + fields = ('pk', 'blob') + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + data = GeoAppData.objects.filter(resource__id=value).first() + if data and data.blob: + if isinstance(data.blob, dict): + return data.blob + return json.loads(data.blob) + return {} + + +class GeoAppSerializer(ResourceBaseSerializer): + + def __init__(self, *args, **kwargs): + # Instantiate the superclass normally + super(GeoAppSerializer, self).__init__(*args, **kwargs) + + class Meta: + model = GeoApp + name = 'geoapp' + fields = ( + 'pk', 'uuid', + 'zoom', 'projection', 'center_x', 'center_y', + 'urlsuffix', 'data' + ) + + def to_internal_value(self, data): + if isinstance(data, six.string_types): + data = json.loads(data) + if 'data' in data: + _data = data.pop('data') + if self.is_valid(): + data['blob'] = _data + + return data + + def create(self, validated_data): + # Sanity checks + if 'name' not in validated_data or \ + 'owner' not in validated_data: + raise ValidationError("No valid data: 'name' and 'owner' are mandatory fields!") + + if GeoApp.objects.filter(name=validated_data['name']).count(): + raise ValidationError("A GeoApp with the same 'name' already exists!") + + # Extract users' profiles + _user_profiles = {} + for _key, _value in validated_data.items(): + if _key in ('owner', 'poc', 'metadata_owner'): + _user_profiles[_key] = _value + for _key, _value in _user_profiles.items(): + validated_data.pop(_key) + _u = get_user_model().objects.filter(username=_value).first() + if _u: + validated_data[_key] = _u + else: + raise ValidationError("The specified '{}' does not exist!".format(_key)) + + # Extract JSON blob + _data = None + if 'blob' in validated_data: + _data = validated_data.pop('blob') + + # Create a new instance + _instance = GeoApp.objects.create(**validated_data) + + if _instance and _data: + try: + _geo_app, _created = GeoAppData.objects.get_or_create(resource=_instance) + _geo_app.blob = _data + _geo_app.save() + except Exception as e: + raise ValidationError(e) + + _instance.save() + return _instance + + def update(self, instance, validated_data): + + # Extract users' profiles + _user_profiles = {} + for _key, _value in validated_data.items(): + if _key in ('owner', 'poc', 'metadata_owner'): + _user_profiles[_key] = _value + for _key, _value in _user_profiles.items(): + validated_data.pop(_key) + _u = get_user_model().objects.filter(username=_value).first() + if _u: + validated_data[_key] = _u + else: + raise ValidationError("The specified '{}' does not exist!".format(_key)) + + # Extract JSON blob + _data = None + if 'blob' in validated_data: + _data = validated_data.pop('blob') + + try: + GeoApp.objects.filter(pk=instance.id).update(**validated_data) + instance.refresh_from_db() + except Exception as e: + raise ValidationError(e) + + if instance and _data: + try: + _geo_app, _created = GeoAppData.objects.get_or_create(resource=instance) + _geo_app.blob = _data + _geo_app.save() + except Exception as e: + raise ValidationError(e) + + instance.save() + return instance + + """ + - Deferred / not Embedded --> ?include[]=data + """ + data = GeoAppDataField( + GeoAppDataSerializer, + source='id', + many=False, + embed=False, + deferred=True) diff --git a/geonode/geoapps/api/tests.py b/geonode/geoapps/api/tests.py new file mode 100644 index 00000000000..6ad18b5000e --- /dev/null +++ b/geonode/geoapps/api/tests.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# 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 . +# +######################################################################### +import json +import logging + +import django +from django.urls import reverse +from django.conf.urls import url, include +from django.views.generic import TemplateView +from django.contrib.auth import get_user_model +from django.views.i18n import JavaScriptCatalog +from rest_framework.test import APITestCase, URLPatternsTestCase + +from geonode.api.urls import router +from geonode.services.views import services +from geonode.geoapps.models import GeoApp, GeoAppData + +from geonode import geoserver +from geonode.utils import check_ogc_backend +from geonode.base.populate_test_data import create_models + +logger = logging.getLogger(__name__) + + +class BaseApiTests(APITestCase, URLPatternsTestCase): + + fixtures = [ + 'initial_data.json', + 'group_test_data.json', + 'default_oauth_apps.json' + ] + + urlpatterns = [ + url(r'^home/$', + TemplateView.as_view(template_name='index.html'), + name='home'), + url(r'^help/$', + TemplateView.as_view(template_name='help.html'), + name='help'), + url(r'^developer/$', + TemplateView.as_view( + template_name='developer.html'), + name='developer'), + url(r'^about/$', + TemplateView.as_view(template_name='about.html'), + name='about'), + url(r'^privacy_cookies/$', + TemplateView.as_view(template_name='privacy-cookies.html'), + name='privacy-cookies'), + url(r"^account/", include("allauth.urls")), + url(r'^people/', include('geonode.people.urls')), + url(r'^api/v2/', include(router.urls)), + url(r'^api/v2/', include('geonode.api.urls')), + url(r'^api/v2/api-auth/', include('rest_framework.urls', namespace='geonode_rest_framework')), + url(r'^$', + TemplateView.as_view(template_name='layers/layer_list.html'), + {'facet_type': 'layers', 'is_layer': True}, + name='layer_browse'), + url(r'^$', + TemplateView.as_view(template_name='maps/map_list.html'), + {'facet_type': 'maps', 'is_map': True}, + name='maps_browse'), + url(r'^$', + TemplateView.as_view(template_name='documents/document_list.html'), + {'facet_type': 'documents', 'is_document': True}, + name='document_browse'), + url(r'^$', + TemplateView.as_view(template_name='groups/group_list.html'), + name='group_list'), + url(r'^search/$', + TemplateView.as_view(template_name='search/search.html'), + name='search'), + url(r'^$', services, name='services'), + url(r'^invitations/', include( + 'geonode.invitations.urls', namespace='geonode.invitations')), + url(r'^i18n/', include(django.conf.urls.i18n), name="i18n"), + url(r'^jsi18n/$', JavaScriptCatalog.as_view(), {}, name='javascript-catalog') + ] + + if check_ogc_backend(geoserver.BACKEND_PACKAGE): + from geonode.geoserver.views import layer_acls, resolve_user + urlpatterns += [ + url(r'^acls/?$', layer_acls, name='layer_acls'), + url(r'^acls_dep/?$', layer_acls, name='layer_acls_dep'), + url(r'^resolve_user/?$', resolve_user, name='layer_resolve_user'), + url(r'^resolve_user_dep/?$', resolve_user, name='layer_resolve_user_dep'), + ] + + def setUp(self): + create_models(b'document') + create_models(b'map') + create_models(b'layer') + self.admin = get_user_model().objects.get(username='admin') + self.bobby = get_user_model().objects.get(username='bobby') + self.norman = get_user_model().objects.get(username='norman') + self.gep_app = GeoApp.objects.create( + title="Test GeoApp", + owner=self.bobby + ) + self.gep_app_data = GeoAppData.objects.create( + blob='{"test_data": {"test": ["test_1","test_2","test_3"]}}', + resource=self.gep_app + ) + self.gep_app.set_default_permissions() + + def test_geoapps_list(self): + """ + Ensure we can access the GeoApps list. + """ + url = reverse('geoapps-list') + # Anonymous + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + self.assertEqual(response.data['total'], 1) + # Pagination + self.assertEqual(len(response.data['geoapps']), 1) + self.assertTrue('data' not in response.data['geoapps'][0]) + + response = self.client.get( + f"{url}?include[]=data", format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + self.assertEqual(response.data['total'], 1) + # Pagination + self.assertEqual(len(response.data['geoapps']), 1) + self.assertTrue('data' in response.data['geoapps'][0]) + self.assertEqual( + response.data['geoapps'][0]['data'], + { + "test_data": { + "test": [ + 'test_1', + 'test_2', + 'test_3' + ] + } + } + ) + + def test_geoapps_crud(self): + """ + Ensure we can create/update GeoApps. + """ + # Bobby + self.assertTrue(self.client.login(username='bobby', password='bob')) + # Create + url = reverse('geoapps-list') + data = { + "geoapp": { + "name": "Test Create", + "title": "Test Create", + "owner": "bobby" + } + } + response = self.client.post(url, data=data, format='json') + self.assertEqual(response.status_code, 201) # 201 - Created + + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 5) + self.assertEqual(response.data['total'], 2) + # Pagination + self.assertEqual(len(response.data['geoapps']), 2) + + # Update: PATCH + url = reverse('geoapps-detail', kwargs={'pk': self.gep_app.pk}) + data = { + "blob": { + "test_data": { + "test": [ + 'test_4', + 'test_5', + 'test_6' + ] + } + } + } + response = self.client.patch(url, data=json.dumps(data), format='json') + self.assertEqual(response.status_code, 200) + + response = self.client.get( + f"{url}?include[]=data", format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 1) + # Pagination + self.assertTrue('data' in response.data['geoapp']) + self.assertEqual( + response.data['geoapp']['data'], + { + "test_data": { + "test": [ + 'test_4', + 'test_5', + 'test_6' + ] + } + } + ) + + # Update: POST + data = response.data['geoapp'] + data['data'] = { + "test_data": { + "test": [ + 'test_1', + 'test_2', + 'test_3' + ] + } + } + response = self.client.post(url, data=json.dumps(data), format='json') + self.assertEqual(response.status_code, 405) # 405 – Method not allowed + + # Delete + response = self.client.delete(url, format='json') + self.assertEqual(response.status_code, 204) # 204 - No Content + + response = self.client.get( + f"{url}?include[]=data", format='json') + self.assertEqual(response.status_code, 404) # 404 - Not Found diff --git a/geonode/geoapps/api/urls.py b/geonode/geoapps/api/urls.py new file mode 100644 index 00000000000..5f2034fda73 --- /dev/null +++ b/geonode/geoapps/api/urls.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from geonode.api.urls import router + +from . import views + +router.register(r'geoapps', views.GeoAppViewSet, 'geoapps') + +urlpatterns = [] diff --git a/geonode/geoapps/api/views.py b/geonode/geoapps/api/views.py new file mode 100644 index 00000000000..e53d40d7468 --- /dev/null +++ b/geonode/geoapps/api/views.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from dynamic_rest.viewsets import DynamicModelViewSet +from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter + +from rest_framework.permissions import IsAdminUser, IsAuthenticated, IsAuthenticatedOrReadOnly, DjangoModelPermissionsOrAnonReadOnly # noqa +from rest_framework.authentication import SessionAuthentication, BasicAuthentication +from oauth2_provider.contrib.rest_framework import OAuth2Authentication + +from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter +from geonode.base.api.permissions import IsOwnerOrReadOnly +from geonode.base.api.pagination import GeoNodeApiPagination +from geonode.geoapps.models import GeoApp + +from .serializers import GeoAppSerializer +from .permissions import GeoAppPermissionsFilter + +import logging + +logger = logging.getLogger(__name__) + + +class GeoAppViewSet(DynamicModelViewSet): + """ + API endpoint that allows geoapps to be viewed or edited. + """ + authentication_classes = [SessionAuthentication, BasicAuthentication, OAuth2Authentication] + permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] + filter_backends = [ + DynamicFilterBackend, DynamicSortingFilter, DynamicSearchFilter, + ExtentFilter, GeoAppPermissionsFilter + ] + queryset = GeoApp.objects.all() + serializer_class = GeoAppSerializer + pagination_class = GeoNodeApiPagination diff --git a/geonode/geoapps/forms.py b/geonode/geoapps/forms.py new file mode 100644 index 00000000000..d9122a32c15 --- /dev/null +++ b/geonode/geoapps/forms.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from geonode.geoapps.models import GeoApp +from geonode.base.forms import ResourceBaseForm + + +class GeoAppForm(ResourceBaseForm): + + class Meta(ResourceBaseForm.Meta): + model = GeoApp + exclude = ResourceBaseForm.Meta.exclude + ( + 'zoom', + 'projection', + 'center_x', + 'center_y', + 'data' + ) diff --git a/geonode/geoapps/migrations/0001_initial.py b/geonode/geoapps/migrations/0001_initial.py new file mode 100644 index 00000000000..8e005017523 --- /dev/null +++ b/geonode/geoapps/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.15 on 2020-10-13 12:48 + +from django.db import migrations, models +import django.db.models.deletion +import jsonfield.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('base', '0045_auto_20200507_0445'), + ] + + operations = [ + migrations.CreateModel( + name='GeoApp', + fields=[ + ('resourcebase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='base.ResourceBase')), + ('name', models.TextField(db_index=True, unique=True, verbose_name='Name')), + ('zoom', models.IntegerField(blank=True, null=True, verbose_name='zoom')), + ('projection', models.CharField(blank=True, max_length=32, null=True, verbose_name='projection')), + ('center_x', models.FloatField(blank=True, null=True, verbose_name='center X')), + ('center_y', models.FloatField(blank=True, null=True, verbose_name='center Y')), + ('last_modified', models.DateTimeField(auto_now_add=True)), + ('urlsuffix', models.CharField(blank=True, max_length=255, null=True, verbose_name='Site URL')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('base.resourcebase',), + ), + migrations.CreateModel( + name='GeoAppData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('blob', jsonfield.fields.JSONField(default={})), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='geoapps.GeoApp')), + ], + ), + migrations.AddField( + model_name='geoapp', + name='data', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='data', to='geoapps.GeoAppData'), + ), + ] diff --git a/geonode/geoapps/migrations/__init__.py b/geonode/geoapps/migrations/__init__.py new file mode 100644 index 00000000000..725b3926923 --- /dev/null +++ b/geonode/geoapps/migrations/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### \ No newline at end of file diff --git a/geonode/geoapps/models.py b/geonode/geoapps/models.py new file mode 100644 index 00000000000..f09cc51760a --- /dev/null +++ b/geonode/geoapps/models.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +import logging + +from django.db import models +from django.urls import reverse +from django.db.models import signals +from django.utils.translation import ugettext_lazy as _ + +from jsonfield import JSONField + +from guardian.shortcuts import get_anonymous_user + +from geonode.base.models import ResourceBase, resourcebase_post_save + +logger = logging.getLogger("geonode.geoapps.models") + + +class GeoApp(ResourceBase): + + """ + A GeoApp it is a generic container for every client applications the + user might want to create or define. + """ + + PERMISSIONS = { + 'write': [ + 'change_geoapp_data', + 'change_geoapp_style', + ] + } + + name = models.TextField(_('Name'), unique=True, db_index=True) + + # viewer configuration + zoom = models.IntegerField(_('zoom'), null=True, blank=True) + # The zoom level to use when initially loading this geoapp. Zoom levels start + # at 0 (most zoomed out) and each increment doubles the resolution. + + projection = models.CharField(_('projection'), max_length=32, null=True, blank=True) + # The projection used for this geoapp. This is stored as a string with the + # projection's SRID. + + center_x = models.FloatField(_('center X'), null=True, blank=True) + # The x coordinate to center on when loading this geoapp. Its interpretation + # depends on the projection. + + center_y = models.FloatField(_('center Y'), null=True, blank=True) + # The y coordinate to center on when loading this geoapp. Its interpretation + # depends on the projection. + + last_modified = models.DateTimeField(auto_now_add=True) + # The last time the geoapp was modified. + + urlsuffix = models.CharField(_('Site URL'), max_length=255, null=True, blank=True) + # Alphanumeric alternative to referencing geoapps by id, appended to end of + # URL instead of id, ie http://domain/geoapps/someview + + data = models.OneToOneField( + "GeoAppData", + related_name="data", + null=True, + blank=True, + on_delete=models.CASCADE) + + def __str__(self): + return '%s by %s' % ( + self.title, (self.owner.username if self.owner else "")) + + @property + def class_name(self): + return self.__class__.__name__ + + @property + def sender(self): + return None + + @property + def center(self): + """ + A handy shortcut for the center_x and center_y properties as a tuple + (read only) + """ + return (self.center_x, self.center_y) + + @property + def type(self): + _ct = self.polymorphic_ctype + _child = _ct.model_class().objects.filter(pk=self.id).first() + if _child and hasattr(_child, 'app_type'): + return _child.app_type + return None + + @property + def is_public(self): + """ + Returns True if anonymous (public) user can view geoapp. + """ + user = get_anonymous_user() + return user.has_perm( + 'base.view_resourcebase', + obj=self.resourcebase_ptr) + + @property + def keywords_list(self): + keywords_qs = self.keywords.all() + if keywords_qs: + return [kw.name for kw in keywords_qs] + else: + return [] + + def get_absolute_url(self): + return reverse('geoapp_detail', None, [str(self.id)]) + + class Meta(ResourceBase.Meta): + pass + + +class GeoAppData(models.Model): + + blob = JSONField( + null=False, + default={}) + + resource = models.ForeignKey( + GeoApp, + null=False, + blank=False, + on_delete=models.CASCADE) + + +# signals.pre_delete.connect(pre_delete_app, sender=GeoApp) +signals.post_save.connect(resourcebase_post_save, sender=GeoApp) diff --git a/geonode/geoapps/templates/apps/app_base.html b/geonode/geoapps/templates/apps/app_base.html new file mode 100644 index 00000000000..fdf158d5290 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_base.html @@ -0,0 +1,11 @@ +{% extends "geonode_base.html" %} +{% load i18n %} + +{% block title %} {{ block.super }} {% endblock %} + +{% block body_class %}{% trans "data" %}{% endblock body_class %} + +{% block body_outer %} + {% block body %}{% endblock body %} + {% block sidebar %}{% endblock sidebar %} +{% endblock body_outer %} diff --git a/geonode/geoapps/templates/apps/app_detail.html b/geonode/geoapps/templates/apps/app_detail.html new file mode 100644 index 00000000000..6447dc29dea --- /dev/null +++ b/geonode/geoapps/templates/apps/app_detail.html @@ -0,0 +1,270 @@ +{% extends "apps/app_base.html" %} +{% load i18n %} +{% load staticfiles %} +{% load dialogos_tags %} +{% load pinax_ratings_tags %} +{% load bootstrap_tags %} +{% load pagination_tags %} +{% load base_tags %} +{% load guardian_tags %} +{% load client_lib_tags %} + +{% block title %}{{ resource.title }} — {{ block.super }}{% endblock %} + +{% block head %} + {% if TWITTER_CARD %} + {% include "base/_resourcebase_twittercard.html" %} + {% endif %} + {% if OPENGRAPH_ENABLED %} + {% include "base/_resourcebase_opengraph.html" %} + {% endif %} + + {{ block.super }} +{% endblock %} + +{% block body_class %}{% blocktrans %}{{ resource.type }}{% endblocktrans %}{% endblock %} + +{% block body_outer %} + {% overall_rating resource "geoapp" as map_rating %} + + + +
+
+ +
+ +
+ +
+ {% include "_actions.html" %} +
+ +
+
+ {% include "base/resourcebase_info_panel.html" %} +
+ {% block social_links %} + {% if DISPLAY_SOCIAL %} + {% include "social_links.html" %} + {% endif %} + {% endblock %} + + {% if DISPLAY_COMMENTS %} +
+ {% include "_comments.html" %} +
+ {% endif %} + + {% if DISPLAY_RATINGS %} +
+ + {% if request.user.is_authenticated %} +

{% trans "Rate this" %} {{ resource.type }}

+ {% user_rating request.user resource "map" as user_map_rating %} +
+ {% endif %} +

{% trans 'Average Rating' %}

+ {% overall_rating resource "map" as map_rating %} + {% num_ratings resource as num_votes %} +
({{num_votes}}) +
+ {% endif %} + +
+ +
+ +
+
    + +
  • + + + +
  • + + {% if not READ_ONLY_MODE %} + {% if "change_resourcebase" in perms_list or "change_resourcebase_metadata" in perms_list %} +
  • + +
  • + + {% endif %} + {% endif %} + {% display_edit_request_button resource request.user perms_list as display_request_button %} + {% if display_request_button %} +
  • + + + +
  • + {% endif %} +
  • + {% if "change_resourcebase" not in perms_list and "change_resourcebase_metadata" not in perms_list %} + {% trans "View" %} {{ resource.type }} + {% else %} + {% trans "View" %} {{ resource.type }} + {% endif %} +
  • + + {% if GEONODE_SECURITY_ENABLED %} + {% if "change_resourcebase_permissions" in perms_list %} + {% if not READ_ONLY_MODE %} +
  • +

    {% trans "Permissions" %}

    +

    {% trans "Specify which users can view or modify this map" %}

    + +
  • + {% endif %} + {% include "_permissions_form.html" %} + {% endif %} + {% endif %} + + {% include "base/_resourcebase_contact_snippet.html" %} + +
+ +
+ +
+ + + {% endblock %} + +{% block extra_script %} +{{ block.super }} + {% if DISPLAY_SOCIAL %} + {% include 'facebook_sdk.html' %} + {% endif %} + {% if request.user.is_authenticated %} + {% user_rating_js request.user resource "map" %} + {% else %} + {% overall_rating resource "map" as the_map_rating %} + {% endif %} + {% include 'rating.html' %} + + + {% if GEONODE_SECURITY_ENABLED %} + {% include "_permissions_form_js.html" %} + {% endif %} + +{% endblock extra_script %} diff --git a/geonode/geoapps/templates/apps/app_download.html b/geonode/geoapps/templates/apps/app_download.html new file mode 100644 index 00000000000..0e40103edf2 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_download.html @@ -0,0 +1,20 @@ +{% extends "geonode_base.html" %} + +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block title %} {% trans "Download" %} {% blocktrans %}{{GEONODE_APPS_NAME}}{% endblocktrans %} - {{ block.super }} {% endblock %} +{% block head %} + + + + {% get_geoapp_download %} + {{ block.super }} +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_edit.html b/geonode/geoapps/templates/apps/app_edit.html new file mode 100644 index 00000000000..8af81fc266c --- /dev/null +++ b/geonode/geoapps/templates/apps/app_edit.html @@ -0,0 +1,20 @@ +{% extends "geonode_base.html" %} + +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block title %} {% trans "Edit" %} {% blocktrans %}{{GEONODE_APPS_NAME}}{% endblocktrans %} - {{ block.super }} {% endblock %} +{% block head %} + + + + {% get_geoapp_edit %} + {{ block.super }} +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_embed.html b/geonode/geoapps/templates/apps/app_embed.html new file mode 100644 index 00000000000..066fde4328e --- /dev/null +++ b/geonode/geoapps/templates/apps/app_embed.html @@ -0,0 +1,14 @@ +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block head %} + + + + {% get_geoapp_embed %} +{% endblock %} \ No newline at end of file diff --git a/geonode/geoapps/templates/apps/app_list.html b/geonode/geoapps/templates/apps/app_list.html new file mode 100644 index 00000000000..1725121eca8 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_list.html @@ -0,0 +1,14 @@ +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block head %} + + + + {% get_geoapp_list %} +{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_list_default.html b/geonode/geoapps/templates/apps/app_list_default.html new file mode 100644 index 00000000000..2be0910c338 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_list_default.html @@ -0,0 +1,36 @@ +{% extends "apps/app_base.html" %} +{% load i18n %} +{% load staticfiles %} + +{% block body_class %}{% trans "apps explore" %}{% endblock %} + +{% block body %} + + {% with include_type_filter='true' %} + {% with header='Type' %} + {% with filter='app_type__in' %} + {% include "search/_search_content.html" %} + {% endwith %} + {% endwith %} + {% endwith %} +{% endblock %} + +{% block extra_script %} +{{ block.super }} + + {% with include_spatial='true' %} + {% include 'search/search_scripts.html' %} + {% endwith %} +{% endblock extra_script %} diff --git a/geonode/geoapps/templates/apps/app_metadata.html b/geonode/geoapps/templates/apps/app_metadata.html new file mode 100644 index 00000000000..34fd3f5d1b1 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_metadata.html @@ -0,0 +1,84 @@ +{% extends "metadata_base.html" %} +{% load i18n %} +{% load bootstrap_tags %} +{% load base_tags %} +{% load guardian_tags %} +{% load floppyforms %} + +{% block title %}{{ geoapp.title }} — {{ block.super }}{% endblock %} + +{% block body_class %}{% trans "data" %}{% endblock body_class %} + +{% block body_outer %} + + + +
+ {% if geoapp.metadata_uploaded %} +
{% blocktrans %}Note: this geoapp's orginal metadata was populated by importing a metadata XML file. + GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin Core metadata elements. + Some of your original metadata may have been lost.{% endblocktrans %}
+ {% endif %} + + {% if geoapp_form.errors or category_form.errors %} +
{% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %} +
    + {% for field in geoapp_form %} + {% if field.errors %} +
  • {{ field.label }}
  • + {% endif %} + {% endfor %} + + {% if category_form.errors %} +
  • {{ category_form.errors.as_ul }}
  • + {% endif %} +
+
+ {% endif %} + + {% csrf_token %} +
+ {% form geoapp_form using "layouts/app_panels.html" %} + {# geoapp_form|as_bootstrap #} +
+ +
+
+ + + +
+ + + >" %}"/> +
+
+
+
+ + + +{{ block.super }} +{% endblock body_outer %} diff --git a/geonode/geoapps/templates/apps/app_metadata_advanced.html b/geonode/geoapps/templates/apps/app_metadata_advanced.html new file mode 100644 index 00000000000..a8be7b743f2 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_metadata_advanced.html @@ -0,0 +1,118 @@ +{% extends "metadata_base.html" %} +{% load i18n %} +{% load static %} +{% load base_tags %} +{% load bootstrap_tags %} +{% load guardian_tags %} + +{% block title %}{{ geoapp.title }} — {{ block.super }}{% endblock %} + +{% block body_class %}{% trans "data" %}{% endblock %} + +{% block body_outer %} + +{{ block.super }} + + + + + + + + + + + + + + +
+
+

+ {% blocktrans with geoapp.title as map_title %} + Editing details for {{ map_title }} + {% endblocktrans %} +

+ +
+ {% if geoapp.metadata_uploaded %} +
{% blocktrans %}Note: this geoapp's orginal metadata was populated by importing a metadata XML file. + GeoNode's metadata import supports a subset of ISO, FGDC, and Dublin Core metadata elements. + Some of your original metadata may have been lost.{% endblocktrans %}
+ {% endif %} + + {% if geoapp_form.errors or category_form.errors %} +
{% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %} +
    + {% for field in geoapp_form %} + {% if field.errors %} +
  • {{ field.label }}
  • + {% endif %} + {% endfor %} + + {% if category_form.errors %} +
  • {{ category_form.errors.as_ul }}
  • + {% endif %} +
+
+ {% endif %} +
+ +
+ {% csrf_token %} + +
+ {{ geoapp_form|as_bootstrap }} + {% if THESAURI_FILTERS %} + {{ tkeywords_form }} + {% endif %} +
+
+
+ +
+ {% autoescape off %} + {% for choice in category_form.category_choice_field.field.choices %} +
+ +
+ + {% endfor %} + {% endautoescape %} +
+
+ +
+ + + +
+ +
+
+
+
+
+
+{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_metadata_detail.html b/geonode/geoapps/templates/apps/app_metadata_detail.html new file mode 100644 index 00000000000..f320e00ff13 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_metadata_detail.html @@ -0,0 +1 @@ +{% extends "metadata_detail.html" %} \ No newline at end of file diff --git a/geonode/geoapps/templates/apps/app_new.html b/geonode/geoapps/templates/apps/app_new.html new file mode 100644 index 00000000000..e6722711311 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_new.html @@ -0,0 +1,20 @@ +{% extends "geonode_base.html" %} + +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block title %} {% trans "New Map" %} - {{ block.super }} {% endblock %} +{% block head %} + + + + {% get_geoapp_new %} + {{ block.super }} +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_remove.html b/geonode/geoapps/templates/apps/app_remove.html new file mode 100644 index 00000000000..4154313736d --- /dev/null +++ b/geonode/geoapps/templates/apps/app_remove.html @@ -0,0 +1,26 @@ +{% extends "apps/app_base.html" %} +{% load i18n %} + +{% block title %}{% trans "Deleting" %} {{ resource.title }} — {{ block.super }}{% endblock %} + +{% block body %} + +
+
+

+ {% blocktrans with resource.title as resource_title %} + Are you sure you want to remove {{ resource_title }}? + {% endblocktrans %} +

+
+ {% csrf_token %} + +
+
+
+{% endblock %} diff --git a/geonode/geoapps/templates/apps/app_update.html b/geonode/geoapps/templates/apps/app_update.html new file mode 100644 index 00000000000..92a7dc4cd34 --- /dev/null +++ b/geonode/geoapps/templates/apps/app_update.html @@ -0,0 +1,20 @@ +{% extends "geonode_base.html" %} + +{% load i18n %} +{% load base_tags %} +{% load client_lib_tags %} + +{% block title %} {% trans "Update" %} {% blocktrans %}{{GEONODE_APPS_NAME}}{% endblocktrans %} - {{ block.super }} {% endblock %} +{% block head %} + + + + {% get_geoapp_update %} + {{ block.super }} +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html new file mode 100644 index 00000000000..ce8d8f5c932 --- /dev/null +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -0,0 +1,575 @@ +{% load i18n %} +{% load static %} +{% load floppyforms %} + + + + + + + + + + + + + + + + + + + + + +{% block body_outer %} + + +
+
+
+ +
+ {% trans "Mandatory" %} +
+
+ {% trans "Mandatory" %} +
+
+ {% trans "Optional" %} +
+
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ + + {{ geoapp_form.title }} +
+ + + {{ geoapp_form.abstract }} +
+
+
+
+ + {{ geoapp_form.keywords }} +
+ {% if THESAURI_FILTERS %} +
+ {{ tkeywords_form }} +
+ {% endif %} +
+ + + {{ geoapp_form.date_type }} +
+
+ + + {{ geoapp_form.date }} +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ + + {{ geoapp_form.language }} +
+
+ + + {{ geoapp_form.license }} +
+
+ + {{ geoapp_form.doi }} +
+
+ + {{ geoapp_form.attribution }} +
+
+
+
+ + {{ geoapp_form.regions }} +
+
+ + + {{ geoapp_form.data_quality_statement }} +
+
+
+
+ + + {{ geoapp_form.restriction_code_type }} +
+
+ + + {{ geoapp_form.constraints_other }} +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+

{% trans "Other, Optional, Metadata" %}

+
+ + + {{ geoapp_form.edition }} +
+
+ + + {{ geoapp_form.purpose }} +
+
+ + + {{ geoapp_form.supplemental_information }} +
+
+
+
+
+ + + {{ geoapp_form.temporal_extent_start }} +
+
+
+
+ + + {{ geoapp_form.temporal_extent_end }} +
+
+
+
+ + + {{ geoapp_form.maintenance_frequency }} +
+
+ + + {{ geoapp_form.spatial_representation_type }} +
+
+
+
+
+
{% trans "Responsible Parties" %}
+
+ + {{ geoapp_form.poc }} +
+
+
+
{% trans "Responsible and Permissions" %}
+
+
+ + + {{ geoapp_form.owner }} +
+
+ + + {{ geoapp_form.metadata_author }} +
+ {% if GEONODE_SECURITY_ENABLED %} + + {% endif %} +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
{% trans "Publishing" %}
+
+
+ + {{ geoapp_form.metadata_uploaded_preserve }} +
+
+ + {{ geoapp_form.is_approved }} +
+
+ + {{ geoapp_form.is_published }} +
+
+ + {{ geoapp_form.featured }} +
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Other Settings" %}
+
+
+ + {{ geoapp_form.urlsuffix }} +
+
+
+
+
+
+
+
+
+{% endblock %} diff --git a/geonode/geoapps/tests.py b/geonode/geoapps/tests.py new file mode 100644 index 00000000000..725b3926923 --- /dev/null +++ b/geonode/geoapps/tests.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### \ No newline at end of file diff --git a/geonode/geoapps/translation.py b/geonode/geoapps/translation.py new file mode 100644 index 00000000000..4016795af12 --- /dev/null +++ b/geonode/geoapps/translation.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# 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 . +# +######################################################################### + +from modeltranslation.translator import translator, TranslationOptions +from geonode.geoapps.models import GeoApp + + +class GeoAppTranslationOptions(TranslationOptions): + fields = () + + +translator.register(GeoApp, GeoAppTranslationOptions) diff --git a/geonode/geoapps/urls.py b/geonode/geoapps/urls.py new file mode 100644 index 00000000000..29d5e7d44b2 --- /dev/null +++ b/geonode/geoapps/urls.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +from django.conf.urls import url, include +from django.views.generic import TemplateView + +from geonode.monitoring import register_url_event + +from . import views + +js_info_dict = { + 'packages': ('geonode.geoapps', ), +} + +apps_list = register_url_event()(TemplateView.as_view(template_name='apps/app_list.html')) + +urlpatterns = [ + # 'geonode.geoapps.views', + url(r'^$', + apps_list, + {'facet_type': 'geoapps'}, + name='apps_browse'), + url(r'^new$', views.new_geoapp, name="new_geoapp"), + url(r'^preview/(?P[^/]*)$', views.geoapp_detail, name="geoapp_detail"), + url(r'^preview/(?P\d+)/metadata$', views.geoapp_metadata, name='geoapp_metadata'), + url(r'^preview/(?P[^/]*)/metadata_detail$', + views.geoapp_metadata_detail, name='geoapp_metadata_detail'), + url(r'^preview/(?P\d+)/metadata_advanced$', + views.geoapp_metadata_advanced, name='geoapp_metadata_advanced'), + url(r'^(?P\d+)/remove$', views.geoapp_remove, name="geoapp_remove"), + url(r'^(?P[^/]+)/view$', views.geoapp_edit, name='geoapp_view'), + url(r'^(?P[^/]+)/edit$', views.geoapp_edit, name='geoapp_edit'), + url(r'^(?P[^/]+)/update$', views.geoapp_edit, + {'template': 'apps/app_update.html'}, name='geoapp_update'), + url(r'^(?P[^/]+)/embed$', views.geoapp_edit, + {'template': 'apps/app_embed.html'}, name='geoapp_embed'), + url(r'^(?P[^/]+)/download$', views.geoapp_edit, + {'template': 'apps/app_download.html'}, name='geoapp_download'), + url(r'^', include('geonode.geoapps.api.urls')), +] diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py new file mode 100644 index 00000000000..e5e3f6bc7f2 --- /dev/null +++ b/geonode/geoapps/views.py @@ -0,0 +1,528 @@ +# -*- coding: utf-8 -*- +######################################################################### +# +# Copyright (C) 2020 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 . +# +######################################################################### +import json +import logging +import traceback + +from itertools import chain + +from django.conf import settings +from django.db.models import F +from django.urls import reverse +from django.shortcuts import render +from django.forms.utils import ErrorList +from django.utils.translation import ugettext as _ +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.views.decorators.clickjacking import xframe_options_sameorigin + +from guardian.shortcuts import get_perms + +from geonode.groups.models import GroupProfile +from geonode.base.auth import get_or_create_token +from geonode.security.views import _perms_info_json +from geonode.geoapps.models import GeoApp, GeoAppData +from geonode.decorators import check_keyword_write_perms +from geonode.monitoring import register_event +from geonode.monitoring.models import EventType + +from geonode.people.forms import ProfileForm +from geonode.base.forms import CategoryForm, TKeywordForm + +from geonode.base.models import ( + Thesaurus, + TopicCategory +) + +from geonode.utils import ( + resolve_object, + build_social_links +) + +from .forms import GeoAppForm + +logger = logging.getLogger("geonode.geoapps.views") + +_PERMISSION_MSG_DELETE = _("You are not permitted to delete this app.") +_PERMISSION_MSG_GENERIC = _("You do not have permissions for this app.") +_PERMISSION_MSG_LOGIN = _("You must be logged in to save this app") +_PERMISSION_MSG_SAVE = _("You are not permitted to save or edit this app.") +_PERMISSION_MSG_METADATA = _( + "You are not allowed to modify this app's metadata.") +_PERMISSION_MSG_VIEW = _("You are not allowed to view this app.") +_PERMISSION_MSG_UNKNOWN = _("An unknown error has occured.") + + +def _resolve_geoapp(request, id, permission='base.change_resourcebase', + msg=_PERMISSION_MSG_GENERIC, **kwargs): + ''' + Resolve the GeoApp by the provided typename and check the optional permission. + ''' + if GeoApp.objects.filter(urlsuffix=id).count() > 0: + key = 'urlsuffix' + else: + key = 'pk' + + return resolve_object(request, GeoApp, {key: id}, permission=permission, + permission_msg=msg, **kwargs) + + +@login_required +def new_geoapp(request, template='apps/app_new.html'): + + 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 + + if request.method == 'GET': + _ctx = { + 'user': request.user, + 'access_token': access_token, + } + return render(request, template, context=_ctx) + + return HttpResponseRedirect(reverse("apps_browse")) + + +def geoapp_detail(request, geoappid, template='apps/app_detail.html'): + """ + The view that returns the app composer opened to + the app with the given app ID. + """ + try: + geoapp_obj = _resolve_geoapp( + request, + geoappid, + 'base.view_resourcebase', + _PERMISSION_MSG_VIEW) + except PermissionDenied: + return HttpResponse(_("Not allowed"), status=403) + except Exception: + raise Http404(_("Not found")) + if not geoapp_obj: + raise Http404(_("Not found")) + + # Add metadata_author or poc if missing + geoapp_obj.add_missing_metadata_author_or_poc() + + # Update count for popularity ranking, + # but do not includes admins or resource owners + if request.user != geoapp_obj.owner and not request.user.is_superuser: + GeoApp.objects.filter( + id=geoapp_obj.id).update( + popular_count=F('popular_count') + 1) + + _data = GeoAppData.objects.filter(resource__id=geoappid).first() + _config = _data.blob if _data else {} + + # Call this first in order to be sure "perms_list" is correct + permissions_json = _perms_info_json(geoapp_obj) + + perms_list = get_perms( + request.user, + geoapp_obj.get_self_resource()) + get_perms(request.user, geoapp_obj) + + group = None + if geoapp_obj.group: + try: + group = GroupProfile.objects.get(slug=geoapp_obj.group.name) + except GroupProfile.DoesNotExist: + group = None + + 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 = { + 'appId': geoappid, + 'appType': geoapp_obj.type, + 'config': _config, + 'user': request.user, + 'access_token': access_token, + 'resource': geoapp_obj, + 'group': group, + 'perms_list': perms_list, + 'permissions_json': permissions_json, + 'preview': getattr( + settings, + 'GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY', + 'mapstore'), + 'crs': getattr( + settings, + 'DEFAULT_MAP_CRS', + 'EPSG:3857') + } + + if settings.SOCIAL_ORIGINS: + context_dict["social_links"] = build_social_links(request, geoapp_obj) + + register_event(request, EventType.EVENT_VIEW, request.path) + + return render(request, template, context=context_dict) + + +@xframe_options_sameorigin +def geoapp_edit(request, geoappid, template='apps/app_edit.html'): + """ + The view that returns the app composer opened to + the app with the given app ID. + """ + try: + geoapp_obj = _resolve_geoapp( + request, + geoappid, + 'base.view_resourcebase', + _PERMISSION_MSG_VIEW) + except PermissionDenied: + return HttpResponse(_("Not allowed"), status=403) + except Exception: + raise Http404(_("Not found")) + if not geoapp_obj: + raise Http404(_("Not found")) + + # Call this first in order to be sure "perms_list" is correct + permissions_json = _perms_info_json(geoapp_obj) + + perms_list = get_perms( + request.user, + geoapp_obj.get_self_resource()) + get_perms(request.user, geoapp_obj) + + group = None + if geoapp_obj.group: + try: + group = GroupProfile.objects.get(slug=geoapp_obj.group.name) + except GroupProfile.DoesNotExist: + group = None + + 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 + + _data = GeoAppData.objects.filter(resource__id=geoappid).first() + _config = _data.blob if _data else {} + _ctx = { + 'appId': geoappid, + 'appType': geoapp_obj.type, + 'config': _config, + 'user': request.user, + 'access_token': access_token, + 'resource': geoapp_obj, + 'group': group, + 'perms_list': perms_list, + "permissions_json": permissions_json, + 'preview': getattr( + settings, + 'GEONODE_CLIENT_LAYER_PREVIEW_LIBRARY', + 'mapstore') + } + + return render(request, template, context=_ctx) + + +@login_required +def geoapp_remove(request, geoappid, template='apps/app_remove.html'): + try: + geoapp_obj = _resolve_geoapp( + request, + geoappid, + 'base.delete_resourcebase', + _PERMISSION_MSG_DELETE) + except PermissionDenied: + return HttpResponse(_("Not allowed"), status=403) + except Exception: + raise Http404(_("Not found")) + if not geoapp_obj: + raise Http404(_("Not found")) + + if request.method == 'GET': + return render(request, template, context={ + "resource": geoapp_obj + }) + elif request.method == 'POST': + geoapp_obj.delete() + register_event(request, EventType.EVENT_REMOVE, geoapp_obj) + return HttpResponseRedirect(reverse("apps_browse")) + else: + return HttpResponse("Not allowed", status=403) + + +def geoapp_metadata_detail(request, geoappid, template='apps/app_metadata_detail.html'): + try: + geoapp_obj = _resolve_geoapp( + request, + geoappid, + 'view_resourcebase', + _PERMISSION_MSG_METADATA) + except PermissionDenied: + return HttpResponse(_("Not allowed"), status=403) + except Exception: + raise Http404(_("Not found")) + if not geoapp_obj: + raise Http404(_("Not found")) + + group = None + if geoapp_obj.group: + try: + group = GroupProfile.objects.get(slug=geoapp_obj.group.name) + except ObjectDoesNotExist: + group = None + site_url = settings.SITEURL.rstrip('/') if settings.SITEURL.startswith('http') else settings.SITEURL + register_event(request, EventType.EVENT_VIEW_METADATA, geoapp_obj) + return render(request, template, context={ + "resource": geoapp_obj, + "group": group, + 'SITEURL': site_url + }) + + +@login_required +@check_keyword_write_perms +def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=True): + geoapp_obj = None + try: + geoapp_obj = _resolve_geoapp( + request, + geoappid, + 'base.change_resourcebase_metadata', + _PERMISSION_MSG_METADATA) + except PermissionDenied: + return HttpResponse(_("Not allowed"), status=403) + except Exception: + raise Http404(_("Not found")) + if not geoapp_obj: + raise Http404(_("Not found")) + + # Add metadata_author or poc if missing + geoapp_obj.add_missing_metadata_author_or_poc() + poc = geoapp_obj.poc + metadata_author = geoapp_obj.metadata_author + topic_category = geoapp_obj.category + current_keywords = [keyword.name for keyword in geoapp_obj.keywords.all()] + + if request.method == "POST": + geoapp_form = GeoAppForm( + request.POST, + instance=geoapp_obj, + prefix="resource") + 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) + tkeywords_form = TKeywordForm(request.POST) + else: + geoapp_form = GeoAppForm(instance=geoapp_obj, prefix="resource") + geoapp_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 + doc_tkeywords = geoapp_obj.tkeywords.all() + tkeywords_list = '' + lang = 'en' # TODO: use user's language + if doc_tkeywords and len(doc_tkeywords) > 0: + tkeywords_ids = doc_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 += "," + \ + tkl_ids if len( + tkeywords_list) > 0 else tkl_ids + except Exception: + tb = traceback.format_exc() + logger.error(tb) + + tkeywords_form = TKeywordForm(instance=geoapp_obj) + + if request.method == "POST" and geoapp_form.is_valid( + ) and category_form.is_valid(): + new_poc = geoapp_form.cleaned_data['poc'] + new_author = geoapp_form.cleaned_data['metadata_author'] + new_keywords = current_keywords if request.keyword_readonly else geoapp_form.cleaned_data['keywords'] + new_regions = geoapp_form.cleaned_data['regions'] + + 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'])) + + if new_poc is None: + if poc is None: + poc_form = ProfileForm( + request.POST, + prefix="poc", + instance=poc) + else: + poc_form = ProfileForm(request.POST, prefix="poc") + if poc_form.is_valid(): + if len(poc_form.cleaned_data['profile']) == 0: + # FIXME use form.add_error in django > 1.7 + errors = poc_form._errors.setdefault( + 'profile', ErrorList()) + errors.append( + _('You must set a point of contact for this resource')) + poc = None + if poc_form.has_changed and poc_form.is_valid(): + new_poc = poc_form.save() + + if new_author is None: + if metadata_author is None: + author_form = ProfileForm(request.POST, prefix="author", + instance=metadata_author) + else: + author_form = ProfileForm(request.POST, prefix="author") + if author_form.is_valid(): + if len(author_form.cleaned_data['profile']) == 0: + # FIXME use form.add_error in django > 1.7 + errors = author_form._errors.setdefault( + 'profile', ErrorList()) + errors.append( + _('You must set an author for this resource')) + metadata_author = None + if author_form.has_changed and author_form.is_valid(): + new_author = author_form.save() + + geoapp_obj = geoapp_form.instance + if new_poc is not None and new_author is not None: + geoapp_obj.poc = new_poc + geoapp_obj.metadata_author = new_author + geoapp_obj.keywords.clear() + geoapp_obj.keywords.add(*new_keywords) + geoapp_obj.regions.clear() + geoapp_obj.regions.add(*new_regions) + geoapp_obj.category = new_category + geoapp_obj.save(notify=True) + + register_event(request, EventType.EVENT_CHANGE_METADATA, geoapp_obj) + if not ajax: + return HttpResponseRedirect( + reverse( + 'geoapp_detail', + args=( + geoapp_obj.id, + ))) + + message = geoapp_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)) + + tkeywords_data = tkeywords_form.cleaned_data['tkeywords'] + + thesaurus_setting = getattr(settings, 'THESAURUS', None) + if thesaurus_setting: + tkeywords_data = tkeywords_data.filter( + thesaurus__identifier=thesaurus_setting['name'] + ) + geoapp_obj.tkeywords.set(tkeywords_data) + except Exception: + tb = traceback.format_exc() + logger.error(tb) + + return HttpResponse(json.dumps({'message': message})) + + # - POST Request Ends here - + + # Request.GET + if poc is not None: + geoapp_form.fields['poc'].initial = poc.id + poc_form = ProfileForm(prefix="poc") + poc_form.hidden = True + + if metadata_author is not None: + geoapp_form.fields['metadata_author'].initial = metadata_author.id + author_form = ProfileForm(prefix="author") + author_form.hidden = True + + metadata_author_groups = [] + if request.user.is_superuser or request.user.is_staff: + metadata_author_groups = GroupProfile.objects.all() + else: + try: + all_metadata_author_groups = chain( + request.user.group_list_all(), + GroupProfile.objects.exclude( + access="private").exclude(access="public-invite")) + except Exception: + all_metadata_author_groups = GroupProfile.objects.exclude( + access="private").exclude(access="public-invite") + [metadata_author_groups.append(item) for item in all_metadata_author_groups + if item not in metadata_author_groups] + + if settings.ADMIN_MODERATE_UPLOADS: + if not request.user.is_superuser: + can_change_metadata = request.user.has_perm( + 'change_resourcebase_metadata', + geoapp_obj.get_self_resource()) + try: + is_manager = request.user.groupmember_set.all().filter(role='manager').exists() + except Exception: + is_manager = False + if not is_manager or not can_change_metadata: + if settings.RESOURCE_PUBLISHING: + geoapp_form.fields['is_published'].widget.attrs.update( + {'disabled': 'true'}) + geoapp_form.fields['is_approved'].widget.attrs.update( + {'disabled': 'true'}) + + register_event(request, EventType.EVENT_VIEW_METADATA, geoapp_obj) + return render(request, template, context={ + "resource": geoapp_obj, + "geoapp": geoapp_obj, + "geoapp_form": geoapp_form, + "poc_form": poc_form, + "author_form": author_form, + "category_form": category_form, + "tkeywords_form": tkeywords_form, + "metadata_author_groups": metadata_author_groups, + "TOPICCATEGORY_MANDATORY": getattr(settings, 'TOPICCATEGORY_MANDATORY', False), + "GROUP_MANDATORY_RESOURCES": getattr(settings, 'GROUP_MANDATORY_RESOURCES', False), + }) + + +@login_required +def geoapp_metadata_advanced(request, geoappid): + return geoapp_metadata( + request, + geoappid, + template='apps/app_metadata_advanced.html') diff --git a/geonode/settings.py b/geonode/settings.py index 0e2ba4c8dea..2c7f2d595e9 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -415,12 +415,18 @@ 'geonode.br', 'geonode.layers', 'geonode.maps', + 'geonode.geoapps', 'geonode.documents', 'geonode.security', 'geonode.catalogue', 'geonode.catalogue.metadataxsl', ) +# GeoNode Apps +GEONODE_APPS_ENABLE = ast.literal_eval(os.getenv("GEONODE_APPS_ENABLE", "True")) +GEONODE_APPS_NAME = os.getenv("GEONODE_APPS_NAME", "Apps") +GEONODE_APPS_NAV_MENU_ENABLE = ast.literal_eval(os.getenv("GEONODE_APPS_NAV_MENU_ENABLE", "True")) + GEONODE_INTERNAL_APPS = ( # GeoNode internal apps 'geonode.people', @@ -1497,6 +1503,8 @@ if 'geonode_mapstore_client' not in INSTALLED_APPS: INSTALLED_APPS += ( 'mapstore2_adapter', + 'mapstore2_adapter.geoapps', + 'mapstore2_adapter.geoapps.geostories', 'geonode_mapstore_client',) def get_geonode_catalogue_service(): diff --git a/geonode/templates/500.html b/geonode/templates/500.html index c54cad8e26b..d0f1686ed76 100644 --- a/geonode/templates/500.html +++ b/geonode/templates/500.html @@ -61,6 +61,11 @@ + {% if GEONODE_APPS_ENABLE and GEONODE_APPS_NAV_MENU_ENABLE %} + + {% endif %} {% block extra_tab %} {% endblock %} + {% if GEONODE_APPS_ENABLE and GEONODE_APPS_NAV_MENU_ENABLE %} + + {% endif %}
  • @@ -99,6 +107,14 @@

    {%

  • {% endverbatim %} + {% elif facet_type == 'geoapps' %} + {% verbatim %} +
  • + {{ groupCategory.name }} + {{ groupCategory.resource_counts.geoapp.total }} + +
  • + {% endverbatim %} {% endif %} diff --git a/geonode/templates/search/_search_content.html b/geonode/templates/search/_search_content.html index ce253b8ceb4..77762f6497a 100644 --- a/geonode/templates/search/_search_content.html +++ b/geonode/templates/search/_search_content.html @@ -51,6 +51,8 @@ {% elif facet_type == 'documents' %} + {% elif facet_type == 'geoapps' %} + {% else %} {% endif %} diff --git a/geonode/templates/search/indexes/geoapps/app_text.txt b/geonode/templates/search/indexes/geoapps/app_text.txt new file mode 100644 index 00000000000..78f6b9a13a2 --- /dev/null +++ b/geonode/templates/search/indexes/geoapps/app_text.txt @@ -0,0 +1,7 @@ +{{ object.content }} +{{ object.title }} +{{ object.abstract }} +{{ object.purpose }} +{{ object.name }} +{{ object.supplemental_information }} +{{ object.keyword_list }} diff --git a/geonode/urls.py b/geonode/urls.py index 4eacb606a2c..db5b5ad8087 100644 --- a/geonode/urls.py +++ b/geonode/urls.py @@ -103,6 +103,9 @@ # Documents views url(r'^documents/', include('geonode.documents.urls')), + # Apps views + url(r'^apps/', include('geonode.geoapps.urls')), + # Catalogue views url(r'^catalogue/', include('geonode.catalogue.urls')), diff --git a/geonode/views.py b/geonode/views.py index 90a15d27e24..72cf604cef6 100644 --- a/geonode/views.py +++ b/geonode/views.py @@ -20,17 +20,19 @@ import json from django import forms +from django.apps import apps from django.db.models import Q from django.urls import reverse from django.conf import settings from django.shortcuts import render_to_response from django.template.response import TemplateResponse +from geonode.base.templatetags.base_tags import facets from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth import authenticate, login, get_user_model from geonode import get_version from geonode.groups.models import GroupProfile -from geonode.base.templatetags.base_tags import facets +from geonode.geoapps.models import GeoApp class AjaxLoginForm(forms.Form): @@ -163,8 +165,27 @@ def ident_json(request): def h_keywords(request): from geonode.base.models import HierarchicalKeyword as hk - keywords = json.dumps(hk.dump_bulk_tree(request.user, type=request.GET.get('type', None))) - return HttpResponse(content=keywords) + p_type = request.GET.get('type', None) + keywords = hk.dump_bulk_tree(request.user, type=p_type) + + subtypes = [] + if p_type == 'geoapp': + 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): + subtypes.append(_model.__name__.lower()) + + for _type in subtypes: + _bulk_tree = hk.dump_bulk_tree(request.user, type=_type) + if isinstance(_bulk_tree, list): + for _elem in _bulk_tree: + keywords.append(_elem) + else: + keywords.append(_bulk_tree) + + return HttpResponse(content=json.dumps(keywords)) def moderator_contacted(request, inactive_user=None): diff --git a/requirements.txt b/requirements.txt index bd08cc6ad9d..d43b923ad43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -86,8 +86,10 @@ pinax-notifications==6.0.0 pinax-ratings==4.0.0 # GeoNode org maintained apps. -django_mapstore_adapter==2.0.6 -django-geonode-mapstore-client==2.0.9 +# django_mapstore_adapter>=2.0.7 +# django-geonode-mapstore-client>=2.0.10 +-e git+https://github.com/GeoNode/geonode-mapstore-client.git@master#egg=django_geonode_mapstore_client +-e git+https://github.com/GeoNode/django-mapstore-adapter.git@master#egg=django_mapstore_adapter geonode-avatar==5.0.7 django-geonode-client==1.0.9 geonode-oauth-toolkit==2.1.0 diff --git a/setup.cfg b/setup.cfg index 67124c23d51..044ca4c7133 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = celery==5.0.2 kombu==5.0.2 vine==5.0.0 - boto3==1.16.29 + boto3==1.16.30 six==1.15.0 tqdm==4.54.0 Deprecated==1.2.10 @@ -112,8 +112,8 @@ install_requires = pinax-ratings==4.0.0 # GeoNode org maintained apps. - django_mapstore_adapter==2.0.6 - django-geonode-mapstore-client==2.0.9 + django_mapstore_adapter>=2.0.7 + django-geonode-mapstore-client>=2.0.10 geonode-avatar==5.0.7 django-geonode-client==1.0.9 geonode-oauth-toolkit==2.1.0 diff --git a/start_django_async.sh b/start_django_async.sh old mode 100644 new mode 100755 diff --git a/test_api_v2.sh b/test_api_v2.sh new file mode 100755 index 00000000000..213e1def849 --- /dev/null +++ b/test_api_v2.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./test.sh geonode.base.api.tests geonode.layers.api.tests geonode.maps.api.tests geonode.documents.api.tests geonode.geoapps.api.tests