Compare commits

..

13 commits

Author SHA1 Message Date
Yohan Boniface
ce876bc69b
Merge 4a1d34540d into be83eddbd0 2025-03-25 16:18:24 +01:00
Yohan Boniface
4a1d34540d wip: allow DataLayer.clear to be sync and undone 2025-03-23 11:32:51 +01:00
Yohan Boniface
9584b64a8f wip: uMap does not inherit anymore from ServerStored
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-21 17:23:18 +01:00
Yohan Boniface
7e85dad2fc wip: remove not effective code
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-21 16:45:10 +01:00
Yohan Boniface
91c1be1dba wip: add permissions related fields in schema
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-21 16:43:10 +01:00
Yohan Boniface
f084481e36 wip: allow to mark an operation as not undoable
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-21 16:33:09 +01:00
Yohan Boniface
92ddeea621 wip: tests pass
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-21 16:17:31 +01:00
Yohan Boniface
591cfce81f fixup: make sure to toggle remote client state at save too
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-20 17:09:44 +01:00
Yohan Boniface
159624c437 wip: derive the dirty status from the undoManager
This should pave the way for removing the SaveManager.

Co-authored-by: David Larlet <david@larlet.fr>
2025-03-20 16:51:30 +01:00
Yohan Boniface
0859254623 Update the tests and remove cancel edits
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-19 08:21:36 +01:00
e24b9f2aa7 Add integration test for batch undo/redo 2025-03-19 08:21:36 +01:00
Yohan Boniface
b4de49116e Batch operations
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-19 08:21:36 +01:00
Yohan Boniface
8f362b3bbc wip: undo redo
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2025-03-19 08:21:36 +01:00
20 changed files with 250 additions and 189 deletions

View file

@ -18,6 +18,7 @@ import { translate } from '../i18n.js'
import { DataLayerPermissions } from '../permissions.js'
import { Point, LineString, Polygon } from './features.js'
import TableEditor from '../tableeditor.js'
import { ServerStored } from '../saving.js'
import * as Schema from '../schema.js'
import { MutatingForm } from '../form/builder.js'
@ -35,8 +36,9 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
return acc
}, {})
export class DataLayer {
export class DataLayer extends ServerStored {
constructor(umap, leafletMap, data = {}) {
super()
this._umap = umap
this.sync = umap.syncEngine.proxy(this)
this._index = Array()
@ -112,6 +114,7 @@ export class DataLayer {
set isDeleted(status) {
this._isDeleted = status
if (status) this.isDirty = status
}
get isDeleted() {
@ -527,6 +530,10 @@ export class DataLayer {
return this._umap.formatter
.parse(raw, format)
.then((geojson) => this.addData(geojson))
.then((data) => {
if (data?.length) this.isDirty = true
return data
})
.catch((error) => {
console.debug(error)
Alert.error(translate('Import failed: invalid data'))
@ -603,6 +610,7 @@ export class DataLayer {
empty() {
if (this.isRemoteLayer()) return
this.clear()
this.isDirty = true
}
clone() {
@ -626,6 +634,25 @@ export class DataLayer {
this.clear()
}
reset() {
if (!this.createdOnServer) {
this.erase()
return
}
this.resetOptions()
this.parentPane.appendChild(this.pane)
if (this._leaflet_events_bk && !this._leaflet_events) {
this._leaflet_events = this._leaflet_events_bk
}
this.clear()
this.hide()
if (this.isRemoteLayer()) this.fetchRemoteData()
else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
this.show()
this.isDirty = false
}
redraw() {
if (!this.isVisible()) return
this.eachFeature((feature) => feature.redraw())
@ -808,9 +835,8 @@ export class DataLayer {
this
)
if (this._umap.properties.urls.datalayer_versions) {
if (this._umap.properties.urls.datalayer_versions)
this.buildVersionsFieldset(container)
}
const advancedActions = DomUtil.createFieldset(
container,
@ -885,15 +911,10 @@ export class DataLayer {
const appendVersion = (data) => {
const date = new Date(Number.parseInt(data.at, 10))
const content = `${date.toLocaleString(U.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
const [el, { button }] = Utils.loadTemplateWithRefs(
`<div class="umap-datalayer-version">
<button type="button" title="${translate('Restore this version')}" data-ref=button>
<i class="icon icon-16 icon-restore"></i> ${content}
</button>
</div>`
)
versionsContainer.appendChild(el)
button.addEventListener('click', () => this.restore(data.ref))
const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
const button = DomUtil.createButton('', el, '', () => this.restore(data.ref))
button.title = translate('Restore this version')
DomUtil.add('span', '', el, content)
}
const versionsContainer = DomUtil.createFieldset(container, translate('Versions'), {
@ -917,14 +938,11 @@ export class DataLayer {
)
if (!error) {
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
if (geojson._umap_options) {
const oldOptions = Utils.CopyJSON(this.options)
this.setOptions(geojson._umap_options)
this.sync.update('options', this.options, oldOptions)
}
if (geojson._umap_options) this.setOptions(geojson._umap_options)
this.empty()
if (this.isRemoteLayer()) this.fetchRemoteData()
else this.addData(geojson)
this.isDirty = true
}
})
}
@ -1105,10 +1123,6 @@ export class DataLayer {
}
async _trySave(url, headers, formData) {
if (this._forceSave) {
headers = {}
this._forceSave = false
}
const [data, response, error] = await this._umap.server.post(url, headers, formData)
if (error) {
if (response && response.status === 412) {
@ -1118,9 +1132,16 @@ export class DataLayer {
'This situation is tricky, you have to choose carefully which version is pertinent.'
),
async () => {
this._forceSave = true
// Save again this layer
const status = await this._trySave(url, {}, formData)
if (status) {
this.isDirty = false
// Call the main save, in case something else needs to be saved
// as the conflict stopped the saving flow
await this._umap.saveAll()
}
}
)
}
} else {

View file

@ -135,13 +135,7 @@ export default class Facets {
for (const [property, { label, type }] of parsed) {
dumped.push([property, label, type].filter(Boolean).join('|'))
}
const oldValue = this._umap.properties.facetKey
this._umap.properties.facetKey = dumped.join(',')
this._umap.sync.update(
'properties.facetKey',
this._umap.properties.facetKey,
oldValue
)
return dumped.join(',')
}
has(property) {
@ -152,13 +146,15 @@ export default class Facets {
const defined = this.getDefined()
if (!defined.has(property)) {
defined.set(property, { label, type })
this.dumps(defined)
this._umap.properties.facetKey = this.dumps(defined)
this._umap.isDirty = true
}
}
remove(property) {
const defined = this.getDefined()
defined.delete(property)
this.dumps(defined)
this._umap.properties.facetKey = this.dumps(defined)
this._umap.isDirty = true
}
}

View file

@ -70,7 +70,21 @@ export class Form extends Utils.WithEvents {
}
setter(field, value) {
Utils.setObjectValue(this.obj, field, value)
const path = field.split('.')
let obj = this.obj
let what
for (let i = 0, l = path.length; i < l; i++) {
what = path[i]
if (what === path[l - 1]) {
if (typeof value === 'undefined') {
delete obj[what]
} else {
obj[what] = value
}
} else {
obj = obj[what]
}
}
}
restoreField(field) {
@ -177,11 +191,7 @@ export class MutatingForm extends Form {
setter(field, value) {
const oldValue = this.getter(field)
if ('setter' in this.obj) {
this.obj.setter(field, value)
} else {
super.setter(field, value)
}
if ('render' in this.obj) {
this.obj.render([field], this)
}

View file

@ -1,15 +1,18 @@
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { ServerStored } from './saving.js'
import * as Utils from './utils.js'
import { MutatingForm } from './form/builder.js'
// Dedicated object so we can deal with a separate dirty status, and thus
// call the endpoint only when needed, saving one call at each save.
export class MapPermissions {
export class MapPermissions extends ServerStored {
constructor(umap) {
super()
this.setProperties(umap.properties.permissions)
this._umap = umap
this._isDirty = false
this.sync = umap.syncEngine.proxy(this)
}
@ -193,6 +196,7 @@ export class MapPermissions {
}
async save() {
if (!this.isDirty) return
const formData = new FormData()
if (!this.isAnonymousMap() && this.properties.editors) {
const editors = this.properties.editors.map((u) => u.id)
@ -251,8 +255,9 @@ export class MapPermissions {
}
}
export class DataLayerPermissions {
export class DataLayerPermissions extends ServerStored {
constructor(umap, datalayer) {
super()
this._umap = umap
this.properties = Object.assign(
{
@ -300,6 +305,7 @@ export class DataLayerPermissions {
}
async save() {
if (!this.isDirty) return
const formData = new FormData()
formData.append('edit_status', this.properties.edit_status)
const [data, response, error] = await this._umap.server.post(

View file

@ -117,7 +117,7 @@ export const Choropleth = FeatureGroup.extend({
},
_getValue: function (feature) {
const key = this.datalayer.options.choropleth?.property || 'value'
const key = this.datalayer.options.choropleth.property || 'value'
const value = +feature.properties[key]
if (!Number.isNaN(value)) return value
},
@ -130,12 +130,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.options.choropleth.mode
let classes = +this.datalayer.options.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.options.choropleth.breaks
if (manualBreaks) {
breaks = manualBreaks
.split(',')

View file

@ -17,10 +17,20 @@ class Rule {
this.parse()
}
get isDirty() {
return this._isDirty
}
set isDirty(status) {
this._isDirty = status
if (status) this._umap.isDirty = status
}
constructor(umap, condition = '', options = {}) {
// TODO make this public properties when browser coverage is ok
// cf https://caniuse.com/?search=public%20class%20field
this._condition = null
this._isDirty = false
this.OPERATORS = [
['>', this.gt],
['<', this.lt],
@ -180,25 +190,17 @@ class Rule {
_delete() {
this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
this._umap.rules.commit()
}
setter(key, value) {
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
Utils.setObjectValue(this, key, value)
this._umap.rules.commit()
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
}
}
export default class Rules {
constructor(umap) {
this._umap = umap
this.rules = []
this.load()
}
load() {
this.rules = []
if (!this._umap.properties.rules?.length) return
for (const { condition, options } of this._umap.properties.rules) {
if (!condition) continue
@ -220,8 +222,8 @@ export default class Rules {
else if (finalIndex > initialIndex) newIdx = referenceIdx
else newIdx = referenceIdx + 1
this.rules.splice(newIdx, 0, moved)
moved.isDirty = true
this._umap.render(['rules'])
this.commit()
}
edit(container) {
@ -240,6 +242,7 @@ export default class Rules {
addRule() {
const rule = new Rule(this._umap)
rule.isDirty = true
this.rules.push(rule)
rule.edit(map)
}

View file

@ -0,0 +1,52 @@
const _queue = new Set()
export let isDirty = false
export async function save() {
for (const obj of _queue) {
const ok = await obj.save()
if (!ok) break
remove(obj)
}
}
export function clear() {
_queue.clear()
onUpdate()
}
function add(obj) {
_queue.add(obj)
onUpdate()
}
function remove(obj) {
_queue.delete(obj)
onUpdate()
}
function has(obj) {
return _queue.has(obj)
}
function onUpdate() {
isDirty = Boolean(_queue.size)
// document.body.classList.toggle('umap-is-dirty', isDirty)
}
export class ServerStored {
set isDirty(status) {
if (status) {
add(this)
} else {
remove(this)
}
this.onDirty(status)
}
get isDirty() {
return has(this)
}
onDirty(status) {}
}

View file

@ -9,6 +9,7 @@ import {
} from './updaters.js'
import { WebSocketTransport } from './websocket.js'
import { UndoManager } from './undo.js'
import * as SaveManager from '../saving.js'
// Start reconnecting after 2 seconds, then double the delay each time
// maxing out at 32 seconds.
@ -72,7 +73,7 @@ export class SyncEngine {
this.websocketConnected = false
this.closeRequested = false
this.peerId = Utils.generateId()
this._undoManager = new UndoManager(umap, this.updaters, this)
this._undoManager = new UndoManager(this.updaters, this)
}
get isOpen() {

View file

@ -2,8 +2,7 @@ import * as Utils from '../utils.js'
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
export class UndoManager {
constructor(umap, updaters, syncEngine) {
this._umap = umap
constructor(updaters, syncEngine) {
this._syncEngine = syncEngine
this.updaters = updaters
this._undoStack = []
@ -11,8 +10,6 @@ export class UndoManager {
}
toggleState() {
// document is undefined during unittests
if (typeof document === 'undefined') return
const undoButton = document.querySelector('.edit-undo')
const redoButton = document.querySelector('.edit-redo')
if (undoButton) undoButton.disabled = !this._undoStack.length
@ -28,7 +25,6 @@ export class UndoManager {
}
isDirty() {
if (!this._umap.id) return true
for (const stage of this._undoStack) {
if (stage.operation.dirty) return true
}

View file

@ -1,4 +1,4 @@
import * as Utils from '../utils.js'
import { fieldInSchema } from '../utils.js'
/**
* Updaters are classes able to convert messages
@ -10,6 +10,27 @@ class BaseUpdater {
this._umap = umap
}
updateObjectValue(obj, key, value) {
const parts = key.split('.')
const lastKey = parts.pop()
// Reduce the current list of attributes,
// to find the object to set the property onto
const objectToSet = parts.reduce((currentObj, part) => {
if (currentObj !== undefined && part in currentObj) return currentObj[part]
}, obj)
// In case the given path doesn't exist, stop here
if (objectToSet === undefined) return
// Set the value (or delete it)
if (typeof value === 'undefined') {
delete objectToSet[lastKey]
} else {
objectToSet[lastKey] = value
}
}
getDataLayerFromID(layerId) {
return this._umap.getDataLayerByUmapId(layerId)
}
@ -22,8 +43,8 @@ class BaseUpdater {
export class MapUpdater extends BaseUpdater {
update({ key, value }) {
if (Utils.fieldInSchema(key)) {
Utils.setObjectValue(this._umap, key, value)
if (fieldInSchema(key)) {
this.updateObjectValue(this._umap, key, value)
}
this._umap.onPropertiesUpdated([key])
@ -52,10 +73,8 @@ export class DataLayerUpdater extends BaseUpdater {
update({ key, metadata, value }) {
const datalayer = this.getDataLayerFromID(metadata.id)
if (key === 'options') {
datalayer.setOptions(value)
} else if (Utils.fieldInSchema(key)) {
Utils.setObjectValue(datalayer, key, value)
if (fieldInSchema(key)) {
this.updateObjectValue(datalayer, key, value)
} else {
console.debug(
'Not applying update for datalayer because key is not in the schema',
@ -108,7 +127,7 @@ export class FeatureUpdater extends BaseUpdater {
const feature = this.getFeatureFromMetadata(metadata)
feature.geometry = value
} else {
Utils.setObjectValue(feature, key, value)
this.updateObjectValue(feature, key, value)
feature.datalayer.indexProperties(feature)
}
@ -129,8 +148,8 @@ export class FeatureUpdater extends BaseUpdater {
export class MapPermissionsUpdater extends BaseUpdater {
update({ key, value }) {
if (Utils.fieldInSchema(key)) {
Utils.setObjectValue(this._umap.permissions, key, value)
if (fieldInSchema(key)) {
this.updateObjectValue(this._umap.permissions, key, value)
}
}
@ -141,8 +160,8 @@ export class MapPermissionsUpdater extends BaseUpdater {
export class DataLayerPermissionsUpdater extends BaseUpdater {
update({ key, value, metadata }) {
if (Utils.fieldInSchema(key)) {
Utils.setObjectValue(this.getDataLayerFromID(metadata.id), key, value)
if (fieldInSchema(key)) {
this.updateObjectValue(this.getDataLayerFromID(metadata.id), key, value)
}
}

View file

@ -162,7 +162,6 @@ export class TopBar extends WithTemplate {
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
this._umap.sync._undoManager.toggleState()
}
}

View file

@ -1159,6 +1159,7 @@ export default class Umap {
}
async save() {
this.rules.commit()
const geojson = {
type: 'Feature',
geometry: this.geometry(),
@ -1319,9 +1320,6 @@ export default class Umap {
this.bottomBar.redraw()
break
case 'data':
if (fields.includes('properties.rules')) {
this.rules.load()
}
this.eachVisibleDataLayer((datalayer) => {
datalayer.redraw()
})

View file

@ -481,27 +481,6 @@ export const debounce = (callback, wait) => {
}
}
export function setObjectValue(obj, key, value) {
const parts = key.split('.')
const lastKey = parts.pop()
// Reduce the current list of attributes,
// to find the object to set the property onto
const objectToSet = parts.reduce((currentObj, part) => {
if (currentObj !== undefined && part in currentObj) return currentObj[part]
}, obj)
// In case the given path doesn't exist, stop here
if (objectToSet === undefined) return
// Set the value (or delete it)
if (typeof value === 'undefined') {
delete objectToSet[lastKey]
} else {
objectToSet[lastKey] = value
}
}
export const COLORS = [
'Black',
'Navy',

View file

@ -475,6 +475,22 @@ ul.photon-autocomplete {
font-style: italic;
}
.umap-datalayer-version {
padding: 5px 0;
border-bottom: 1px solid #202425;
}
.umap-datalayer-version button {
display: inline-block;
width: 24px;
min-height: 24px;
background-position: -122px -73px;
background-repeat: no-repeat;
background-image: url('./img/16-white.svg');
margin-inline-end: 10px;
}
.leaflet-toolbar-tip {
background-color: var(--color-darkGray);
}

View file

@ -49,6 +49,63 @@ describe('#dispatch', () => {
})
})
describe('Updaters', () => {
describe('BaseUpdater', () => {
let updater
let map
let obj
beforeEach(() => {
map = {}
updater = new MapUpdater(map)
obj = {}
})
it('should be able to set object properties', () => {
let obj = {}
updater.updateObjectValue(obj, 'foo', 'foo')
expect(obj).deep.equal({ foo: 'foo' })
})
it('should be able to set object properties recursively on existing objects', () => {
let obj = { foo: {} }
updater.updateObjectValue(obj, 'foo.bar', 'foo')
expect(obj).deep.equal({ foo: { bar: 'foo' } })
})
it('should be able to set object properties recursively on deep objects', () => {
let obj = { foo: { bar: { baz: {} } } }
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
})
it('should be able to replace object properties recursively on deep objects', () => {
let obj = { foo: { bar: { baz: { test: 'test' } } } }
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
})
it('should not set object properties recursively on non-existing objects', () => {
let obj = { foo: {} }
updater.updateObjectValue(obj, 'bar.bar', 'value')
expect(obj).deep.equal({ foo: {} })
})
it('should delete keys for undefined values', () => {
let obj = { foo: 'foo' }
updater.updateObjectValue(obj, 'foo', undefined)
expect(obj).deep.equal({})
})
it('should delete keys for undefined values, recursively', () => {
let obj = { foo: { bar: 'bar' } }
updater.updateObjectValue(obj, 'foo.bar', undefined)
expect(obj).deep.equal({ foo: {} })
})
})
})
describe('Operations', () => {
describe('haveSameContext', () => {

View file

@ -862,51 +862,4 @@ describe('Utils', () => {
assert.equal(Utils.isObject(''), false)
})
})
describe('setObjectValue', () => {
it('should be able to set object properties', () => {
let obj = {}
Utils.setObjectValue(obj, 'foo', 'foo')
expect(obj).deep.equal({ foo: 'foo' })
})
it('should be able to set object properties recursively on existing objects', () => {
let obj = { foo: {} }
Utils.setObjectValue(obj, 'foo.bar', 'foo')
expect(obj).deep.equal({ foo: { bar: 'foo' } })
})
it('should be able to set object properties recursively on deep objects', () => {
let obj = { foo: { bar: { baz: {} } } }
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
})
it('should be able to replace object properties recursively on deep objects', () => {
let obj = { foo: { bar: { baz: { test: 'test' } } } }
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
})
it('should not set object properties recursively on non-existing objects', () => {
let obj = { foo: {} }
Utils.setObjectValue(obj, 'bar.bar', 'value')
expect(obj).deep.equal({ foo: {} })
})
it('should delete keys for undefined values', () => {
let obj = { foo: 'foo' }
Utils.setObjectValue(obj, 'foo', undefined)
expect(obj).deep.equal({})
})
it('should delete keys for undefined values, recursively', () => {
let obj = { foo: { bar: 'bar' } }
Utils.setObjectValue(obj, 'foo.bar', undefined)
expect(obj).deep.equal({ foo: {} })
})
})
})

View file

@ -261,9 +261,6 @@ def test_can_create_new_rule(live_server, page, openmap):
page.get_by_title("AliceBlue").first.click()
colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 3
page.get_by_role("button", name="Undo").click()
colors = getColors(markers)
assert colors.count("rgb(240, 248, 255)") == 0
def test_can_deactive_rule_from_list(live_server, page, openmap):

View file

@ -689,43 +689,3 @@ def test_should_sync_datalayer_clear(
peerA.get_by_role("button", name="Undo").click()
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
@pytest.mark.xdist_group(name="websockets")
def test_should_save_remote_dirty_datalayers(new_page, asgi_live_server, tilelayer):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
map.save()
assert not DataLayer.objects.count()
# Create two tabs
peerA = new_page("Page A")
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
peerB = new_page("Page B")
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
# Create a new layer from peerA
peerA.get_by_role("button", name="Manage layers").click()
peerA.get_by_role("button", name="Add a layer").click()
# Create a new layer from peerB
peerB.get_by_role("button", name="Manage layers").click()
peerB.get_by_role("button", name="Add a layer").click()
# Save from peerA to the server
counter = 0
def on_response(response):
nonlocal counter
if "/datalayer/create/" in response.url:
counter += 1
# Wait for the two datalayer saves
if counter == 2:
return True
return False
with peerA.expect_response(on_response):
peerA.get_by_role("button", name="Save").click()
assert DataLayer.objects.count() == 2

View file

@ -103,7 +103,6 @@ def test_get_version(map, datalayer):
],
"type": "Point",
},
"id": "ExNTQ",
"properties": {
"_umap_options": {
"color": "DarkCyan",

View file

@ -694,7 +694,6 @@ def test_download(client, map, datalayer):
"coordinates": [14.68896484375, 48.55297816440071],
"type": "Point",
},
"id": "ExNTQ",
"properties": {
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
"description": "Da place anonymous again 755",