umap/umap/tests/integration/test_optimistic_merge.py
Alexis Métaireau 7dadb83ff5 feat(sync): Let the clients set layers UUID
This make it possible to synchronize datalayers before their creation on
the server, allowing at the same time to solve issues related to them
not being saved (e.g. duplication of geometries)

We use the DataLayer._referenceVersion to track if a DataLayer has been
saved on the server.

After a save, the _referenceVersion is synched with other peers.

To pass the reference version from the server to the frontend, we have two
options:
- use a header
- populate the `options.version` field

In the case of a GET on a Datalayer, we could not use the `options` scenario
because:
- the value in the file is not up to date (it was the value the client has
 before the save)
- the python cannot change it on the fly, as the file is served by nginx

So we decided to keep using a header. But on the map view, we load all
datalayers metadatas in the map options, so here we cannot use the header
scenario, so in this specific case we had to populate `options._referenceVersion`.

At the same time, we also changed:
- Umap.options.umap_id => Umap.id
- DataLayer.umap_id => Datalayer.id
- fixed the version number returned by DataLayer.version_metadata
2024-11-15 15:22:36 +01:00

319 lines
12 KiB
Python

import json
import re
from pathlib import Path
from time import sleep
from playwright.sync_api import expect
from umap.models import DataLayer
from ..base import DataLayerFactory, MapFactory
DATALAYER_UPDATE = re.compile(r".*/datalayer/update/.*")
def test_created_markers_are_merged(context, live_server, tilelayer):
# Let's create a new map with an empty datalayer
map = MapFactory(name="server-side merge")
datalayer = DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
# Now navigate to this map and create marker
page_one = context.new_page()
page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
save_p1 = page_one.get_by_role("button", name="Save")
expect(save_p1).to_be_visible()
# Click on the Draw a marker button on a new map.
create_marker_p1 = page_one.get_by_title("Draw a marker")
expect(create_marker_p1).to_be_visible()
create_marker_p1.click()
# Check no marker is present by default.
marker_pane_p1 = page_one.locator(".leaflet-marker-pane > div")
expect(marker_pane_p1).to_have_count(0)
# Click on the map, it will place a marker at the given position.
map_el_p1 = page_one.locator("#map")
map_el_p1.click(position={"x": 200, "y": 200})
expect(marker_pane_p1).to_have_count(1)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click()
# 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 == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"editMode": "advanced",
"inCaption": True,
"id": str(datalayer.pk),
}
# Now navigate to this map from another tab
page_two = context.new_page()
page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
save_p2 = page_two.get_by_role("button", name="Save")
expect(save_p2).to_be_visible()
# Click on the Draw a marker button on a new map.
create_marker_p2 = page_two.get_by_title("Draw a marker")
expect(create_marker_p2).to_be_visible()
create_marker_p2.click()
# Check that the marker created in the orther tab is present.
marker_pane_p2 = page_two.locator(".leaflet-marker-pane > div")
expect(marker_pane_p2).to_have_count(1)
# Click on the map, it will place a marker at the given position.
map_el_p2 = page_two.locator("#map")
map_el_p2.click(position={"x": 220, "y": 220})
expect(marker_pane_p2).to_have_count(2)
with page_two.expect_response(DATALAYER_UPDATE):
save_p2.click()
sleep(1)
# No change after the save
expect(marker_pane_p2).to_have_count(2)
datalayer_v2 = DataLayer.objects.get(pk=datalayer.pk)
assert datalayer_v2.settings == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
}
# Now create another marker in the first tab
create_marker_p1.click()
map_el_p1.click(position={"x": 150, "y": 150})
expect(marker_pane_p1).to_have_count(2)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click()
# Should now get the other marker too
expect(marker_pane_p1).to_have_count(3)
datalayer_v3 = DataLayer.objects.get(pk=datalayer.pk)
assert datalayer_v3.settings == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
}
# And again
create_marker_p1.click()
map_el_p1.click(position={"x": 180, "y": 150})
expect(marker_pane_p1).to_have_count(4)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click()
sleep(1)
# Should now get the other marker too
datalayer_v4 = DataLayer.objects.get(pk=datalayer.pk)
assert datalayer_v4.settings == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
}
expect(marker_pane_p1).to_have_count(4)
# And again from the second tab
expect(marker_pane_p2).to_have_count(2)
create_marker_p2.click()
map_el_p2.click(position={"x": 250, "y": 150})
expect(marker_pane_p2).to_have_count(3)
with page_two.expect_response(DATALAYER_UPDATE):
save_p2.click()
sleep(1)
# Should now get the other markers too
datalayer_v5 = DataLayer.objects.get(pk=datalayer.pk)
assert datalayer_v5.settings == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
}
expect(marker_pane_p2).to_have_count(5)
def test_empty_datalayers_can_be_merged(context, live_server, tilelayer):
# Let's create a new map with an empty datalayer
map = MapFactory(name="server-side merge")
DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
# Open two tabs at the same time, on the same empty map
page_one = context.new_page()
page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
page_two = context.new_page()
page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
save_p1 = page_one.get_by_role("button", name="Save")
expect(save_p1).to_be_visible()
# Click on the Draw a marker button on a new map.
create_marker_p1 = page_one.get_by_title("Draw a marker")
expect(create_marker_p1).to_be_visible()
create_marker_p1.click()
# Check no marker is present by default.
marker_pane_p1 = page_one.locator(".leaflet-marker-pane > div")
expect(marker_pane_p1).to_have_count(0)
# Click on the map, it will place a marker at the given position.
map_el_p1 = page_one.locator("#map")
map_el_p1.click(position={"x": 200, "y": 200})
expect(marker_pane_p1).to_have_count(1)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click()
sleep(1)
save_p2 = page_two.get_by_role("button", name="Save")
expect(save_p2).to_be_visible()
# Click on the Draw a marker button on a new map.
create_marker_p2 = page_two.get_by_title("Draw a marker")
expect(create_marker_p2).to_be_visible()
create_marker_p2.click()
marker_pane_p2 = page_two.locator(".leaflet-marker-pane > div")
# Click on the map, it will place a marker at the given position.
map_el_p2 = page_two.locator("#map")
map_el_p2.click(position={"x": 220, "y": 220})
expect(marker_pane_p2).to_have_count(1)
# Save p1 and p2 at the same time
with page_two.expect_response(DATALAYER_UPDATE):
save_p2.click()
sleep(1)
expect(marker_pane_p2).to_have_count(2)
def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
# Let's create a new map with an empty datalayer
map = MapFactory(name="server-side merge")
datalayer = DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
# Open the created map on two pages.
page_one = context.new_page()
page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
page_two = context.new_page()
page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
save_p1 = page_one.get_by_role("button", name="Save")
expect(save_p1).to_be_visible()
save_p2 = page_two.get_by_role("button", name="Save")
expect(save_p2).to_be_visible()
# Create a point on the first map
create_marker_p1 = page_one.get_by_title("Draw a marker")
expect(create_marker_p1).to_be_visible()
create_marker_p1.click()
# Check no marker is present by default.
marker_pane_p1 = page_one.locator(".leaflet-marker-pane > div")
expect(marker_pane_p1).to_have_count(0)
# Click on the map, it will place a marker at the given position.
map_el_p1 = page_one.locator("#map")
map_el_p1.click(position={"x": 200, "y": 200})
expect(marker_pane_p1).to_have_count(1)
# And add one on the second map as well.
create_marker_p2 = page_two.get_by_title("Draw a marker")
expect(create_marker_p2).to_be_visible()
create_marker_p2.click()
marker_pane_p2 = page_two.locator(".leaflet-marker-pane > div")
# Click on the map, it will place a marker at the given position.
map_el_p2 = page_two.locator("#map")
map_el_p2.click(position={"x": 220, "y": 220})
expect(marker_pane_p2).to_have_count(1)
# Save the two tabs at the same time
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click()
sleep(0.2) # Needed to avoid having multiple requests coming at the same time.
save_p2.click()
# Now create another marker in the first tab
create_marker_p1.click()
map_el_p1.click(position={"x": 150, "y": 150})
expect(marker_pane_p1).to_have_count(2)
with page_one.expect_response(DATALAYER_UPDATE):
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 == {
"browsable": True,
"displayOnLoad": True,
"name": "test datalayer",
"inCaption": True,
"editMode": "advanced",
"id": str(datalayer.pk),
"permissions": {"edit_status": 1},
}
def test_should_display_alert_on_conflict(context, live_server, datalayer, openmap):
# Open the map on two pages.
page_one = context.new_page()
page_one.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
page_two = context.new_page()
page_two.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
# Change name on page one and save
page_one.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
page_one.locator('input[name="name"]').fill("name from page one")
with page_one.expect_response(re.compile(r".*/datalayer/update/.*")):
page_one.get_by_role("button", name="Save").click()
# Change name on page two and save
page_two.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
page_two.locator('input[name="name"]').fill("name from page two")
# Map should be in dirty status
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_role("button", name="Save").click()
# Make sure data is unchanged on the server
saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text())
assert data["features"][0]["properties"]["name"] == "name from page one"
# We should have an alert with some actions
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
# Map should still be in dirty status
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
# Override data from page two
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
page_two.get_by_text("Keep your changes and loose theirs").click()
# Make sure server has page two data
saved = DataLayer.objects.last()
data = json.loads(Path(saved.geojson.path).read_text())
assert data["features"][0]["properties"]["name"] == "name from page two"
# Map should not be in dirty status anymore
expect(page_two.get_by_text("Cancel edits")).to_be_hidden()