From 41e65223c57fe515dbf694935ed6a2fcd31f55c1 Mon Sep 17 00:00:00 2001 From: Dimas Ciputra Date: Sun, 18 Aug 2024 19:04:31 +0100 Subject: [PATCH] Multi tenant mini sass (#4157) * Update third party layer: Minissas (#3720) * update minisass third party layer * update minisass url * remove unused file * Add api to retrieve minisass * Store minisass token, update frontend --------- Co-authored-by: Faneva Andriamiadantsoa --- bims/admins/site_setting.py | 5 + bims/api_urls.py | 4 + bims/api_views/minisass_observations.py | 45 ++++ .../0433_sitesetting_minisass_token.py | 18 ++ bims/models/site_setting.py | 5 + .../control_panel/third_party_layer_panel.js | 200 +++++++++++++----- .../map_page/search-panel-templates.html | 4 +- 7 files changed, 231 insertions(+), 50 deletions(-) create mode 100644 bims/api_views/minisass_observations.py create mode 100644 bims/migrations/0433_sitesetting_minisass_token.py diff --git a/bims/admins/site_setting.py b/bims/admins/site_setting.py index 2e528dc9f..22b27cbcd 100644 --- a/bims/admins/site_setting.py +++ b/bims/admins/site_setting.py @@ -48,6 +48,11 @@ class Meta: required=False ) + minisass_token = forms.CharField( + widget=forms.PasswordInput(render_value=True), + required=False + ) + def __init__(self, *args, **kwargs): super(SiteSettingAdminForm, self).__init__(*args, **kwargs) if self.instance and self.instance.pk: diff --git a/bims/api_urls.py b/bims/api_urls.py index a20f4e814..85f143d1f 100644 --- a/bims/api_urls.py +++ b/bims/api_urls.py @@ -5,6 +5,7 @@ IsHarvestingGeocontext, HarvestGeocontextView, ClearHarvestingGeocontextCache, GetGeocontextLogLinesView ) +from bims.api_views.minisass_observations import MiniSASSObservationsView from bims.api_views.invasions import InvasionsList from bims.api_views.taxon_update import UpdateTaxon, ReviewTaxonProposal from bims.api_views.reference import DeleteRecordsByReferenceId @@ -376,6 +377,9 @@ path('harvesting-geocontext-logs/', GetGeocontextLogLinesView.as_view(), name='get_log_lines'), + path('minisass-observations/', + MiniSASSObservationsView.as_view(), + name='minisass-observations'), path('taxon-group-validated//', TaxonGroupTotalValidated.as_view(), name='taxon-group-total-validated'), diff --git a/bims/api_views/minisass_observations.py b/bims/api_views/minisass_observations.py new file mode 100644 index 000000000..763c8aaaa --- /dev/null +++ b/bims/api_views/minisass_observations.py @@ -0,0 +1,45 @@ +import requests +import os +import json + +from preferences import preferences +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from django.conf import settings + + +class MiniSASSObservationsView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, format=None): + file_path = os.path.join( + settings.MEDIA_ROOT, + 'observations_data.json') + data = None + + if os.path.exists(file_path): + with open(file_path, 'r') as file: + data = json.load(file) + + if data: + return Response(data) + + url = "https://minisass.org/monitor/observations/" + token = preferences.SiteSetting.minisass_token + headers = { + "Authorization": f"Bearer {token}" + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + with open(file_path, 'w') as file: + json.dump(data, file) + return Response(data) + else: + return Response( + response.text, + status=response.status_code) diff --git a/bims/migrations/0433_sitesetting_minisass_token.py b/bims/migrations/0433_sitesetting_minisass_token.py new file mode 100644 index 000000000..fa5223a47 --- /dev/null +++ b/bims/migrations/0433_sitesetting_minisass_token.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-08-18 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bims', '0432_sitesetting_park_layer_csv'), + ] + + operations = [ + migrations.AddField( + model_name='sitesetting', + name='minisass_token', + field=models.CharField(blank=True, default=''), + ), + ] diff --git a/bims/models/site_setting.py b/bims/models/site_setting.py index b4f8792f5..348993ff6 100644 --- a/bims/models/site_setting.py +++ b/bims/models/site_setting.py @@ -122,6 +122,11 @@ class SiteSetting(Preferences): blank=True ) + minisass_token = models.CharField( + default='', + blank=True + ) + iucn_api_key = models.CharField( max_length=255, default='', diff --git a/bims/static/js/views/control_panel/third_party_layer_panel.js b/bims/static/js/views/control_panel/third_party_layer_panel.js index 0a5e82309..e25c9cee9 100644 --- a/bims/static/js/views/control_panel/third_party_layer_panel.js +++ b/bims/static/js/views/control_panel/third_party_layer_panel.js @@ -9,7 +9,9 @@ define(['shared', 'backbone', 'underscore', 'jqueryUi', miniSASSSelected: false, inWARDSelected: false, fetchingInWARDSData: false, + fetchingMiniSASS: false, inWARDSStationsUrl: "/bims_proxy/https://inwards.award.org.za/app_json/wq_stations.php", + miniSASSUrl: "/api/minisass-observations/", events: { 'click .close-button': 'closeClicked', 'click .update-search': 'updateSearch', @@ -39,6 +41,22 @@ define(['shared', 'backbone', 'underscore', 'jqueryUi', image: image }); }, + miniSASSStyleFunction: function (feature) { + let properties = feature.getProperties(); + let color = 'gray'; + if (properties['color']) { + color = properties['color']; + } else { + color = '#1dc6c0'; + } + let image = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({color: color}) + }); + return new ol.style.Style({ + image: image + }); + }, addInWARDSLayer: function () { this.inWARDSLayer = new ol.layer.Vector({ source: null, @@ -48,44 +66,77 @@ define(['shared', 'backbone', 'underscore', 'jqueryUi', this.map.addLayer(this.inWARDSLayer); }, addMiniSASSLayer: function () { - let options = { - url: '/bims_proxy/http://minisass.org/geoserver/wms', - params: { - name: 'MiniSASS', - layers: 'miniSASS:minisass_observations', - format: 'image/png', - getFeatureFormat: 'text/html' - } - }; - this.miniSASSLayer = new ol.layer.Tile({ - source: new ol.source.TileWMS(options) + this.miniSASSLayer = new ol.layer.Vector({ + source: null, + style: this.miniSASSStyleFunction }); this.miniSASSLayer.setVisible(false); this.map.addLayer(this.miniSASSLayer); - Shared.Dispatcher.trigger( - 'layers:renderLegend', - options['params']['layers'], - options['params']['name'], - options['url'], - options['params']['layers'], - false - ); + }, + isValidCoordinate: function(coordinate) { + const [longitude, latitude] = coordinate; + if ( + typeof longitude !== 'number' || + typeof latitude !== 'number' || + longitude < -180 || + longitude > 180 || + latitude < -90 || + latitude > 90 + ) { + return false; + } + return true; }, toggleMiniSASSLayer: function (e) { + let self = this; this.miniSASSSelected = $(e.target).is(":checked"); if (this.miniSASSSelected) { this.miniSASSLayer.setVisible(true); // Move layer to top this.map.removeLayer(this.miniSASSLayer); this.map.getLayers().insertAt(this.map.getLayers().getLength(), this.miniSASSLayer); - let mapLegend = $('#map-legend'); - mapLegend.find(`[data-name='${this.miniSASSLayer.getSource().getParams()['layers']}']`).show(); - if (!mapLegend.is('visible')) { - Shared.Dispatcher.trigger('map:showMapLegends'); + + // Show fetching message + if (!this.fetchingMiniSASS) { + let fetchingMessage = $(' (fetching)'); + $(e.target).parent().find('.label').append(fetchingMessage); + + $.ajax({ + type: 'GET', + url: this.miniSASSUrl, + success: function (data) { + let geojson = { + "type": "FeatureCollection", + "features": [] + } + for(let i=0; i < data.length; i++) { + let observation = data[i]; + let coordinate = [parseFloat(observation.longitude), parseFloat(observation.latitude)]; + if (self.isValidCoordinate(coordinate)) { + let properties = observation; + delete properties.longitude; + delete properties.latitude; + let feature = { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": coordinate}, + "properties": properties + } + geojson.features.push(feature); + } + } + let source = new ol.source.Vector({ + features: ( + new ol.format.GeoJSON() + ).readFeatures(geojson, {featureProjection: 'EPSG:3857'}) + }); + self.miniSASSLayer.setSource(source); + $(e.target).parent().find('.fetching').remove(); + } + }) + this.fetchingMiniSASS = true; } } else { this.miniSASSLayer.setVisible(false); - $('#map-legend').find(`[data-name='${this.miniSASSLayer.getSource().getParams()['layers']}']`).hide(); } }, toggleInward: function (e) { @@ -123,6 +174,68 @@ define(['shared', 'backbone', 'underscore', 'jqueryUi', self.inWARDSLayer.setVisible(false); } }, + objectToTable: function (obj) { + // Create the table and the table body + let table = document.createElement('table'); + let tbody = document.createElement('tbody'); + + function handleValue(value) { + if (value && typeof value === 'object') { + try { + // Attempt to convert the object to a string + return JSON.stringify(value, getCircularReplacer(), 2); + } catch (error) { + // Fallback for objects that cannot be stringified + return '[Circular]'; + } + } else { + return value !== null ? value : 'null'; + } + } + + function getCircularReplacer() { + const seen = new WeakSet(); + return (key, value) => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + } + return value; + }; + } + + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + let tr = document.createElement('tr'); + + let tdKey = document.createElement('td'); + tdKey.textContent = key; + tr.appendChild(tdKey); + + let tdValue = document.createElement('td'); + tdValue.textContent = handleValue(obj[key]); + tr.appendChild(tdValue); + + tbody.appendChild(tr); + } + } + + table.appendChild(tbody); + + table.style.borderCollapse = 'collapse'; + table.style.width = '100%'; + table.style.margin = '20px 0'; + + let cells = table.querySelectorAll('td'); + cells.forEach(cell => { + cell.style.border = '1px solid #ddd'; + cell.style.padding = '8px'; + }); + + return table; + }, showFeatureInfo: function (lon, lat, siteExist = false, featureData = null) { if (!this.miniSASSSelected && !this.inWARDSelected) { return false; @@ -152,34 +265,25 @@ define(['shared', 'backbone', 'underscore', 'jqueryUi', if (this.miniSASSSelected) { const source = this.miniSASSLayer.getSource(); - const getFeatureFormat = source.getParams()['getFeatureFormat']; - const layerName = source.getParams()['name']; - const queryLayer = source.getParams()['layers']; - let layerSource = source.getGetFeatureInfoUrl( - coordinate, - view.getResolution(), - view.getProjection(), - {'INFO_FORMAT': getFeatureFormat} - ); - layerSource += '&QUERY_LAYERS=' + queryLayer; - $.ajax({ - type: 'POST', - url: '/get_feature/', - data: { - 'layerSource': layerSource - }, - success: function (result) { - if (!result) { - return true; - } - const data = result['feature_data']; - if (!data) return true; + const pixel = this.map.getPixelFromCoordinate(coordinate); + let minisassData = []; + self.map.forEachFeatureAtPixel(pixel, function(feature) { + if (feature.getProperties().hasOwnProperty('minisass_ml_score')) { + minisassData.push(feature.getProperties()) + } + if (minisassData.length > 0) { + let minisassDataTable = minisassData[0]; + delete minisassDataTable['geometry']; + delete minisassDataTable['organisationtype']; + delete minisassDataTable['images']; + delete minisassDataTable['site']; + const minisassTable = $(self.objectToTable(minisassDataTable)); self.showContentToSidePanel( - lon, lat, layerName, data, siteExist, openSidePanel + lon, lat, minisassData[0]['sitename'], minisassTable.prop('outerHTML'), siteExist, openSidePanel ) - openSidePanel = true; } }); + } }, showContentToSidePanel: function (lon, lat, panelTitle, panelContent, siteExist, openSidePanel = false) { diff --git a/bims/templates/map_page/search-panel-templates.html b/bims/templates/map_page/search-panel-templates.html index 226477af8..b6bb36a54 100644 --- a/bims/templates/map_page/search-panel-templates.html +++ b/bims/templates/map_page/search-panel-templates.html @@ -631,13 +631,13 @@
- MiniSASS + MiniSASS
MiniSASS is a citizen science tool which can be used by anyone to monitor the health of a river. The data are served with permission from http://www.minisass.org/en/, + href="https://minisass.org/">https://minisass.org/, and further information about the tool is available via this website.