Store DataLayer's settings in DB

This allows to known the full datalayer behaviour without needing
to load all the data, including the zoom from and to (new settings),
but also the color for example.

This will help also understanding datalayers usage and making
stats.

But no data migration is provided, it's retrocompatible (data
migration in OSM FR servers would be huge, so let's see if it's
really needed).
This commit is contained in:
Yohan Boniface 2023-08-20 09:48:01 +02:00
parent bb922d1418
commit fa090b89df
6 changed files with 79 additions and 40 deletions

View file

@ -59,7 +59,7 @@ class DataLayerForm(forms.ModelForm):
class Meta: class Meta:
model = DataLayer model = DataLayer
fields = ('geojson', 'name', 'display_on_load', 'rank') fields = ('geojson', 'name', 'display_on_load', 'rank', 'settings')
class MapSettingsForm(forms.ModelForm): class MapSettingsForm(forms.ModelForm):

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-08-16 05:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("umap", "0011_alter_map_edit_status_alter_map_share_status"),
]
operations = [
migrations.AddField(
model_name="datalayer",
name="settings",
field=models.JSONField(
blank=True, default=dict, null=True, verbose_name="settings"
),
),
]

View file

@ -176,10 +176,14 @@ class Map(NamedModel):
settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors") settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors")
) )
edit_status = models.SmallIntegerField( edit_status = models.SmallIntegerField(
choices=EDIT_STATUS, default=get_default_edit_status, verbose_name=_("edit status") choices=EDIT_STATUS,
default=get_default_edit_status,
verbose_name=_("edit status"),
) )
share_status = models.SmallIntegerField( share_status = models.SmallIntegerField(
choices=SHARE_STATUS, default=get_default_share_status, verbose_name=_("share status") choices=SHARE_STATUS,
default=get_default_share_status,
verbose_name=_("share status"),
) )
settings = models.JSONField( settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict blank=True, null=True, verbose_name=_("settings"), default=dict
@ -308,6 +312,9 @@ class DataLayer(NamedModel):
help_text=_("Display this layer on load."), help_text=_("Display this layer on load."),
) )
rank = models.SmallIntegerField(default=0) rank = models.SmallIntegerField(default=0)
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
)
class Meta: class Meta:
ordering = ("rank",) ordering = ("rank",)
@ -340,7 +347,14 @@ class DataLayer(NamedModel):
@property @property
def metadata(self): def metadata(self):
return {"name": self.name, "id": self.pk, "displayOnLoad": self.display_on_load} # Retrocompat: minimal settings for maps not saved after settings property
# has been introduced
obj = self.settings or {
"name": self.name,
"displayOnLoad": self.display_on_load,
}
obj["id"] = self.pk
return obj
def clone(self, map_inst=None): def clone(self, map_inst=None):
new = self.__class__.objects.get(pk=self.pk) new = self.__class__.objects.get(pk=self.pk)

View file

@ -248,11 +248,6 @@ L.U.DataLayer = L.Evented.extend({
} }
this.setUmapId(data.id) this.setUmapId(data.id)
this.setOptions(data) this.setOptions(data)
this.backupOptions()
this.connectToMap()
if (this.displayedOnLoad()) this.show()
if (!this.umap_id) this.isDirty = true
// Retrocompat // Retrocompat
if (this.options.remoteData && this.options.remoteData.from) { if (this.options.remoteData && this.options.remoteData.from) {
this.options.fromZoom = this.options.remoteData.from this.options.fromZoom = this.options.remoteData.from
@ -260,11 +255,15 @@ L.U.DataLayer = L.Evented.extend({
if (this.options.remoteData && this.options.remoteData.to) { if (this.options.remoteData && this.options.remoteData.to) {
this.options.toZoom = this.options.remoteData.to this.options.toZoom = this.options.remoteData.to
} }
this.backupOptions()
this.connectToMap()
if (this.displayedOnLoad() && this.showAtZoom()) this.show()
if (!this.umap_id) this.isDirty = true
this.onceLoaded(function () { this.onceLoaded(function () {
this.map.on('moveend', this.onMoveEnd, this) this.map.on('moveend', this.onMoveEnd, this)
this.map.on('zoomend', this.onZoomEnd, this)
}) })
this.map.on('zoomend', this.onZoomEnd, this)
}, },
onMoveEnd: function (e) { onMoveEnd: function (e) {
@ -1185,6 +1184,7 @@ L.U.DataLayer = L.Evented.extend({
formData.append('name', this.options.name) formData.append('name', this.options.name)
formData.append('display_on_load', !!this.options.displayOnLoad) formData.append('display_on_load', !!this.options.displayOnLoad)
formData.append('rank', this.getRank()) formData.append('rank', this.getRank())
formData.append('settings', JSON.stringify(this.options))
// Filename support is shaky, don't do it for now. // Filename support is shaky, don't do it for now.
const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' }) const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
formData.append('geojson', blob) formData.append('geojson', blob)

View file

@ -27,10 +27,11 @@ class TileLayerFactory(factory.django.DjangoModelFactory):
class UserFactory(factory.django.DjangoModelFactory): class UserFactory(factory.django.DjangoModelFactory):
username = 'Joe' username = "Joe"
email = factory.LazyAttribute( email = factory.LazyAttribute(
lambda a: '{0}@example.com'.format(a.username).lower()) lambda a: "{0}@example.com".format(a.username).lower()
password = factory.PostGenerationMethodCall('set_password', '123123') )
password = factory.PostGenerationMethodCall("set_password", "123123")
class Meta: class Meta:
model = User model = User
@ -41,32 +42,32 @@ class MapFactory(factory.django.DjangoModelFactory):
slug = "test-map" slug = "test-map"
center = DEFAULT_CENTER center = DEFAULT_CENTER
settings = { settings = {
'geometry': { "geometry": {
'coordinates': [13.447265624999998, 48.94415123418794], "coordinates": [13.447265624999998, 48.94415123418794],
'type': 'Point' "type": "Point",
}, },
'properties': { "properties": {
'datalayersControl': True, "datalayersControl": True,
'description': 'Which is just the Danube, at the end', "description": "Which is just the Danube, at the end",
'displayCaptionOnLoad': False, "displayCaptionOnLoad": False,
'displayDataBrowserOnLoad': False, "displayDataBrowserOnLoad": False,
'displayPopupFooter': False, "displayPopupFooter": False,
'licence': '', "licence": "",
'miniMap': False, "miniMap": False,
'moreControl': True, "moreControl": True,
'name': 'Cruising on the Donau', "name": "Cruising on the Donau",
'scaleControl': True, "scaleControl": True,
'tilelayer': { "tilelayer": {
'attribution': u'\xa9 OSM Contributors', "attribution": "\xa9 OSM Contributors",
'maxZoom': 18, "maxZoom": 18,
'minZoom': 0, "minZoom": 0,
'url_template': 'http://{s}.osm.fr/{z}/{x}/{y}.png' "url_template": "http://{s}.osm.fr/{z}/{x}/{y}.png",
}, },
'tilelayersControl': True, "tilelayersControl": True,
'zoom': 7, "zoom": 7,
'zoomControl': True "zoomControl": True,
}, },
'type': 'Feature' "type": "Feature",
} }
licence = factory.SubFactory(LicenceFactory) licence = factory.SubFactory(LicenceFactory)
@ -81,7 +82,10 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
name = "test datalayer" name = "test datalayer"
description = "test description" description = "test description"
display_on_load = True display_on_load = True
geojson = factory.django.FileField(data="""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[13.68896484375,48.55297816440071]},"properties":{"_umap_options":{"color":"DarkCyan","iconClass":"Ball"},"name":"Here","description":"Da place anonymous again 755"}}],"_umap_options":{"displayOnLoad":true,"name":"Donau","id":926}}""") # noqa settings = {"displayOnLoad": True, "browsable": True, name: "test datalayer"}
geojson = factory.django.FileField(
data="""{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[13.68896484375,48.55297816440071]},"properties":{"_umap_options":{"color":"DarkCyan","iconClass":"Ball"},"name":"Here","description":"Da place anonymous again 755"}}],"_umap_options":{"displayOnLoad":true,"name":"Donau","id":926}}"""
) # noqa
class Meta: class Meta:
model = DataLayer model = DataLayer
@ -90,7 +94,7 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
def login_required(response): def login_required(response):
assert response.status_code == 200 assert response.status_code == 200
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert 'login_required' in j assert "login_required" in j
redirect_url = reverse('login') redirect_url = reverse("login")
assert j['login_required'] == redirect_url assert j["login_required"] == redirect_url
return True return True

View file

@ -17,6 +17,7 @@ def post_data():
return { return {
"name": "name", "name": "name",
"display_on_load": True, "display_on_load": True,
"settings": '{"displayOnLoad": true, "browsable": true, "name": "name"}',
"rank": 0, "rank": 0,
"geojson": '{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.1640625,53.014783245859235],[-3.1640625,51.86292391360244],[-0.50537109375,51.385495069223204],[1.16455078125,52.38901106223456],[-0.41748046875,53.91728101547621],[-2.109375,53.85252660044951],[-3.1640625,53.014783245859235]]]},"properties":{"_umap_options":{},"name":"Ho god, sounds like a polygouine"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[1.8017578124999998,51.16556659836182],[-0.48339843749999994,49.710272582105695],[-3.1640625,50.0923932109388],[-5.60302734375,51.998410382390325]]},"properties":{"_umap_options":{},"name":"Light line"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.63720703125,51.15178610143037]},"properties":{"_umap_options":{},"name":"marker he"}}],"_umap_options":{"displayOnLoad":true,"name":"new name","id":1668,"remoteData":{},"color":"LightSeaGreen","description":"test"}}', "geojson": '{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-3.1640625,53.014783245859235],[-3.1640625,51.86292391360244],[-0.50537109375,51.385495069223204],[1.16455078125,52.38901106223456],[-0.41748046875,53.91728101547621],[-2.109375,53.85252660044951],[-3.1640625,53.014783245859235]]]},"properties":{"_umap_options":{},"name":"Ho god, sounds like a polygouine"}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[1.8017578124999998,51.16556659836182],[-0.48339843749999994,49.710272582105695],[-3.1640625,50.0923932109388],[-5.60302734375,51.998410382390325]]},"properties":{"_umap_options":{},"name":"Light line"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.63720703125,51.15178610143037]},"properties":{"_umap_options":{},"name":"marker he"}}],"_umap_options":{"displayOnLoad":true,"name":"new name","id":1668,"remoteData":{},"color":"LightSeaGreen","description":"test"}}',
} }
@ -61,6 +62,7 @@ def test_update(client, datalayer, map, post_data):
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert "id" in j assert "id" in j
assert datalayer.pk == j["id"] assert datalayer.pk == j["id"]
assert j["browsable"] is True
assert Path(modified_datalayer.geojson.path).exists() assert Path(modified_datalayer.geojson.path).exists()