Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion geonode_mapstore_client/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
import os
from django import forms
from django.contrib import admin
from geonode_mapstore_client.models import SearchService
from geonode_mapstore_client.models import SearchService, Extension


@admin.register(SearchService)
class SearchServiceAdmin(admin.ModelAdmin):
pass


class ExtensionAdminForm(forms.ModelForm):
class Meta:
model = Extension
fields = '__all__'

def clean_uploaded_file(self):
"""
It checks the uploaded file's name for uniqueness before the model is saved.
"""
uploaded_file = self.cleaned_data.get('uploaded_file')

if uploaded_file:
extension_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]

queryset = Extension.objects.filter(name=extension_name)

# If we are updating an existing instance, we can exclude it from the check
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)

# If the queryset finds any conflicting extension, raise a validation error
if queryset.exists():
raise forms.ValidationError(
f"An extension with the name '{extension_name}' already exists. Please upload a file with a different name."
)

return uploaded_file


@admin.register(Extension)
class ExtensionAdmin(admin.ModelAdmin):

form = ExtensionAdminForm
list_display = ('name', 'active', 'is_map_extension', 'updated_at')
list_filter = ('active', 'is_map_extension')
search_fields = ('name',)
readonly_fields = ('name', 'created_at', 'updated_at')
3 changes: 3 additions & 0 deletions geonode_mapstore_client/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def run_setup_hooks(*args, **kwargs):
pass

urlpatterns += [
re_path("/client/extensions", views.ExtensionsView.as_view(), name="mapstore-extension"),
re_path("/client/pluginsconfig", views.PluginsConfigView.as_view(), name="mapstore-pluginsconfig"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested /client/pluginsconfig locally but I'm seeing only the uploaded extension in the list, all the static plugins are missing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m currently using this path for the static plugins. Is this different from what it should be, @allyoucanmap?

        base_config_path = os.path.join(
            settings.PROJECT_ROOT, "static", "mapstore", "configs", "pluginsConfig.json"
        )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nrjadkry I'm checking again locally, if I access from browser the path /static/mapstore/configs/pluginsConfig.json I have a full list of plugins corresponding to this file:

image

but when I try to access the new endpoint /client/pluginsconfig, I'm seeing only a plugin

image


re_path(
r"^catalogue/",
TemplateView.as_view(
Expand Down
10 changes: 8 additions & 2 deletions geonode_mapstore_client/client/devServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ module.exports = (devServerDefault, projectConfig) => {
{
context: [
'**',
'!**/static/mapstore/**',
'!**/static/mapstore/configs/**',
'!**/static/mapstore/dist/**',
'!**/static/mapstore/gn-translations/**',
'!**/static/mapstore/img/**',
'!**/static/mapstore/ms-translations/**',
'!**/static/mapstore/symbols/**',
'!**/static/mapstore/version.txt',
'!**/MapStore2/**',
'!**/node_modules/**',
'!**/docs/**'
Expand All @@ -76,4 +82,4 @@ module.exports = (devServerDefault, projectConfig) => {
}
]
};
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import axios from '@mapstore/framework/libs/ajax';
import getPluginsConfig from '@mapstore/framework/observables/config/getPluginsConfig';
import { getGeoNodeLocalConfig } from '@js/utils/APIUtils';

let cache = {};

Expand Down Expand Up @@ -50,9 +49,7 @@ export const getStyleTemplates = (styleTemplatesUrl = '/static/mapstore/configs/
export const getDefaultPluginsConfig = () => {
return cache?.pluginsConfig
? Promise.resolve(cache.pluginsConfig)
: getPluginsConfig(
getGeoNodeLocalConfig('geoNodeSettings.staticPath', '/static/') + 'mapstore/configs/pluginsConfig.json'
)
: getPluginsConfig('/client/pluginsconfig')
.then((pluginsConfig) => {
cache.pluginsConfig = pluginsConfig;
return pluginsConfig;
Expand Down
2 changes: 1 addition & 1 deletion geonode_mapstore_client/client/js/utils/AppUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function setupConfiguration({
const { query } = url.parse(window.location.href, true);
// set the extensions path before get the localConfig
// so it's possible to override in a custom project
setConfigProp('extensionsRegistry', '/static/mapstore/extensions/index.json');
setConfigProp('extensionsRegistry', '/client/extensions');
const {
supportedLocales: defaultSupportedLocales,
...config
Expand Down
4 changes: 2 additions & 2 deletions geonode_mapstore_client/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def resource_urls(request):
"PLUGINS_CONFIG_PATCH_RULES": getattr(
settings, "MAPSTORE_PLUGINS_CONFIG_PATCH_RULES", []
),
"EXTENSIONS_FOLDER_PATH": getattr(
settings, "MAPSTORE_EXTENSIONS_FOLDER_PATH", "/static/mapstore/extensions/"
"EXTENSIONS_FOLDER_PATH": settings.STATIC_URL + getattr(
settings, "MAPSTORE_EXTENSIONS_FOLDER_PATH", "mapstore/extensions/"
),
"CUSTOM_FILTERS": getattr(settings, "MAPSTORE_CUSTOM_FILTERS", None),
"TIME_ENABLED": getattr(settings, "UPLOADER", dict())
Expand Down
66 changes: 66 additions & 0 deletions geonode_mapstore_client/migrations/0005_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 4.2.23 on 2025-10-03 12:14

from django.db import migrations, models
import geonode_mapstore_client.models


class Migration(migrations.Migration):

dependencies = [
("geonode_mapstore_client", "0004_auto_20231114_1705"),
]

operations = [
migrations.CreateModel(
name="Extension",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
blank=True,
help_text="Name of the extension, derived from the zip file name. Must be unique.",
max_length=255,
unique=True,
),
),
(
"uploaded_file",
models.FileField(
help_text="Upload the MapStore extension as a zip folder.",
upload_to=geonode_mapstore_client.models.extension_upload_path,
validators=[geonode_mapstore_client.models.validate_zip_file],
),
),
(
"active",
models.BooleanField(
default=True,
help_text="Whether the extension is active and should be included in the index.",
),
),
(
"is_map_extension",
models.BooleanField(
default=False,
help_text="Check if this extension is a map-specific plugin for Map Viewers.",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"verbose_name": "MapStore Extension",
"verbose_name_plural": "MapStore Extensions",
"ordering": ("name",),
},
),
]
88 changes: 87 additions & 1 deletion geonode_mapstore_client/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import os
import shutil
import zipfile
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.postgres.fields import ArrayField
from django.dispatch import receiver
from django.db.models import signals
from django.core.cache import caches

from django.db import models
from geonode_mapstore_client.utils import validate_zip_file, clear_extension_caches
from geonode_mapstore_client.templatetags.get_search_services import (
populate_search_service_options,
)
from django.conf import settings


class SearchService(models.Model):
Expand Down Expand Up @@ -73,3 +78,84 @@ def post_save_search_service(instance, sender, created, **kwargs):
services_cache.delete("search_services")

services_cache.set("search_services", populate_search_service_options(), 300)



def extension_upload_path(instance, filename):
return f"mapstore_extensions/{filename}"


class Extension(models.Model):
name = models.CharField(
max_length=255,
unique=True,
blank=True, # Will be populated from the zip filename
help_text="Name of the extension, derived from the zip file name. Must be unique.",
)
uploaded_file = models.FileField(
upload_to=extension_upload_path,
validators=[validate_zip_file],
help_text="Upload the MapStore extension as a zip folder.",
)
active = models.BooleanField(
default=True,
help_text="Whether the extension is active and should be included in the index.",
)
is_map_extension = models.BooleanField(
default=False,
help_text="Check if this extension is a map-specific plugin for Map Viewers.",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.name

def save(self, *args, **kwargs):
if not self.name and self.uploaded_file:
self.name = os.path.splitext(os.path.basename(self.uploaded_file.name))[0]
super().save(*args, **kwargs)

class Meta:
ordering = ("name",)
verbose_name = "MapStore Extension"
verbose_name_plural = "MapStore Extensions"


@receiver(signals.post_save, sender=Extension)
def handle_extension_upload(sender, instance, **kwargs):
"""
Unzips the extension file and clears the API cache after saving.
"""
target_path = os.path.join(
settings.STATIC_ROOT, settings.MAPSTORE_EXTENSIONS_FOLDER_PATH, instance.name
)

if os.path.exists(target_path):
shutil.rmtree(target_path)

try:
with zipfile.ZipFile(instance.uploaded_file.path, "r") as zip_ref:
zip_ref.extractall(target_path)
except FileNotFoundError:
pass

clear_extension_caches()


@receiver(signals.post_delete, sender=Extension)
def handle_extension_delete(sender, instance, **kwargs):
"""
Removes the extension's files and clears the API cache on deletion.
"""
if instance.name:
extension_path = os.path.join(
settings.STATIC_ROOT, settings.MAPSTORE_EXTENSIONS_FOLDER_PATH, instance.name
)
if os.path.exists(extension_path):
shutil.rmtree(extension_path)

if instance.uploaded_file and os.path.exists(instance.uploaded_file.path):
os.remove(instance.uploaded_file.path)

clear_extension_caches()
Loading