WIP: rename DataLayer/Map.settings to DataLayer/Map.metadata and more

The main goal is to clarify the use of options/settings/properties…

- `settings` should be reserved to Django
- `options` should be reserved to pure Leaflet context
- `properties` used in the geojson context, for user data

Main changes:
- `DataLayer.settings` is renamed to `DataLayer.metadata`
- `DataLayer.geojson` is renamed to `Datalayer.data`
- `DataLayer.metadata` are not saved anymore in the geojson data file
- `Map.settings` is renamed to `Map.metadata`
- `Map.metadata` is now a flat key/value object, not a geojson Feature anymore
- `Map.zoom` is now populated and reused

Note: U.Map is still inheriting from Leaflet Map, so it mixes
`options` and `metadata`.

This changes is very intrusive, it changes

- the DB schema
- the way we store data in the FS (now without metadata)
- the way we backup map data

fix #1636
prepares #1335
prepares #1635
prepares #175
This commit is contained in:
Yohan Boniface 2024-08-26 12:51:22 +02:00
parent ab8bce985e
commit 787f22efd4
59 changed files with 695 additions and 699 deletions

View file

@ -55,7 +55,7 @@ class AnonymousMapPermissionsForm(forms.ModelForm):
class DataLayerForm(forms.ModelForm): class DataLayerForm(forms.ModelForm):
class Meta: class Meta:
model = DataLayer model = DataLayer
fields = ("geojson", "name", "display_on_load", "rank", "settings") fields = ("data", "name", "display_on_load", "rank", "metadata")
class DataLayerPermissionsForm(forms.ModelForm): class DataLayerPermissionsForm(forms.ModelForm):
@ -78,9 +78,9 @@ class AnonymousDataLayerPermissionsForm(forms.ModelForm):
fields = ("edit_status",) fields = ("edit_status",)
class MapSettingsForm(forms.ModelForm): class MapMetadataForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MapSettingsForm, self).__init__(*args, **kwargs) super(MapMetadataForm, self).__init__(*args, **kwargs)
self.fields["slug"].required = False self.fields["slug"].required = False
self.fields["center"].widget.map_srid = 4326 self.fields["center"].widget.map_srid = 4326
@ -102,7 +102,7 @@ class MapSettingsForm(forms.ModelForm):
return self.cleaned_data["center"] return self.cleaned_data["center"]
class Meta: class Meta:
fields = ("settings", "name", "center", "slug") fields = ("metadata", "name", "center", "slug", "zoom")
model = Map model = Map

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.8 on 2024-08-21 10:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("umap", "0021_remove_map_description"),
]
operations = [
migrations.RenameField(
model_name="datalayer",
old_name="settings",
new_name="metadata",
),
migrations.RenameField(
model_name="datalayer",
old_name="geojson",
new_name="data",
),
migrations.RenameField(
model_name="map",
old_name="settings",
new_name="metadata",
),
migrations.RunSQL("UPDATE umap_map SET metadata=metadata->'properties'"),
]

View file

@ -190,8 +190,8 @@ class Map(NamedModel):
default=get_default_share_status, default=get_default_share_status,
verbose_name=_("share status"), verbose_name=_("share status"),
) )
settings = models.JSONField( metadata = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict blank=True, null=True, verbose_name=_("metadata"), default=dict
) )
objects = models.Manager() objects = models.Manager()
@ -200,18 +200,20 @@ class Map(NamedModel):
@property @property
def description(self): def description(self):
try: try:
return self.settings["properties"]["description"] return self.metadata["description"]
except KeyError: except KeyError:
return "" return ""
@property @property
def preview_settings(self): def geometry(self):
return {"type": "Point", "coordinates": [self.center.x, self.center.y]}
@property
def preview_metadata(self):
layers = self.datalayer_set.all() layers = self.datalayer_set.all()
datalayer_data = [c.metadata() for c in layers] datalayer_data = [l.get_metadata() for l in layers]
map_settings = self.settings metadata = self.metadata
if "properties" not in map_settings: metadata.update(
map_settings["properties"] = {}
map_settings["properties"].update(
{ {
"tilelayers": [TileLayer.get_default().json], "tilelayers": [TileLayer.get_default().json],
"datalayers": datalayer_data, "datalayers": datalayer_data,
@ -224,22 +226,23 @@ class Map(NamedModel):
"umap_id": self.pk, "umap_id": self.pk,
"schema": self.extra_schema, "schema": self.extra_schema,
"slideshow": {}, "slideshow": {},
"geometry": self.geometry,
} }
) )
return map_settings return metadata
def generate_umapjson(self, request): def generate_umapjson(self, request):
umapjson = self.settings umapjson = {
umapjson["type"] = "umap" "metadata": {"geometry": self.geometry, **self.metadata},
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url()) "type": "umap",
datalayers = [] "uri": request.build_absolute_uri(self.get_absolute_url()),
"layers": [],
}
for datalayer in self.datalayer_set.all(): for datalayer in self.datalayer_set.all():
with open(datalayer.geojson.path, "rb") as f: with open(datalayer.data.path, "rb") as f:
layer = json.loads(f.read()) layer = json.loads(f.read())
if datalayer.settings: layer["metadata"] = datalayer.metadata
layer["_umap_options"] = datalayer.settings umapjson["layers"].append(layer)
datalayers.append(layer)
umapjson["layers"] = datalayers
return umapjson return umapjson
def get_absolute_url(self): def get_absolute_url(self):
@ -397,15 +400,15 @@ class DataLayer(NamedModel):
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
map = models.ForeignKey(Map, on_delete=models.CASCADE) map = models.ForeignKey(Map, on_delete=models.CASCADE)
description = models.TextField(blank=True, null=True, verbose_name=_("description")) description = models.TextField(blank=True, null=True, verbose_name=_("description"))
geojson = models.FileField(upload_to=upload_to, blank=True, null=True) data = models.FileField(upload_to=upload_to, blank=True, null=True)
display_on_load = models.BooleanField( display_on_load = models.BooleanField(
default=False, default=False,
verbose_name=_("display on load"), verbose_name=_("display on load"),
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( metadata = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict blank=True, null=True, verbose_name=_("metadata"), default=dict
) )
edit_status = models.SmallIntegerField( edit_status = models.SmallIntegerField(
choices=EDIT_STATUS, choices=EDIT_STATUS,
@ -423,10 +426,10 @@ class DataLayer(NamedModel):
if is_new: if is_new:
force_insert, force_update = False, True force_insert, force_update = False, True
filename = self.upload_to() filename = self.upload_to()
old_name = self.geojson.name old_name = self.data.name
new_name = self.geojson.storage.save(filename, self.geojson) new_name = self.data.storage.save(filename, self.data)
self.geojson.storage.delete(old_name) self.data.storage.delete(old_name)
self.geojson.name = new_name self.data.name = new_name
super(DataLayer, self).save(force_insert, force_update, **kwargs) super(DataLayer, self).save(force_insert, force_update, **kwargs)
self.purge_gzip() self.purge_gzip()
self.purge_old_versions() self.purge_old_versions()
@ -443,10 +446,10 @@ class DataLayer(NamedModel):
path.append(str(self.map.pk)) path.append(str(self.map.pk))
return os.path.join(*path) return os.path.join(*path)
def metadata(self, user=None, request=None): def get_metadata(self, user=None, request=None):
# Retrocompat: minimal settings for maps not saved after settings property # Retrocompat: minimal settings for maps not saved after settings property
# has been introduced # has been introduced
obj = self.settings or { obj = self.metadata or {
"name": self.name, "name": self.name,
"displayOnLoad": self.display_on_load, "displayOnLoad": self.display_on_load,
} }
@ -463,7 +466,7 @@ class DataLayer(NamedModel):
new.pk = None new.pk = None
if map_inst: if map_inst:
new.map = map_inst new.map = map_inst
new.geojson = File(new.geojson.file.file) new.data = File(new.data.file.file)
new.save() new.save()
return new return new
@ -478,13 +481,13 @@ class DataLayer(NamedModel):
return { return {
"name": name, "name": name,
"at": els[1], "at": els[1],
"size": self.geojson.storage.size(self.get_version_path(name)), "size": self.data.storage.size(self.get_version_path(name)),
} }
@property @property
def versions(self): def versions(self):
root = self.storage_root() root = self.storage_root()
names = self.geojson.storage.listdir(root)[1] names = self.data.storage.listdir(root)[1]
names = [name for name in names if self.is_valid_version(name)] names = [name for name in names if self.is_valid_version(name)]
versions = [self.version_metadata(name) for name in names] versions = [self.version_metadata(name) for name in names]
versions.sort(reverse=True, key=operator.itemgetter("at")) versions.sort(reverse=True, key=operator.itemgetter("at"))
@ -492,7 +495,7 @@ class DataLayer(NamedModel):
def get_version(self, name): def get_version(self, name):
path = self.get_version_path(name) path = self.get_version_path(name)
with self.geojson.storage.open(path, "r") as f: with self.data.storage.open(path, "r") as f:
return f.read() return f.read()
def get_version_path(self, name): def get_version_path(self, name):
@ -505,23 +508,23 @@ class DataLayer(NamedModel):
name = version["name"] name = version["name"]
# Should not be in the list, but ensure to not delete the file # Should not be in the list, but ensure to not delete the file
# currently used in database # currently used in database
if self.geojson.name.endswith(name): if self.data.name.endswith(name):
continue continue
try: try:
self.geojson.storage.delete(os.path.join(root, name)) self.data.storage.delete(os.path.join(root, name))
except FileNotFoundError: except FileNotFoundError:
pass pass
def purge_gzip(self): def purge_gzip(self):
root = self.storage_root() root = self.storage_root()
names = self.geojson.storage.listdir(root)[1] names = self.data.storage.listdir(root)[1]
prefixes = [f"{self.pk}_"] prefixes = [f"{self.pk}_"]
if self.old_id: if self.old_id:
prefixes.append(f"{self.old_id}_") prefixes.append(f"{self.old_id}_")
prefixes = tuple(prefixes) prefixes = tuple(prefixes)
for name in names: for name in names:
if name.startswith(prefixes) and name.endswith(".gz"): if name.startswith(prefixes) and name.endswith(".gz"):
self.geojson.storage.delete(os.path.join(root, name)) self.data.storage.delete(os.path.join(root, name))
def can_edit(self, user=None, request=None): def can_edit(self, user=None, request=None):
""" """

View file

@ -1,6 +1,6 @@
class UmapFragment extends HTMLElement { class UmapFragment extends HTMLElement {
connectedCallback() { connectedCallback() {
new U.Map(this.firstElementChild.id, JSON.parse(this.dataset.settings)) new U.Map(this.firstElementChild.id, JSON.parse(this.dataset.metadata))
} }
} }

View file

@ -86,7 +86,7 @@ export default class Browser {
DomEvent.on(toggle, 'click', toggleList) DomEvent.on(toggle, 'click', toggleList)
datalayer.renderToolbox(headline) datalayer.renderToolbox(headline)
const name = DomUtil.create('span', 'datalayer-name', headline) const name = DomUtil.create('span', 'datalayer-name', headline)
name.textContent = datalayer.options.name name.textContent = datalayer.metadata.name
DomEvent.on(name, 'click', toggleList) DomEvent.on(name, 'click', toggleList)
container.innerHTML = '' container.innerHTML = ''
datalayer.eachFeature((feature) => this.addFeature(feature, container)) datalayer.eachFeature((feature) => this.addFeature(feature, container))

View file

@ -39,20 +39,20 @@ export default class Caption {
} }
addDataLayer(datalayer, container) { addDataLayer(datalayer, container) {
if (!datalayer.options.inCaption) return if (!datalayer.metadata.inCaption) return
const p = DomUtil.create('p', 'datalayer-legend', container) const p = DomUtil.create('p', 'datalayer-legend', container)
const legend = DomUtil.create('span', '', p) const legend = DomUtil.create('span', '', p)
const headline = DomUtil.create('strong', '', p) const headline = DomUtil.create('strong', '', p)
datalayer.renderLegend(legend) datalayer.renderLegend(legend)
if (datalayer.options.description) { if (datalayer.metadata.description) {
DomUtil.element({ DomUtil.element({
tagName: 'span', tagName: 'span',
parent: p, parent: p,
safeHTML: Utils.toHTML(datalayer.options.description), safeHTML: Utils.toHTML(datalayer.metadata.description),
}) })
} }
datalayer.renderToolbox(headline) datalayer.renderToolbox(headline)
DomUtil.add('span', '', headline, `${datalayer.options.name} `) DomUtil.add('span', '', headline, `${datalayer.metadata.name} `)
} }
addCredits(container) { addCredits(container) {

View file

@ -26,15 +26,14 @@ class Feature {
// DataLayer the feature belongs to // DataLayer the feature belongs to
this.datalayer = datalayer this.datalayer = datalayer
this.properties = { _umap_options: {}, ...(geojson.properties || {}) } this.properties = { ...(geojson.properties || {}) }
this.metadata = {}
this.staticOptions = {} this.staticOptions = {}
if (geojson.coordinates) { if (geojson.coordinates) {
geojson = { geometry: geojson } geojson = { geometry: geojson }
} }
if (geojson.geometry) { this.populate(geojson)
this.populate(geojson)
}
if (id) { if (id) {
this.id = id this.id = id
@ -187,7 +186,7 @@ class Feature {
window.top.location = outlink window.top.location = outlink
break break
default: default:
window.open(this.properties._umap_options.outlink) window.open(this.metadata.outlink)
} }
return return
} }
@ -298,13 +297,13 @@ class Feature {
getInteractionOptions() { getInteractionOptions() {
return [ return [
'properties._umap_options.popupShape', 'metadata.popupShape',
'properties._umap_options.popupTemplate', 'metadata.popupTemplate',
'properties._umap_options.showLabel', 'metadata.showLabel',
'properties._umap_options.labelDirection', 'metadata.labelDirection',
'properties._umap_options.labelInteractive', 'metadata.labelInteractive',
'properties._umap_options.outlink', 'metadata.outlink',
'properties._umap_options.outlinkTarget', 'metadata.outlinkTarget',
] ]
} }
@ -321,7 +320,7 @@ class Feature {
} }
hasPopupFooter() { hasPopupFooter() {
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) { if (this.datalayer.isRemoteLayer() && this.datalayer.metadata.remoteData.dynamic) {
return false return false
} }
return this.map.getOption('displayPopupFooter') return this.map.getOption('displayPopupFooter')
@ -375,20 +374,24 @@ class Feature {
return [key, value] return [key, value]
} }
populate(geojson) { populate(geojson = {}) {
this._geometry = geojson.geometry this._geometry = geojson.geometry
this.properties = Object.fromEntries( this.properties = Object.fromEntries(
Object.entries(geojson.properties || {}).map(this.cleanProperty) Object.entries(geojson.properties || {}).map(this.cleanProperty)
) )
this.properties._umap_options = L.extend( this.metadata = L.extend(
{}, {},
this.properties._storage_options, this.properties._storage_options,
this.properties._umap_options this.properties._umap_options,
geojson.metadata
) )
// Legacy
delete this.properties._umap_options
delete this.properties._storage_options
// Retrocompat // Retrocompat
if (this.properties._umap_options.clickable === false) { if (this.metadata.clickable === false) {
this.properties._umap_options.interactive = false this.metadata.interactive = false
delete this.properties._umap_options.clickable delete this.metadata.clickable
} }
} }
@ -408,8 +411,8 @@ class Feature {
let value = fallback let value = fallback
if (typeof this.staticOptions[option] !== 'undefined') { if (typeof this.staticOptions[option] !== 'undefined') {
value = this.staticOptions[option] value = this.staticOptions[option]
} else if (U.Utils.usableOption(this.properties._umap_options, option)) { } else if (U.Utils.usableOption(this.metadata, option)) {
value = this.properties._umap_options[option] value = this.metadata[option]
} else if (this.datalayer) { } else if (this.datalayer) {
value = this.datalayer.getOption(option, this) value = this.datalayer.getOption(option, this)
} else { } else {
@ -451,15 +454,12 @@ class Feature {
return this.datalayer.getPreviousFeature(this) return this.datalayer.getPreviousFeature(this)
} }
cloneMetadata() {
return L.extend({}, this.metadata)
}
cloneProperties() { cloneProperties() {
const properties = L.extend({}, this.properties) return L.extend({}, this.properties)
properties._umap_options = L.extend({}, properties._umap_options)
if (Object.keys && Object.keys(properties._umap_options).length === 0) {
delete properties._umap_options // It can make a difference on big data sets
}
// Legacy
delete properties._storage_options
return properties
} }
deleteProperty(property) { deleteProperty(property) {
@ -473,10 +473,16 @@ class Feature {
} }
toGeoJSON() { toGeoJSON() {
return Utils.CopyJSON({ let metadata = this.cloneMetadata()
if (!Object.keys(metadata).length) {
// Remove empty object from exported json
metadata = undefined
}
return Utils.copyJSON({
type: 'Feature', type: 'Feature',
geometry: this.geometry, geometry: this.geometry,
properties: this.cloneProperties(), properties: this.cloneProperties(),
metadata: metadata,
id: this.id, id: this.id,
}) })
} }
@ -615,15 +621,15 @@ export class Point extends Feature {
getShapeOptions() { getShapeOptions() {
return [ return [
'properties._umap_options.color', 'metadata.color',
'properties._umap_options.iconClass', 'metadata.iconClass',
'properties._umap_options.iconUrl', 'metadata.iconUrl',
'properties._umap_options.iconOpacity', 'metadata.iconOpacity',
] ]
} }
getAdvancedOptions() { getAdvancedOptions() {
return ['properties._umap_options.zoomTo'] return ['metadata.zoomTo']
} }
appendEditFieldsets(container) { appendEditFieldsets(container) {
@ -695,19 +701,11 @@ class Path extends Feature {
} }
getShapeOptions() { getShapeOptions() {
return [ return ['metadata.color', 'metadata.opacity', 'metadata.weight']
'properties._umap_options.color',
'properties._umap_options.opacity',
'properties._umap_options.weight',
]
} }
getAdvancedOptions() { getAdvancedOptions() {
return [ return ['metadata.smoothFactor', 'metadata.dashArray', 'metadata.zoomTo']
'properties._umap_options.smoothFactor',
'properties._umap_options.dashArray',
'properties._umap_options.zoomTo',
]
} }
getBestZoom() { getBestZoom() {
@ -732,9 +730,10 @@ class Path extends Feature {
isolateShape(latlngs) { isolateShape(latlngs) {
const properties = this.cloneProperties() const properties = this.cloneProperties()
const metadata = this.cloneMetadata()
const type = this instanceof LineString ? 'LineString' : 'Polygon' const type = this instanceof LineString ? 'LineString' : 'Polygon'
const geometry = this.convertLatLngs(latlngs) const geometry = this.convertLatLngs(latlngs)
const other = this.datalayer.makeFeature({ type, geometry, properties }) const other = this.datalayer.makeFeature({ type, geometry, properties, metadata })
other.edit() other.edit()
return other return other
} }
@ -919,10 +918,10 @@ export class Polygon extends Path {
getShapeOptions() { getShapeOptions() {
const options = super.getShapeOptions() const options = super.getShapeOptions()
options.push( options.push(
'properties._umap_options.stroke', 'metadata.stroke',
'properties._umap_options.fill', 'metadata.fill',
'properties._umap_options.fillColor', 'metadata.fillColor',
'properties._umap_options.fillOpacity' 'metadata.fillOpacity'
) )
return options return options
} }
@ -937,7 +936,7 @@ export class Polygon extends Path {
getInteractionOptions() { getInteractionOptions() {
const options = super.getInteractionOptions() const options = super.getInteractionOptions()
options.push('properties._umap_options.interactive') options.push('metadata.interactive')
return options return options
} }
@ -956,7 +955,7 @@ export class Polygon extends Path {
getAdvancedOptions() { getAdvancedOptions() {
const actions = super.getAdvancedOptions() const actions = super.getAdvancedOptions()
actions.push('properties._umap_options.mask') actions.push('metadata.mask')
return actions return actions
} }

View file

@ -51,7 +51,7 @@ export class DataLayer {
this.pane.dataset.id = stamp(this) this.pane.dataset.id = stamp(this)
// FIXME: should be on layer // FIXME: should be on layer
this.renderer = L.svg({ pane: this.pane }) this.renderer = L.svg({ pane: this.pane })
this.defaultOptions = { this.defaultMetadata = {
displayOnLoad: true, displayOnLoad: true,
inCaption: true, inCaption: true,
browsable: true, browsable: true,
@ -61,21 +61,21 @@ export class DataLayer {
this._isDirty = false this._isDirty = false
this._isDeleted = false this._isDeleted = false
this.setUmapId(data.id) this.setUmapId(data.id)
this.setOptions(data) this.setMetadata(data)
if (!Utils.isObject(this.options.remoteData)) { if (!Utils.isObject(this.metadata.remoteData)) {
this.options.remoteData = {} this.metadata.remoteData = {}
} }
// Retrocompat // Retrocompat
if (this.options.remoteData?.from) { if (this.metadata.remoteData?.from) {
this.options.fromZoom = this.options.remoteData.from this.metadata.fromZoom = this.metadata.remoteData.from
delete this.options.remoteData.from delete this.metadata.remoteData.from
} }
if (this.options.remoteData?.to) { if (this.metadata.remoteData?.to) {
this.options.toZoom = this.options.remoteData.to this.metadata.toZoom = this.metadata.remoteData.to
delete this.options.remoteData.to delete this.metadata.remoteData.to
} }
this.backupOptions() this.backupMetadata()
this.connectToMap() this.connectToMap()
this.permissions = new DataLayerPermissions(this) this.permissions = new DataLayerPermissions(this)
if (!this.umap_id) { if (!this.umap_id) {
@ -133,7 +133,7 @@ export class DataLayer {
this.map.onDataLayersChanged() this.map.onDataLayersChanged()
break break
case 'data': case 'data':
if (fields.includes('options.type')) { if (fields.includes('metadata.type')) {
this.resetLayer() this.resetLayer()
} }
this.hide() this.hide()
@ -155,11 +155,11 @@ export class DataLayer {
} }
autoLoaded() { autoLoaded() {
if (!this.map.datalayersFromQueryString) return this.options.displayOnLoad if (!this.map.datalayersFromQueryString) return this.metadata.displayOnLoad
const datalayerIds = this.map.datalayersFromQueryString const datalayerIds = this.map.datalayersFromQueryString
let loadMe = datalayerIds.includes(this.umap_id.toString()) let loadMe = datalayerIds.includes(this.umap_id.toString())
if (this.options.old_id) { if (this.metadata.old_id) {
loadMe = loadMe || datalayerIds.includes(this.options.old_id.toString()) loadMe = loadMe || datalayerIds.includes(this.metadata.old_id.toString())
} }
return loadMe return loadMe
} }
@ -186,7 +186,7 @@ export class DataLayer {
// Only reset if type is defined (undefined is the default) and different from current type // Only reset if type is defined (undefined is the default) and different from current type
if ( if (
this.layer && this.layer &&
(!this.options.type || this.options.type === this.layer.getType()) && (!this.metadata.type || this.metadata.type === this.layer.getType()) &&
!force !force
) { ) {
return return
@ -195,7 +195,7 @@ export class DataLayer {
if (this.layer) this.layer.clearLayers() if (this.layer) this.layer.clearLayers()
// delete this.layer? // delete this.layer?
if (visible) this.map.removeLayer(this.layer) if (visible) this.map.removeLayer(this.layer)
const Class = LAYER_MAP[this.options.type] || DefaultLayer const Class = LAYER_MAP[this.metadata.type] || DefaultLayer
this.layer = new Class(this) this.layer = new Class(this)
// Rendering layer changed, so let's force reset the feature rendering too. // Rendering layer changed, so let's force reset the feature rendering too.
this.eachFeature((feature) => feature.makeUI()) this.eachFeature((feature) => feature.makeUI())
@ -218,18 +218,8 @@ export class DataLayer {
const [geojson, response, error] = await this.map.server.get(this._dataUrl()) const [geojson, response, error] = await this.map.server.get(this._dataUrl())
if (!error) { if (!error) {
this._reference_version = response.headers.get('X-Datalayer-Version') this._reference_version = response.headers.get('X-Datalayer-Version')
// FIXME: for now this property is set dynamically from backend
// And thus it's not in the geojson file in the server
// So do not let all options to be reset
// Fix is a proper migration so all datalayers settings are
// in DB, and we remove it from geojson flat files.
if (geojson._umap_options) {
geojson._umap_options.editMode = this.options.editMode
}
// In case of maps pre 1.0 still around
if (geojson._storage) geojson._storage.editMode = this.options.editMode
await this.fromUmapGeoJSON(geojson) await this.fromUmapGeoJSON(geojson)
this.backupOptions() this.backupMetadata()
this._loading = false this._loading = false
} }
} }
@ -247,8 +237,11 @@ export class DataLayer {
} }
async fromUmapGeoJSON(geojson) { async fromUmapGeoJSON(geojson) {
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat if (!geojson.metadata) {
if (geojson._umap_options) this.setOptions(geojson._umap_options) // Retrocompat
geojson.metadata = geojson._umap_options || geojson._storage
}
if (geojson.metadata) this.setMetadata(geojson.metadata)
if (this.isRemoteLayer()) await this.fetchRemoteData() if (this.isRemoteLayer()) await this.fetchRemoteData()
else this.fromGeoJSON(geojson, false) else this.fromGeoJSON(geojson, false)
this._loaded = true this._loaded = true
@ -266,7 +259,7 @@ export class DataLayer {
} }
backupData() { backupData() {
this._geojson_bk = Utils.CopyJSON(this._geojson) this._geojson_bk = Utils.copyJSON(this._geojson)
} }
reindex() { reindex() {
@ -276,29 +269,29 @@ export class DataLayer {
} }
showAtZoom() { showAtZoom() {
const from = Number.parseInt(this.options.fromZoom, 10) const from = Number.parseInt(this.metadata.fromZoom, 10)
const to = Number.parseInt(this.options.toZoom, 10) const to = Number.parseInt(this.metadata.toZoom, 10)
const zoom = this.map.getZoom() const zoom = this.map.getZoom()
return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to)) return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to))
} }
hasDynamicData() { hasDynamicData() {
return !!this.options.remoteData?.dynamic return !!this.metadata.remoteData?.dynamic
} }
async fetchRemoteData(force) { async fetchRemoteData(force) {
if (!this.isRemoteLayer()) return if (!this.isRemoteLayer()) return
if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return
if (!this.isVisible()) return if (!this.isVisible()) return
let url = this.map.localizeUrl(this.options.remoteData.url) let url = this.map.localizeUrl(this.metadata.remoteData.url)
if (this.options.remoteData.proxy) { if (this.metadata.remoteData.proxy) {
url = this.map.proxyUrl(url, this.options.remoteData.ttl) url = this.map.proxyUrl(url, this.metadata.remoteData.ttl)
} }
const response = await this.map.request.get(url) const response = await this.map.request.get(url)
if (response?.ok) { if (response?.ok) {
this.clear() this.clear()
this.map.formatter this.map.formatter
.parse(await response.text(), this.options.remoteData.format) .parse(await response.text(), this.metadata.remoteData.format)
.then((geojson) => this.fromGeoJSON(geojson)) .then((geojson) => this.fromGeoJSON(geojson))
} }
} }
@ -316,22 +309,22 @@ export class DataLayer {
if (!this.umap_id && id) this.umap_id = id if (!this.umap_id && id) this.umap_id = id
} }
backupOptions() { backupMetadata() {
this._backupOptions = Utils.CopyJSON(this.options) this._backupMetadata = Utils.copyJSON(this.metadata)
} }
resetOptions() { resetMetadata() {
this.options = Utils.CopyJSON(this._backupOptions) this.metadata = Utils.copyJSON(this._backupMetadata)
} }
setOptions(options) { setMetadata(metadata) {
delete options.geojson delete metadata.geojson
this.options = Utils.CopyJSON(this.defaultOptions) // Start from fresh. this.metadata = Utils.copyJSON(this.defaultMetadata) // Start from fresh.
this.updateOptions(options) this.updateMetadata(metadata)
} }
updateOptions(options) { updateMetadata(metadata) {
this.options = Object.assign(this.options, options) this.metadata = Object.assign(this.metadata, metadata)
this.resetLayer() this.resetLayer()
} }
@ -360,11 +353,11 @@ export class DataLayer {
} }
isRemoteLayer() { isRemoteLayer() {
return Boolean(this.options.remoteData?.url && this.options.remoteData.format) return Boolean(this.metadata.remoteData?.url && this.metadata.remoteData.format)
} }
isClustered() { isClustered() {
return this.options.type === 'Cluster' return this.metadata.type === 'Cluster'
} }
showFeature(feature) { showFeature(feature) {
@ -511,7 +504,7 @@ export class DataLayer {
} }
getColor() { getColor() {
return this.options.color || this.map.getOption('color') return this.metadata.color || this.map.getOption('color')
} }
getDeleteUrl() { getDeleteUrl() {
@ -548,11 +541,11 @@ export class DataLayer {
} }
clone() { clone() {
const options = Utils.CopyJSON(this.options) const metadata = Utils.copyJSON(this.metadata)
options.name = translate('Clone of {name}', { name: this.options.name }) metadata.name = translate('Clone of {name}', { name: this.metadata.name })
delete options.id delete metadata.id
const geojson = Utils.CopyJSON(this._geojson) const geojson = Utils.copyJSON(this._geojson)
const datalayer = this.map.createDataLayer(options) const datalayer = this.map.createDataLayer(metadata)
datalayer.fromGeoJSON(geojson) datalayer.fromGeoJSON(geojson)
return datalayer return datalayer
} }
@ -574,7 +567,7 @@ export class DataLayer {
reset() { reset() {
if (!this.umap_id) this.erase() if (!this.umap_id) this.erase()
this.resetOptions() this.resetMetadata()
this.parentPane.appendChild(this.pane) this.parentPane.appendChild(this.pane)
if (this._leaflet_events_bk && !this._leaflet_events) { if (this._leaflet_events_bk && !this._leaflet_events) {
this._leaflet_events = this._leaflet_events_bk this._leaflet_events = this._leaflet_events_bk
@ -600,18 +593,18 @@ export class DataLayer {
} }
const container = DomUtil.create('div', 'umap-layer-properties-container') const container = DomUtil.create('div', 'umap-layer-properties-container')
const metadataFields = [ const metadataFields = [
'options.name', 'metadata.name',
'options.description', 'metadata.description',
[ [
'options.type', 'metadata.type',
{ handler: 'LayerTypeChooser', label: translate('Type of layer') }, { handler: 'LayerTypeChooser', label: translate('Type of layer') },
], ],
[ [
'options.displayOnLoad', 'metadata.displayOnLoad',
{ label: translate('Display on load'), handler: 'Switch' }, { label: translate('Display on load'), handler: 'Switch' },
], ],
[ [
'options.browsable', 'metadata.browsable',
{ {
label: translate('Data is browsable'), label: translate('Data is browsable'),
handler: 'Switch', handler: 'Switch',
@ -619,7 +612,7 @@ export class DataLayer {
}, },
], ],
[ [
'options.inCaption', 'metadata.inCaption',
{ {
label: translate('Show this layer in the caption'), label: translate('Show this layer in the caption'),
handler: 'Switch', handler: 'Switch',
@ -630,7 +623,7 @@ export class DataLayer {
let builder = new U.FormBuilder(this, metadataFields, { let builder = new U.FormBuilder(this, metadataFields, {
callback(e) { callback(e) {
this.map.onDataLayersChanged() this.map.onDataLayersChanged()
if (e.helper.field === 'options.type') { if (e.helper.field === 'metadata.type') {
this.edit() this.edit()
} }
}, },
@ -651,16 +644,16 @@ export class DataLayer {
} }
const shapeOptions = [ const shapeOptions = [
'options.color', 'metadata.color',
'options.iconClass', 'metadata.iconClass',
'options.iconUrl', 'metadata.iconUrl',
'options.iconOpacity', 'metadata.iconOpacity',
'options.opacity', 'metadata.opacity',
'options.stroke', 'metadata.stroke',
'options.weight', 'metadata.weight',
'options.fill', 'metadata.fill',
'options.fillColor', 'metadata.fillColor',
'options.fillOpacity', 'metadata.fillOpacity',
] ]
builder = new U.FormBuilder(this, shapeOptions, { builder = new U.FormBuilder(this, shapeOptions, {
@ -673,12 +666,12 @@ export class DataLayer {
shapeProperties.appendChild(builder.build()) shapeProperties.appendChild(builder.build())
const optionsFields = [ const optionsFields = [
'options.smoothFactor', 'metadata.smoothFactor',
'options.dashArray', 'metadata.dashArray',
'options.zoomTo', 'metadata.zoomTo',
'options.fromZoom', 'metadata.fromZoom',
'options.toZoom', 'metadata.toZoom',
'options.labelKey', 'metadata.labelKey',
] ]
builder = new U.FormBuilder(this, optionsFields, { builder = new U.FormBuilder(this, optionsFields, {
@ -691,14 +684,14 @@ export class DataLayer {
advancedProperties.appendChild(builder.build()) advancedProperties.appendChild(builder.build())
const popupFields = [ const popupFields = [
'options.popupShape', 'metadata.popupShape',
'options.popupTemplate', 'metadata.popupTemplate',
'options.popupContentTemplate', 'metadata.popupContentTemplate',
'options.showLabel', 'metadata.showLabel',
'options.labelDirection', 'metadata.labelDirection',
'options.labelInteractive', 'metadata.labelInteractive',
'options.outlinkTarget', 'metadata.outlinkTarget',
'options.interactive', 'metadata.interactive',
] ]
builder = new U.FormBuilder(this, popupFields) builder = new U.FormBuilder(this, popupFields)
const popupFieldset = DomUtil.createFieldset( const popupFieldset = DomUtil.createFieldset(
@ -709,23 +702,23 @@ export class DataLayer {
// XXX I'm not sure **why** this is needed (as it's set during `this.initialize`) // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`)
// but apparently it's needed. // but apparently it's needed.
if (!Utils.isObject(this.options.remoteData)) { if (!Utils.isObject(this.metadata.remoteData)) {
this.options.remoteData = {} this.metadata.remoteData = {}
} }
const remoteDataFields = [ const remoteDataFields = [
[ [
'options.remoteData.url', 'metadata.remoteData.url',
{ handler: 'Url', label: translate('Url'), helpEntries: 'formatURL' }, { handler: 'Url', label: translate('Url'), helpEntries: 'formatURL' },
], ],
[ [
'options.remoteData.format', 'metadata.remoteData.format',
{ handler: 'DataFormat', label: translate('Format') }, { handler: 'DataFormat', label: translate('Format') },
], ],
'options.fromZoom', 'metadata.fromZoom',
'options.toZoom', 'metadata.toZoom',
[ [
'options.remoteData.dynamic', 'metadata.remoteData.dynamic',
{ {
handler: 'Switch', handler: 'Switch',
label: translate('Dynamic'), label: translate('Dynamic'),
@ -733,7 +726,7 @@ export class DataLayer {
}, },
], ],
[ [
'options.remoteData.licence', 'metadata.remoteData.licence',
{ {
label: translate('Licence'), label: translate('Licence'),
helpText: translate('Please be sure the licence is compliant with your use.'), helpText: translate('Please be sure the licence is compliant with your use.'),
@ -742,14 +735,14 @@ export class DataLayer {
] ]
if (this.map.options.urls.ajax_proxy) { if (this.map.options.urls.ajax_proxy) {
remoteDataFields.push([ remoteDataFields.push([
'options.remoteData.proxy', 'metadata.remoteData.proxy',
{ {
handler: 'Switch', handler: 'Switch',
label: translate('Proxy request'), label: translate('Proxy request'),
helpEntries: 'proxyRemoteData', helpEntries: 'proxyRemoteData',
}, },
]) ])
remoteDataFields.push('options.remoteData.ttl') remoteDataFields.push('metadata.remoteData.ttl')
} }
const remoteDataContainer = DomUtil.createFieldset( const remoteDataContainer = DomUtil.createFieldset(
@ -828,7 +821,7 @@ export class DataLayer {
} }
getOwnOption(option) { getOwnOption(option) {
if (Utils.usableOption(this.options, option)) return this.options[option] if (Utils.usableOption(this.metadata, option)) return this.metadata[option]
} }
getOption(option, feature) { getOption(option, feature) {
@ -879,8 +872,11 @@ export class DataLayer {
this.getVersionUrl(version) this.getVersionUrl(version)
) )
if (!error) { if (!error) {
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat. if (!geojson.metadata) {
if (geojson._umap_options) this.setOptions(geojson._umap_options) // Retrocompat.
geojson.metadata = geojson._umap_options || geojson._storage
}
if (geojson.metadata) this.setMetadata(geojson.metadata)
this.empty() this.empty()
if (this.isRemoteLayer()) this.fetchRemoteData() if (this.isRemoteLayer()) this.fetchRemoteData()
else this.addData(geojson) else this.addData(geojson)
@ -930,7 +926,7 @@ export class DataLayer {
// Is this layer browsable in theorie // Is this layer browsable in theorie
// AND the user allows it // AND the user allows it
allowBrowse() { allowBrowse() {
return !!this.options.browsable && this.isBrowsable() return !!this.metadata.browsable && this.isBrowsable()
} }
// Is this layer browsable in theorie // Is this layer browsable in theorie
@ -1003,21 +999,13 @@ export class DataLayer {
return prev return prev
} }
umapGeoJSON() {
return {
type: 'FeatureCollection',
features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(),
_umap_options: this.options,
}
}
getRank() { getRank() {
return this.map.datalayers_index.indexOf(this) return this.map.datalayers_index.indexOf(this)
} }
isReadOnly() { isReadOnly() {
// isReadOnly must return true if unset // isReadOnly must return true if unset
return this.options.editMode === 'disabled' return this.metadata.editMode === 'disabled'
} }
isDataReadOnly() { isDataReadOnly() {
@ -1025,20 +1013,32 @@ export class DataLayer {
return this.isReadOnly() || this.isRemoteLayer() return this.isReadOnly() || this.isRemoteLayer()
} }
cleanedMetadata() {
const metadata = Utils.copyJSON(this.metadata)
delete metadata.permissions
delete metadata.editMode
delete metadata.id
return metadata
}
async save() { async save() {
if (this.isDeleted) return this.saveDelete() if (this.isDeleted) return this.saveDelete()
if (!this.isLoaded()) { if (!this.isLoaded()) {
return return
} }
const geojson = this.umapGeoJSON() const geojson = {
type: 'FeatureCollection',
features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(),
}
const formData = new FormData() const formData = new FormData()
formData.append('name', this.options.name) formData.append('name', this.metadata.name)
formData.append('display_on_load', !!this.options.displayOnLoad) formData.append('display_on_load', Boolean(this.metadata.displayOnLoad))
formData.append('rank', this.getRank()) formData.append('rank', this.getRank())
formData.append('settings', JSON.stringify(this.options)) formData.append('metadata', JSON.stringify(this.cleanedMetadata()))
// 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('data', blob)
const saveUrl = this.map.urls.get('datalayer_save', { const saveUrl = this.map.urls.get('datalayer_save', {
map_id: this.map.options.umap_id, map_id: this.map.options.umap_id,
pk: this.umap_id, pk: this.umap_id,
@ -1067,17 +1067,17 @@ export class DataLayer {
} else { } else {
// Response contains geojson only if save has conflicted and conflicts have // Response contains geojson only if save has conflicted and conflicts have
// been resolved. So we need to reload to get extra data (added by someone else) // been resolved. So we need to reload to get extra data (added by someone else)
if (data.geojson) { if (data.data) {
this.clear() this.clear()
this.fromGeoJSON(data.geojson) this.fromGeoJSON(data.data)
delete data.geojson delete data.data
} }
this._reference_version = response.headers.get('X-Datalayer-Version') this._reference_version = response.headers.get('X-Datalayer-Version')
this.sync.update('_reference_version', this._reference_version) this.sync.update('_reference_version', this._reference_version)
this.setUmapId(data.id) this.setUmapId(data.id)
this.updateOptions(data) this.updateMetadata(data)
this.backupOptions() this.backupMetadata()
this.connectToMap() this.connectToMap()
this._loaded = true this._loaded = true
this.redraw() // Needed for reordering features this.redraw() // Needed for reordering features
@ -1099,7 +1099,7 @@ export class DataLayer {
} }
getName() { getName() {
return this.options.name || translate('Untitled layer') return this.metadata.name || translate('Untitled layer')
} }
tableEdit() { tableEdit() {

View file

@ -23,7 +23,6 @@ export const EXPORT_FORMATS = {
map.eachFeature((feature) => { map.eachFeature((feature) => {
const row = feature.toGeoJSON().properties const row = feature.toGeoJSON().properties
const center = feature.center const center = feature.center
delete row._umap_options
row.Latitude = center.lat row.Latitude = center.lat
row.Longitude = center.lng row.Longitude = center.lng
table.push(row) table.push(row)

View file

@ -208,7 +208,7 @@ export default class Importer {
DomUtil.element({ DomUtil.element({
tagName: 'option', tagName: 'option',
parent: layerSelect, parent: layerSelect,
textContent: datalayer.options.name, textContent: datalayer.metadata.name,
value: L.stamp(datalayer), value: L.stamp(datalayer),
}) })
} }
@ -275,13 +275,13 @@ export default class Importer {
return false return false
} }
const layer = this.layer const layer = this.layer
layer.options.remoteData = { layer.metadata.remoteData = {
url: this.url, url: this.url,
format: this.format, format: this.format,
} }
if (this.map.options.urls.ajax_proxy) { if (this.map.options.urls.ajax_proxy) {
layer.options.remoteData.proxy = true layer.metadata.remoteData.proxy = true
layer.options.remoteData.ttl = SCHEMA.ttl.default layer.metadata.remoteData.ttl = SCHEMA.ttl.default
} }
layer.fetchRemoteData(true) layer.fetchRemoteData(true)
} }

View file

@ -214,7 +214,7 @@ export class DataLayerPermissions {
{ {
edit_status: null, edit_status: null,
}, },
datalayer.options.permissions datalayer.metadata.permissions
) )
this.datalayer = datalayer this.datalayer = datalayer
@ -277,8 +277,8 @@ export class DataLayerPermissions {
} }
commit() { commit() {
this.datalayer.options.permissions = Object.assign( this.datalayer.metadata.permissions = Object.assign(
this.datalayer.options.permissions, this.datalayer.metadata.permissions,
this.options this.options
) )
} }

View file

@ -217,8 +217,8 @@ export const Cluster = DivIcon.extend({
computeTextColor: function (el) { computeTextColor: function (el) {
let color let color
const backgroundColor = this.datalayer.getColor() const backgroundColor = this.datalayer.getColor()
if (this.datalayer.options.cluster?.textColor) { if (this.datalayer.metadata.cluster?.textColor) {
color = this.datalayer.options.cluster.textColor color = this.datalayer.metadata.cluster.textColor
} }
return color || DomUtil.TextColorFromBackgroundColor(el, backgroundColor) return color || DomUtil.TextColorFromBackgroundColor(el, backgroundColor)
}, },

View file

@ -14,11 +14,11 @@ const ClassifiedMixin = {
.filter((k) => k !== 'schemeGroups') .filter((k) => k !== 'schemeGroups')
.sort() .sort()
const key = this.getType().toLowerCase() const key = this.getType().toLowerCase()
if (!Utils.isObject(this.datalayer.options[key])) { if (!Utils.isObject(this.datalayer.metadata[key])) {
this.datalayer.options[key] = {} this.datalayer.metadata[key] = {}
} }
this.ensureOptions(this.datalayer.options[key]) this.ensureOptions(this.datalayer.metadata[key])
FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key]) FeatureGroup.prototype.initialize.call(this, [], this.datalayer.metadata[key])
LayerMixin.onInit.call(this, this.datalayer.map) LayerMixin.onInit.call(this, this.datalayer.map)
}, },
@ -111,7 +111,7 @@ export const Choropleth = FeatureGroup.extend({
}, },
_getValue: function (feature) { _getValue: function (feature) {
const key = this.datalayer.options.choropleth.property || 'value' const key = this.datalayer.metadata.choropleth.property || 'value'
const value = +feature.properties[key] const value = +feature.properties[key]
if (!Number.isNaN(value)) return value if (!Number.isNaN(value)) return value
}, },
@ -124,12 +124,12 @@ export const Choropleth = FeatureGroup.extend({
this.options.colors = [] this.options.colors = []
return return
} }
const mode = this.datalayer.options.choropleth.mode const mode = this.datalayer.metadata.choropleth.mode
let classes = +this.datalayer.options.choropleth.classes || 5 let classes = +this.datalayer.metadata.choropleth.classes || 5
let breaks let breaks
classes = Math.min(classes, values.length) classes = Math.min(classes, values.length)
if (mode === 'manual') { if (mode === 'manual') {
const manualBreaks = this.datalayer.options.choropleth.breaks const manualBreaks = this.datalayer.metadata.choropleth.breaks
if (manualBreaks) { if (manualBreaks) {
breaks = manualBreaks breaks = manualBreaks
.split(',') .split(',')
@ -148,10 +148,10 @@ export const Choropleth = FeatureGroup.extend({
breaks.push(ss.max(values)) // Needed for computing the legend breaks.push(ss.max(values)) // Needed for computing the legend
} }
this.options.breaks = breaks || [] this.options.breaks = breaks || []
this.datalayer.options.choropleth.breaks = this.options.breaks this.datalayer.metadata.choropleth.breaks = this.options.breaks
.map((b) => +b.toFixed(2)) .map((b) => +b.toFixed(2))
.join(',') .join(',')
let colorScheme = this.datalayer.options.choropleth.brewer let colorScheme = this.datalayer.metadata.choropleth.brewer
if (!colorbrewer[colorScheme]) colorScheme = 'Blues' if (!colorbrewer[colorScheme]) colorScheme = 'Blues'
this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || [] this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || []
}, },
@ -169,24 +169,24 @@ export const Choropleth = FeatureGroup.extend({
onEdit: function (field, builder) { onEdit: function (field, builder) {
// Only compute the breaks if we're dealing with choropleth // Only compute the breaks if we're dealing with choropleth
if (!field.startsWith('options.choropleth')) return if (!field.startsWith('metadata.choropleth')) return
// If user touches the breaks, then force manual mode // If user touches the breaks, then force manual mode
if (field === 'options.choropleth.breaks') { if (field === 'metadata.choropleth.breaks') {
this.datalayer.options.choropleth.mode = 'manual' this.datalayer.metadata.choropleth.mode = 'manual'
if (builder) builder.helpers['options.choropleth.mode'].fetch() if (builder) builder.helpers['metadata.choropleth.mode'].fetch()
} }
this.compute() this.compute()
// If user changes the mode or the number of classes, // If user changes the mode or the number of classes,
// then update the breaks input value // then update the breaks input value
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') { if (field === 'metadata.choropleth.mode' || field === 'metadata.choropleth.classes') {
if (builder) builder.helpers['options.choropleth.breaks'].fetch() if (builder) builder.helpers['metadata.choropleth.breaks'].fetch()
} }
}, },
getEditableOptions: function () { getEditableOptions: function () {
return [ return [
[ [
'options.choropleth.property', 'metadata.choropleth.property',
{ {
handler: 'Select', handler: 'Select',
selectOptions: this.datalayer._propertiesIndex, selectOptions: this.datalayer._propertiesIndex,
@ -194,7 +194,7 @@ export const Choropleth = FeatureGroup.extend({
}, },
], ],
[ [
'options.choropleth.brewer', 'metadata.choropleth.brewer',
{ {
handler: 'Select', handler: 'Select',
label: translate('Choropleth color palette'), label: translate('Choropleth color palette'),
@ -202,7 +202,7 @@ export const Choropleth = FeatureGroup.extend({
}, },
], ],
[ [
'options.choropleth.classes', 'metadata.choropleth.classes',
{ {
handler: 'Range', handler: 'Range',
min: 3, min: 3,
@ -213,7 +213,7 @@ export const Choropleth = FeatureGroup.extend({
}, },
], ],
[ [
'options.choropleth.breaks', 'metadata.choropleth.breaks',
{ {
handler: 'BlurInput', handler: 'BlurInput',
label: translate('Choropleth breakpoints'), label: translate('Choropleth breakpoints'),
@ -223,7 +223,7 @@ export const Choropleth = FeatureGroup.extend({
}, },
], ],
[ [
'options.choropleth.mode', 'metadata.choropleth.mode',
{ {
handler: 'MultiChoice', handler: 'MultiChoice',
default: 'kmeans', default: 'kmeans',
@ -255,13 +255,13 @@ export const Circles = FeatureGroup.extend({
}, },
ensureOptions: function (options) { ensureOptions: function (options) {
if (!Utils.isObject(this.datalayer.options.circles.radius)) { if (!Utils.isObject(this.datalayer.metadata.circles.radius)) {
this.datalayer.options.circles.radius = {} this.datalayer.metadata.circles.radius = {}
} }
}, },
_getValue: function (feature) { _getValue: function (feature) {
const key = this.datalayer.options.circles.property || 'value' const key = this.datalayer.metadata.circles.property || 'value'
const value = +feature.properties[key] const value = +feature.properties[key]
if (!Number.isNaN(value)) return value if (!Number.isNaN(value)) return value
}, },
@ -270,8 +270,8 @@ export const Circles = FeatureGroup.extend({
const values = this.getValues() const values = this.getValues()
this.options.minValue = Math.sqrt(Math.min(...values)) this.options.minValue = Math.sqrt(Math.min(...values))
this.options.maxValue = Math.sqrt(Math.max(...values)) this.options.maxValue = Math.sqrt(Math.max(...values))
this.options.minPX = this.datalayer.options.circles.radius?.min || 2 this.options.minPX = this.datalayer.metadata.circles.radius?.min || 2
this.options.maxPX = this.datalayer.options.circles.radius?.max || 50 this.options.maxPX = this.datalayer.metadata.circles.radius?.max || 50
}, },
onEdit: function (field, builder) { onEdit: function (field, builder) {
@ -375,7 +375,7 @@ export const Categorized = FeatureGroup.extend({
_getValue: function (feature) { _getValue: function (feature) {
const key = const key =
this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0] this.datalayer.metadata.categorized.property || this.datalayer._propertiesIndex[0]
return feature.properties[key] return feature.properties[key]
}, },
@ -397,10 +397,10 @@ export const Categorized = FeatureGroup.extend({
this.options.colors = [] this.options.colors = []
return return
} }
const mode = this.datalayer.options.categorized.mode const mode = this.datalayer.metadata.categorized.mode
let categories = [] let categories = []
if (mode === 'manual') { if (mode === 'manual') {
const manualCategories = this.datalayer.options.categorized.categories const manualCategories = this.datalayer.metadata.categorized.categories
if (manualCategories) { if (manualCategories) {
categories = manualCategories.split(',') categories = manualCategories.split(',')
} }
@ -410,8 +410,8 @@ export const Categorized = FeatureGroup.extend({
.sort(Utils.naturalSort) .sort(Utils.naturalSort)
} }
this.options.categories = categories this.options.categories = categories
this.datalayer.options.categorized.categories = this.options.categories.join(',') this.datalayer.metadata.categorized.categories = this.options.categories.join(',')
const colorScheme = this.datalayer.options.categorized.brewer const colorScheme = this.datalayer.metadata.categorized.brewer
this._classes = this.options.categories.length this._classes = this.options.categories.length
if (colorbrewer[colorScheme]?.[this._classes]) { if (colorbrewer[colorScheme]?.[this._classes]) {
this.options.colors = colorbrewer[colorScheme][this._classes] this.options.colors = colorbrewer[colorScheme][this._classes]
@ -425,7 +425,7 @@ export const Categorized = FeatureGroup.extend({
getEditableOptions: function () { getEditableOptions: function () {
return [ return [
[ [
'options.categorized.property', 'metadata.categorized.property',
{ {
handler: 'Select', handler: 'Select',
selectOptions: this.datalayer._propertiesIndex, selectOptions: this.datalayer._propertiesIndex,
@ -433,7 +433,7 @@ export const Categorized = FeatureGroup.extend({
}, },
], ],
[ [
'options.categorized.brewer', 'metadata.categorized.brewer',
{ {
handler: 'Select', handler: 'Select',
label: translate('Color palette'), label: translate('Color palette'),
@ -441,7 +441,7 @@ export const Categorized = FeatureGroup.extend({
}, },
], ],
[ [
'options.categorized.categories', 'metadata.categorized.categories',
{ {
handler: 'BlurInput', handler: 'BlurInput',
label: translate('Categories'), label: translate('Categories'),
@ -449,7 +449,7 @@ export const Categorized = FeatureGroup.extend({
}, },
], ],
[ [
'options.categorized.mode', 'metadata.categorized.mode',
{ {
handler: 'MultiChoice', handler: 'MultiChoice',
default: 'alpha', default: 'alpha',
@ -462,17 +462,17 @@ export const Categorized = FeatureGroup.extend({
onEdit: function (field, builder) { onEdit: function (field, builder) {
// Only compute the categories if we're dealing with categorized // Only compute the categories if we're dealing with categorized
if (!field.startsWith('options.categorized')) return if (!field.startsWith('metadata.categorized')) return
// If user touches the categories, then force manual mode // If user touches the categories, then force manual mode
if (field === 'options.categorized.categories') { if (field === 'metadata.categorized.categories') {
this.datalayer.options.categorized.mode = 'manual' this.datalayer.metadata.categorized.mode = 'manual'
if (builder) builder.helpers['options.categorized.mode'].fetch() if (builder) builder.helpers['metadata.categorized.mode'].fetch()
} }
this.compute() this.compute()
// If user changes the mode // If user changes the mode
// then update the categories input value // then update the categories input value
if (field === 'options.categorized.mode') { if (field === 'metadata.categorized.mode') {
if (builder) builder.helpers['options.categorized.categories'].fetch() if (builder) builder.helpers['metadata.categorized.categories'].fetch()
} }
}, },

View file

@ -27,8 +27,8 @@ export const Cluster = L.MarkerClusterGroup.extend({
initialize: function (datalayer) { initialize: function (datalayer) {
this.datalayer = datalayer this.datalayer = datalayer
if (!Utils.isObject(this.datalayer.options.cluster)) { if (!Utils.isObject(this.datalayer.metadata.cluster)) {
this.datalayer.options.cluster = {} this.datalayer.metadata.cluster = {}
} }
const options = { const options = {
polygonOptions: { polygonOptions: {
@ -36,8 +36,8 @@ export const Cluster = L.MarkerClusterGroup.extend({
}, },
iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster), iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster),
} }
if (this.datalayer.options.cluster?.radius) { if (this.datalayer.metadata.cluster?.radius) {
options.maxClusterRadius = this.datalayer.options.cluster.radius options.maxClusterRadius = this.datalayer.metadata.cluster.radius
} }
L.MarkerClusterGroup.prototype.initialize.call(this, options) L.MarkerClusterGroup.prototype.initialize.call(this, options)
LayerMixin.onInit.call(this, this.datalayer.map) LayerMixin.onInit.call(this, this.datalayer.map)
@ -73,7 +73,7 @@ export const Cluster = L.MarkerClusterGroup.extend({
getEditableOptions: () => [ getEditableOptions: () => [
[ [
'options.cluster.radius', 'metadata.cluster.radius',
{ {
handler: 'BlurIntInput', handler: 'BlurIntInput',
placeholder: translate('Clustering radius'), placeholder: translate('Clustering radius'),
@ -81,7 +81,7 @@ export const Cluster = L.MarkerClusterGroup.extend({
}, },
], ],
[ [
'options.cluster.textColor', 'metadata.cluster.textColor',
{ {
handler: 'TextColorPicker', handler: 'TextColorPicker',
placeholder: translate('Auto'), placeholder: translate('Auto'),
@ -91,7 +91,7 @@ export const Cluster = L.MarkerClusterGroup.extend({
], ],
onEdit: function (field, builder) { onEdit: function (field, builder) {
if (field === 'options.cluster.radius') { if (field === 'metadata.cluster.radius') {
// No way to reset radius of an already instanciated MarkerClusterGroup... // No way to reset radius of an already instanciated MarkerClusterGroup...
this.datalayer.resetLayer(true) this.datalayer.resetLayer(true)
return return

View file

@ -20,10 +20,10 @@ export const Heat = L.HeatLayer.extend({
initialize: function (datalayer) { initialize: function (datalayer) {
this.datalayer = datalayer this.datalayer = datalayer
L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat) L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.metadata.heat)
LayerMixin.onInit.call(this, this.datalayer.map) LayerMixin.onInit.call(this, this.datalayer.map)
if (!Utils.isObject(this.datalayer.options.heat)) { if (!Utils.isObject(this.datalayer.metadata.heat)) {
this.datalayer.options.heat = {} this.datalayer.metadata.heat = {}
} }
}, },
@ -31,9 +31,9 @@ export const Heat = L.HeatLayer.extend({
if (layer instanceof Marker) { if (layer instanceof Marker) {
let latlng = layer.getLatLng() let latlng = layer.getLatLng()
let alt let alt
if (this.datalayer.options.heat?.intensityProperty) { if (this.datalayer.metadata.heat?.intensityProperty) {
alt = Number.parseFloat( alt = Number.parseFloat(
layer.feature.properties[this.datalayer.options.heat.intensityProperty || 0] layer.feature.properties[this.datalayer.metadata.heat.intensityProperty || 0]
) )
latlng = new LatLng(latlng.lat, latlng.lng, alt) latlng = new LatLng(latlng.lat, latlng.lng, alt)
} }
@ -63,7 +63,7 @@ export const Heat = L.HeatLayer.extend({
getEditableOptions: () => [ getEditableOptions: () => [
[ [
'options.heat.radius', 'metadata.heat.radius',
{ {
handler: 'Range', handler: 'Range',
min: 10, min: 10,
@ -74,7 +74,7 @@ export const Heat = L.HeatLayer.extend({
}, },
], ],
[ [
'options.heat.intensityProperty', 'metadata.heat.intensityProperty',
{ {
handler: 'BlurInput', handler: 'BlurInput',
placeholder: translate('Heatmap intensity property'), placeholder: translate('Heatmap intensity property'),
@ -84,12 +84,12 @@ export const Heat = L.HeatLayer.extend({
], ],
onEdit: function (field, builder) { onEdit: function (field, builder) {
if (field === 'options.heat.intensityProperty') { if (field === 'metadata.heat.intensityProperty') {
this.datalayer.resetLayer(true) // We need to repopulate the latlngs this.datalayer.resetLayer(true) // We need to repopulate the latlngs
return return
} }
if (field === 'options.heat.radius') { if (field === 'metadata.heat.radius') {
this.options.radius = this.datalayer.options.heat.radius this.options.radius = this.datalayer.metadata.heat.radius
} }
this._updateOptions() this._updateOptions()
}, },

View file

@ -41,7 +41,7 @@ export function getImpactsFromSchema(fields, schema) {
// remove the option prefix for fields // remove the option prefix for fields
// And only keep the first part in case of a subfield // And only keep the first part in case of a subfield
// (e.g "options.limitBounds.foobar" will just return "limitBounds") // (e.g "options.limitBounds.foobar" will just return "limitBounds")
return field.replace('options.', '').split('.')[0] return field.replace('options.', '').replace('metadata.', '').split('.')[0]
}) })
.reduce((acc, field) => { .reduce((acc, field) => {
// retrieve the "impacts" field from the schema // retrieve the "impacts" field from the schema
@ -181,7 +181,7 @@ export function isObject(what) {
return typeof what === 'object' && what !== null return typeof what === 'object' && what !== null
} }
export function CopyJSON(geojson) { export function copyJSON(geojson) {
return JSON.parse(JSON.stringify(geojson)) return JSON.parse(JSON.stringify(geojson))
} }

View file

@ -725,9 +725,9 @@ const ControlsMixin = {
const row = L.DomUtil.create('li', 'orderable', ul) const row = L.DomUtil.create('li', 'orderable', ul)
L.DomUtil.createIcon(row, 'icon-drag', L._('Drag to reorder')) L.DomUtil.createIcon(row, 'icon-drag', L._('Drag to reorder'))
datalayer.renderToolbox(row) datalayer.renderToolbox(row)
const title = L.DomUtil.add('span', '', row, datalayer.options.name) const title = L.DomUtil.add('span', '', row, datalayer.metadata.name)
row.classList.toggle('off', !datalayer.isVisible()) row.classList.toggle('off', !datalayer.isVisible())
title.textContent = datalayer.options.name title.textContent = datalayer.metadata.name
row.dataset.id = L.stamp(datalayer) row.dataset.id = L.stamp(datalayer)
}) })
const onReorder = (src, dst, initialIndex, finalIndex) => { const onReorder = (src, dst, initialIndex, finalIndex) => {

View file

@ -29,30 +29,30 @@ L.Map.mergeOptions({
U.Map = L.Map.extend({ U.Map = L.Map.extend({
includes: [ControlsMixin], includes: [ControlsMixin],
initialize: async function (el, geojson) { initialize: async function (el, metadata) {
this.sync_engine = new U.SyncEngine(this) this.sync_engine = new U.SyncEngine(this)
this.sync = this.sync_engine.proxy(this) this.sync = this.sync_engine.proxy(this)
// Locale name (pt_PT, en_US…) // Locale name (pt_PT, en_US…)
// To be used for Django localization // To be used for Django localization
if (geojson.properties.locale) L.setLocale(geojson.properties.locale) if (metadata.locale) L.setLocale(metadata.locale)
// Language code (pt-pt, en-us…) // Language code (pt-pt, en-us…)
// To be used in javascript APIs // To be used in javascript APIs
if (geojson.properties.lang) L.lang = geojson.properties.lang if (metadata.lang) L.lang = metadata.lang
this.setOptionsFromQueryString(geojson.properties) this.setOptionsFromQueryString(metadata)
// Prevent default creation of controls // Prevent default creation of controls
const zoomControl = geojson.properties.zoomControl const zoomControl = metadata.zoomControl
const fullscreenControl = geojson.properties.fullscreenControl const fullscreenControl = metadata.fullscreenControl
geojson.properties.zoomControl = false metadata.zoomControl = false
geojson.properties.fullscreenControl = false metadata.fullscreenControl = false
L.Map.prototype.initialize.call(this, el, geojson.properties) L.Map.prototype.initialize.call(this, el, metadata)
if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) if (metadata.schema) this.overrideSchema(metadata.schema)
// After calling parent initialize, as we are doing initCenter our-selves // After calling parent initialize, as we are doing initCenter our-selves
if (geojson.geometry) this.options.center = this.latLng(geojson.geometry) if (metadata.geometry) this.options.center = this.latLng(metadata.geometry)
this.urls = new U.URLs(this.options.urls) this.urls = new U.URLs(this.options.urls)
this.panel = new U.Panel(this) this.panel = new U.Panel(this)
@ -805,7 +805,7 @@ U.Map = L.Map.extend({
const datalayer = new U.DataLayer(this, options, sync) const datalayer = new U.DataLayer(this, options, sync)
if (sync !== false) { if (sync !== false) {
datalayer.sync.upsert(datalayer.options) datalayer.sync.upsert(datalayer.metadata)
} }
return datalayer return datalayer
}, },
@ -906,15 +906,16 @@ U.Map = L.Map.extend({
} }
if (importedData.geometry) this.options.center = this.latLng(importedData.geometry) if (importedData.geometry) this.options.center = this.latLng(importedData.geometry)
importedData.layers.forEach((geojson) => { for (const geojson of importedData.layers) {
if (!geojson._umap_options && geojson._storage) { if (!geojson.metadata) {
geojson._umap_options = geojson._storage geojson.metadata = geojson._umap_options || geojson._storage
delete geojson._umap_options
delete geojson._storage delete geojson._storage
} }
delete geojson._umap_options?.id // Never trust an id at this stage delete geojson.metadata?.id // Never trust an id at this stage
const dataLayer = this.createDataLayer(geojson._umap_options) const dataLayer = this.createDataLayer(geojson.metadata)
dataLayer.fromUmapGeoJSON(geojson) dataLayer.fromUmapGeoJSON(geojson)
}) }
this.initTileLayers() this.initTileLayers()
this.renderControls() this.renderControls()
@ -1032,22 +1033,14 @@ U.Map = L.Map.extend({
saveSelf: async function () { saveSelf: async function () {
this.rules.commit() this.rules.commit()
const geojson = {
type: 'Feature',
geometry: this.geometry(),
properties: this.exportOptions(),
}
const formData = new FormData() const formData = new FormData()
formData.append('name', this.options.name) formData.append('name', this.options.name)
formData.append('center', JSON.stringify(this.geometry())) formData.append('center', JSON.stringify(this.geometry()))
formData.append('settings', JSON.stringify(geojson)) formData.append('zoom', this.getZoom())
formData.append('metadata', JSON.stringify(this.exportOptions()))
const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) const uri = this.urls.get('map_save', { map_id: this.options.umap_id })
const [data, _, error] = await this.server.post(uri, {}, formData) const [data, _, error] = await this.server.post(uri, {}, formData)
// FIXME: login_required response will not be an error, so it will not if (error) return
// stop code while it should
if (error) {
return
}
if (data.login_required) { if (data.login_required) {
window.onLogin = () => this.saveSelf() window.onLogin = () => this.saveSelf()
window.open(data.login_required) window.open(data.login_required)

View file

@ -682,7 +682,7 @@ describe('Utils', () => {
describe('#copyJSON', () => { describe('#copyJSON', () => {
it('should actually copy the JSON', () => { it('should actually copy the JSON', () => {
const originalJSON = { some: 'json' } const originalJSON = { some: 'json' }
const returned = Utils.CopyJSON(originalJSON) const returned = Utils.copyJSON(originalJSON)
// Change the original JSON // Change the original JSON
originalJSON.anotherKey = 'value' originalJSON.anotherKey = 'value'

View file

@ -1,6 +1,6 @@
{% load umap_tags %} {% load umap_tags %}
<umap-fragment data-settings='{{ map_settings|escape }}'> <umap-fragment data-metadata='{{ map_metadata|escape }}'>
<div id="{{ unique_id }}" class="map_fragment"> <div id="{{ unique_id }}" class="map_fragment">
</div> </div>
</umap-fragment> </umap-fragment>

View file

@ -6,7 +6,7 @@
<!-- djlint:off --> <!-- djlint:off -->
<script defer type="text/javascript"> <script defer type="text/javascript">
window.addEventListener('DOMContentLoaded', (event) => { window.addEventListener('DOMContentLoaded', (event) => {
U.MAP = new U.Map("map", {{ map_settings|notag|safe }}) U.MAP = new U.Map("map", {{ map_metadata|notag|safe }})
}) })
</script> </script>
<!-- djlint:on --> <!-- djlint:on -->

View file

@ -35,7 +35,7 @@
<a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a> <a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
</th> </th>
<td> <td>
{{ map_inst.preview_settings|json_script:unique_id }} {{ map_inst.preview_metadata|json_script:unique_id }}
<button class="map-icon map-opener" <button class="map-icon map-opener"
data-map-id="{{ unique_id }}" data-map-id="{{ unique_id }}"
title="{% translate "Open preview" %}"> title="{% translate "Open preview" %}">

View file

@ -20,13 +20,13 @@ def umap_js(locale=None):
@register.inclusion_tag("umap/map_fragment.html") @register.inclusion_tag("umap/map_fragment.html")
def map_fragment(map_instance, **kwargs): def map_fragment(map_instance, **kwargs):
map_settings = map_instance.preview_settings map_metadata = map_instance.preview_metadata
map_settings["properties"].update(kwargs) map_metadata.update(kwargs)
prefix = kwargs.pop("prefix", None) or "map" prefix = kwargs.pop("prefix", None) or "map"
page = kwargs.pop("page", None) or "" page = kwargs.pop("page", None) or ""
unique_id = prefix + str(page) + "_" + str(map_instance.pk) unique_id = prefix + str(page) + "_" + str(map_instance.pk)
return { return {
"map_settings": json_dumps(map_settings), "map_metadata": json_dumps(map_metadata),
"map": map_instance, "map": map_instance,
"unique_id": unique_id, "unique_id": unique_id,
} }

View file

@ -3,10 +3,10 @@ import json
import factory import factory
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.gis.geos import Point
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.urls import reverse from django.urls import reverse
from umap.forms import DEFAULT_CENTER
from umap.models import DataLayer, Licence, Map, TileLayer from umap.models import DataLayer, Licence, Map, TileLayer
User = get_user_model() User = get_user_model()
@ -20,14 +20,13 @@ DATALAYER_DATA = {
"type": "Point", "type": "Point",
"coordinates": [14.68896484375, 48.55297816440071], "coordinates": [14.68896484375, 48.55297816440071],
}, },
"metadata": {"color": "DarkCyan", "iconClass": "Ball"},
"properties": { "properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"name": "Here", "name": "Here",
"description": "Da place anonymous again 755", "description": "Da place anonymous again 755",
}, },
} }
], ],
"_umap_options": {"displayOnLoad": True, "name": "Donau", "id": 926},
} }
@ -61,34 +60,27 @@ class UserFactory(factory.django.DjangoModelFactory):
class MapFactory(factory.django.DjangoModelFactory): class MapFactory(factory.django.DjangoModelFactory):
name = "test map" name = "test map"
slug = "test-map" slug = "test-map"
center = DEFAULT_CENTER center = Point(13.447265624999998, 48.94415123418794)
settings = factory.Dict( metadata = factory.Dict(
{ {
"geometry": { "datalayersControl": True,
"coordinates": [13.447265624999998, 48.94415123418794], "description": "Which is just the Danube, at the end",
"type": "Point", "displayPopupFooter": False,
"licence": "",
"miniMap": False,
"moreControl": True,
"name": name,
"scaleControl": True,
"tilelayer": {
"attribution": "\xa9 OSM Contributors",
"maxZoom": 18,
"minZoom": 0,
"url_template": "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
}, },
"properties": { "tilelayersControl": True,
"datalayersControl": True, "zoom": 7,
"description": "Which is just the Danube, at the end", "zoomControl": True,
"displayPopupFooter": False, },
"licence": "",
"miniMap": False,
"moreControl": True,
"name": name,
"scaleControl": True,
"tilelayer": {
"attribution": "\xa9 OSM Contributors",
"maxZoom": 18,
"minZoom": 0,
"url_template": "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
},
"tilelayersControl": True,
"zoom": 7,
"zoomControl": True,
},
"type": "Feature",
}
) )
licence = factory.SubFactory(LicenceFactory) licence = factory.SubFactory(LicenceFactory)
@ -97,8 +89,8 @@ class MapFactory(factory.django.DjangoModelFactory):
@classmethod @classmethod
def _adjust_kwargs(cls, **kwargs): def _adjust_kwargs(cls, **kwargs):
# Make sure there is no persistency # Make sure there is no persistency
kwargs["settings"] = copy.deepcopy(kwargs["settings"]) kwargs["metadata"] = copy.deepcopy(kwargs["metadata"])
kwargs["settings"]["properties"]["name"] = kwargs["name"] kwargs["metadata"]["name"] = kwargs["name"]
return kwargs return kwargs
class Meta: class Meta:
@ -110,26 +102,18 @@ 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
settings = factory.Dict({"displayOnLoad": True, "browsable": True, "name": name}) metadata = factory.Dict({"displayOnLoad": True, "browsable": True, "name": name})
@classmethod @classmethod
def _adjust_kwargs(cls, **kwargs): def _adjust_kwargs(cls, **kwargs):
if "data" in kwargs: if "data" in kwargs:
data = copy.deepcopy(kwargs.pop("data")) data = copy.deepcopy(kwargs.pop("data"))
if "settings" not in kwargs:
kwargs["settings"] = data.get("_umap_options", {})
else: else:
data = DATALAYER_DATA.copy() data = DATALAYER_DATA.copy()
data["_umap_options"] = { kwargs["metadata"]["name"] = kwargs["name"]
**DataLayerFactory.settings._defaults,
**kwargs["settings"],
}
data.setdefault("_umap_options", {})
kwargs["settings"]["name"] = kwargs["name"]
data["_umap_options"]["name"] = kwargs["name"]
data.setdefault("type", "FeatureCollection") data.setdefault("type", "FeatureCollection")
data.setdefault("features", []) data.setdefault("features", [])
kwargs["geojson"] = ContentFile(json.dumps(data), "foo.json") kwargs["data"] = ContentFile(json.dumps(data), "foo.json")
return kwargs return kwargs
class Meta: class Meta:

File diff suppressed because one or more lines are too long

View file

@ -12,4 +12,4 @@
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[2.062908,44.976505],[2.09421,44.872012],[2.171637,44.790027],[2.169416,44.63807],[2.326791,44.669693],[2.435001,44.638875],[2.556123,44.721284],[2.602682,44.843163],[2.738258,44.941219],[2.849652,44.87149],[2.923267,44.728643],[2.998574,44.674443],[3.105495,44.886775],[3.182317,44.863735],[3.337942,44.955907],[3.412832,44.944842],[3.475771,44.815371],[3.740649,44.838697],[3.875462,44.740627],[3.905171,44.592709],[4.068445,44.405112],[4.051457,44.317322],[4.25885,44.264784],[4.336071,44.339519],[4.503539,44.340188],[4.649227,44.27036],[4.762255,44.325382],[4.81409,44.232315],[5.060561,44.308137],[5.1549,44.230941],[5.384527,44.201049],[5.454715,44.119226],[5.576192,44.188037],[5.686443,44.197158],[5.631598,44.328306],[5.49307,44.337174],[5.418533,44.424945],[5.597253,44.543274],[5.649631,44.617885],[5.790624,44.653293],[5.850394,44.750747],[6.136227,44.864072],[6.355363,44.854775],[6.318202,45.003859],[6.203923,45.012471],[6.229392,45.10875],[6.331295,45.118124],[6.393911,45.061818],[6.629992,45.109325],[6.767941,45.15974],[6.849855,45.127165],[6.968762,45.208058],[7.137593,45.255693],[7.110693,45.326509],[7.184271,45.407484],[7.000332,45.504414],[7.000692,45.6399],[6.829113,45.702831],[6.818078,45.834974],[6.939609,45.846733],[7.043891,45.922087],[7.018252,45.984185],[6.81473,46.129696],[6.864511,46.282986],[6.722865,46.40755],[6.545176,46.394725],[6.390033,46.340163],[6.279914,46.351093],[6.295651,46.226055],[6.126621,46.14046],[5.985317,46.143309],[5.971781,46.211519],[6.124246,46.251016],[6.169736,46.367935],[6.064006,46.416223],[5.908936,46.283951],[5.725182,46.260732],[5.649345,46.339495],[5.473052,46.265067],[5.310563,46.44677],[5.20114,46.508211],[5.052372,46.484874],[4.940022,46.517199],[4.780208,46.176676],[4.69311,46.302197],[4.618558,46.264794],[4.405814,46.296058],[4.389398,46.213601],[4.261025,46.178754],[4.104087,46.198391],[3.988788,46.169805],[3.890131,46.214487],[3.891239,46.285246],[3.986627,46.319196],[3.99804,46.465464],[3.890467,46.481246],[3.743289,46.567565],[3.696958,46.660583],[3.598001,46.723983],[3.215545,46.682893],[3.032063,46.794909],[2.959919,46.803872],[2.774489,46.718903],[2.70497,46.73939],[2.596648,46.637215],[2.614961,46.553276],[2.352004,46.512207],[2.281044,46.420404],[2.323023,46.329277],[2.478945,46.281146],[2.5598,46.173367],[2.59442,45.989441],[2.492228,45.86403],[2.388014,45.827373],[2.52836,45.681924],[2.465345,45.60082],[2.516327,45.553428],[2.487472,45.418842],[2.37825,45.414302],[2.350481,45.327561],[2.195364,45.220851],[2.171759,45.081497],[2.062908,44.976505]]]},"properties":{"code":"84","nom":"Auvergne-Rhône-Alpes","taux":"6.1"}}, {"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[2.062908,44.976505],[2.09421,44.872012],[2.171637,44.790027],[2.169416,44.63807],[2.326791,44.669693],[2.435001,44.638875],[2.556123,44.721284],[2.602682,44.843163],[2.738258,44.941219],[2.849652,44.87149],[2.923267,44.728643],[2.998574,44.674443],[3.105495,44.886775],[3.182317,44.863735],[3.337942,44.955907],[3.412832,44.944842],[3.475771,44.815371],[3.740649,44.838697],[3.875462,44.740627],[3.905171,44.592709],[4.068445,44.405112],[4.051457,44.317322],[4.25885,44.264784],[4.336071,44.339519],[4.503539,44.340188],[4.649227,44.27036],[4.762255,44.325382],[4.81409,44.232315],[5.060561,44.308137],[5.1549,44.230941],[5.384527,44.201049],[5.454715,44.119226],[5.576192,44.188037],[5.686443,44.197158],[5.631598,44.328306],[5.49307,44.337174],[5.418533,44.424945],[5.597253,44.543274],[5.649631,44.617885],[5.790624,44.653293],[5.850394,44.750747],[6.136227,44.864072],[6.355363,44.854775],[6.318202,45.003859],[6.203923,45.012471],[6.229392,45.10875],[6.331295,45.118124],[6.393911,45.061818],[6.629992,45.109325],[6.767941,45.15974],[6.849855,45.127165],[6.968762,45.208058],[7.137593,45.255693],[7.110693,45.326509],[7.184271,45.407484],[7.000332,45.504414],[7.000692,45.6399],[6.829113,45.702831],[6.818078,45.834974],[6.939609,45.846733],[7.043891,45.922087],[7.018252,45.984185],[6.81473,46.129696],[6.864511,46.282986],[6.722865,46.40755],[6.545176,46.394725],[6.390033,46.340163],[6.279914,46.351093],[6.295651,46.226055],[6.126621,46.14046],[5.985317,46.143309],[5.971781,46.211519],[6.124246,46.251016],[6.169736,46.367935],[6.064006,46.416223],[5.908936,46.283951],[5.725182,46.260732],[5.649345,46.339495],[5.473052,46.265067],[5.310563,46.44677],[5.20114,46.508211],[5.052372,46.484874],[4.940022,46.517199],[4.780208,46.176676],[4.69311,46.302197],[4.618558,46.264794],[4.405814,46.296058],[4.389398,46.213601],[4.261025,46.178754],[4.104087,46.198391],[3.988788,46.169805],[3.890131,46.214487],[3.891239,46.285246],[3.986627,46.319196],[3.99804,46.465464],[3.890467,46.481246],[3.743289,46.567565],[3.696958,46.660583],[3.598001,46.723983],[3.215545,46.682893],[3.032063,46.794909],[2.959919,46.803872],[2.774489,46.718903],[2.70497,46.73939],[2.596648,46.637215],[2.614961,46.553276],[2.352004,46.512207],[2.281044,46.420404],[2.323023,46.329277],[2.478945,46.281146],[2.5598,46.173367],[2.59442,45.989441],[2.492228,45.86403],[2.388014,45.827373],[2.52836,45.681924],[2.465345,45.60082],[2.516327,45.553428],[2.487472,45.418842],[2.37825,45.414302],[2.350481,45.327561],[2.195364,45.220851],[2.171759,45.081497],[2.062908,44.976505]]]},"properties":{"code":"84","nom":"Auvergne-Rhône-Alpes","taux":"6.1"}},
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[4.230281,43.460184],[4.554917,43.446213],[4.562798,43.372135],[4.855045,43.332619],[4.86685,43.404678],[4.96771,43.4261],[5.04104,43.327285],[5.36205,43.32196],[5.363649,43.207122],[5.53693,43.21449],[5.600895,43.162546],[5.812732,43.109367],[6.124052,43.079307],[6.387568,43.1449],[6.635535,43.172509],[6.665956,43.31822],[6.739809,43.412882],[6.917972,43.447739],[6.971833,43.545451],[7.040444,43.541583],[7.167666,43.657402],[7.320289,43.691329],[7.528519,43.790518],[7.495441,43.864356],[7.648598,43.97411],[7.716938,44.081763],[7.670853,44.153737],[7.426953,44.112875],[7.188913,44.197801],[7.008059,44.236435],[6.896505,44.374301],[6.854014,44.529125],[6.933509,44.575953],[6.987061,44.690138],[7.006773,44.839316],[6.859866,44.852903],[6.749751,44.907359],[6.740812,45.016733],[6.629992,45.109325],[6.393911,45.061818],[6.331295,45.118124],[6.229392,45.10875],[6.203923,45.012471],[6.318202,45.003859],[6.355363,44.854775],[6.136227,44.864072],[5.850394,44.750747],[5.790624,44.653293],[5.649631,44.617885],[5.597253,44.543274],[5.418533,44.424945],[5.49307,44.337174],[5.631598,44.328306],[5.686443,44.197158],[5.576192,44.188037],[5.454715,44.119226],[5.384527,44.201049],[5.1549,44.230941],[5.060561,44.308137],[4.81409,44.232315],[4.762255,44.325382],[4.649227,44.27036],[4.722071,44.187421],[4.70746,44.10367],[4.8421,43.986474],[4.690546,43.883899],[4.593035,43.68746],[4.487234,43.699241],[4.409353,43.561127],[4.230281,43.460184]]]},"properties":{"code":"93","nom":"Provence-Alpes-Côte d'Azur","taux":"7.8"}}, {"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[4.230281,43.460184],[4.554917,43.446213],[4.562798,43.372135],[4.855045,43.332619],[4.86685,43.404678],[4.96771,43.4261],[5.04104,43.327285],[5.36205,43.32196],[5.363649,43.207122],[5.53693,43.21449],[5.600895,43.162546],[5.812732,43.109367],[6.124052,43.079307],[6.387568,43.1449],[6.635535,43.172509],[6.665956,43.31822],[6.739809,43.412882],[6.917972,43.447739],[6.971833,43.545451],[7.040444,43.541583],[7.167666,43.657402],[7.320289,43.691329],[7.528519,43.790518],[7.495441,43.864356],[7.648598,43.97411],[7.716938,44.081763],[7.670853,44.153737],[7.426953,44.112875],[7.188913,44.197801],[7.008059,44.236435],[6.896505,44.374301],[6.854014,44.529125],[6.933509,44.575953],[6.987061,44.690138],[7.006773,44.839316],[6.859866,44.852903],[6.749751,44.907359],[6.740812,45.016733],[6.629992,45.109325],[6.393911,45.061818],[6.331295,45.118124],[6.229392,45.10875],[6.203923,45.012471],[6.318202,45.003859],[6.355363,44.854775],[6.136227,44.864072],[5.850394,44.750747],[5.790624,44.653293],[5.649631,44.617885],[5.597253,44.543274],[5.418533,44.424945],[5.49307,44.337174],[5.631598,44.328306],[5.686443,44.197158],[5.576192,44.188037],[5.454715,44.119226],[5.384527,44.201049],[5.1549,44.230941],[5.060561,44.308137],[4.81409,44.232315],[4.762255,44.325382],[4.649227,44.27036],[4.722071,44.187421],[4.70746,44.10367],[4.8421,43.986474],[4.690546,43.883899],[4.593035,43.68746],[4.487234,43.699241],[4.409353,43.561127],[4.230281,43.460184]]]},"properties":{"code":"93","nom":"Provence-Alpes-Côte d'Azur","taux":"7.8"}},
{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[9.402271,41.858702],[9.412569,41.952476],[9.54998,42.104166],[9.558829,42.285265],[9.533196,42.545947],[9.449194,42.66224],[9.492385,42.8051],[9.463558,42.986401],[9.340873,42.994465],[9.311015,42.834679],[9.344478,42.73781],[9.085764,42.714609],[9.020694,42.644273],[8.886527,42.628966],[8.666509,42.515224],[8.674792,42.476243],[8.555885,42.36475],[8.689105,42.263528],[8.570341,42.230301],[8.590174,42.163885],[8.741329,42.040912],[8.5977,41.953238],[8.64145,41.909889],[8.803133,41.891381],[8.717242,41.722775],[8.914508,41.689724],[8.793077,41.629554],[8.788535,41.557736],[9.082201,41.441974],[9.219679,41.368212],[9.327205,41.616357],[9.387491,41.657359],[9.402271,41.858702]]]},"properties":{"code":"94","nom":"Corse","taux":"6.2"}} {"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[9.402271,41.858702],[9.412569,41.952476],[9.54998,42.104166],[9.558829,42.285265],[9.533196,42.545947],[9.449194,42.66224],[9.492385,42.8051],[9.463558,42.986401],[9.340873,42.994465],[9.311015,42.834679],[9.344478,42.73781],[9.085764,42.714609],[9.020694,42.644273],[8.886527,42.628966],[8.666509,42.515224],[8.674792,42.476243],[8.555885,42.36475],[8.689105,42.263528],[8.570341,42.230301],[8.590174,42.163885],[8.741329,42.040912],[8.5977,41.953238],[8.64145,41.909889],[8.803133,41.891381],[8.717242,41.722775],[8.914508,41.689724],[8.793077,41.629554],[8.788535,41.557736],[9.082201,41.441974],[9.219679,41.368212],[9.327205,41.616357],[9.387491,41.657359],[9.402271,41.858702]]]},"properties":{"code":"94","nom":"Corse","taux":"6.2"}}
],"_umap_options": {"displayOnLoad": true,"browsable": true,"name": "Taux de chômage","labelKey": "{nom} ({taux})","type": "Choropleth","choropleth": {"property": "taux"}}} ]}

View file

@ -47,7 +47,7 @@
} }
} }
], ],
"_umap_options": { "metadata": {
"displayOnLoad": true, "displayOnLoad": true,
"inCaption": true, "inCaption": true,
"browsable": true, "browsable": true,
@ -71,7 +71,7 @@
} }
} }
], ],
"_umap_options": { "metadata": {
"displayOnLoad": false, "displayOnLoad": false,
"inCaption": true, "inCaption": true,
"browsable": true, "browsable": true,

View file

@ -203,7 +203,7 @@
"id": "c1160" "id": "c1160"
} }
], ],
"_umap_options": { "metadata": {
"displayOnLoad": true, "displayOnLoad": true,
"inCaption": true, "inCaption": true,
"browsable": true, "browsable": true,

View file

@ -98,16 +98,16 @@
50.6269 50.6269
] ]
}, },
"metadata": {
"color": "Orange"
},
"properties": { "properties": {
"_umap_options": {
"color": "Orange"
},
"name": "Lille", "name": "Lille",
"description": "une ville" "description": "une ville"
} }
} }
], ],
"_umap_options": { "metadata": {
"displayOnLoad": true, "displayOnLoad": true,
"name": "Cities", "name": "Cities",
"id": 108, "id": 108,
@ -152,15 +152,15 @@
] ]
] ]
}, },
"metadata": {
"weight": "4"
},
"properties": { "properties": {
"_umap_options": {
"weight": "4"
},
"name": "tunnel sous la Manche" "name": "tunnel sous la Manche"
} }
} }
], ],
"_umap_options": { "metadata": {
"displayOnLoad": true, "displayOnLoad": true,
"name": "Tunnels", "name": "Tunnels",
"id": 109, "id": 109,

View file

@ -9,4 +9,4 @@ def save_and_get_json(page):
with page.expect_response(re.compile(r".*/datalayer/create/.*")): with page.expect_response(re.compile(r".*/datalayer/create/.*")):
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
datalayer = DataLayer.objects.last() datalayer = DataLayer.objects.last()
return json.loads(Path(datalayer.geojson.path).read_text()) return json.loads(Path(datalayer.data.path).read_text())

View file

@ -55,17 +55,12 @@ DATALAYER_DATA = {
}, },
}, },
], ],
"_umap_options": {
"displayOnLoad": True,
"browsable": True,
"name": "Calque 1",
},
} }
@pytest.fixture @pytest.fixture
def bootstrap(map, live_server): def bootstrap(map, live_server):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)
@ -113,7 +108,7 @@ def test_data_browser_should_be_filterable(live_server, page, bootstrap, map):
def test_filter_uses_layer_setting_if_any(live_server, page, bootstrap, map): def test_filter_uses_layer_setting_if_any(live_server, page, bootstrap, map):
datalayer = map.datalayer_set.first() datalayer = map.datalayer_set.first()
datalayer.settings["labelKey"] = "foo" datalayer.metadata["labelKey"] = "foo"
datalayer.save() datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_title("Features in this layer: 3")).to_be_visible() expect(page.get_by_title("Features in this layer: 3")).to_be_visible()
@ -149,11 +144,10 @@ def test_filter_uses_layer_setting_if_any(live_server, page, bootstrap, map):
def test_filter_works_with_variable_in_labelKey(live_server, page, map): def test_filter_works_with_variable_in_labelKey(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.save() map.save()
data = deepcopy(DATALAYER_DATA) data = deepcopy(DATALAYER_DATA)
data["_umap_options"]["labelKey"] = "{name} ({bar})" DataLayerFactory(map=map, data=data, metadata={"labelKey": "{name} ({bar})"})
DataLayerFactory(map=map, data=data)
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_title("Features in this layer: 3")).to_be_visible() expect(page.get_by_title("Features in this layer: 3")).to_be_visible()
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
@ -277,7 +271,7 @@ def test_data_browser_bbox_filtered_is_clickable(live_server, page, bootstrap, m
def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map): def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map):
# Include a variable # Include a variable
map.settings["properties"]["labelKey"] = "{name} ({foo})" map.metadata["labelKey"] = "{name} ({foo})"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_text("one point in france (point)")).to_be_visible() expect(page.get_by_text("one point in france (point)")).to_be_visible()
@ -299,7 +293,7 @@ def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map):
def test_should_sort_features_in_natural_order(live_server, map, page): def test_should_sort_features_in_natural_order(live_server, map, page):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.save() map.save()
datalayer_data = deepcopy(DATALAYER_DATA) datalayer_data = deepcopy(DATALAYER_DATA)
datalayer_data["features"][0]["properties"]["name"] = "9. a marker" datalayer_data["features"][0]["properties"]["name"] = "9. a marker"
@ -331,8 +325,8 @@ def test_should_redraw_list_on_feature_delete(live_server, openmap, page, bootst
def test_should_show_header_for_display_on_load_false( def test_should_show_header_for_display_on_load_false(
live_server, page, bootstrap, map, datalayer live_server, page, bootstrap, map, datalayer
): ):
datalayer.settings["displayOnLoad"] = False datalayer.metadata["displayOnLoad"] = False
datalayer.settings["name"] = "This layer is not loaded" datalayer.metadata["name"] = "This layer is not loaded"
datalayer.save() datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
browser = page.locator(".umap-browser") browser = page.locator(".umap-browser")
@ -341,8 +335,8 @@ def test_should_show_header_for_display_on_load_false(
def test_should_use_color_variable(live_server, map, page): def test_should_use_color_variable(live_server, map, page):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.settings["properties"]["color"] = "{mycolor}" map.metadata["color"] = "{mycolor}"
map.save() map.save()
datalayer_data = deepcopy(DATALAYER_DATA) datalayer_data = deepcopy(DATALAYER_DATA)
datalayer_data["features"][0]["properties"]["mycolor"] = "DarkRed" datalayer_data["features"][0]["properties"]["mycolor"] = "DarkRed"

View file

@ -9,13 +9,13 @@ pytestmark = pytest.mark.django_db
def test_caption(live_server, page, map): def test_caption(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "caption" map.metadata["onLoadPanel"] = "caption"
map.save() map.save()
basic = DataLayerFactory(map=map, name="Basic layer") basic = DataLayerFactory(map=map, name="Basic layer")
non_loaded = DataLayerFactory( non_loaded = DataLayerFactory(
map=map, name="Non loaded", settings={"displayOnLoad": False} map=map, name="Non loaded", metadata={"displayOnLoad": False}
) )
hidden = DataLayerFactory(map=map, name="Hidden", settings={"inCaption": False}) hidden = DataLayerFactory(map=map, name="Hidden", metadata={"inCaption": False})
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
panel = page.locator(".panel.left.on") panel = page.locator(".panel.left.on")
expect(panel).to_have_class(re.compile(".*condensed.*")) expect(panel).to_have_class(re.compile(".*condensed.*"))

View file

@ -1,4 +1,5 @@
import json import json
from copy import deepcopy
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -8,11 +9,24 @@ from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
METADATA = {
"displayOnLoad": True,
"inCaption": True,
"browsable": True,
"name": "Grisy-sur-Seine",
"id": "769b2bb0-920d-4531-8055-dd198a33456a",
"type": "Categorized",
"weight": 3,
"opacity": 0.9,
"categorized": {"property": "highway"},
"popupContentTemplate": "# {name}\n{highway}",
}
def test_basic_categorized_map_with_default_color(map, live_server, page): def test_basic_categorized_map_with_default_color(map, live_server, page):
path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson" path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson"
data = json.loads(path.read_text()) data = json.loads(path.read_text())
DataLayerFactory(data=data, map=map) DataLayerFactory(data=data, map=map, metadata=METADATA)
page.goto(f"{live_server.url}{map.get_absolute_url()}#13/48.4378/3.3043") page.goto(f"{live_server.url}{map.get_absolute_url()}#13/48.4378/3.3043")
# residential # residential
expect(page.locator("path[stroke='#7fc97f']")).to_have_count(5) expect(page.locator("path[stroke='#7fc97f']")).to_have_count(5)
@ -33,8 +47,9 @@ def test_basic_categorized_map_with_custom_brewer(openmap, live_server, page):
data = json.loads(path.read_text()) data = json.loads(path.read_text())
# Change brewer at load # Change brewer at load
data["_umap_options"]["categorized"]["brewer"] = "Spectral" metadata = deepcopy(METADATA)
DataLayerFactory(data=data, map=openmap) metadata["categorized"]["brewer"] = "Spectral"
DataLayerFactory(data=data, map=openmap, metadata=metadata)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043") page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043")
# residential # residential
@ -76,11 +91,12 @@ def test_basic_categorized_map_with_custom_categories(openmap, live_server, page
data = json.loads(path.read_text()) data = json.loads(path.read_text())
# Change categories at load # Change categories at load
data["_umap_options"]["categorized"]["categories"] = ( metadata = deepcopy(METADATA)
metadata["categorized"]["categories"] = (
"unclassified,track,service,residential,tertiary,secondary" "unclassified,track,service,residential,tertiary,secondary"
) )
data["_umap_options"]["categorized"]["mode"] = "manual" metadata["categorized"]["mode"] = "manual"
DataLayerFactory(data=data, map=openmap) DataLayerFactory(data=data, map=openmap, metadata=metadata)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043") page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043")

View file

@ -1,4 +1,5 @@
import json import json
from copy import deepcopy
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -9,10 +10,20 @@ from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
METADATA = {
"displayOnLoad": True,
"browsable": True,
"name": "Taux de chômage",
"labelKey": "{nom} ({taux})",
"type": "Choropleth",
"choropleth": {"property": "taux"},
}
def test_basic_choropleth_map_with_default_color(map, live_server, page): def test_basic_choropleth_map_with_default_color(map, live_server, page):
path = Path(__file__).parent.parent / "fixtures/choropleth_region_chomage.geojson" path = Path(__file__).parent.parent / "fixtures/choropleth_region_chomage.geojson"
data = json.loads(path.read_text()) data = json.loads(path.read_text())
DataLayerFactory(data=data, map=map) DataLayerFactory(data=data, map=map, metadata=METADATA)
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
# Hauts-de-France # Hauts-de-France
expect(page.locator("path[fill='#08519c']")).to_have_count(1) expect(page.locator("path[fill='#08519c']")).to_have_count(1)
@ -31,8 +42,13 @@ def test_basic_choropleth_map_with_custom_brewer(openmap, live_server, page):
data = json.loads(path.read_text()) data = json.loads(path.read_text())
# Change brewer at load # Change brewer at load
data["_umap_options"]["choropleth"]["brewer"] = "Reds" metadata = deepcopy(METADATA)
DataLayerFactory(data=data, map=openmap) metadata["choropleth"]["brewer"] = "Reds"
DataLayerFactory(
data=data,
map=openmap,
metadata=metadata,
)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}") page.goto(f"{live_server.url}{openmap.get_absolute_url()}")
# Hauts-de-France # Hauts-de-France
@ -70,8 +86,9 @@ def test_basic_choropleth_map_with_custom_classes(openmap, live_server, page):
data = json.loads(path.read_text()) data = json.loads(path.read_text())
# Change brewer at load # Change brewer at load
data["_umap_options"]["choropleth"]["classes"] = 6 metadata = deepcopy(METADATA)
DataLayerFactory(data=data, map=openmap) metadata["choropleth"]["classes"] = 6
DataLayerFactory(data=data, map=openmap, metadata=metadata)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}") page.goto(f"{live_server.url}{openmap.get_absolute_url()}")

View file

@ -39,9 +39,6 @@ DATALAYER_DATA1 = {
"geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]}, "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
}, },
], ],
"_umap_options": {
"name": "Calque 1",
},
} }
@ -70,14 +67,11 @@ DATALAYER_DATA2 = {
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]}, "geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
}, },
], ],
"_umap_options": {
"name": "Calque 2",
},
} }
def test_simple_equal_rule_at_load(live_server, page, map): def test_simple_equal_rule_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "mytype=odd", "options": {"color": "aliceblue"}} {"condition": "mytype=odd", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -91,7 +85,7 @@ def test_simple_equal_rule_at_load(live_server, page, map):
def test_simple_not_equal_rule_at_load(live_server, page, map): def test_simple_not_equal_rule_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "mytype!=even", "options": {"color": "aliceblue"}} {"condition": "mytype!=even", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -105,7 +99,7 @@ def test_simple_not_equal_rule_at_load(live_server, page, map):
def test_gt_rule_with_number_at_load(live_server, page, map): def test_gt_rule_with_number_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "mynumber>10", "options": {"color": "aliceblue"}} {"condition": "mynumber>10", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -119,7 +113,7 @@ def test_gt_rule_with_number_at_load(live_server, page, map):
def test_lt_rule_with_number_at_load(live_server, page, map): def test_lt_rule_with_number_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "mynumber<14", "options": {"color": "aliceblue"}} {"condition": "mynumber<14", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -133,7 +127,7 @@ def test_lt_rule_with_number_at_load(live_server, page, map):
def test_lt_rule_with_float_at_load(live_server, page, map): def test_lt_rule_with_float_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "mynumber<12.3", "options": {"color": "aliceblue"}} {"condition": "mynumber<12.3", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -147,7 +141,7 @@ def test_lt_rule_with_float_at_load(live_server, page, map):
def test_equal_rule_with_boolean_at_load(live_server, page, map): def test_equal_rule_with_boolean_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [ map.metadata["rules"] = [
{"condition": "myboolean=true", "options": {"color": "aliceblue"}} {"condition": "myboolean=true", "options": {"color": "aliceblue"}}
] ]
map.save() map.save()
@ -179,7 +173,7 @@ def test_can_create_new_rule(live_server, page, openmap):
def test_can_deactive_rule_from_list(live_server, page, openmap): def test_can_deactive_rule_from_list(live_server, page, openmap):
openmap.settings["properties"]["rules"] = [ openmap.metadata["rules"] = [
{"condition": "mytype=odd", "options": {"color": "aliceblue"}} {"condition": "mytype=odd", "options": {"color": "aliceblue"}}
] ]
openmap.save() openmap.save()

View file

@ -1,8 +1,6 @@
import json
import re import re
import pytest import pytest
from django.core.files.base import ContentFile
from playwright.sync_api import expect from playwright.sync_api import expect
from ..base import DataLayerFactory from ..base import DataLayerFactory
@ -10,17 +8,13 @@ from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
def set_options(datalayer, **options): def set_metadata(datalayer, **options):
# For now we need to change both the DB and the FS… datalayer.metadata.update(options)
datalayer.settings.update(options)
data = json.load(datalayer.geojson.file)
data["_umap_options"].update(**options)
datalayer.geojson = ContentFile(json.dumps(data), "foo.json")
datalayer.save() datalayer.save()
def test_honour_displayOnLoad_false(map, live_server, datalayer, page): def test_honour_displayOnLoad_false(map, live_server, datalayer, page):
set_options(datalayer, displayOnLoad=False) set_metadata(datalayer, displayOnLoad=False)
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
expect(page.locator(".leaflet-marker-icon")).to_be_hidden() expect(page.locator(".leaflet-marker-icon")).to_be_hidden()
layers = page.locator(".umap-browser .datalayer") layers = page.locator(".umap-browser .datalayer")
@ -37,7 +31,7 @@ def test_honour_displayOnLoad_false(map, live_server, datalayer, page):
def test_should_honour_fromZoom(live_server, map, datalayer, page): def test_should_honour_fromZoom(live_server, map, datalayer, page):
set_options(datalayer, displayOnLoad=True, fromZoom=6) set_metadata(datalayer, displayOnLoad=True, fromZoom=6)
page.goto(f"{live_server.url}{map.get_absolute_url()}#5/48.55/14.68") page.goto(f"{live_server.url}{map.get_absolute_url()}#5/48.55/14.68")
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
expect(markers).to_be_hidden() expect(markers).to_be_hidden()
@ -55,7 +49,7 @@ def test_should_honour_fromZoom(live_server, map, datalayer, page):
def test_should_honour_toZoom(live_server, map, datalayer, page): def test_should_honour_toZoom(live_server, map, datalayer, page):
set_options(datalayer, displayOnLoad=True, toZoom=6) set_metadata(datalayer, displayOnLoad=True, toZoom=6)
page.goto(f"{live_server.url}{map.get_absolute_url()}#7/48.55/14.68") page.goto(f"{live_server.url}{map.get_absolute_url()}#7/48.55/14.68")
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
expect(markers).to_be_hidden() expect(markers).to_be_hidden()
@ -99,13 +93,15 @@ def test_should_honour_color_variable(live_server, map, page):
}, },
}, },
], ],
"_umap_options": { }
"name": "Calque 2", DataLayerFactory(
map=map,
data=data,
metadata={
"color": "{mycolor}", "color": "{mycolor}",
"fillColor": "{mycolor}", "fillColor": "{mycolor}",
}, },
} )
DataLayerFactory(map=map, data=data)
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-overlay-pane path[fill='tomato']")) expect(page.locator(".leaflet-overlay-pane path[fill='tomato']"))
markers = page.locator(".leaflet-marker-icon .icon_container") markers = page.locator(".leaflet-marker-icon .icon_container")
@ -113,10 +109,10 @@ def test_should_honour_color_variable(live_server, map, page):
def test_datalayers_in_query_string(live_server, datalayer, map, page): def test_datalayers_in_query_string(live_server, datalayer, map, page):
map.settings["properties"]["onLoadPanel"] = "datalayers" map.metadata["onLoadPanel"] = "datalayers"
map.save() map.save()
with_old_id = DataLayerFactory(old_id=134, map=map, name="with old id") with_old_id = DataLayerFactory(old_id=134, map=map, name="with old id")
set_options(with_old_id, name="with old id") set_metadata(with_old_id, name="with old id")
visible = page.locator(".umap-browser .datalayer:not(.off) .datalayer-name") visible = page.locator(".umap-browser .datalayer:not(.off) .datalayer-name")
hidden = page.locator(".umap-browser .datalayer.off .datalayer-name") hidden = page.locator(".umap-browser .datalayer.off .datalayer-name")
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")

View file

@ -99,16 +99,15 @@ def test_should_follow_datalayer_style_when_changing_datalayer(
live_server, openmap, page live_server, openmap, page
): ):
data = deepcopy(DATALAYER_DATA) data = deepcopy(DATALAYER_DATA)
data["_umap_options"] = {"color": "DarkCyan"} DataLayerFactory(map=openmap, data=data, metadata={"color": "DarkCyan"})
DataLayerFactory(map=openmap, data=data)
DataLayerFactory( DataLayerFactory(
map=openmap, map=openmap,
name="other datalayer", name="other datalayer",
data={ data={
"type": "FeatureCollection", "type": "FeatureCollection",
"features": [], "features": [],
"_umap_options": {"color": "DarkViolet"},
}, },
metadata={"color": "DarkViolet"},
) )
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
marker = page.locator(".leaflet-marker-icon .icon_container") marker = page.locator(".leaflet-marker-icon .icon_container")

View file

@ -1,6 +1,7 @@
import platform import platform
import pytest import pytest
from django.contrib.gis.geos import Point
from playwright.sync_api import expect from playwright.sync_api import expect
from ..base import DataLayerFactory from ..base import DataLayerFactory
@ -37,11 +38,8 @@ DATALAYER_DATA = {
@pytest.fixture @pytest.fixture
def bootstrap(map, live_server): def bootstrap(map, live_server):
map.settings["properties"]["zoom"] = 6 map.zoom = 6
map.settings["geometry"] = { map.center = Point(8.429, 53.239)
"type": "Point",
"coordinates": [8.429, 53.239],
}
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)
@ -124,7 +122,7 @@ def test_should_reset_style_on_cancel(live_server, openmap, page, bootstrap):
def test_can_change_datalayer(live_server, openmap, page, bootstrap): def test_can_change_datalayer(live_server, openmap, page, bootstrap):
other = DataLayerFactory( other = DataLayerFactory(
name="Layer 2", map=openmap, settings={"color": "GoldenRod"} name="Layer 2", map=openmap, metadata={"color": "GoldenRod"}
) )
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
expect(page.locator("path[fill='DarkBlue']")).to_have_count(1) expect(page.locator("path[fill='DarkBlue']")).to_have_count(1)

View file

@ -34,10 +34,10 @@ DATALAYER_DATA = {
}, },
{ {
"type": "Feature", "type": "Feature",
"metadata": {
"color": "OliveDrab",
},
"properties": { "properties": {
"_umap_options": {
"color": "OliveDrab",
},
"name": "test", "name": "test",
"description": "Some description", "description": "Some description",
}, },
@ -49,11 +49,11 @@ DATALAYER_DATA = {
}, },
{ {
"type": "Feature", "type": "Feature",
"metadata": {
"fill": False,
"opacity": 0.6,
},
"properties": { "properties": {
"_umap_options": {
"fill": False,
"opacity": 0.6,
},
"name": "test", "name": "test",
}, },
"id": "YwMTM", "id": "YwMTM",
@ -76,7 +76,7 @@ DATALAYER_DATA = {
@pytest.fixture @pytest.fixture
def bootstrap(map, live_server): def bootstrap(map, live_server):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)
@ -94,13 +94,9 @@ def test_umap_export(map, live_server, bootstrap, page):
downloaded = json.loads(path.read_text()) downloaded = json.loads(path.read_text())
del downloaded["uri"] # Port changes at each run del downloaded["uri"] # Port changes at each run
assert downloaded == { assert downloaded == {
"geometry": {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
},
"layers": [ "layers": [
{ {
"_umap_options": { "metadata": {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
@ -131,8 +127,8 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "Point", "type": "Point",
}, },
"id": "QwNjg", "id": "QwNjg",
"metadata": {"color": "OliveDrab"},
"properties": { "properties": {
"_umap_options": {"color": "OliveDrab"},
"name": "test", "name": "test",
"description": "Some description", "description": "Some description",
}, },
@ -152,8 +148,8 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "LineString", "type": "LineString",
}, },
"id": "YwMTM", "id": "YwMTM",
"metadata": {"fill": False, "opacity": 0.6},
"properties": { "properties": {
"_umap_options": {"fill": False, "opacity": 0.6},
"name": "test", "name": "test",
}, },
"type": "Feature", "type": "Feature",
@ -162,7 +158,11 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "FeatureCollection", "type": "FeatureCollection",
} }
], ],
"properties": { "metadata": {
"geometry": {
"type": "Point",
"coordinates": [13.447265624999998, 48.94415123418794],
},
"datalayersControl": True, "datalayersControl": True,
"description": "Which is just the Danube, at the end", "description": "Which is just the Danube, at the end",
"displayPopupFooter": False, "displayPopupFooter": False,
@ -234,7 +234,7 @@ def test_kml_export(map, live_server, bootstrap, page):
download.save_as(path) download.save_as(path)
assert ( assert (
path.read_text() path.read_text()
== """<kml xmlns="http://www.opengis.net/kml/2.2"><Document>\n<Placemark id="gyNzM">\n<name>name poly</name><ExtendedData></ExtendedData>\n <Polygon>\n<outerBoundaryIs>\n <LinearRing><coordinates>11.25,53.585984\n10.151367,52.975108\n12.689209,52.167194\n14.084473,53.199452\n12.634277,53.618579\n11.25,53.585984\n11.25,53.585984</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>\n<Placemark id="QwNjg">\n<name>test</name><description>Some description</description><ExtendedData>\n <Data name="_umap_options"><value>{"color":"OliveDrab"}</value></Data></ExtendedData>\n <Point><coordinates>-0.274658,52.57635</coordinates></Point></Placemark>\n<Placemark id="YwMTM">\n<name>test</name><ExtendedData>\n <Data name="_umap_options"><value>{"fill":false,"opacity":0.6}</value></Data></ExtendedData>\n <LineString><coordinates>-0.571289,54.476422\n0.439453,54.610255\n1.724854,53.448807\n4.163818,53.988395\n5.306396,53.533778\n6.591797,53.709714\n7.042236,53.350551</coordinates></LineString></Placemark></Document></kml>""" == """<kml xmlns="http://www.opengis.net/kml/2.2"><Document>\n<Placemark id="gyNzM">\n<name>name poly</name><ExtendedData></ExtendedData>\n <Polygon>\n<outerBoundaryIs>\n <LinearRing><coordinates>11.25,53.585984\n10.151367,52.975108\n12.689209,52.167194\n14.084473,53.199452\n12.634277,53.618579\n11.25,53.585984\n11.25,53.585984</coordinates></LinearRing></outerBoundaryIs></Polygon></Placemark>\n<Placemark id="QwNjg">\n<name>test</name><description>Some description</description><ExtendedData></ExtendedData>\n <Point><coordinates>-0.274658,52.57635</coordinates></Point></Placemark>\n<Placemark id="YwMTM">\n<name>test</name><ExtendedData></ExtendedData>\n <LineString><coordinates>-0.571289,54.476422\n0.439453,54.610255\n1.724854,53.448807\n4.163818,53.988395\n5.306396,53.533778\n6.591797,53.709714\n7.042236,53.350551</coordinates></LineString></Placemark></Document></kml>"""
) )
@ -272,8 +272,8 @@ def test_geojson_export(map, live_server, bootstrap, page):
{ {
"geometry": {"coordinates": [-0.274658, 52.57635], "type": "Point"}, "geometry": {"coordinates": [-0.274658, 52.57635], "type": "Point"},
"id": "QwNjg", "id": "QwNjg",
"metadata": {"color": "OliveDrab"},
"properties": { "properties": {
"_umap_options": {"color": "OliveDrab"},
"name": "test", "name": "test",
"description": "Some description", "description": "Some description",
}, },
@ -293,8 +293,8 @@ def test_geojson_export(map, live_server, bootstrap, page):
"type": "LineString", "type": "LineString",
}, },
"id": "YwMTM", "id": "YwMTM",
"metadata": {"fill": False, "opacity": 0.6},
"properties": { "properties": {
"_umap_options": {"fill": False, "opacity": 0.6},
"name": "test", "name": "test",
}, },
"type": "Feature", "type": "Feature",

View file

@ -33,9 +33,6 @@ DATALAYER_DATA1 = {
"geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]}, "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
}, },
], ],
"_umap_options": {
"name": "Calque 1",
},
} }
@ -63,9 +60,6 @@ DATALAYER_DATA2 = {
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]}, "geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
}, },
], ],
"_umap_options": {
"name": "Calque 2",
},
} }
@ -89,18 +83,17 @@ DATALAYER_DATA3 = {
}, },
}, },
], ],
"_umap_options": {"name": "Calque 2", "browsable": False},
} }
def test_simple_facet_search(live_server, page, map): def test_simple_facet_search(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "datafilters" map.metadata["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" map.metadata["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.settings["properties"]["showLabel"] = True map.metadata["showLabel"] = True
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA1)
DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA2)
DataLayerFactory(map=map, data=DATALAYER_DATA3) DataLayerFactory(map=map, data=DATALAYER_DATA3, metadata={"browsable": False})
page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670") page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
panel = page.locator(".panel.left.on") panel = page.locator(".panel.left.on")
expect(panel).to_have_class(re.compile(".*expanded.*")) expect(panel).to_have_class(re.compile(".*expanded.*"))
@ -170,8 +163,8 @@ def test_simple_facet_search(live_server, page, map):
def test_date_facet_search(live_server, page, map): def test_date_facet_search(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "datafilters" map.metadata["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mydate|Date filter|date" map.metadata["facetKey"] = "mydate|Date filter|date"
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA1)
DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA2)
@ -188,8 +181,8 @@ def test_date_facet_search(live_server, page, map):
def test_choice_with_empty_value(live_server, page, map): def test_choice_with_empty_value(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "datafilters" map.metadata["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type" map.metadata["facetKey"] = "mytype|My type"
map.save() map.save()
data = copy.deepcopy(DATALAYER_DATA1) data = copy.deepcopy(DATALAYER_DATA1)
data["features"][0]["properties"]["mytype"] = "" data["features"][0]["properties"]["mytype"] = ""
@ -205,8 +198,8 @@ def test_choice_with_empty_value(live_server, page, map):
def test_number_with_zero_value(live_server, page, map): def test_number_with_zero_value(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "datafilters" map.metadata["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mynumber|Filter|number" map.metadata["facetKey"] = "mynumber|Filter|number"
map.save() map.save()
data = copy.deepcopy(DATALAYER_DATA1) data = copy.deepcopy(DATALAYER_DATA1)
data["features"][0]["properties"]["mynumber"] = 0 data["features"][0]["properties"]["mynumber"] = 0
@ -222,8 +215,8 @@ def test_number_with_zero_value(live_server, page, map):
def test_facets_search_are_persistent_when_closing_panel(live_server, page, map): def test_facets_search_are_persistent_when_closing_panel(live_server, page, map):
map.settings["properties"]["onLoadPanel"] = "datafilters" map.metadata["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number" map.metadata["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1) DataLayerFactory(map=map, data=DATALAYER_DATA1)
DataLayerFactory(map=map, data=DATALAYER_DATA2) DataLayerFactory(map=map, data=DATALAYER_DATA2)

View file

@ -348,7 +348,7 @@ def test_should_remove_dot_in_property_names(live_server, page, settings, tilela
with page.expect_response(re.compile(r".*/datalayer/create/.*")): with page.expect_response(re.compile(r".*/datalayer/create/.*")):
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
datalayer = DataLayer.objects.last() datalayer = DataLayer.objects.last()
saved_data = json.loads(Path(datalayer.geojson.path).read_text()) saved_data = json.loads(Path(datalayer.data.path).read_text())
assert saved_data["features"][0]["properties"] == { assert saved_data["features"][0]["properties"] == {
"color": "", "color": "",
"name": "Chez Rémy", "name": "Chez Rémy",

View file

@ -14,7 +14,7 @@ def test_preconnect_for_tilelayer(map, page, live_server, tilelayer):
expect(meta).to_have_count(1) expect(meta).to_have_count(1)
expect(meta).to_have_attribute("href", "//a.tile.openstreetmap.fr") expect(meta).to_have_attribute("href", "//a.tile.openstreetmap.fr")
# Add custom tilelayer # Add custom tilelayer
map.settings["properties"]["tilelayer"] = { map.metadata["tilelayer"] = {
"name": "OSM Piano FR", "name": "OSM Piano FR",
"maxZoom": 20, "maxZoom": 20,
"minZoom": 0, "minZoom": 0,
@ -25,7 +25,7 @@ def test_preconnect_for_tilelayer(map, page, live_server, tilelayer):
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(meta).to_have_attribute("href", "//a.piano.tiles.quaidorsay.fr") expect(meta).to_have_attribute("href", "//a.piano.tiles.quaidorsay.fr")
# Add custom tilelayer with variable in domain, should create a preconnect # Add custom tilelayer with variable in domain, should create a preconnect
map.settings["properties"]["tilelayer"] = { map.metadata["tilelayer"] = {
"name": "OSM Piano FR", "name": "OSM Piano FR",
"maxZoom": 20, "maxZoom": 20,
"minZoom": 0, "minZoom": 0,
@ -40,7 +40,7 @@ def test_preconnect_for_tilelayer(map, page, live_server, tilelayer):
def test_default_view_without_datalayer_should_use_default_center( def test_default_view_without_datalayer_should_use_default_center(
map, live_server, datalayer, page map, live_server, datalayer, page
): ):
datalayer.settings["displayOnLoad"] = False datalayer.metadata["displayOnLoad"] = False
datalayer.save() datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# Hash is defined, so map is initialized # Hash is defined, so map is initialized
@ -52,9 +52,9 @@ def test_default_view_without_datalayer_should_use_default_center(
def test_default_view_latest_without_datalayer_should_use_default_center( def test_default_view_latest_without_datalayer_should_use_default_center(
map, live_server, datalayer, page map, live_server, datalayer, page
): ):
datalayer.settings["displayOnLoad"] = False datalayer.metadata["displayOnLoad"] = False
datalayer.save() datalayer.save()
map.settings["properties"]["defaultView"] = "latest" map.metadata["defaultView"] = "latest"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# Hash is defined, so map is initialized # Hash is defined, so map is initialized
@ -66,9 +66,9 @@ def test_default_view_latest_without_datalayer_should_use_default_center(
def test_default_view_data_without_datalayer_should_use_default_center( def test_default_view_data_without_datalayer_should_use_default_center(
map, live_server, datalayer, page map, live_server, datalayer, page
): ):
datalayer.settings["displayOnLoad"] = False datalayer.metadata["displayOnLoad"] = False
datalayer.save() datalayer.save()
map.settings["properties"]["defaultView"] = "data" map.metadata["defaultView"] = "data"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# Hash is defined, so map is initialized # Hash is defined, so map is initialized
@ -78,7 +78,7 @@ def test_default_view_data_without_datalayer_should_use_default_center(
def test_default_view_latest_with_marker(map, live_server, datalayer, page): def test_default_view_latest_with_marker(map, live_server, datalayer, page):
map.settings["properties"]["defaultView"] = "latest" map.metadata["defaultView"] = "latest"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# Hash is defined, so map is initialized # Hash is defined, so map is initialized
@ -108,7 +108,7 @@ def test_default_view_latest_with_line(map, live_server, page):
], ],
} }
DataLayerFactory(map=map, data=data) DataLayerFactory(map=map, data=data)
map.settings["properties"]["defaultView"] = "latest" map.metadata["defaultView"] = "latest"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
expect(page).to_have_url(re.compile(r".*#8/48\..+/2\..+")) expect(page).to_have_url(re.compile(r".*#8/48\..+/2\..+"))
@ -139,7 +139,7 @@ def test_default_view_latest_with_polygon(map, live_server, page):
], ],
} }
DataLayerFactory(map=map, data=data) DataLayerFactory(map=map, data=data)
map.settings["properties"]["defaultView"] = "latest" map.metadata["defaultView"] = "latest"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers") page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
expect(page).to_have_url(re.compile(r".*#8/48\..+/2\..+")) expect(page).to_have_url(re.compile(r".*#8/48\..+/2\..+"))
@ -152,7 +152,7 @@ def test_default_view_locate(browser, live_server, map):
geolocation={"longitude": 8.52967, "latitude": 39.16267}, geolocation={"longitude": 8.52967, "latitude": 39.16267},
permissions=["geolocation"], permissions=["geolocation"],
) )
map.settings["properties"]["defaultView"] = "locate" map.metadata["defaultView"] = "locate"
map.save() map.save()
page = context.new_page() page = context.new_page()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
@ -162,7 +162,7 @@ def test_default_view_locate(browser, live_server, map):
def test_remote_layer_should_not_be_used_as_datalayer_for_created_features( def test_remote_layer_should_not_be_used_as_datalayer_for_created_features(
openmap, live_server, datalayer, page openmap, live_server, datalayer, page
): ):
datalayer.settings["remoteData"] = { datalayer.metadata["remoteData"] = {
"url": "https://overpass-api.de/api/interpreter?data=[out:xml];node[harbour=yes]({south},{west},{north},{east});out body;", "url": "https://overpass-api.de/api/interpreter?data=[out:xml];node[harbour=yes]({south},{west},{north},{east});out body;",
"format": "osm", "format": "osm",
"from": "10", "from": "10",
@ -191,7 +191,7 @@ def test_remote_layer_should_not_be_used_as_datalayer_for_created_features(
def test_minimap_on_load(map, live_server, datalayer, page): def test_minimap_on_load(map, live_server, datalayer, page):
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-minimap")).to_be_hidden() expect(page.locator(".leaflet-control-minimap")).to_be_hidden()
map.settings["properties"]["miniMap"] = True map.metadata["miniMap"] = True
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-minimap")).to_be_visible() expect(page.locator(".leaflet-control-minimap")).to_be_visible()
@ -200,7 +200,7 @@ def test_minimap_on_load(map, live_server, datalayer, page):
def test_zoom_control_on_load(map, live_server, page): def test_zoom_control_on_load(map, live_server, page):
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-zoom")).to_be_visible() expect(page.locator(".leaflet-control-zoom")).to_be_visible()
map.settings["properties"]["zoomControl"] = False map.metadata["zoomControl"] = False
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-zoom")).to_be_hidden() expect(page.locator(".leaflet-control-zoom")).to_be_hidden()
@ -209,7 +209,7 @@ def test_zoom_control_on_load(map, live_server, page):
def test_feature_in_query_string_has_precedence_over_onloadpanel( def test_feature_in_query_string_has_precedence_over_onloadpanel(
map, live_server, page map, live_server, page
): ):
map.settings["properties"]["onLoadPanel"] = "caption" map.metadata["onLoadPanel"] = "caption"
map.name = "This is my map" map.name = "This is my map"
map.save() map.save()
data = { data = {
@ -224,9 +224,8 @@ def test_feature_in_query_string_has_precedence_over_onloadpanel(
}, },
} }
], ],
"_umap_options": {"popupShape": "Panel"},
} }
DataLayerFactory(map=map, data=data) DataLayerFactory(map=map, data=data, metadata={"popupShape": "Panel"})
page.goto(f"{live_server.url}{map.get_absolute_url()}?feature=FooBar") page.goto(f"{live_server.url}{map.get_absolute_url()}?feature=FooBar")
expect(page.get_by_role("heading", name="FooBar")).to_be_visible() expect(page.get_by_role("heading", name="FooBar")).to_be_visible()
expect(page.get_by_role("heading", name="This is my map")).to_be_hidden() expect(page.get_by_role("heading", name="This is my map")).to_be_hidden()

View file

@ -5,9 +5,9 @@ pytestmark = pytest.mark.django_db
def test_should_not_render_any_control(live_server, tilelayer, page, map): def test_should_not_render_any_control(live_server, tilelayer, page, map):
map.settings["properties"]["onLoadPanel"] = "databrowser" map.metadata["onLoadPanel"] = "databrowser"
map.settings["properties"]["miniMap"] = True map.metadata["miniMap"] = True
map.settings["properties"]["captionBar"] = True map.metadata["captionBar"] = True
map.save() map.save()
# Make sure those controls are visible in normal view # Make sure those controls are visible in normal view
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")

View file

@ -43,12 +43,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
# Prevent two layers to be saved on the same second, as we compare them based # Prevent two layers to be saved on the same second, as we compare them based
# on time in case of conflict. FIXME do not use time for comparison. # on time in case of conflict. FIXME do not use time for comparison.
sleep(1) sleep(1)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"editMode": "advanced",
"inCaption": True, "inCaption": True,
"remoteData": {},
} }
# Now navigate to this map from another tab # Now navigate to this map from another tab
@ -78,12 +78,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
sleep(1) sleep(1)
# No change after the save # No change after the save
expect(marker_pane_p2).to_have_count(2) expect(marker_pane_p2).to_have_count(2)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"inCaption": True, "inCaption": True,
"editMode": "advanced", "remoteData": {},
} }
# Now create another marker in the first tab # Now create another marker in the first tab
@ -94,14 +94,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
save_p1.click() save_p1.click()
# Should now get the other marker too # Should now get the other marker too
expect(marker_pane_p1).to_have_count(3) expect(marker_pane_p1).to_have_count(3)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"inCaption": True, "inCaption": True,
"editMode": "advanced", "remoteData": {},
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
} }
# And again # And again
@ -112,14 +110,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
save_p1.click() save_p1.click()
sleep(1) sleep(1)
# Should now get the other marker too # Should now get the other marker too
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"inCaption": True, "inCaption": True,
"editMode": "advanced", "remoteData": {},
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
} }
expect(marker_pane_p1).to_have_count(4) expect(marker_pane_p1).to_have_count(4)
@ -132,14 +128,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
save_p2.click() save_p2.click()
sleep(1) sleep(1)
# Should now get the other markers too # Should now get the other markers too
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"inCaption": True, "inCaption": True,
"editMode": "advanced", "remoteData": {},
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
} }
expect(marker_pane_p2).to_have_count(5) expect(marker_pane_p2).to_have_count(5)
@ -258,14 +252,12 @@ def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
# Should now get the other marker too # Should now get the other marker too
expect(marker_pane_p1).to_have_count(3) expect(marker_pane_p1).to_have_count(3)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"inCaption": True, "inCaption": True,
"editMode": "advanced", "remoteData": {},
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
} }
@ -286,11 +278,11 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")): with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_role("button", name="Save").click() page_two.get_by_role("button", name="Save").click()
saved = DataLayer.objects.last() saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text()) data = json.loads(Path(saved.data.path).read_text())
assert data["features"][0]["properties"]["name"] == "new name" assert data["features"][0]["properties"]["name"] == "new name"
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible() expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")): with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_text("Keep your changes and loose theirs").click() page_two.get_by_text("Keep your changes and loose theirs").click()
saved = DataLayer.objects.last() saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text()) data = json.loads(Path(saved.data.path).read_text())
assert data["features"][0]["properties"]["name"] == "custom name" assert data["features"][0]["properties"]["name"] == "custom name"

View file

@ -21,10 +21,10 @@ DATALAYER_DATA = {
"type": "Point", "type": "Point",
"coordinates": [13.68896484375, 48.55297816440071], "coordinates": [13.68896484375, 48.55297816440071],
}, },
"properties": {"_umap_options": {"color": "DarkCyan"}, "name": "Here"}, "metadata": {"color": "DarkCyan"},
"properties": {"name": "Here"},
} }
], ],
"_umap_options": {"displayOnLoad": True, "name": "FooBarFoo"},
} }
FIXTURES = Path(__file__).parent.parent / "fixtures" FIXTURES = Path(__file__).parent.parent / "fixtures"
@ -69,7 +69,7 @@ def test_can_change_picto_at_map_level(openmap, live_server, page, pictos):
def test_can_change_picto_at_datalayer_level(openmap, live_server, page, pictos): def test_can_change_picto_at_datalayer_level(openmap, live_server, page, pictos):
openmap.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg" openmap.metadata["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.save() openmap.save()
DataLayerFactory(map=openmap, data=DATALAYER_DATA) DataLayerFactory(map=openmap, data=DATALAYER_DATA)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
@ -108,7 +108,7 @@ def test_can_change_picto_at_datalayer_level(openmap, live_server, page, pictos)
def test_can_change_picto_at_marker_level(openmap, live_server, page, pictos): def test_can_change_picto_at_marker_level(openmap, live_server, page, pictos):
openmap.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg" openmap.metadata["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.save() openmap.save()
DataLayerFactory(map=openmap, data=DATALAYER_DATA) DataLayerFactory(map=openmap, data=DATALAYER_DATA)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit") page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")

View file

@ -38,7 +38,7 @@ DATALAYER_DATA = {
def test_can_use_slideshow_manually(map, live_server, page): def test_can_use_slideshow_manually(map, live_server, page):
map.settings["properties"]["slideshow"] = {"active": True, "delay": 5000} map.metadata["slideshow"] = {"active": True, "delay": 5000}
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")

View file

@ -28,9 +28,9 @@ def staticfiles(settings):
def test_javascript_have_been_loaded( def test_javascript_have_been_loaded(
map, live_server, datalayer, page, settings, staticfiles map, live_server, datalayer, page, settings, staticfiles
): ):
datalayer.settings["displayOnLoad"] = False datalayer.metadata["displayOnLoad"] = False
datalayer.save() datalayer.save()
map.settings["properties"]["defaultView"] = "latest" map.metadata["defaultView"] = "latest"
map.save() map.save()
with override("fr"): with override("fr"):
url = f"{live_server.url}{map.get_absolute_url()}" url = f"{live_server.url}{map.get_absolute_url()}"

View file

@ -59,9 +59,6 @@ DATALAYER_DATA = {
"id": "poin3", "id": "poin3",
}, },
], ],
"_umap_options": {
"name": "Calque 2",
},
} }
@ -81,7 +78,7 @@ def test_table_editor(live_server, openmap, datalayer, page):
with page.expect_response(re.compile(r".*/datalayer/update/.*")): with page.expect_response(re.compile(r".*/datalayer/update/.*")):
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
saved = DataLayer.objects.last() saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text()) data = json.loads(Path(saved.data.path).read_text())
assert data["features"][0]["properties"]["newprop"] == "newvalue" assert data["features"][0]["properties"]["newprop"] == "newvalue"
assert "name" not in data["features"][0]["properties"] assert "name" not in data["features"][0]["properties"]

View file

@ -85,8 +85,8 @@ def test_map_should_display_selected_tilelayer(map, live_server, tilelayers, pag
url_pattern = re.compile( url_pattern = re.compile(
r"https://[abc]{1}.piano.tiles.quaidorsay.fr/fr/\d+/\d+/\d+.png" r"https://[abc]{1}.piano.tiles.quaidorsay.fr/fr/\d+/\d+/\d+.png"
) )
map.settings["properties"]["tilelayer"]["url_template"] = piano.url_template map.metadata["tilelayer"]["url_template"] = piano.url_template
map.settings["properties"]["tilelayersControl"] = True map.metadata["tilelayersControl"] = True
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
tiles = page.locator(".leaflet-tile-pane img") tiles = page.locator(".leaflet-tile-pane img")
@ -101,10 +101,10 @@ def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page)
url_pattern = re.compile( url_pattern = re.compile(
r"https://[abc]{1}.basemaps.cartocdn.com/rastertiles/voyager/\d+/\d+/\d+.png" r"https://[abc]{1}.basemaps.cartocdn.com/rastertiles/voyager/\d+/\d+/\d+.png"
) )
map.settings["properties"]["tilelayer"]["url_template"] = ( map.metadata["tilelayer"]["url_template"] = (
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png" "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
) )
map.settings["properties"]["tilelayersControl"] = True map.metadata["tilelayersControl"] = True
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
tiles = page.locator(".leaflet-tile-pane img") tiles = page.locator(".leaflet-tile-pane img")
@ -115,7 +115,7 @@ def test_map_should_display_custom_tilelayer(map, live_server, tilelayers, page)
def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page): def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page):
map.settings["properties"]["tilelayer"]["attribution"] = ( map.metadata["tilelayer"]["attribution"] = (
"&copy; [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors" "&copy; [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors"
) )
map.save() map.save()
@ -125,7 +125,7 @@ def test_can_have_smart_text_in_attribution(tilelayer, map, live_server, page):
def test_map_should_display_a_more_button(map, live_server, tilelayers, page): def test_map_should_display_a_more_button(map, live_server, tilelayers, page):
map.settings["properties"]["tilelayersControl"] = True map.metadata["tilelayersControl"] = True
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
page.locator(".leaflet-iconLayers").hover() page.locator(".leaflet-iconLayers").hover()

View file

@ -58,15 +58,15 @@ def test_should_handle_locale_var_in_description(live_server, map, page):
def test_should_display_tooltip_with_variable(live_server, map, page, bootstrap): def test_should_display_tooltip_with_variable(live_server, map, page, bootstrap):
map.settings["properties"]["showLabel"] = True map.metadata["showLabel"] = True
map.settings["properties"]["labelKey"] = "Foo {name}" map.metadata["labelKey"] = "Foo {name}"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_text("Foo test marker")).to_be_visible() expect(page.get_by_text("Foo test marker")).to_be_visible()
def test_should_open_popup_panel_on_click(live_server, map, page, bootstrap): def test_should_open_popup_panel_on_click(live_server, map, page, bootstrap):
map.settings["properties"]["popupShape"] = "Panel" map.metadata["popupShape"] = "Panel"
map.save() map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
panel = page.locator(".panel.left.on") panel = page.locator(".panel.left.on")
@ -82,7 +82,7 @@ def test_should_open_popup_panel_on_click(live_server, map, page, bootstrap):
def test_extended_properties_in_popup(live_server, map, page, bootstrap): def test_extended_properties_in_popup(live_server, map, page, bootstrap):
map.settings["properties"]["popupContentTemplate"] = """ map.metadata["popupContentTemplate"] = """
Rank: {rank} Rank: {rank}
Locale: {locale} Locale: {locale}
Lang: {lang} Lang: {lang}

View file

@ -1,6 +1,7 @@
import re import re
import pytest import pytest
from django.contrib.gis.geos import Point
from playwright.sync_api import expect from playwright.sync_api import expect
from ..base import DataLayerFactory from ..base import DataLayerFactory
@ -35,11 +36,8 @@ DATALAYER_DATA = {
@pytest.fixture @pytest.fixture
def bootstrap(map, live_server): def bootstrap(map, live_server):
map.settings["properties"]["zoom"] = 6 map.zoom = 6
map.settings["geometry"] = { map.center = Point(8.429, 53.239)
"type": "Point",
"coordinates": [8.429, 53.239],
}
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)

View file

@ -1,4 +1,5 @@
import pytest import pytest
from django.contrib.gis.geos import Point
from playwright.sync_api import expect from playwright.sync_api import expect
from ..base import DataLayerFactory from ..base import DataLayerFactory
@ -27,11 +28,8 @@ DATALAYER_DATA = {
@pytest.fixture @pytest.fixture
def bootstrap(map, live_server): def bootstrap(map, live_server):
map.settings["properties"]["zoom"] = 6 map.zoom = 6
map.settings["geometry"] = { map.center = Point(8.429, 53.239)
"type": "Point",
"coordinates": [8.429, 53.239],
}
map.save() map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA) DataLayerFactory(map=map, data=DATALAYER_DATA)

View file

@ -15,7 +15,7 @@ def test_websocket_connection_can_sync_markers(
new_page, live_server, websocket_server, tilelayer new_page, live_server, websocket_server, tilelayer
): ):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True map.metadata["syncEnabled"] = True
map.save() map.save()
DataLayerFactory(map=map, data={}) DataLayerFactory(map=map, data={})
@ -80,7 +80,7 @@ def test_websocket_connection_can_sync_polygons(
context, live_server, websocket_server, tilelayer context, live_server, websocket_server, tilelayer
): ):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True map.metadata["syncEnabled"] = True
map.save() map.save()
DataLayerFactory(map=map, data={}) DataLayerFactory(map=map, data={})
@ -164,7 +164,7 @@ def test_websocket_connection_can_sync_map_properties(
context, live_server, websocket_server, tilelayer context, live_server, websocket_server, tilelayer
): ):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True map.metadata["syncEnabled"] = True
map.save() map.save()
DataLayerFactory(map=map, data={}) DataLayerFactory(map=map, data={})
@ -196,7 +196,7 @@ def test_websocket_connection_can_sync_datalayer_properties(
context, live_server, websocket_server, tilelayer context, live_server, websocket_server, tilelayer
): ):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True map.metadata["syncEnabled"] = True
map.save() map.save()
DataLayerFactory(map=map, data={}) DataLayerFactory(map=map, data={})
@ -225,7 +225,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
context, live_server, websocket_server, tilelayer context, live_server, websocket_server, tilelayer
): ):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS) map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True map.metadata["syncEnabled"] = True
map.save() map.save()
DataLayerFactory(map=map, data={}) DataLayerFactory(map=map, data={})

View file

@ -28,14 +28,14 @@ def test_upload_to(map, datalayer):
def test_save_should_use_pk_as_name(map, datalayer): def test_save_should_use_pk_as_name(map, datalayer):
assert "/{}_".format(datalayer.pk) in datalayer.geojson.name assert "/{}_".format(datalayer.pk) in datalayer.data.name
def test_same_geojson_file_name_will_be_suffixed(map, datalayer): def test_same_geojson_file_name_will_be_suffixed(map, datalayer):
before = datalayer.geojson.name before = datalayer.data.name
datalayer.geojson.save(before, ContentFile("{}")) datalayer.data.save(before, ContentFile("{}"))
assert datalayer.geojson.name != before assert datalayer.data.name != before
assert "/{}_".format(datalayer.pk) in datalayer.geojson.name assert "/{}_".format(datalayer.pk) in datalayer.data.name
def test_clone_should_return_new_instance(map, datalayer): def test_clone_should_return_new_instance(map, datalayer):
@ -57,38 +57,38 @@ def test_clone_should_update_map_if_passed(datalayer, user, licence):
def test_clone_should_clone_geojson_too(datalayer): def test_clone_should_clone_geojson_too(datalayer):
clone = datalayer.clone() clone = datalayer.clone()
assert datalayer.pk != clone.pk assert datalayer.pk != clone.pk
assert clone.geojson is not None assert clone.data is not None
assert clone.geojson.path != datalayer.geojson.path assert clone.data.path != datalayer.data.path
def test_should_remove_old_versions_on_save(map, settings): def test_should_remove_old_versions_on_save(map, settings):
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17) datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
settings.UMAP_KEEP_VERSIONS = 3 settings.UMAP_KEEP_VERSIONS = 3
root = Path(datalayer.storage_root()) root = Path(datalayer.storage_root())
before = len(datalayer.geojson.storage.listdir(root)[1]) before = len(datalayer.data.storage.listdir(root)[1])
newer = f"{datalayer.pk}_1440924889.geojson" newer = f"{datalayer.pk}_1440924889.geojson"
medium = f"{datalayer.pk}_1440923687.geojson" medium = f"{datalayer.pk}_1440923687.geojson"
older = f"{datalayer.pk}_1440918637.geojson" older = f"{datalayer.pk}_1440918637.geojson"
with_old_id = f"{datalayer.old_id}_1440918537.geojson" with_old_id = f"{datalayer.old_id}_1440918537.geojson"
other = "123456_1440918637.geojson" other = "123456_1440918637.geojson"
for path in [medium, newer, older, with_old_id, other]: for path in [medium, newer, older, with_old_id, other]:
datalayer.geojson.storage.save(root / path, ContentFile("{}")) datalayer.data.storage.save(root / path, ContentFile("{}"))
datalayer.geojson.storage.save(root / f"{path}.gz", ContentFile("{}")) datalayer.data.storage.save(root / f"{path}.gz", ContentFile("{}"))
assert len(datalayer.geojson.storage.listdir(root)[1]) == 10 + before assert len(datalayer.data.storage.listdir(root)[1]) == 10 + before
files = datalayer.geojson.storage.listdir(root)[1] files = datalayer.data.storage.listdir(root)[1]
# Those files should be present before save, which will purge them # Those files should be present before save, which will purge them
assert older in files assert older in files
assert older + ".gz" in files assert older + ".gz" in files
assert with_old_id in files assert with_old_id in files
assert with_old_id + ".gz" in files assert with_old_id + ".gz" in files
datalayer.save() datalayer.save()
files = datalayer.geojson.storage.listdir(root)[1] files = datalayer.data.storage.listdir(root)[1]
# Flat + gz files, but not latest gz, which is created at first datalayer read. # Flat + gz files, but not latest gz, which is created at first datalayer read.
# older and with_old_id should have been removed # older and with_old_id should have been removed
assert len(files) == 5 assert len(files) == 5
assert newer in files assert newer in files
assert medium in files assert medium in files
assert Path(datalayer.geojson.path).name in files assert Path(datalayer.data.path).name in files
# File from another datalayer, purge should have impacted it. # File from another datalayer, purge should have impacted it.
assert other in files assert other in files
assert other + ".gz" in files assert other + ".gz" in files
@ -97,7 +97,7 @@ def test_should_remove_old_versions_on_save(map, settings):
assert with_old_id not in files assert with_old_id not in files
assert with_old_id + ".gz" not in files assert with_old_id + ".gz" not in files
names = [v["name"] for v in datalayer.versions] names = [v["name"] for v in datalayer.versions]
assert names == [Path(datalayer.geojson.name).name, newer, medium] assert names == [Path(datalayer.data.name).name, newer, medium]
def test_anonymous_cannot_edit_in_editors_mode(datalayer): def test_anonymous_cannot_edit_in_editors_mode(datalayer):

View file

@ -21,9 +21,9 @@ def post_data():
"display_on_load": True, "display_on_load": True,
"settings": '{"displayOnLoad": true, "browsable": true, "name": "name"}', "settings": '{"displayOnLoad": true, "browsable": true, "name": "name"}',
"rank": 0, "rank": 0,
"geojson": SimpleUploadedFile( "data": SimpleUploadedFile(
"name.json", "name.json",
b'{"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"}}', b'{"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":{},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[1.8017578124999998,51.16556659836182],[-0.48339843749999994,49.710272582105695],[-3.1640625,50.0923932109388],[-5.60302734375,51.998410382390325]]},"properties":{"name":"Light line"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[0.63720703125,51.15178610143037]},"properties":{"name":"marker he"}}]}',
), ),
} }
@ -38,7 +38,6 @@ def test_get_with_public_mode(client, settings, datalayer, map):
assert response["Cache-Control"] is not None assert response["Cache-Control"] is not None
assert "Content-Encoding" not in response assert "Content-Encoding" not in response
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert "_umap_options" in j
assert "features" in j assert "features" in j
assert j["type"] == "FeatureCollection" assert j["type"] == "FeatureCollection"
@ -98,8 +97,8 @@ def test_gzip_should_be_created_if_accepted(client, datalayer, map, post_data):
url = reverse("datalayer_view", args=(map.pk, datalayer.pk)) url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
response = client.get(url, headers={"ACCEPT_ENCODING": "gzip"}) response = client.get(url, headers={"ACCEPT_ENCODING": "gzip"})
assert response.status_code == 200 assert response.status_code == 200
flat = datalayer.geojson.path flat = datalayer.data.path
gzipped = datalayer.geojson.path + ".gz" gzipped = datalayer.data.path + ".gz"
assert Path(flat).exists() assert Path(flat).exists()
assert Path(gzipped).exists() assert Path(gzipped).exists()
assert Path(flat).stat().st_mtime_ns == Path(gzipped).stat().st_mtime_ns assert Path(flat).stat().st_mtime_ns == Path(gzipped).stat().st_mtime_ns
@ -122,7 +121,7 @@ def test_update(client, datalayer, map, post_data):
assert "id" in j assert "id" in j
assert str(datalayer.pk) == j["id"] assert str(datalayer.pk) == j["id"]
assert j["browsable"] is True assert j["browsable"] is True
assert Path(modified_datalayer.geojson.path).exists() assert Path(modified_datalayer.data.path).exists()
def test_should_not_be_possible_to_update_with_wrong_map_id_in_url( def test_should_not_be_possible_to_update_with_wrong_map_id_in_url(
@ -216,13 +215,13 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
map.share_status = Map.PUBLIC map.share_status = Map.PUBLIC
map.save() map.save()
root = datalayer.storage_root() root = datalayer.storage_root()
datalayer.geojson.storage.save( datalayer.data.storage.save(
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}") "%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
) )
datalayer.geojson.storage.save( datalayer.data.storage.save(
"%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}") "%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
) )
datalayer.geojson.storage.save( datalayer.data.storage.save(
"%s/%s_1440918637.geojson" % (root, datalayer.pk), ContentFile("{}") "%s/%s_1440918637.geojson" % (root, datalayer.pk), ContentFile("{}")
) )
url = reverse("datalayer_versions", args=(map.pk, datalayer.pk)) url = reverse("datalayer_versions", args=(map.pk, datalayer.pk))
@ -243,18 +242,16 @@ def test_versions_can_return_old_format(client, datalayer, map, settings):
datalayer.old_id = 123 # old datalayer id (now replaced by uuid) datalayer.old_id = 123 # old datalayer id (now replaced by uuid)
datalayer.save() datalayer.save()
datalayer.geojson.storage.save( datalayer.data.storage.save(
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}") "%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
) )
datalayer.geojson.storage.save( datalayer.data.storage.save(
"%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}") "%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
) )
# store with the id prefix (rather than the uuid) # store with the id prefix (rather than the uuid)
old_format_version = "%s_1440918637.geojson" % datalayer.old_id old_format_version = "%s_1440918637.geojson" % datalayer.old_id
datalayer.geojson.storage.save( datalayer.data.storage.save(("%s/" % root) + old_format_version, ContentFile("{}"))
("%s/" % root) + old_format_version, ContentFile("{}")
)
url = reverse("datalayer_versions", args=(map.pk, datalayer.pk)) url = reverse("datalayer_versions", args=(map.pk, datalayer.pk))
versions = json.loads(client.get(url).content.decode()) versions = json.loads(client.get(url).content.decode())
@ -276,7 +273,7 @@ def test_version_should_return_one_version_geojson(client, datalayer, map):
map.save() map.save()
root = datalayer.storage_root() root = datalayer.storage_root()
name = "%s_1440924889.geojson" % datalayer.pk name = "%s_1440924889.geojson" % datalayer.pk
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}")) datalayer.data.storage.save("%s/%s" % (root, name), ContentFile("{}"))
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name)) url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name))
assert client.get(url).content.decode() == "{}" assert client.get(url).content.decode() == "{}"
@ -286,7 +283,7 @@ def test_version_should_return_403_if_not_allowed(client, datalayer, map):
map.save() map.save()
root = datalayer.storage_root() root = datalayer.storage_root()
name = "%s_1440924889.geojson" % datalayer.pk name = "%s_1440924889.geojson" % datalayer.pk
datalayer.geojson.storage.save("%s/%s" % (root, name), ContentFile("{}")) datalayer.data.storage.save("%s/%s" % (root, name), ContentFile("{}"))
url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name)) url = reverse("datalayer_version", args=(map.pk, datalayer.pk, name))
assert client.get(url).status_code == 403 assert client.get(url).status_code == 403
@ -447,27 +444,19 @@ def reference_data():
{ {
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [-1, 2]}, "geometry": {"type": "Point", "coordinates": [-1, 2]},
"properties": {"_umap_options": {}, "name": "foo"}, "properties": {"name": "foo"},
}, },
{ {
"type": "Feature", "type": "Feature",
"geometry": {"type": "LineString", "coordinates": [2, 3]}, "geometry": {"type": "LineString", "coordinates": [2, 3]},
"properties": {"_umap_options": {}, "name": "bar"}, "properties": {"name": "bar"},
}, },
{ {
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [3, 4]}, "geometry": {"type": "Point", "coordinates": [3, 4]},
"properties": {"_umap_options": {}, "name": "marker"}, "properties": {"name": "marker"},
}, },
], ],
"_umap_options": {
"displayOnLoad": True,
"name": "new name",
"id": 1668,
"remoteData": {},
"color": "LightSeaGreen",
"description": "test",
},
} }
@ -482,7 +471,7 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
"name": "name", "name": "name",
"display_on_load": True, "display_on_load": True,
"rank": 0, "rank": 0,
"geojson": SimpleUploadedFile( "data": SimpleUploadedFile(
"foo.json", json.dumps(reference_data).encode("utf-8") "foo.json", json.dumps(reference_data).encode("utf-8")
), ),
} }
@ -496,12 +485,12 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
client1_feature = { client1_feature = {
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [5, 6]}, "geometry": {"type": "Point", "coordinates": [5, 6]},
"properties": {"_umap_options": {}, "name": "marker"}, "properties": {"name": "marker"},
} }
client1_data = deepcopy(reference_data) client1_data = deepcopy(reference_data)
client1_data["features"].append(client1_feature) client1_data["features"].append(client1_feature)
post_data["geojson"] = SimpleUploadedFile( post_data["data"] = SimpleUploadedFile(
"foo.json", "foo.json",
json.dumps(client1_data).encode("utf-8"), json.dumps(client1_data).encode("utf-8"),
) )
@ -517,12 +506,12 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
client2_feature = { client2_feature = {
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [7, 8]}, "geometry": {"type": "Point", "coordinates": [7, 8]},
"properties": {"_umap_options": {}, "name": "marker"}, "properties": {"name": "marker"},
} }
client2_data = deepcopy(reference_data) client2_data = deepcopy(reference_data)
client2_data["features"].append(client2_feature) client2_data["features"].append(client2_feature)
post_data["geojson"] = SimpleUploadedFile( post_data["data"] = SimpleUploadedFile(
"foo.json", "foo.json",
json.dumps(client2_data).encode("utf-8"), json.dumps(client2_data).encode("utf-8"),
) )
@ -534,7 +523,7 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
) )
assert response.status_code == 200 assert response.status_code == 200
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
merged_features = json.load(modified_datalayer.geojson)["features"] merged_features = json.load(modified_datalayer.data)["features"]
for reference_feature in reference_data["features"]: for reference_feature in reference_data["features"]:
assert reference_feature in merged_features assert reference_feature in merged_features
@ -556,7 +545,7 @@ def test_optimistic_merge_conflicting_change_raises(
"name": "name", "name": "name",
"display_on_load": True, "display_on_load": True,
"rank": 0, "rank": 0,
"geojson": SimpleUploadedFile( "data": SimpleUploadedFile(
"foo.json", json.dumps(reference_data).encode("utf-8") "foo.json", json.dumps(reference_data).encode("utf-8")
), ),
} }
@ -571,7 +560,7 @@ def test_optimistic_merge_conflicting_change_raises(
client1_data = deepcopy(reference_data) client1_data = deepcopy(reference_data)
client1_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [5, 6]} client1_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [5, 6]}
post_data["geojson"] = SimpleUploadedFile( post_data["data"] = SimpleUploadedFile(
"foo.json", "foo.json",
json.dumps(client1_data).encode("utf-8"), json.dumps(client1_data).encode("utf-8"),
) )
@ -587,7 +576,7 @@ def test_optimistic_merge_conflicting_change_raises(
client2_data = deepcopy(reference_data) client2_data = deepcopy(reference_data)
client2_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [7, 8]} client2_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [7, 8]}
post_data["geojson"] = SimpleUploadedFile( post_data["data"] = SimpleUploadedFile(
"foo.json", "foo.json",
json.dumps(client2_data).encode("utf-8"), json.dumps(client2_data).encode("utf-8"),
) )
@ -601,5 +590,5 @@ def test_optimistic_merge_conflicting_change_raises(
# Check that the server rejected conflicting changes. # Check that the server rejected conflicting changes.
modified_datalayer = DataLayer.objects.get(pk=datalayer.pk) modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
merged_features = json.load(modified_datalayer.geojson)["features"] merged_features = json.load(modified_datalayer.data)["features"]
assert merged_features == client1_data["features"] assert merged_features == client1_data["features"]

View file

@ -73,7 +73,7 @@ def test_clone_should_return_new_instance(map, user):
clone = map.clone() clone = map.clone()
assert map.pk != clone.pk assert map.pk != clone.pk
assert "Clone of " + map.name == clone.name assert "Clone of " + map.name == clone.name
assert map.settings == clone.settings assert map.metadata == clone.metadata
assert map.center == clone.center assert map.center == clone.center
assert map.zoom == clone.zoom assert map.zoom == clone.zoom
assert map.licence == clone.licence assert map.licence == clone.licence
@ -103,8 +103,8 @@ def test_clone_should_clone_datalayers_and_features_too(map, user, datalayer):
assert datalayer in map.datalayer_set.all() assert datalayer in map.datalayer_set.all()
assert other.pk != datalayer.pk assert other.pk != datalayer.pk
assert other.name == datalayer.name assert other.name == datalayer.name
assert other.geojson is not None assert other.data is not None
assert other.geojson.path != datalayer.geojson.path assert other.data.path != datalayer.data.path
def test_publicmanager_should_get_only_public_maps(map, user, licence): def test_publicmanager_should_get_only_public_maps(map, user, licence):

View file

@ -310,12 +310,14 @@ def test_non_editor_cannot_access_map_if_share_status_private(client, map, user)
assert response.status_code == 403 assert response.status_code == 403
def test_map_geojson_view(client, map): def test_map_metadata_view(client, map):
url = reverse("map_geojson", args=(map.pk,)) url = reverse("map_metadata", args=(map.pk,))
response = client.get(url) response = client.get(url)
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert "json" in response["content-type"] assert "json" in response["content-type"]
assert "type" in j assert "geometry" in j
assert "zoom" in j
assert "umap_id" in j
def test_only_owner_can_delete(client, map, user): def test_only_owner_can_delete(client, map, user):
@ -640,11 +642,7 @@ def test_download(client, map, datalayer):
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert j["type"] == "umap" assert j["type"] == "umap"
assert j["uri"] == f"http://testserver/en/map/test-map_{map.pk}" assert j["uri"] == f"http://testserver/en/map/test-map_{map.pk}"
assert j["geometry"] == { assert j["metadata"] == {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
}
assert j["properties"] == {
"datalayersControl": True, "datalayersControl": True,
"description": "Which is just the Danube, at the end", "description": "Which is just the Danube, at the end",
"displayPopupFooter": False, "displayPopupFooter": False,
@ -662,10 +660,14 @@ def test_download(client, map, datalayer):
"tilelayersControl": True, "tilelayersControl": True,
"zoom": 7, "zoom": 7,
"zoomControl": True, "zoomControl": True,
"geometry": {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
},
} }
assert j["layers"] == [ assert j["layers"] == [
{ {
"_umap_options": { "metadata": {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
@ -676,8 +678,8 @@ def test_download(client, map, datalayer):
"coordinates": [14.68896484375, 48.55297816440071], "coordinates": [14.68896484375, 48.55297816440071],
"type": "Point", "type": "Point",
}, },
"metadata": {"color": "DarkCyan", "iconClass": "Ball"},
"properties": { "properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"description": "Da place anonymous again 755", "description": "Da place anonymous again 755",
"name": "Here", "name": "Here",
}, },
@ -705,13 +707,12 @@ def test_download_multiple_maps(client, map, datalayer):
assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap" assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap"
with f.open(f.infolist()[1]) as umap_file: with f.open(f.infolist()[1]) as umap_file:
umapjson = json.loads(umap_file.read().decode()) umapjson = json.loads(umap_file.read().decode())
assert list(umapjson.keys()) == [ assert set(umapjson.keys()) == {
"type", "type",
"geometry", "metadata",
"properties",
"uri", "uri",
"layers", "layers",
] }
assert umapjson["type"] == "umap" assert umapjson["type"] == "umap"
assert umapjson["uri"] == f"http://testserver/en/map/test-map_{map.id}" assert umapjson["uri"] == f"http://testserver/en/map/test-map_{map.id}"

View file

@ -55,9 +55,9 @@ i18n_urls = [
), ),
re_path(r"^logout/$", views.logout, name="logout"), re_path(r"^logout/$", views.logout, name="logout"),
re_path( re_path(
r"^map/(?P<map_id>\d+)/geojson/$", r"^map/(?P<map_id>\d+)/metadata/$",
views.MapViewGeoJSON.as_view(), views.MapMetadata.as_view(),
name="map_geojson", name="map_metadata",
), ),
re_path( re_path(
r"^map/anonymous-edit/(?P<signature>.+)$", r"^map/anonymous-edit/(?P<signature>.+)$",

View file

@ -60,7 +60,7 @@ from .forms import (
DataLayerForm, DataLayerForm,
DataLayerPermissionsForm, DataLayerPermissionsForm,
FlatErrorList, FlatErrorList,
MapSettingsForm, MapMetadataForm,
SendLinkForm, SendLinkForm,
UpdateMapPermissionsForm, UpdateMapPermissionsForm,
UserProfileForm, UserProfileForm,
@ -321,9 +321,9 @@ class UserDownload(DetailView, SearchMixin):
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
for map_ in self.get_maps(): for map_ in self.get_maps():
umapjson = map_.generate_umapjson(self.request) umapjson = map_.generate_umapjson(self.request)
geojson_file = io.StringIO(json_dumps(umapjson)) json_file = io.StringIO(json_dumps(umapjson))
file_name = f"umap_backup_{map_.slug}_{map_.pk}.umap" file_name = f"umap_backup_{map_.slug}_{map_.pk}.umap"
zip_file.writestr(file_name, geojson_file.getvalue()) zip_file.writestr(file_name, json_file.getvalue())
response = HttpResponse(zip_buffer.getvalue(), content_type="application/zip") response = HttpResponse(zip_buffer.getvalue(), content_type="application/zip")
response["Content-Disposition"] = ( response["Content-Disposition"] = (
@ -352,10 +352,9 @@ class MapsShowCase(View):
description = "{}\n[[{}|{}]]".format( description = "{}\n[[{}|{}]]".format(
description, m.get_absolute_url(), _("View the map") description, m.get_absolute_url(), _("View the map")
) )
geometry = m.settings.get("geometry", json.loads(m.center.geojson))
return { return {
"type": "Feature", "type": "Feature",
"geometry": geometry, "geometry": m.geometry,
"properties": {"name": m.name, "description": description}, "properties": {"name": m.name, "description": description},
} }
@ -489,13 +488,13 @@ class MapDetailMixin(SessionMixin):
model = Map model = Map
pk_url_kwarg = "map_id" pk_url_kwarg = "map_id"
def set_preconnect(self, properties, context): def set_preconnect(self, metadata, context):
# Try to extract the tilelayer domain, in order to but a preconnect meta. # Try to extract the tilelayer domain, in order to but a preconnect meta.
url_template = properties.get("tilelayer", {}).get("url_template") url_template = metadata.get("tilelayer", {}).get("url_template")
# Not explicit tilelayer set, take the first of the list, which will be # Not explicit tilelayer set, take the first of the list, which will be
# used by frontend too. # used by frontend too.
if not url_template: if not url_template:
tilelayers = properties.get("tilelayers") tilelayers = metadata.get("tilelayers")
if tilelayers: if tilelayers:
url_template = tilelayers[0].get("url_template") url_template = tilelayers[0].get("url_template")
if url_template: if url_template:
@ -504,9 +503,9 @@ class MapDetailMixin(SessionMixin):
if domain and "{" not in domain: if domain and "{" not in domain:
context["preconnect_domains"] = [f"//{domain}"] context["preconnect_domains"] = [f"//{domain}"]
def get_map_properties(self): def get_metadata(self):
user = self.request.user user = self.request.user
properties = { metadata = {
"urls": _urls_for_js(), "urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(), "tilelayers": TileLayer.get_list(),
"editMode": self.edit_mode, "editMode": self.edit_mode,
@ -522,6 +521,8 @@ class MapDetailMixin(SessionMixin):
"websocketEnabled": settings.WEBSOCKET_ENABLED, "websocketEnabled": settings.WEBSOCKET_ENABLED,
"websocketURI": settings.WEBSOCKET_FRONT_URI, "websocketURI": settings.WEBSOCKET_FRONT_URI,
"importers": settings.UMAP_IMPORTERS, "importers": settings.UMAP_IMPORTERS,
"zoom": self.get_zoom(),
"geometry": self.get_geometry(),
} }
created = bool(getattr(self, "object", None)) created = bool(getattr(self, "object", None))
if (created and self.object.owner) or (not created and not user.is_anonymous): if (created and self.object.owner) or (not created and not user.is_anonymous):
@ -530,35 +531,31 @@ class MapDetailMixin(SessionMixin):
else: else:
map_statuses = AnonymousMapPermissionsForm.STATUS map_statuses = AnonymousMapPermissionsForm.STATUS
datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS
properties["edit_statuses"] = [(i, str(label)) for i, label in map_statuses] metadata["edit_statuses"] = [(i, str(label)) for i, label in map_statuses]
properties["datalayer_edit_statuses"] = [ metadata["datalayer_edit_statuses"] = [
(i, str(label)) for i, label in datalayer_statuses (i, str(label)) for i, label in datalayer_statuses
] ]
if self.get_short_url(): if self.get_short_url():
properties["shortUrl"] = self.get_short_url() metadata["shortUrl"] = self.get_short_url()
properties["user"] = self.get_user_data() metadata["user"] = self.get_user_data()
return properties return metadata
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
properties = self.get_map_properties() metadata = self.get_metadata()
if settings.USE_I18N: if settings.USE_I18N:
lang = settings.LANGUAGE_CODE lang = settings.LANGUAGE_CODE
# Check attr in case the middleware is not active # Check attr in case the middleware is not active
if hasattr(self.request, "LANGUAGE_CODE"): if hasattr(self.request, "LANGUAGE_CODE"):
lang = self.request.LANGUAGE_CODE lang = self.request.LANGUAGE_CODE
properties["lang"] = lang metadata["lang"] = lang
locale = translation.to_locale(lang) locale = translation.to_locale(lang)
properties["locale"] = locale metadata["locale"] = locale
context["locale"] = locale context["locale"] = locale
geojson = self.get_geojson() metadata["datalayers"] = self.get_datalayers()
if "properties" not in geojson: context["map_metadata"] = json_dumps(metadata, indent=settings.DEBUG)
geojson["properties"] = {} self.set_preconnect(metadata, context)
geojson["properties"].update(properties)
geojson["properties"]["datalayers"] = self.get_datalayers()
context["map_settings"] = json_dumps(geojson, indent=settings.DEBUG)
self.set_preconnect(geojson["properties"], context)
return context return context
def get_datalayers(self): def get_datalayers(self):
@ -574,18 +571,24 @@ class MapDetailMixin(SessionMixin):
def is_starred(self): def is_starred(self):
return False return False
def get_geojson(self): def get_geometry(self):
return { return {
"geometry": { "coordinates": [DEFAULT_LONGITUDE, DEFAULT_LATITUDE],
"coordinates": [DEFAULT_LONGITUDE, DEFAULT_LATITUDE], "type": "Point",
"type": "Point",
},
"properties": {
"zoom": getattr(settings, "LEAFLET_ZOOM", 6),
"datalayers": [],
},
} }
def get_zoom(self):
return settings.LEAFLET_ZOOM
# def get_geojson(self):
# return {
# "geometry": ,
# "properties": {
# "zoom": getattr(settings, "LEAFLET_ZOOM", 6),
# "datalayers": [],
# },
# }
def get_short_url(self): def get_short_url(self):
return None return None
@ -637,7 +640,7 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get_datalayers(self): def get_datalayers(self):
return [ return [
dl.metadata(self.request.user, self.request) dl.get_metadata(self.request.user, self.request)
for dl in self.object.datalayer_set.all() for dl in self.object.datalayer_set.all()
] ]
@ -663,13 +666,19 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
short_url = "%s%s" % (settings.SHORT_SITE_URL, short_path) short_url = "%s%s" % (settings.SHORT_SITE_URL, short_path)
return short_url return short_url
def get_geojson(self): def get_geometry(self):
map_settings = self.object.settings return self.object.geometry
if "properties" not in map_settings:
map_settings["properties"] = {} def get_zoom(self):
map_settings["properties"]["name"] = self.object.name return self.object.zoom or self.object.metadata.get(
map_settings["properties"]["permissions"] = self.get_permissions() "zoom", settings.LEAFLET_ZOOM
return map_settings )
def get_metadata(self):
metadata = super().get_metadata()
metadata["name"] = self.object.name
metadata["permissions"] = self.get_permissions()
return {**self.object.metadata, **metadata}
def is_starred(self): def is_starred(self):
user = self.request.user user = self.request.user
@ -744,12 +753,12 @@ class MapOEmbed(View):
return response return response
class MapViewGeoJSON(MapView): class MapMetadata(MapView):
def get_canonical_url(self): def get_canonical_url(self):
return reverse("map_geojson", args=(self.object.pk,)) return reverse("map_metadata", args=(self.object.pk,))
def render_to_response(self, context, *args, **kwargs): def render_to_response(self, context, *args, **kwargs):
return HttpResponse(context["map_settings"], content_type="application/json") return HttpResponse(context["map_metadata"], content_type="application/json")
class MapNew(MapDetailMixin, TemplateView): class MapNew(MapDetailMixin, TemplateView):
@ -759,15 +768,15 @@ class MapNew(MapDetailMixin, TemplateView):
class MapPreview(MapDetailMixin, TemplateView): class MapPreview(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html" template_name = "umap/map_detail.html"
def get_map_properties(self): def get_metadata(self):
properties = super().get_map_properties() properties = super().get_metadata()
properties["preview"] = True properties["preview"] = True
return properties return properties
class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView): class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView):
model = Map model = Map
form_class = MapSettingsForm form_class = MapMetadataForm
def form_valid(self, form): def form_valid(self, form):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@ -821,11 +830,11 @@ def get_websocket_auth_token(request, map_id, map_inst):
class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView): class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView):
model = Map model = Map
form_class = MapSettingsForm form_class = MapMetadataForm
pk_url_kwarg = "map_id" pk_url_kwarg = "map_id"
def form_valid(self, form): def form_valid(self, form):
self.object.settings = form.cleaned_data["settings"] self.object.metadata = form.cleaned_data["metadata"]
self.object.save() self.object.save()
return simple_json_response( return simple_json_response(
id=self.object.pk, id=self.object.pk,
@ -1016,7 +1025,7 @@ class GZipMixin(object):
@property @property
def path(self): def path(self):
return Path(self.object.geojson.path) return Path(self.object.data.path)
@property @property
def gzip_path(self): def gzip_path(self):
@ -1092,7 +1101,7 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
self.object = form.save() self.object = form.save()
# Simple response with only metadata (including new id) # Simple response with only metadata (including new id)
response = simple_json_response( response = simple_json_response(
**self.object.metadata(self.request.user, self.request) **self.object.get_metadata(self.request.user, self.request)
) )
response["X-Datalayer-Version"] = self.version response["X-Datalayer-Version"] = self.version
return response return response
@ -1125,7 +1134,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
# If the reference document is not found, we can't merge. # If the reference document is not found, we can't merge.
return None return None
# New data received in the request. # New data received in the request.
incoming = json.loads(self.request.FILES["geojson"].read()) incoming = json.loads(self.request.FILES["data"].read())
# Latest known version of the data. # Latest known version of the data.
with open(self.path) as f: with open(self.path) as f:
@ -1157,7 +1166,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
return HttpResponse(status=412) return HttpResponse(status=412)
# Replace the uploaded file by the merged version. # Replace the uploaded file by the merged version.
self.request.FILES["geojson"].file = BytesIO( self.request.FILES["data"].file = BytesIO(
json_dumps(merged).encode("utf-8") json_dumps(merged).encode("utf-8")
) )
@ -1167,11 +1176,11 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
self.object = form.save() self.object = form.save()
data = {**self.object.metadata(self.request.user, self.request)} body = {**self.object.get_metadata(self.request.user, self.request)}
if self.request.session.get("needs_reload"): if self.request.session.get("needs_reload"):
data["geojson"] = json.loads(self.object.geojson.read().decode()) body["data"] = json.loads(self.object.data.read().decode())
self.request.session["needs_reload"] = False self.request.session["needs_reload"] = False
response = simple_json_response(**data) response = simple_json_response(**body)
response["X-Datalayer-Version"] = self.version response["X-Datalayer-Version"] = self.version
return response return response