Skip to content

Commit deeaae3

Browse files
authored
feat: extension model and form validation (#2176)
1 parent 86ac79e commit deeaae3

File tree

11 files changed

+522
-12
lines changed

11 files changed

+522
-12
lines changed

geonode_mapstore_client/admin.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,48 @@
1+
import os
2+
from django import forms
13
from django.contrib import admin
2-
from geonode_mapstore_client.models import SearchService
4+
from geonode_mapstore_client.models import SearchService, Extension
35

46

57
@admin.register(SearchService)
68
class SearchServiceAdmin(admin.ModelAdmin):
79
pass
10+
11+
12+
class ExtensionAdminForm(forms.ModelForm):
13+
class Meta:
14+
model = Extension
15+
fields = '__all__'
16+
17+
def clean_uploaded_file(self):
18+
"""
19+
It checks the uploaded file's name for uniqueness before the model is saved.
20+
"""
21+
uploaded_file = self.cleaned_data.get('uploaded_file')
22+
23+
if uploaded_file:
24+
extension_name = os.path.splitext(os.path.basename(uploaded_file.name))[0]
25+
26+
queryset = Extension.objects.filter(name=extension_name)
27+
28+
# If we are updating an existing instance, we can exclude it from the check
29+
if self.instance.pk:
30+
queryset = queryset.exclude(pk=self.instance.pk)
31+
32+
# If the queryset finds any conflicting extension, raise a validation error
33+
if queryset.exists():
34+
raise forms.ValidationError(
35+
f"An extension with the name '{extension_name}' already exists. Please upload a file with a different name."
36+
)
37+
38+
return uploaded_file
39+
40+
41+
@admin.register(Extension)
42+
class ExtensionAdmin(admin.ModelAdmin):
43+
44+
form = ExtensionAdminForm
45+
list_display = ('name', 'active', 'is_map_extension', 'updated_at')
46+
list_filter = ('active', 'is_map_extension')
47+
search_fields = ('name',)
48+
readonly_fields = ('name', 'created_at', 'updated_at')

geonode_mapstore_client/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ def run_setup_hooks(*args, **kwargs):
7777
pass
7878

7979
urlpatterns += [
80+
re_path("/client/extensions", views.ExtensionsView.as_view(), name="mapstore-extension"),
81+
re_path("/client/pluginsconfig", views.PluginsConfigView.as_view(), name="mapstore-pluginsconfig"),
82+
8083
re_path(
8184
r"^catalogue/",
8285
TemplateView.as_view(

geonode_mapstore_client/client/devServer.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ module.exports = (devServerDefault, projectConfig) => {
4949
{
5050
context: [
5151
'**',
52-
'!**/static/mapstore/**',
52+
'!**/static/mapstore/configs/**',
53+
'!**/static/mapstore/dist/**',
54+
'!**/static/mapstore/gn-translations/**',
55+
'!**/static/mapstore/img/**',
56+
'!**/static/mapstore/ms-translations/**',
57+
'!**/static/mapstore/symbols/**',
58+
'!**/static/mapstore/version.txt',
5359
'!**/MapStore2/**',
5460
'!**/node_modules/**',
5561
'!**/docs/**'
@@ -76,4 +82,4 @@ module.exports = (devServerDefault, projectConfig) => {
7682
}
7783
]
7884
};
79-
};
85+
};

geonode_mapstore_client/client/js/api/geonode/config/index.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

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

1312
let cache = {};
1413

@@ -50,9 +49,7 @@ export const getStyleTemplates = (styleTemplatesUrl = '/static/mapstore/configs/
5049
export const getDefaultPluginsConfig = () => {
5150
return cache?.pluginsConfig
5251
? Promise.resolve(cache.pluginsConfig)
53-
: getPluginsConfig(
54-
getGeoNodeLocalConfig('geoNodeSettings.staticPath', '/static/') + 'mapstore/configs/pluginsConfig.json'
55-
)
52+
: getPluginsConfig('/client/pluginsconfig')
5653
.then((pluginsConfig) => {
5754
cache.pluginsConfig = pluginsConfig;
5855
return pluginsConfig;

geonode_mapstore_client/client/js/utils/AppUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function setupConfiguration({
176176
const { query } = url.parse(window.location.href, true);
177177
// set the extensions path before get the localConfig
178178
// so it's possible to override in a custom project
179-
setConfigProp('extensionsRegistry', '/static/mapstore/extensions/index.json');
179+
setConfigProp('extensionsRegistry', '/client/extensions');
180180
const {
181181
supportedLocales: defaultSupportedLocales,
182182
...config

geonode_mapstore_client/context_processors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def resource_urls(request):
5555
"PLUGINS_CONFIG_PATCH_RULES": getattr(
5656
settings, "MAPSTORE_PLUGINS_CONFIG_PATCH_RULES", []
5757
),
58-
"EXTENSIONS_FOLDER_PATH": getattr(
59-
settings, "MAPSTORE_EXTENSIONS_FOLDER_PATH", "/static/mapstore/extensions/"
58+
"EXTENSIONS_FOLDER_PATH": settings.STATIC_URL + getattr(
59+
settings, "MAPSTORE_EXTENSIONS_FOLDER_PATH", "mapstore/extensions/"
6060
),
6161
"CUSTOM_FILTERS": getattr(settings, "MAPSTORE_CUSTOM_FILTERS", None),
6262
"TIME_ENABLED": getattr(settings, "UPLOADER", dict())
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Generated by Django 4.2.23 on 2025-10-03 12:14
2+
3+
from django.db import migrations, models
4+
import geonode_mapstore_client.models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("geonode_mapstore_client", "0004_auto_20231114_1705"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Extension",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
(
27+
"name",
28+
models.CharField(
29+
blank=True,
30+
help_text="Name of the extension, derived from the zip file name. Must be unique.",
31+
max_length=255,
32+
unique=True,
33+
),
34+
),
35+
(
36+
"uploaded_file",
37+
models.FileField(
38+
help_text="Upload the MapStore extension as a zip folder.",
39+
upload_to=geonode_mapstore_client.models.extension_upload_path,
40+
validators=[geonode_mapstore_client.models.validate_zip_file],
41+
),
42+
),
43+
(
44+
"active",
45+
models.BooleanField(
46+
default=True,
47+
help_text="Whether the extension is active and should be included in the index.",
48+
),
49+
),
50+
(
51+
"is_map_extension",
52+
models.BooleanField(
53+
default=False,
54+
help_text="Check if this extension is a map-specific plugin for Map Viewers.",
55+
),
56+
),
57+
("created_at", models.DateTimeField(auto_now_add=True)),
58+
("updated_at", models.DateTimeField(auto_now=True)),
59+
],
60+
options={
61+
"verbose_name": "MapStore Extension",
62+
"verbose_name_plural": "MapStore Extensions",
63+
"ordering": ("name",),
64+
},
65+
),
66+
]

geonode_mapstore_client/models.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import os
2+
import shutil
3+
import zipfile
14
from django.db import models
25
from django.utils.translation import gettext_lazy as _
36
from django.contrib.postgres.fields import ArrayField
47
from django.dispatch import receiver
58
from django.db.models import signals
69
from django.core.cache import caches
7-
10+
from django.db import models
11+
from geonode_mapstore_client.utils import validate_zip_file, clear_extension_caches
812
from geonode_mapstore_client.templatetags.get_search_services import (
913
populate_search_service_options,
1014
)
15+
from django.conf import settings
1116

1217

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

7580
services_cache.set("search_services", populate_search_service_options(), 300)
81+
82+
83+
84+
def extension_upload_path(instance, filename):
85+
return f"mapstore_extensions/{filename}"
86+
87+
88+
class Extension(models.Model):
89+
name = models.CharField(
90+
max_length=255,
91+
unique=True,
92+
blank=True, # Will be populated from the zip filename
93+
help_text="Name of the extension, derived from the zip file name. Must be unique.",
94+
)
95+
uploaded_file = models.FileField(
96+
upload_to=extension_upload_path,
97+
validators=[validate_zip_file],
98+
help_text="Upload the MapStore extension as a zip folder.",
99+
)
100+
active = models.BooleanField(
101+
default=True,
102+
help_text="Whether the extension is active and should be included in the index.",
103+
)
104+
is_map_extension = models.BooleanField(
105+
default=False,
106+
help_text="Check if this extension is a map-specific plugin for Map Viewers.",
107+
)
108+
created_at = models.DateTimeField(auto_now_add=True)
109+
updated_at = models.DateTimeField(auto_now=True)
110+
111+
def __str__(self):
112+
return self.name
113+
114+
def save(self, *args, **kwargs):
115+
if not self.name and self.uploaded_file:
116+
self.name = os.path.splitext(os.path.basename(self.uploaded_file.name))[0]
117+
super().save(*args, **kwargs)
118+
119+
class Meta:
120+
ordering = ("name",)
121+
verbose_name = "MapStore Extension"
122+
verbose_name_plural = "MapStore Extensions"
123+
124+
125+
@receiver(signals.post_save, sender=Extension)
126+
def handle_extension_upload(sender, instance, **kwargs):
127+
"""
128+
Unzips the extension file and clears the API cache after saving.
129+
"""
130+
target_path = os.path.join(
131+
settings.STATIC_ROOT, settings.MAPSTORE_EXTENSIONS_FOLDER_PATH, instance.name
132+
)
133+
134+
if os.path.exists(target_path):
135+
shutil.rmtree(target_path)
136+
137+
try:
138+
with zipfile.ZipFile(instance.uploaded_file.path, "r") as zip_ref:
139+
zip_ref.extractall(target_path)
140+
except FileNotFoundError:
141+
pass
142+
143+
clear_extension_caches()
144+
145+
146+
@receiver(signals.post_delete, sender=Extension)
147+
def handle_extension_delete(sender, instance, **kwargs):
148+
"""
149+
Removes the extension's files and clears the API cache on deletion.
150+
"""
151+
if instance.name:
152+
extension_path = os.path.join(
153+
settings.STATIC_ROOT, settings.MAPSTORE_EXTENSIONS_FOLDER_PATH, instance.name
154+
)
155+
if os.path.exists(extension_path):
156+
shutil.rmtree(extension_path)
157+
158+
if instance.uploaded_file and os.path.exists(instance.uploaded_file.path):
159+
os.remove(instance.uploaded_file.path)
160+
161+
clear_extension_caches()

0 commit comments

Comments
 (0)