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 Meta:
model = DataLayer
fields = ("geojson", "name", "display_on_load", "rank", "settings")
fields = ("data", "name", "display_on_load", "rank", "metadata")
class DataLayerPermissionsForm(forms.ModelForm):
@ -78,9 +78,9 @@ class AnonymousDataLayerPermissionsForm(forms.ModelForm):
fields = ("edit_status",)
class MapSettingsForm(forms.ModelForm):
class MapMetadataForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MapSettingsForm, self).__init__(*args, **kwargs)
super(MapMetadataForm, self).__init__(*args, **kwargs)
self.fields["slug"].required = False
self.fields["center"].widget.map_srid = 4326
@ -102,7 +102,7 @@ class MapSettingsForm(forms.ModelForm):
return self.cleaned_data["center"]
class Meta:
fields = ("settings", "name", "center", "slug")
fields = ("metadata", "name", "center", "slug", "zoom")
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,
verbose_name=_("share status"),
)
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
metadata = models.JSONField(
blank=True, null=True, verbose_name=_("metadata"), default=dict
)
objects = models.Manager()
@ -200,18 +200,20 @@ class Map(NamedModel):
@property
def description(self):
try:
return self.settings["properties"]["description"]
return self.metadata["description"]
except KeyError:
return ""
@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()
datalayer_data = [c.metadata() for c in layers]
map_settings = self.settings
if "properties" not in map_settings:
map_settings["properties"] = {}
map_settings["properties"].update(
datalayer_data = [l.get_metadata() for l in layers]
metadata = self.metadata
metadata.update(
{
"tilelayers": [TileLayer.get_default().json],
"datalayers": datalayer_data,
@ -224,22 +226,23 @@ class Map(NamedModel):
"umap_id": self.pk,
"schema": self.extra_schema,
"slideshow": {},
"geometry": self.geometry,
}
)
return map_settings
return metadata
def generate_umapjson(self, request):
umapjson = self.settings
umapjson["type"] = "umap"
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
datalayers = []
umapjson = {
"metadata": {"geometry": self.geometry, **self.metadata},
"type": "umap",
"uri": request.build_absolute_uri(self.get_absolute_url()),
"layers": [],
}
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())
if datalayer.settings:
layer["_umap_options"] = datalayer.settings
datalayers.append(layer)
umapjson["layers"] = datalayers
layer["metadata"] = datalayer.metadata
umapjson["layers"].append(layer)
return umapjson
def get_absolute_url(self):
@ -397,15 +400,15 @@ class DataLayer(NamedModel):
old_id = models.IntegerField(null=True, blank=True)
map = models.ForeignKey(Map, on_delete=models.CASCADE)
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(
default=False,
verbose_name=_("display on load"),
help_text=_("Display this layer on load."),
)
rank = models.SmallIntegerField(default=0)
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
metadata = models.JSONField(
blank=True, null=True, verbose_name=_("metadata"), default=dict
)
edit_status = models.SmallIntegerField(
choices=EDIT_STATUS,
@ -423,10 +426,10 @@ class DataLayer(NamedModel):
if is_new:
force_insert, force_update = False, True
filename = self.upload_to()
old_name = self.geojson.name
new_name = self.geojson.storage.save(filename, self.geojson)
self.geojson.storage.delete(old_name)
self.geojson.name = new_name
old_name = self.data.name
new_name = self.data.storage.save(filename, self.data)
self.data.storage.delete(old_name)
self.data.name = new_name
super(DataLayer, self).save(force_insert, force_update, **kwargs)
self.purge_gzip()
self.purge_old_versions()
@ -443,10 +446,10 @@ class DataLayer(NamedModel):
path.append(str(self.map.pk))
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
# has been introduced
obj = self.settings or {
obj = self.metadata or {
"name": self.name,
"displayOnLoad": self.display_on_load,
}
@ -463,7 +466,7 @@ class DataLayer(NamedModel):
new.pk = None
if map_inst:
new.map = map_inst
new.geojson = File(new.geojson.file.file)
new.data = File(new.data.file.file)
new.save()
return new
@ -478,13 +481,13 @@ class DataLayer(NamedModel):
return {
"name": name,
"at": els[1],
"size": self.geojson.storage.size(self.get_version_path(name)),
"size": self.data.storage.size(self.get_version_path(name)),
}
@property
def versions(self):
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)]
versions = [self.version_metadata(name) for name in names]
versions.sort(reverse=True, key=operator.itemgetter("at"))
@ -492,7 +495,7 @@ class DataLayer(NamedModel):
def get_version(self, 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()
def get_version_path(self, name):
@ -505,23 +508,23 @@ class DataLayer(NamedModel):
name = version["name"]
# Should not be in the list, but ensure to not delete the file
# currently used in database
if self.geojson.name.endswith(name):
if self.data.name.endswith(name):
continue
try:
self.geojson.storage.delete(os.path.join(root, name))
self.data.storage.delete(os.path.join(root, name))
except FileNotFoundError:
pass
def purge_gzip(self):
root = self.storage_root()
names = self.geojson.storage.listdir(root)[1]
names = self.data.storage.listdir(root)[1]
prefixes = [f"{self.pk}_"]
if self.old_id:
prefixes.append(f"{self.old_id}_")
prefixes = tuple(prefixes)
for name in names:
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):
"""

View file

@ -1,6 +1,6 @@
class UmapFragment extends HTMLElement {
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)
datalayer.renderToolbox(headline)
const name = DomUtil.create('span', 'datalayer-name', headline)
name.textContent = datalayer.options.name
name.textContent = datalayer.metadata.name
DomEvent.on(name, 'click', toggleList)
container.innerHTML = ''
datalayer.eachFeature((feature) => this.addFeature(feature, container))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@ export function getImpactsFromSchema(fields, schema) {
// remove the option prefix for fields
// And only keep the first part in case of a subfield
// (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) => {
// retrieve the "impacts" field from the schema
@ -181,7 +181,7 @@ export function isObject(what) {
return typeof what === 'object' && what !== null
}
export function CopyJSON(geojson) {
export function copyJSON(geojson) {
return JSON.parse(JSON.stringify(geojson))
}

View file

@ -725,9 +725,9 @@ const ControlsMixin = {
const row = L.DomUtil.create('li', 'orderable', ul)
L.DomUtil.createIcon(row, 'icon-drag', L._('Drag to reorder'))
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())
title.textContent = datalayer.options.name
title.textContent = datalayer.metadata.name
row.dataset.id = L.stamp(datalayer)
})
const onReorder = (src, dst, initialIndex, finalIndex) => {

View file

@ -29,30 +29,30 @@ L.Map.mergeOptions({
U.Map = L.Map.extend({
includes: [ControlsMixin],
initialize: async function (el, geojson) {
initialize: async function (el, metadata) {
this.sync_engine = new U.SyncEngine(this)
this.sync = this.sync_engine.proxy(this)
// Locale name (pt_PT, en_US…)
// 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…)
// 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
const zoomControl = geojson.properties.zoomControl
const fullscreenControl = geojson.properties.fullscreenControl
geojson.properties.zoomControl = false
geojson.properties.fullscreenControl = false
const zoomControl = metadata.zoomControl
const fullscreenControl = metadata.fullscreenControl
metadata.zoomControl = 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
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.panel = new U.Panel(this)
@ -805,7 +805,7 @@ U.Map = L.Map.extend({
const datalayer = new U.DataLayer(this, options, sync)
if (sync !== false) {
datalayer.sync.upsert(datalayer.options)
datalayer.sync.upsert(datalayer.metadata)
}
return datalayer
},
@ -906,15 +906,16 @@ U.Map = L.Map.extend({
}
if (importedData.geometry) this.options.center = this.latLng(importedData.geometry)
importedData.layers.forEach((geojson) => {
if (!geojson._umap_options && geojson._storage) {
geojson._umap_options = geojson._storage
for (const geojson of importedData.layers) {
if (!geojson.metadata) {
geojson.metadata = geojson._umap_options || geojson._storage
delete geojson._umap_options
delete geojson._storage
}
delete geojson._umap_options?.id // Never trust an id at this stage
const dataLayer = this.createDataLayer(geojson._umap_options)
delete geojson.metadata?.id // Never trust an id at this stage
const dataLayer = this.createDataLayer(geojson.metadata)
dataLayer.fromUmapGeoJSON(geojson)
})
}
this.initTileLayers()
this.renderControls()
@ -1032,22 +1033,14 @@ U.Map = L.Map.extend({
saveSelf: async function () {
this.rules.commit()
const geojson = {
type: 'Feature',
geometry: this.geometry(),
properties: this.exportOptions(),
}
const formData = new FormData()
formData.append('name', this.options.name)
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 [data, _, error] = await this.server.post(uri, {}, formData)
// FIXME: login_required response will not be an error, so it will not
// stop code while it should
if (error) {
return
}
if (error) return
if (data.login_required) {
window.onLogin = () => this.saveSelf()
window.open(data.login_required)

View file

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

View file

@ -1,6 +1,6 @@
{% 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>
</umap-fragment>

View file

@ -6,7 +6,7 @@
<!-- djlint:off -->
<script defer type="text/javascript">
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>
<!-- djlint:on -->

View file

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

View file

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

View file

@ -3,10 +3,10 @@ import json
import factory
from django.contrib.auth import get_user_model
from django.contrib.gis.geos import Point
from django.core.files.base import ContentFile
from django.urls import reverse
from umap.forms import DEFAULT_CENTER
from umap.models import DataLayer, Licence, Map, TileLayer
User = get_user_model()
@ -20,14 +20,13 @@ DATALAYER_DATA = {
"type": "Point",
"coordinates": [14.68896484375, 48.55297816440071],
},
"metadata": {"color": "DarkCyan", "iconClass": "Ball"},
"properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"name": "Here",
"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):
name = "test map"
slug = "test-map"
center = DEFAULT_CENTER
settings = factory.Dict(
center = Point(13.447265624999998, 48.94415123418794)
metadata = factory.Dict(
{
"geometry": {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
"datalayersControl": True,
"description": "Which is just the Danube, at the end",
"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": {
"datalayersControl": True,
"description": "Which is just the Danube, at the end",
"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",
}
"tilelayersControl": True,
"zoom": 7,
"zoomControl": True,
},
)
licence = factory.SubFactory(LicenceFactory)
@ -97,8 +89,8 @@ class MapFactory(factory.django.DjangoModelFactory):
@classmethod
def _adjust_kwargs(cls, **kwargs):
# Make sure there is no persistency
kwargs["settings"] = copy.deepcopy(kwargs["settings"])
kwargs["settings"]["properties"]["name"] = kwargs["name"]
kwargs["metadata"] = copy.deepcopy(kwargs["metadata"])
kwargs["metadata"]["name"] = kwargs["name"]
return kwargs
class Meta:
@ -110,26 +102,18 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
name = "test datalayer"
description = "test description"
display_on_load = True
settings = factory.Dict({"displayOnLoad": True, "browsable": True, "name": name})
metadata = factory.Dict({"displayOnLoad": True, "browsable": True, "name": name})
@classmethod
def _adjust_kwargs(cls, **kwargs):
if "data" in kwargs:
data = copy.deepcopy(kwargs.pop("data"))
if "settings" not in kwargs:
kwargs["settings"] = data.get("_umap_options", {})
else:
data = DATALAYER_DATA.copy()
data["_umap_options"] = {
**DataLayerFactory.settings._defaults,
**kwargs["settings"],
}
data.setdefault("_umap_options", {})
kwargs["settings"]["name"] = kwargs["name"]
data["_umap_options"]["name"] = kwargs["name"]
kwargs["metadata"]["name"] = kwargs["name"]
data.setdefault("type", "FeatureCollection")
data.setdefault("features", [])
kwargs["geojson"] = ContentFile(json.dumps(data), "foo.json")
kwargs["data"] = ContentFile(json.dumps(data), "foo.json")
return kwargs
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":[[[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"}}
],"_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,
"inCaption": true,
"browsable": true,
@ -71,7 +71,7 @@
}
}
],
"_umap_options": {
"metadata": {
"displayOnLoad": false,
"inCaption": true,
"browsable": true,

View file

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

View file

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

View file

@ -9,4 +9,4 @@ def save_and_get_json(page):
with page.expect_response(re.compile(r".*/datalayer/create/.*")):
page.get_by_role("button", name="Save").click()
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
def bootstrap(map, live_server):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.metadata["onLoadPanel"] = "databrowser"
map.save()
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):
datalayer = map.datalayer_set.first()
datalayer.settings["labelKey"] = "foo"
datalayer.metadata["labelKey"] = "foo"
datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.metadata["onLoadPanel"] = "databrowser"
map.save()
data = deepcopy(DATALAYER_DATA)
data["_umap_options"]["labelKey"] = "{name} ({bar})"
DataLayerFactory(map=map, data=data)
DataLayerFactory(map=map, data=data, metadata={"labelKey": "{name} ({bar})"})
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_title("Features in this layer: 3")).to_be_visible()
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):
# Include a variable
map.settings["properties"]["labelKey"] = "{name} ({foo})"
map.metadata["labelKey"] = "{name} ({foo})"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.metadata["onLoadPanel"] = "databrowser"
map.save()
datalayer_data = deepcopy(DATALAYER_DATA)
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(
live_server, page, bootstrap, map, datalayer
):
datalayer.settings["displayOnLoad"] = False
datalayer.settings["name"] = "This layer is not loaded"
datalayer.metadata["displayOnLoad"] = False
datalayer.metadata["name"] = "This layer is not loaded"
datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.settings["properties"]["color"] = "{mycolor}"
map.metadata["onLoadPanel"] = "databrowser"
map.metadata["color"] = "{mycolor}"
map.save()
datalayer_data = deepcopy(DATALAYER_DATA)
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):
map.settings["properties"]["onLoadPanel"] = "caption"
map.metadata["onLoadPanel"] = "caption"
map.save()
basic = DataLayerFactory(map=map, name="Basic layer")
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()}")
panel = page.locator(".panel.left.on")
expect(panel).to_have_class(re.compile(".*condensed.*"))

View file

@ -1,4 +1,5 @@
import json
from copy import deepcopy
from pathlib import Path
import pytest
@ -8,11 +9,24 @@ from ..base import DataLayerFactory
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):
path = Path(__file__).parent.parent / "fixtures/categorized_highway.geojson"
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")
# residential
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())
# Change brewer at load
data["_umap_options"]["categorized"]["brewer"] = "Spectral"
DataLayerFactory(data=data, map=openmap)
metadata = deepcopy(METADATA)
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")
# residential
@ -76,11 +91,12 @@ def test_basic_categorized_map_with_custom_categories(openmap, live_server, page
data = json.loads(path.read_text())
# Change categories at load
data["_umap_options"]["categorized"]["categories"] = (
metadata = deepcopy(METADATA)
metadata["categorized"]["categories"] = (
"unclassified,track,service,residential,tertiary,secondary"
)
data["_umap_options"]["categorized"]["mode"] = "manual"
DataLayerFactory(data=data, map=openmap)
metadata["categorized"]["mode"] = "manual"
DataLayerFactory(data=data, map=openmap, metadata=metadata)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}#13/48.4378/3.3043")

View file

@ -1,4 +1,5 @@
import json
from copy import deepcopy
from pathlib import Path
import pytest
@ -9,10 +10,20 @@ from ..base import DataLayerFactory
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):
path = Path(__file__).parent.parent / "fixtures/choropleth_region_chomage.geojson"
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()}")
# Hauts-de-France
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())
# Change brewer at load
data["_umap_options"]["choropleth"]["brewer"] = "Reds"
DataLayerFactory(data=data, map=openmap)
metadata = deepcopy(METADATA)
metadata["choropleth"]["brewer"] = "Reds"
DataLayerFactory(
data=data,
map=openmap,
metadata=metadata,
)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}")
# 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())
# Change brewer at load
data["_umap_options"]["choropleth"]["classes"] = 6
DataLayerFactory(data=data, map=openmap)
metadata = deepcopy(METADATA)
metadata["choropleth"]["classes"] = 6
DataLayerFactory(data=data, map=openmap, metadata=metadata)
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]},
},
],
"_umap_options": {
"name": "Calque 1",
},
}
@ -70,14 +67,11 @@ DATALAYER_DATA2 = {
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
},
],
"_umap_options": {
"name": "Calque 2",
},
}
def test_simple_equal_rule_at_load(live_server, page, map):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "mytype=odd", "options": {"color": "aliceblue"}}
]
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):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "mytype!=even", "options": {"color": "aliceblue"}}
]
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):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "mynumber>10", "options": {"color": "aliceblue"}}
]
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):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "mynumber<14", "options": {"color": "aliceblue"}}
]
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):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "mynumber<12.3", "options": {"color": "aliceblue"}}
]
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):
map.settings["properties"]["rules"] = [
map.metadata["rules"] = [
{"condition": "myboolean=true", "options": {"color": "aliceblue"}}
]
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):
openmap.settings["properties"]["rules"] = [
openmap.metadata["rules"] = [
{"condition": "mytype=odd", "options": {"color": "aliceblue"}}
]
openmap.save()

View file

@ -1,8 +1,6 @@
import json
import re
import pytest
from django.core.files.base import ContentFile
from playwright.sync_api import expect
from ..base import DataLayerFactory
@ -10,17 +8,13 @@ from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db
def set_options(datalayer, **options):
# For now we need to change both the DB and the FS…
datalayer.settings.update(options)
data = json.load(datalayer.geojson.file)
data["_umap_options"].update(**options)
datalayer.geojson = ContentFile(json.dumps(data), "foo.json")
def set_metadata(datalayer, **options):
datalayer.metadata.update(options)
datalayer.save()
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")
expect(page.locator(".leaflet-marker-icon")).to_be_hidden()
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):
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")
markers = page.locator(".leaflet-marker-icon")
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):
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")
markers = page.locator(".leaflet-marker-icon")
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}",
"fillColor": "{mycolor}",
},
}
DataLayerFactory(map=map, data=data)
)
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-overlay-pane path[fill='tomato']"))
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):
map.settings["properties"]["onLoadPanel"] = "datalayers"
map.metadata["onLoadPanel"] = "datalayers"
map.save()
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")
hidden = page.locator(".umap-browser .datalayer.off .datalayer-name")
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
):
data = deepcopy(DATALAYER_DATA)
data["_umap_options"] = {"color": "DarkCyan"}
DataLayerFactory(map=openmap, data=data)
DataLayerFactory(map=openmap, data=data, metadata={"color": "DarkCyan"})
DataLayerFactory(
map=openmap,
name="other datalayer",
data={
"type": "FeatureCollection",
"features": [],
"_umap_options": {"color": "DarkViolet"},
},
metadata={"color": "DarkViolet"},
)
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
marker = page.locator(".leaflet-marker-icon .icon_container")

View file

@ -1,6 +1,7 @@
import platform
import pytest
from django.contrib.gis.geos import Point
from playwright.sync_api import expect
from ..base import DataLayerFactory
@ -37,11 +38,8 @@ DATALAYER_DATA = {
@pytest.fixture
def bootstrap(map, live_server):
map.settings["properties"]["zoom"] = 6
map.settings["geometry"] = {
"type": "Point",
"coordinates": [8.429, 53.239],
}
map.zoom = 6
map.center = Point(8.429, 53.239)
map.save()
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):
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")
expect(page.locator("path[fill='DarkBlue']")).to_have_count(1)

View file

@ -34,10 +34,10 @@ DATALAYER_DATA = {
},
{
"type": "Feature",
"metadata": {
"color": "OliveDrab",
},
"properties": {
"_umap_options": {
"color": "OliveDrab",
},
"name": "test",
"description": "Some description",
},
@ -49,11 +49,11 @@ DATALAYER_DATA = {
},
{
"type": "Feature",
"metadata": {
"fill": False,
"opacity": 0.6,
},
"properties": {
"_umap_options": {
"fill": False,
"opacity": 0.6,
},
"name": "test",
},
"id": "YwMTM",
@ -76,7 +76,7 @@ DATALAYER_DATA = {
@pytest.fixture
def bootstrap(map, live_server):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.metadata["onLoadPanel"] = "databrowser"
map.save()
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())
del downloaded["uri"] # Port changes at each run
assert downloaded == {
"geometry": {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
},
"layers": [
{
"_umap_options": {
"metadata": {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
@ -131,8 +127,8 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "Point",
},
"id": "QwNjg",
"metadata": {"color": "OliveDrab"},
"properties": {
"_umap_options": {"color": "OliveDrab"},
"name": "test",
"description": "Some description",
},
@ -152,8 +148,8 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "LineString",
},
"id": "YwMTM",
"metadata": {"fill": False, "opacity": 0.6},
"properties": {
"_umap_options": {"fill": False, "opacity": 0.6},
"name": "test",
},
"type": "Feature",
@ -162,7 +158,11 @@ def test_umap_export(map, live_server, bootstrap, page):
"type": "FeatureCollection",
}
],
"properties": {
"metadata": {
"geometry": {
"type": "Point",
"coordinates": [13.447265624999998, 48.94415123418794],
},
"datalayersControl": True,
"description": "Which is just the Danube, at the end",
"displayPopupFooter": False,
@ -234,7 +234,7 @@ def test_kml_export(map, live_server, bootstrap, page):
download.save_as(path)
assert (
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"},
"id": "QwNjg",
"metadata": {"color": "OliveDrab"},
"properties": {
"_umap_options": {"color": "OliveDrab"},
"name": "test",
"description": "Some description",
},
@ -293,8 +293,8 @@ def test_geojson_export(map, live_server, bootstrap, page):
"type": "LineString",
},
"id": "YwMTM",
"metadata": {"fill": False, "opacity": 0.6},
"properties": {
"_umap_options": {"fill": False, "opacity": 0.6},
"name": "test",
},
"type": "Feature",

View file

@ -33,9 +33,6 @@ DATALAYER_DATA1 = {
"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]},
},
],
"_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):
map.settings["properties"]["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.settings["properties"]["showLabel"] = True
map.metadata["onLoadPanel"] = "datafilters"
map.metadata["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.metadata["showLabel"] = True
map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1)
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")
panel = page.locator(".panel.left.on")
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):
map.settings["properties"]["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mydate|Date filter|date"
map.metadata["onLoadPanel"] = "datafilters"
map.metadata["facetKey"] = "mydate|Date filter|date"
map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1)
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):
map.settings["properties"]["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type"
map.metadata["onLoadPanel"] = "datafilters"
map.metadata["facetKey"] = "mytype|My type"
map.save()
data = copy.deepcopy(DATALAYER_DATA1)
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):
map.settings["properties"]["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mynumber|Filter|number"
map.metadata["onLoadPanel"] = "datafilters"
map.metadata["facetKey"] = "mynumber|Filter|number"
map.save()
data = copy.deepcopy(DATALAYER_DATA1)
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):
map.settings["properties"]["onLoadPanel"] = "datafilters"
map.settings["properties"]["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.metadata["onLoadPanel"] = "datafilters"
map.metadata["facetKey"] = "mytype|My type,mynumber|My Number|number"
map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA1)
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/.*")):
page.get_by_role("button", name="Save").click()
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"] == {
"color": "",
"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_attribute("href", "//a.tile.openstreetmap.fr")
# Add custom tilelayer
map.settings["properties"]["tilelayer"] = {
map.metadata["tilelayer"] = {
"name": "OSM Piano FR",
"maxZoom": 20,
"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()}")
expect(meta).to_have_attribute("href", "//a.piano.tiles.quaidorsay.fr")
# Add custom tilelayer with variable in domain, should create a preconnect
map.settings["properties"]["tilelayer"] = {
map.metadata["tilelayer"] = {
"name": "OSM Piano FR",
"maxZoom": 20,
"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(
map, live_server, datalayer, page
):
datalayer.settings["displayOnLoad"] = False
datalayer.metadata["displayOnLoad"] = False
datalayer.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# 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(
map, live_server, datalayer, page
):
datalayer.settings["displayOnLoad"] = False
datalayer.metadata["displayOnLoad"] = False
datalayer.save()
map.settings["properties"]["defaultView"] = "latest"
map.metadata["defaultView"] = "latest"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# 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(
map, live_server, datalayer, page
):
datalayer.settings["displayOnLoad"] = False
datalayer.metadata["displayOnLoad"] = False
datalayer.save()
map.settings["properties"]["defaultView"] = "data"
map.metadata["defaultView"] = "data"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# 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):
map.settings["properties"]["defaultView"] = "latest"
map.metadata["defaultView"] = "latest"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
# 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)
map.settings["properties"]["defaultView"] = "latest"
map.metadata["defaultView"] = "latest"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
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)
map.settings["properties"]["defaultView"] = "latest"
map.metadata["defaultView"] = "latest"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?onLoadPanel=datalayers")
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},
permissions=["geolocation"],
)
map.settings["properties"]["defaultView"] = "locate"
map.metadata["defaultView"] = "locate"
map.save()
page = context.new_page()
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(
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;",
"format": "osm",
"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):
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-minimap")).to_be_hidden()
map.settings["properties"]["miniMap"] = True
map.metadata["miniMap"] = True
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.locator(".leaflet-control-zoom")).to_be_visible()
map.settings["properties"]["zoomControl"] = False
map.metadata["zoomControl"] = False
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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(
map, live_server, page
):
map.settings["properties"]["onLoadPanel"] = "caption"
map.metadata["onLoadPanel"] = "caption"
map.name = "This is my map"
map.save()
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")
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()

View file

@ -5,9 +5,9 @@ pytestmark = pytest.mark.django_db
def test_should_not_render_any_control(live_server, tilelayer, page, map):
map.settings["properties"]["onLoadPanel"] = "databrowser"
map.settings["properties"]["miniMap"] = True
map.settings["properties"]["captionBar"] = True
map.metadata["onLoadPanel"] = "databrowser"
map.metadata["miniMap"] = True
map.metadata["captionBar"] = True
map.save()
# Make sure those controls are visible in normal view
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
# on time in case of conflict. FIXME do not use time for comparison.
sleep(1)
assert DataLayer.objects.get(pk=datalayer.pk).settings == {
assert DataLayer.objects.get(pk=datalayer.pk).metadata == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"editMode": "advanced",
"inCaption": True,
"remoteData": {},
}
# Now navigate to this map from another tab
@ -78,12 +78,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
sleep(1)
# No change after the save
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,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"remoteData": {},
}
# 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()
# Should now get the other marker too
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,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
"remoteData": {},
}
# And again
@ -112,14 +110,12 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
save_p1.click()
sleep(1)
# 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,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
"remoteData": {},
}
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()
sleep(1)
# 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,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
"remoteData": {},
}
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
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,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
"remoteData": {},
}
@ -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/.*")):
page_two.get_by_role("button", name="Save").click()
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"
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_text("Keep your changes and loose theirs").click()
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"

View file

@ -21,10 +21,10 @@ DATALAYER_DATA = {
"type": "Point",
"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"
@ -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):
openmap.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.metadata["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.save()
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
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):
openmap.settings["properties"]["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.metadata["iconUrl"] = "/uploads/pictogram/star.svg"
openmap.save()
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
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):
map.settings["properties"]["slideshow"] = {"active": True, "delay": 5000}
map.metadata["slideshow"] = {"active": True, "delay": 5000}
map.save()
DataLayerFactory(map=map, data=DATALAYER_DATA)
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(
map, live_server, datalayer, page, settings, staticfiles
):
datalayer.settings["displayOnLoad"] = False
datalayer.metadata["displayOnLoad"] = False
datalayer.save()
map.settings["properties"]["defaultView"] = "latest"
map.metadata["defaultView"] = "latest"
map.save()
with override("fr"):
url = f"{live_server.url}{map.get_absolute_url()}"

View file

@ -59,9 +59,6 @@ DATALAYER_DATA = {
"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/.*")):
page.get_by_role("button", name="Save").click()
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 "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(
r"https://[abc]{1}.piano.tiles.quaidorsay.fr/fr/\d+/\d+/\d+.png"
)
map.settings["properties"]["tilelayer"]["url_template"] = piano.url_template
map.settings["properties"]["tilelayersControl"] = True
map.metadata["tilelayer"]["url_template"] = piano.url_template
map.metadata["tilelayersControl"] = True
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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(
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"
)
map.settings["properties"]["tilelayersControl"] = True
map.metadata["tilelayersControl"] = True
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["tilelayer"]["attribution"] = (
map.metadata["tilelayer"]["attribution"] = (
"&copy; [[http://www.openstreetmap.org/copyright|OpenStreetMap]] contributors"
)
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):
map.settings["properties"]["tilelayersControl"] = True
map.metadata["tilelayersControl"] = True
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["showLabel"] = True
map.settings["properties"]["labelKey"] = "Foo {name}"
map.metadata["showLabel"] = True
map.metadata["labelKey"] = "Foo {name}"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_text("Foo test marker")).to_be_visible()
def test_should_open_popup_panel_on_click(live_server, map, page, bootstrap):
map.settings["properties"]["popupShape"] = "Panel"
map.metadata["popupShape"] = "Panel"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
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):
map.settings["properties"]["popupContentTemplate"] = """
map.metadata["popupContentTemplate"] = """
Rank: {rank}
Locale: {locale}
Lang: {lang}

View file

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

View file

@ -1,4 +1,5 @@
import pytest
from django.contrib.gis.geos import Point
from playwright.sync_api import expect
from ..base import DataLayerFactory
@ -27,11 +28,8 @@ DATALAYER_DATA = {
@pytest.fixture
def bootstrap(map, live_server):
map.settings["properties"]["zoom"] = 6
map.settings["geometry"] = {
"type": "Point",
"coordinates": [8.429, 53.239],
}
map.zoom = 6
map.center = Point(8.429, 53.239)
map.save()
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
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.metadata["syncEnabled"] = True
map.save()
DataLayerFactory(map=map, data={})
@ -80,7 +80,7 @@ def test_websocket_connection_can_sync_polygons(
context, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.metadata["syncEnabled"] = True
map.save()
DataLayerFactory(map=map, data={})
@ -164,7 +164,7 @@ def test_websocket_connection_can_sync_map_properties(
context, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.metadata["syncEnabled"] = True
map.save()
DataLayerFactory(map=map, data={})
@ -196,7 +196,7 @@ def test_websocket_connection_can_sync_datalayer_properties(
context, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.metadata["syncEnabled"] = True
map.save()
DataLayerFactory(map=map, data={})
@ -225,7 +225,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
context, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.metadata["syncEnabled"] = True
map.save()
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):
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):
before = datalayer.geojson.name
datalayer.geojson.save(before, ContentFile("{}"))
assert datalayer.geojson.name != before
assert "/{}_".format(datalayer.pk) in datalayer.geojson.name
before = datalayer.data.name
datalayer.data.save(before, ContentFile("{}"))
assert datalayer.data.name != before
assert "/{}_".format(datalayer.pk) in datalayer.data.name
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):
clone = datalayer.clone()
assert datalayer.pk != clone.pk
assert clone.geojson is not None
assert clone.geojson.path != datalayer.geojson.path
assert clone.data is not None
assert clone.data.path != datalayer.data.path
def test_should_remove_old_versions_on_save(map, settings):
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
settings.UMAP_KEEP_VERSIONS = 3
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"
medium = f"{datalayer.pk}_1440923687.geojson"
older = f"{datalayer.pk}_1440918637.geojson"
with_old_id = f"{datalayer.old_id}_1440918537.geojson"
other = "123456_1440918637.geojson"
for path in [medium, newer, older, with_old_id, other]:
datalayer.geojson.storage.save(root / path, ContentFile("{}"))
datalayer.geojson.storage.save(root / f"{path}.gz", ContentFile("{}"))
assert len(datalayer.geojson.storage.listdir(root)[1]) == 10 + before
files = datalayer.geojson.storage.listdir(root)[1]
datalayer.data.storage.save(root / path, ContentFile("{}"))
datalayer.data.storage.save(root / f"{path}.gz", ContentFile("{}"))
assert len(datalayer.data.storage.listdir(root)[1]) == 10 + before
files = datalayer.data.storage.listdir(root)[1]
# Those files should be present before save, which will purge them
assert older in files
assert older + ".gz" in files
assert with_old_id in files
assert with_old_id + ".gz" in files
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.
# older and with_old_id should have been removed
assert len(files) == 5
assert newer 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.
assert other 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 + ".gz" not in files
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):

View file

@ -21,9 +21,9 @@ def post_data():
"display_on_load": True,
"settings": '{"displayOnLoad": true, "browsable": true, "name": "name"}',
"rank": 0,
"geojson": SimpleUploadedFile(
"data": SimpleUploadedFile(
"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 "Content-Encoding" not in response
j = json.loads(response.content.decode())
assert "_umap_options" in j
assert "features" in j
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))
response = client.get(url, headers={"ACCEPT_ENCODING": "gzip"})
assert response.status_code == 200
flat = datalayer.geojson.path
gzipped = datalayer.geojson.path + ".gz"
flat = datalayer.data.path
gzipped = datalayer.data.path + ".gz"
assert Path(flat).exists()
assert Path(gzipped).exists()
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 str(datalayer.pk) == j["id"]
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(
@ -216,13 +215,13 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
map.share_status = Map.PUBLIC
map.save()
root = datalayer.storage_root()
datalayer.geojson.storage.save(
datalayer.data.storage.save(
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
)
datalayer.geojson.storage.save(
datalayer.data.storage.save(
"%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
)
datalayer.geojson.storage.save(
datalayer.data.storage.save(
"%s/%s_1440918637.geojson" % (root, datalayer.pk), ContentFile("{}")
)
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.save()
datalayer.geojson.storage.save(
datalayer.data.storage.save(
"%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
)
datalayer.geojson.storage.save(
datalayer.data.storage.save(
"%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
)
# store with the id prefix (rather than the uuid)
old_format_version = "%s_1440918637.geojson" % datalayer.old_id
datalayer.geojson.storage.save(
("%s/" % root) + old_format_version, ContentFile("{}")
)
datalayer.data.storage.save(("%s/" % root) + old_format_version, ContentFile("{}"))
url = reverse("datalayer_versions", args=(map.pk, datalayer.pk))
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()
root = datalayer.storage_root()
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))
assert client.get(url).content.decode() == "{}"
@ -286,7 +283,7 @@ def test_version_should_return_403_if_not_allowed(client, datalayer, map):
map.save()
root = datalayer.storage_root()
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))
assert client.get(url).status_code == 403
@ -447,27 +444,19 @@ def reference_data():
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-1, 2]},
"properties": {"_umap_options": {}, "name": "foo"},
"properties": {"name": "foo"},
},
{
"type": "Feature",
"geometry": {"type": "LineString", "coordinates": [2, 3]},
"properties": {"_umap_options": {}, "name": "bar"},
"properties": {"name": "bar"},
},
{
"type": "Feature",
"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",
"display_on_load": True,
"rank": 0,
"geojson": SimpleUploadedFile(
"data": SimpleUploadedFile(
"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 = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [5, 6]},
"properties": {"_umap_options": {}, "name": "marker"},
"properties": {"name": "marker"},
}
client1_data = deepcopy(reference_data)
client1_data["features"].append(client1_feature)
post_data["geojson"] = SimpleUploadedFile(
post_data["data"] = SimpleUploadedFile(
"foo.json",
json.dumps(client1_data).encode("utf-8"),
)
@ -517,12 +506,12 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
client2_feature = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [7, 8]},
"properties": {"_umap_options": {}, "name": "marker"},
"properties": {"name": "marker"},
}
client2_data = deepcopy(reference_data)
client2_data["features"].append(client2_feature)
post_data["geojson"] = SimpleUploadedFile(
post_data["data"] = SimpleUploadedFile(
"foo.json",
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
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"]:
assert reference_feature in merged_features
@ -556,7 +545,7 @@ def test_optimistic_merge_conflicting_change_raises(
"name": "name",
"display_on_load": True,
"rank": 0,
"geojson": SimpleUploadedFile(
"data": SimpleUploadedFile(
"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["features"][0]["geometry"] = {"type": "Point", "coordinates": [5, 6]}
post_data["geojson"] = SimpleUploadedFile(
post_data["data"] = SimpleUploadedFile(
"foo.json",
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["features"][0]["geometry"] = {"type": "Point", "coordinates": [7, 8]}
post_data["geojson"] = SimpleUploadedFile(
post_data["data"] = SimpleUploadedFile(
"foo.json",
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.
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"]

View file

@ -73,7 +73,7 @@ def test_clone_should_return_new_instance(map, user):
clone = map.clone()
assert map.pk != clone.pk
assert "Clone of " + map.name == clone.name
assert map.settings == clone.settings
assert map.metadata == clone.metadata
assert map.center == clone.center
assert map.zoom == clone.zoom
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 other.pk != datalayer.pk
assert other.name == datalayer.name
assert other.geojson is not None
assert other.geojson.path != datalayer.geojson.path
assert other.data is not None
assert other.data.path != datalayer.data.path
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
def test_map_geojson_view(client, map):
url = reverse("map_geojson", args=(map.pk,))
def test_map_metadata_view(client, map):
url = reverse("map_metadata", args=(map.pk,))
response = client.get(url)
j = json.loads(response.content.decode())
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):
@ -640,11 +642,7 @@ def test_download(client, map, datalayer):
j = json.loads(response.content.decode())
assert j["type"] == "umap"
assert j["uri"] == f"http://testserver/en/map/test-map_{map.pk}"
assert j["geometry"] == {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
}
assert j["properties"] == {
assert j["metadata"] == {
"datalayersControl": True,
"description": "Which is just the Danube, at the end",
"displayPopupFooter": False,
@ -662,10 +660,14 @@ def test_download(client, map, datalayer):
"tilelayersControl": True,
"zoom": 7,
"zoomControl": True,
"geometry": {
"coordinates": [13.447265624999998, 48.94415123418794],
"type": "Point",
},
}
assert j["layers"] == [
{
"_umap_options": {
"metadata": {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
@ -676,8 +678,8 @@ def test_download(client, map, datalayer):
"coordinates": [14.68896484375, 48.55297816440071],
"type": "Point",
},
"metadata": {"color": "DarkCyan", "iconClass": "Ball"},
"properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"description": "Da place anonymous again 755",
"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"
with f.open(f.infolist()[1]) as umap_file:
umapjson = json.loads(umap_file.read().decode())
assert list(umapjson.keys()) == [
assert set(umapjson.keys()) == {
"type",
"geometry",
"properties",
"metadata",
"uri",
"layers",
]
}
assert umapjson["type"] == "umap"
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"^map/(?P<map_id>\d+)/geojson/$",
views.MapViewGeoJSON.as_view(),
name="map_geojson",
r"^map/(?P<map_id>\d+)/metadata/$",
views.MapMetadata.as_view(),
name="map_metadata",
),
re_path(
r"^map/anonymous-edit/(?P<signature>.+)$",

View file

@ -60,7 +60,7 @@ from .forms import (
DataLayerForm,
DataLayerPermissionsForm,
FlatErrorList,
MapSettingsForm,
MapMetadataForm,
SendLinkForm,
UpdateMapPermissionsForm,
UserProfileForm,
@ -321,9 +321,9 @@ class UserDownload(DetailView, SearchMixin):
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
for map_ in self.get_maps():
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"
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["Content-Disposition"] = (
@ -352,10 +352,9 @@ class MapsShowCase(View):
description = "{}\n[[{}|{}]]".format(
description, m.get_absolute_url(), _("View the map")
)
geometry = m.settings.get("geometry", json.loads(m.center.geojson))
return {
"type": "Feature",
"geometry": geometry,
"geometry": m.geometry,
"properties": {"name": m.name, "description": description},
}
@ -489,13 +488,13 @@ class MapDetailMixin(SessionMixin):
model = Map
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.
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
# used by frontend too.
if not url_template:
tilelayers = properties.get("tilelayers")
tilelayers = metadata.get("tilelayers")
if tilelayers:
url_template = tilelayers[0].get("url_template")
if url_template:
@ -504,9 +503,9 @@ class MapDetailMixin(SessionMixin):
if domain and "{" not in domain:
context["preconnect_domains"] = [f"//{domain}"]
def get_map_properties(self):
def get_metadata(self):
user = self.request.user
properties = {
metadata = {
"urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(),
"editMode": self.edit_mode,
@ -522,6 +521,8 @@ class MapDetailMixin(SessionMixin):
"websocketEnabled": settings.WEBSOCKET_ENABLED,
"websocketURI": settings.WEBSOCKET_FRONT_URI,
"importers": settings.UMAP_IMPORTERS,
"zoom": self.get_zoom(),
"geometry": self.get_geometry(),
}
created = bool(getattr(self, "object", None))
if (created and self.object.owner) or (not created and not user.is_anonymous):
@ -530,35 +531,31 @@ class MapDetailMixin(SessionMixin):
else:
map_statuses = AnonymousMapPermissionsForm.STATUS
datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS
properties["edit_statuses"] = [(i, str(label)) for i, label in map_statuses]
properties["datalayer_edit_statuses"] = [
metadata["edit_statuses"] = [(i, str(label)) for i, label in map_statuses]
metadata["datalayer_edit_statuses"] = [
(i, str(label)) for i, label in datalayer_statuses
]
if self.get_short_url():
properties["shortUrl"] = self.get_short_url()
metadata["shortUrl"] = self.get_short_url()
properties["user"] = self.get_user_data()
return properties
metadata["user"] = self.get_user_data()
return metadata
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
properties = self.get_map_properties()
metadata = self.get_metadata()
if settings.USE_I18N:
lang = settings.LANGUAGE_CODE
# Check attr in case the middleware is not active
if hasattr(self.request, "LANGUAGE_CODE"):
lang = self.request.LANGUAGE_CODE
properties["lang"] = lang
metadata["lang"] = lang
locale = translation.to_locale(lang)
properties["locale"] = locale
metadata["locale"] = locale
context["locale"] = locale
geojson = self.get_geojson()
if "properties" not in geojson:
geojson["properties"] = {}
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)
metadata["datalayers"] = self.get_datalayers()
context["map_metadata"] = json_dumps(metadata, indent=settings.DEBUG)
self.set_preconnect(metadata, context)
return context
def get_datalayers(self):
@ -574,18 +571,24 @@ class MapDetailMixin(SessionMixin):
def is_starred(self):
return False
def get_geojson(self):
def get_geometry(self):
return {
"geometry": {
"coordinates": [DEFAULT_LONGITUDE, DEFAULT_LATITUDE],
"type": "Point",
},
"properties": {
"zoom": getattr(settings, "LEAFLET_ZOOM", 6),
"datalayers": [],
},
"coordinates": [DEFAULT_LONGITUDE, DEFAULT_LATITUDE],
"type": "Point",
}
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):
return None
@ -637,7 +640,7 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get_datalayers(self):
return [
dl.metadata(self.request.user, self.request)
dl.get_metadata(self.request.user, self.request)
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)
return short_url
def get_geojson(self):
map_settings = self.object.settings
if "properties" not in map_settings:
map_settings["properties"] = {}
map_settings["properties"]["name"] = self.object.name
map_settings["properties"]["permissions"] = self.get_permissions()
return map_settings
def get_geometry(self):
return self.object.geometry
def get_zoom(self):
return self.object.zoom or self.object.metadata.get(
"zoom", settings.LEAFLET_ZOOM
)
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):
user = self.request.user
@ -744,12 +753,12 @@ class MapOEmbed(View):
return response
class MapViewGeoJSON(MapView):
class MapMetadata(MapView):
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):
return HttpResponse(context["map_settings"], content_type="application/json")
return HttpResponse(context["map_metadata"], content_type="application/json")
class MapNew(MapDetailMixin, TemplateView):
@ -759,15 +768,15 @@ class MapNew(MapDetailMixin, TemplateView):
class MapPreview(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html"
def get_map_properties(self):
properties = super().get_map_properties()
def get_metadata(self):
properties = super().get_metadata()
properties["preview"] = True
return properties
class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView):
model = Map
form_class = MapSettingsForm
form_class = MapMetadataForm
def form_valid(self, form):
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):
model = Map
form_class = MapSettingsForm
form_class = MapMetadataForm
pk_url_kwarg = "map_id"
def form_valid(self, form):
self.object.settings = form.cleaned_data["settings"]
self.object.metadata = form.cleaned_data["metadata"]
self.object.save()
return simple_json_response(
id=self.object.pk,
@ -1016,7 +1025,7 @@ class GZipMixin(object):
@property
def path(self):
return Path(self.object.geojson.path)
return Path(self.object.data.path)
@property
def gzip_path(self):
@ -1092,7 +1101,7 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
self.object = form.save()
# Simple response with only metadata (including new id)
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
return response
@ -1125,7 +1134,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
# If the reference document is not found, we can't merge.
return None
# 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.
with open(self.path) as f:
@ -1157,7 +1166,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
return HttpResponse(status=412)
# 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")
)
@ -1167,11 +1176,11 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
def form_valid(self, form):
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"):
data["geojson"] = json.loads(self.object.geojson.read().decode())
body["data"] = json.loads(self.object.data.read().decode())
self.request.session["needs_reload"] = False
response = simple_json_response(**data)
response = simple_json_response(**body)
response["X-Datalayer-Version"] = self.version
return response