Compare commits

..

No commits in common. "82b81706ab0192d91a68e7940cc645a344ffdd9c" and "c15ecfc29f7c3ed618267fcb05b8b8f6e0c1d05b" have entirely different histories.

21 changed files with 182 additions and 363 deletions

View file

@ -287,6 +287,14 @@ How many total maps to return in the search.
How many maps to show in the user "my maps" page.
#### UMAP_PURGATORY_ROOT
Path where files are moved when a datalayer is deleted. They will stay there until
`umap purge_purgatory` is run. May be useful in case a user deletes by mistake
a datalayer, or even a map.
Default is `/tmp/umappurgatory/`, so beware that this folder will be deleted on
each server restart.
#### UMAP_SEARCH_CONFIGURATION
Use it if you take control over the search configuration.

View file

@ -40,12 +40,16 @@ class UpdateMapPermissionsForm(forms.ModelForm):
class AnonymousMapPermissionsForm(forms.ModelForm):
edit_status = forms.ChoiceField(choices=Map.ANONYMOUS_EDIT_STATUS)
share_status = forms.ChoiceField(choices=Map.ANONYMOUS_SHARE_STATUS)
STATUS = (
(Map.OWNER, _("Only editable with secret edit link")),
(Map.ANONYMOUS, _("Everyone can edit")),
)
edit_status = forms.ChoiceField(choices=STATUS)
class Meta:
model = Map
fields = ("edit_status", "share_status")
fields = ("edit_status",)
class DataLayerForm(forms.ModelForm):
@ -61,7 +65,13 @@ class DataLayerPermissionsForm(forms.ModelForm):
class AnonymousDataLayerPermissionsForm(forms.ModelForm):
edit_status = forms.ChoiceField(choices=DataLayer.ANONYMOUS_EDIT_STATUS)
STATUS = (
(DataLayer.INHERIT, _("Inherit")),
(DataLayer.OWNER, _("Only editable with secret edit link")),
(DataLayer.ANONYMOUS, _("Everyone can edit")),
)
edit_status = forms.ChoiceField(choices=STATUS)
class Meta:
model = DataLayer

View file

@ -1,32 +0,0 @@
from datetime import datetime, timedelta
from django.core.management.base import BaseCommand
from umap.models import Map
class Command(BaseCommand):
help = "Remove maps in trash. Eg.: umap empty_trash --days 7"
def add_arguments(self, parser):
parser.add_argument(
"--days",
help="Number of days to consider maps for removal",
default=30,
type=int,
)
parser.add_argument(
"--dry-run",
help="Pretend to delete but just report",
action="store_true",
)
def handle(self, *args, **options):
days = options["days"]
since = datetime.utcnow() - timedelta(days=days)
print(f"Deleting map in trash since {since}")
maps = Map.objects.filter(share_status=Map.DELETED, modified_at__lt=since)
for map in maps:
if not options["dry_run"]:
map.delete()
print(f"Deleted map {map.name} ({map.id}), trashed on {map.modified_at}")

View file

@ -0,0 +1,28 @@
import time
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Remove old files from purgatory. Eg.: umap purge_purgatory --days 7"
def add_arguments(self, parser):
parser.add_argument(
"--days",
help="Number of days to consider files for removal",
default=30,
type=int,
)
def handle(self, *args, **options):
days = options["days"]
root = Path(settings.UMAP_PURGATORY_ROOT)
threshold = time.time() - days * 86400
for path in root.iterdir():
stats = path.stat()
filestamp = stats.st_mtime
if filestamp < threshold:
path.unlink()
print(f"Removed old file {path}")

View file

@ -1,30 +0,0 @@
# Generated by Django 5.1.2 on 2024-12-10 16:48
from django.db import migrations, models
import umap.models
class Migration(migrations.Migration):
dependencies = [
("umap", "0023_alter_datalayer_uuid"),
]
operations = [
migrations.AlterField(
model_name="map",
name="share_status",
field=models.SmallIntegerField(
choices=[
(0, "Draft (private)"),
(1, "Everyone (public)"),
(2, "Anyone with link"),
(3, "Editors and team only"),
(9, "Blocked"),
(99, "Deleted"),
],
default=umap.models.get_default_share_status,
verbose_name="share status",
),
),
]

View file

@ -38,22 +38,13 @@ def get_user_stars_url(self):
return reverse("user_stars", kwargs={"identifier": identifier})
def get_user_metadata(self):
return {
"id": self.pk,
"name": str(self),
"url": self.get_url(),
}
User.add_to_class("__str__", display_name)
User.add_to_class("get_url", get_user_url)
User.add_to_class("get_stars_url", get_user_stars_url)
User.add_to_class("get_metadata", get_user_metadata)
def get_default_share_status():
return settings.UMAP_DEFAULT_SHARE_STATUS or Map.DRAFT
return settings.UMAP_DEFAULT_SHARE_STATUS or Map.PUBLIC
def get_default_edit_status():
@ -170,30 +161,20 @@ class Map(NamedModel):
ANONYMOUS = 1
COLLABORATORS = 2
OWNER = 3
DRAFT = 0
PUBLIC = 1
OPEN = 2
PRIVATE = 3
BLOCKED = 9
DELETED = 99
ANONYMOUS_EDIT_STATUS = (
(OWNER, _("Only editable with secret edit link")),
(ANONYMOUS, _("Everyone can edit")),
)
EDIT_STATUS = (
(ANONYMOUS, _("Everyone")),
(COLLABORATORS, _("Editors and team only")),
(OWNER, _("Owner only")),
)
ANONYMOUS_SHARE_STATUS = (
(DRAFT, _("Draft (private)")),
SHARE_STATUS = (
(PUBLIC, _("Everyone (public)")),
)
SHARE_STATUS = ANONYMOUS_SHARE_STATUS + (
(OPEN, _("Anyone with link")),
(PRIVATE, _("Editors and team only")),
(BLOCKED, _("Blocked")),
(DELETED, _("Deleted")),
)
slug = models.SlugField(db_index=True)
center = models.PointField(geography=True, verbose_name=_("center"))
@ -276,10 +257,6 @@ class Map(NamedModel):
)
return map_settings
def move_to_trash(self):
self.share_status = Map.DELETED
self.save()
def delete(self, **kwargs):
# Explicitely call datalayers.delete, so we can deal with removing files
# (the cascade delete would not call the model delete method)
@ -375,20 +352,19 @@ class Map(NamedModel):
return can
def can_view(self, request):
if self.share_status in [Map.BLOCKED, Map.DELETED]:
if self.share_status == self.BLOCKED:
can = False
elif self.share_status in [Map.PUBLIC, Map.OPEN]:
can = True
elif self.owner is None:
can = settings.UMAP_ALLOW_ANONYMOUS and self.is_anonymous_owner(request)
can = True
elif self.share_status in [self.PUBLIC, self.OPEN]:
can = True
elif not request.user.is_authenticated:
can = False
elif request.user == self.owner:
can = True
else:
restricted = self.share_status in [Map.PRIVATE, Map.DRAFT]
can = not (
restricted
self.share_status == self.PRIVATE
and request.user not in self.editors.all()
and self.team not in request.user.teams.all()
)
@ -468,11 +444,6 @@ class DataLayer(NamedModel):
(COLLABORATORS, _("Editors and team only")),
(OWNER, _("Owner only")),
)
ANONYMOUS_EDIT_STATUS = (
(INHERIT, _("Inherit")),
(OWNER, _("Only editable with secret edit link")),
(ANONYMOUS, _("Everyone can edit")),
)
uuid = models.UUIDField(unique=True, primary_key=True, editable=False)
old_id = models.IntegerField(null=True, blank=True)
map = models.ForeignKey(Map, on_delete=models.CASCADE)
@ -513,13 +484,21 @@ class DataLayer(NamedModel):
force_insert=force_insert, force_update=force_update, **kwargs
)
self.purge_gzip()
self.purge_old_versions(keep=settings.UMAP_KEEP_VERSIONS)
self.purge_old_versions()
def delete(self, **kwargs):
self.purge_gzip()
self.purge_old_versions(keep=None)
self.to_purgatory()
return super().delete(**kwargs)
def to_purgatory(self):
dest = Path(settings.UMAP_PURGATORY_ROOT)
dest.mkdir(parents=True, exist_ok=True)
src = Path(self.geojson.storage.location) / self.storage_root()
for version in self.versions:
name = version["name"]
shutil.move(src / name, dest / f"{self.map.pk}_{name}")
def upload_to(self):
root = self.storage_root()
name = "%s_%s.geojson" % (self.pk, int(time.time() * 1000))
@ -597,16 +576,14 @@ class DataLayer(NamedModel):
def get_version_path(self, name):
return "{root}/{name}".format(root=self.storage_root(), name=name)
def purge_old_versions(self, keep=None):
def purge_old_versions(self):
root = self.storage_root()
versions = self.versions
if keep is not None:
versions = versions[keep:]
versions = self.versions[settings.UMAP_KEEP_VERSIONS :]
for version in versions:
name = version["name"]
# Should not be in the list, but ensure to not delete the file
# currently used in database
if keep is not None and self.geojson.name.endswith(name):
if self.geojson.name.endswith(name):
continue
try:
self.geojson.storage.delete(os.path.join(root, name))

View file

@ -272,6 +272,7 @@ UMAP_DEFAULT_FEATURES_HAVE_OWNERS = False
UMAP_HOME_FEED = "latest"
UMAP_IMPORTERS = {}
UMAP_HOST_INFOS = {}
UMAP_PURGATORY_ROOT = "/tmp/umappurgatory"
UMAP_LABEL_KEYS = ["name", "title"]
UMAP_READONLY = env("UMAP_READONLY", default=False)

View file

@ -50,14 +50,6 @@ export class MapPermissions extends ServerStored {
selectOptions: this._umap.properties.edit_statuses,
},
])
fields.push([
'properties.share_status',
{
handler: 'IntSelect',
label: translate('Who can view'),
selectOptions: this._umap.properties.share_statuses,
},
])
const builder = new U.FormBuilder(this, fields)
const form = builder.build()
container.appendChild(form)
@ -192,11 +184,11 @@ export class MapPermissions extends ServerStored {
}
if (this.isOwner() || this.isAnonymousMap()) {
formData.append('edit_status', this.properties.edit_status)
formData.append('share_status', this.properties.share_status)
}
if (this.isOwner()) {
formData.append('owner', this.properties.owner?.id)
formData.append('team', this.properties.team?.id || '')
formData.append('share_status', this.properties.share_status)
}
const [data, response, error] = await this._umap.server.post(
this.getUrl(),
@ -236,10 +228,6 @@ export class MapPermissions extends ServerStored {
]
}
}
isDraft() {
return this.properties.share_status === 0
}
}
export class DataLayerPermissions extends ServerStored {

View file

@ -31,8 +31,7 @@ const TOP_BAR_TEMPLATE = `
<button class="edit-save button round" type="button" data-ref="save">
<i class="icon icon-16 icon-save"></i>
<i class="icon icon-16 icon-save-disabled"></i>
<span hidden data-ref="saveLabel">${translate('Save')}</span>
<span hidden data-ref="saveDraftLabel">${translate('Save draft')}</span>
<span class="">${translate('Save')}</span>
</button>
</div>
</div>`
@ -146,8 +145,6 @@ export class TopBar extends WithTemplate {
redraw() {
this.elements.peers.hidden = !this._umap.getProperty('syncEnabled')
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
}
}

View file

@ -1321,7 +1321,6 @@ export default class Umap extends ServerStored {
})
})
}
this.topBar.redraw()
},
numberOfConnectedPeers: () => {
Utils.eachElement('.connected-peers span', (el) => {

View file

@ -102,7 +102,6 @@ class MapFactory(factory.django.DjangoModelFactory):
licence = factory.SubFactory(LicenceFactory)
owner = factory.SubFactory(UserFactory)
share_status = Map.PUBLIC
@classmethod
def _adjust_kwargs(cls, **kwargs):

View file

@ -76,6 +76,8 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
edit_permissions = owner_session.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = owner_session.locator(".umap-field-share_status select")
expect(select).to_be_hidden()
owner_field = owner_session.locator(".umap-field-owner")
expect(owner_field).to_be_hidden()
editors_field = owner_session.locator(".umap-field-editors input")
@ -90,15 +92,8 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")
expect(owner_session.locator(".umap-field-share_status select")).to_be_visible()
options = [
int(option.get_attribute("value"))
for option in owner_session.locator(
".umap-field-share_status select option"
).all()
]
assert options == [Map.DRAFT, Map.PUBLIC]
# This field should not be present in anonymous maps
# Those fields should not be present in anonymous maps
expect(owner_session.locator(".umap-field-share_status select")).to_be_hidden()
expect(owner_session.locator(".umap-field-owner")).to_be_hidden()
@ -140,15 +135,15 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
page.get_by_title("Manage layers").click()
page.get_by_title("Add a layer").click()
page.locator("input[name=name]").fill("Layer 1")
expect(
page.get_by_role("button", name="Visibility: Draft (private)")
).to_be_visible()
expect(page.get_by_role("button", name="Save", exact=True)).to_be_hidden()
save = page.get_by_role("button", name="Save")
expect(save).to_be_visible()
with page.expect_response(re.compile(r".*/datalayer/create/.*")):
page.get_by_role("button", name="Save draft", exact=True).click()
save.click()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_hidden()
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_hidden()
editors_field = page.locator(".umap-field-editors input")
@ -162,9 +157,6 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
)
expect(option).to_have_text("Inherit")
expect(page.get_by_label("Secret edit link:")).to_be_visible()
page.locator('select[name="share_status"]').select_option("1")
expect(page.get_by_role("button", name="Save draft", exact=True)).to_be_hidden()
expect(page.get_by_role("button", name="Save", exact=True)).to_be_visible()
def test_alert_message_after_create(
@ -240,7 +232,7 @@ def test_anonymous_owner_can_delete_the_map(anonymap, live_server, owner_session
owner_session.get_by_role("button", name="Delete").click()
with owner_session.expect_response(re.compile(r".*/update/delete/.*")):
owner_session.get_by_role("button", name="OK").click()
assert Map.objects.get(pk=anonymap.pk).share_status == Map.DELETED
assert not Map.objects.count()
def test_non_owner_cannot_see_delete_button(anonymap, live_server, page):

View file

@ -22,7 +22,7 @@ def test_owner_can_delete_map_after_confirmation(map, live_server, login):
with page.expect_navigation():
delete_button.click()
assert dialog_shown
assert Map.objects.get(pk=map.pk).share_status == Map.DELETED
assert Map.objects.all().count() == 0
def test_dashboard_map_preview(map, live_server, datalayer, login):

View file

@ -61,12 +61,8 @@ def test_owner_permissions_form(map, datalayer, live_server, login):
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
expect(page.locator(".umap-field-share_status select")).to_be_visible()
options = [
int(option.get_attribute("value"))
for option in page.locator(".umap-field-share_status select option").all()
]
assert options == [Map.DRAFT, Map.PUBLIC, Map.OPEN, Map.PRIVATE]
select = page.locator(".umap-field-share_status select")
expect(select).to_be_visible()
# expect(select).to_have_value(Map.PUBLIC) # Does not work
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_visible()
@ -141,7 +137,7 @@ def test_owner_has_delete_map_button(map, live_server, login):
delete.click()
with page.expect_navigation():
page.get_by_role("button", name="OK").click()
assert Map.objects.get(pk=map.pk).share_status == Map.DELETED
assert Map.objects.all().count() == 0
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
@ -185,31 +181,29 @@ def test_can_change_perms_after_create(tilelayer, live_server, login, user):
page.get_by_title("Manage layers").click()
page.get_by_title("Add a layer").click()
page.locator("input[name=name]").fill("Layer 1")
expect(
page.get_by_role("button", name="Visibility: Draft (private)")
).to_be_visible()
expect(page.get_by_role("button", name="Save", exact=True)).to_be_hidden()
save = page.get_by_role("button", name="Save")
expect(save).to_be_visible()
with page.expect_response(re.compile(r".*/map/create/")):
page.get_by_role("button", name="Save draft", exact=True).click()
save.click()
edit_permissions = page.get_by_title("Update permissions and editors")
expect(edit_permissions).to_be_visible()
edit_permissions.click()
expect(page.locator(".umap-field-share_status select")).to_be_visible()
expect(page.locator("select[name='share_status'] option:checked")).to_have_text(
"Draft (private)"
)
expect(page.locator(".umap-field-owner")).to_be_visible()
expect(page.locator(".umap-field-editors input")).to_be_visible()
expect(page.get_by_text('Who can edit "Layer 1"')).to_be_visible()
select = page.locator(".umap-field-share_status select")
expect(select).to_be_visible()
option = page.locator("select[name='share_status'] option:checked")
expect(option).to_have_text("Everyone (public)")
owner_field = page.locator(".umap-field-owner")
expect(owner_field).to_be_visible()
editors_field = page.locator(".umap-field-editors input")
expect(editors_field).to_be_visible()
datalayer_label = page.get_by_text('Who can edit "Layer 1"')
expect(datalayer_label).to_be_visible()
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
expect(options).to_have_count(4)
option = page.locator(
".datalayer-permissions select[name='edit_status'] option:checked"
)
expect(option).to_have_text("Inherit")
page.locator('select[name="share_status"]').select_option("1")
expect(page.get_by_role("button", name="Save draft", exact=True)).to_be_hidden()
expect(page.get_by_role("button", name="Save", exact=True)).to_be_visible()
def test_can_change_owner(map, live_server, login, user):

View file

@ -273,6 +273,7 @@ def test_anonymous_can_edit_in_inherit_mode_and_map_in_public_mode(
def test_should_remove_all_versions_on_delete(map, settings):
settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
root = Path(datalayer.storage_root())
before = len(datalayer.geojson.storage.listdir(root)[1])
@ -291,3 +292,4 @@ def test_should_remove_all_versions_on_delete(map, settings):
datalayer.delete()
found = datalayer.geojson.storage.listdir(root)[1]
assert found == [other, f"{other}.gz"]
assert len(list(Path(settings.UMAP_PURGATORY_ROOT).iterdir())) == 4 + before

View file

@ -1,34 +0,0 @@
from datetime import datetime, timedelta
from unittest import mock
import pytest
from django.core.management import call_command
from umap.models import Map
from .base import MapFactory
pytestmark = pytest.mark.django_db
def test_empty_trash(user):
recent = MapFactory(owner=user)
recent_deleted = MapFactory(owner=user)
recent_deleted.move_to_trash()
recent_deleted.save()
with mock.patch("django.utils.timezone.now") as mocked:
mocked.return_value = datetime.utcnow() - timedelta(days=8)
old_deleted = MapFactory(owner=user)
old_deleted.move_to_trash()
old_deleted.save()
old = MapFactory(owner=user)
assert Map.objects.count() == 4
call_command("empty_trash", "--days=7", "--dry-run")
assert Map.objects.count() == 4
call_command("empty_trash", "--days=9")
assert Map.objects.count() == 4
call_command("empty_trash", "--days=7")
assert not Map.objects.filter(pk=old_deleted.pk)
assert Map.objects.filter(pk=old.pk)
assert Map.objects.filter(pk=recent.pk)
assert Map.objects.filter(pk=recent_deleted.pk)

View file

@ -2,7 +2,6 @@ import pytest
from django.contrib.auth.models import AnonymousUser
from django.urls import reverse
from umap.forms import DEFAULT_CENTER
from umap.models import Map
from .base import MapFactory
@ -161,16 +160,8 @@ def test_can_change_default_edit_status(user, settings):
def test_can_change_default_share_status(user, settings):
map = Map.objects.create(owner=user, center=DEFAULT_CENTER)
assert map.share_status == Map.DRAFT
settings.UMAP_DEFAULT_SHARE_STATUS = Map.PUBLIC
map = Map.objects.create(owner=user, center=DEFAULT_CENTER)
map = MapFactory(owner=user)
assert map.share_status == Map.PUBLIC
def test_move_to_trash(user, map):
map.move_to_trash()
map.save()
reloaded = Map.objects.get(pk=map.pk)
assert reloaded.share_status == Map.DELETED
settings.UMAP_DEFAULT_SHARE_STATUS = Map.PRIVATE
map = MapFactory(owner=user)
assert map.share_status == Map.PRIVATE

View file

@ -42,7 +42,7 @@ def test_create(client, user, post_data):
assert created_map.center.y == 48.94415123418794
assert j["permissions"] == {
"edit_status": 3,
"share_status": 0,
"share_status": 1,
"owner": {"id": user.pk, "name": "Joe", "url": "/en/user/Joe/"},
"editors": [],
}
@ -114,10 +114,8 @@ def test_delete(client, map, datalayer):
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
)
assert response.status_code == 200
assert Map.objects.filter(pk=map.pk).exists()
assert DataLayer.objects.filter(pk=datalayer.pk).exists()
reloaded = Map.objects.get(pk=map.pk)
assert reloaded.share_status == Map.DELETED
assert not Map.objects.filter(pk=map.pk).exists()
assert not DataLayer.objects.filter(pk=datalayer.pk).exists()
# Check that user has not been impacted
assert User.objects.filter(pk=map.owner.pk).exists()
# Test response is a json
@ -243,46 +241,42 @@ def test_map_creation_should_allow_unicode_names(client, map, post_data):
assert created_map.slug == "map"
@pytest.mark.parametrize("share_status", [Map.PUBLIC, Map.OPEN])
def test_anonymous_can_access_map_with_share_status_accessible(
client, map, share_status
):
def test_anonymous_can_access_map_with_share_status_public(client, map):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = share_status
map.share_status = map.PUBLIC
map.save()
response = client.get(url)
assert response.status_code == 200
@pytest.mark.parametrize(
"share_status", [Map.PRIVATE, Map.DRAFT, Map.BLOCKED, Map.DELETED]
)
def test_anonymous_cannot_access_map_with_share_status_restricted(
client, map, share_status
):
def test_anonymous_can_access_map_with_share_status_open(client, map):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = share_status
map.share_status = map.OPEN
map.save()
response = client.get(url)
assert response.status_code == 200
def test_anonymous_cannot_access_map_with_share_status_private(client, map):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = map.PRIVATE
map.save()
response = client.get(url)
assert response.status_code == 403
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
def test_owner_can_access_map_with_share_status_restricted(client, map, share_status):
def test_owner_can_access_map_with_share_status_private(client, map):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = share_status
map.share_status = map.PRIVATE
map.save()
client.login(username=map.owner.username, password="123123")
response = client.get(url)
assert response.status_code == 200
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
def test_editors_can_access_map_with_share_status_resricted(
client, map, user, share_status
):
def test_editors_can_access_map_with_share_status_private(client, map, user):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = share_status
map.share_status = map.PRIVATE
map.editors.add(user)
map.save()
client.login(username=user.username, password="123123")
@ -290,11 +284,10 @@ def test_editors_can_access_map_with_share_status_resricted(
assert response.status_code == 200
def test_owner_cannot_access_map_with_share_status_deleted(client, map):
def test_anonymous_cannot_access_map_with_share_status_blocked(client, map):
url = reverse("map", args=(map.slug, map.pk))
map.share_status = map.DELETED
map.share_status = map.BLOCKED
map.save()
client.login(username=map.owner.username, password="123123")
response = client.get(url)
assert response.status_code == 403
@ -408,16 +401,14 @@ def test_anonymous_delete(cookieclient, anonymap):
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
)
assert response.status_code == 200
assert Map.objects.filter(pk=anonymap.pk).exists()
reloaded = Map.objects.get(pk=anonymap.pk)
assert reloaded.share_status == Map.DELETED
assert not Map.objects.filter(pk=anonymap.pk).count()
# Test response is a json
j = json.loads(response.content.decode())
assert "redirect" in j
@pytest.mark.usefixtures("allow_anonymous")
def test_no_cookie_cannot_delete(client, anonymap):
def test_no_cookie_cant_delete(client, anonymap):
url = reverse("map_delete", args=(anonymap.pk,))
response = client.post(
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
@ -425,24 +416,6 @@ def test_no_cookie_cannot_delete(client, anonymap):
assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous")
def test_no_cookie_cannot_view_anonymous_owned_map_in_draft(client, anonymap):
anonymap.share_status = Map.DRAFT
anonymap.save()
url = reverse("map", kwargs={"map_id": anonymap.pk, "slug": anonymap.slug})
response = client.get(url)
assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous")
def test_owner_can_view_anonymous_owned_map_in_draft(cookieclient, anonymap):
anonymap.share_status = Map.DRAFT
anonymap.save()
url = reverse("map", kwargs={"map_id": anonymap.pk, "slug": anonymap.slug})
response = cookieclient.get(url)
assert response.status_code == 200
@pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_edit_url(cookieclient, anonymap):
url = anonymap.get_anonymous_edit_url()
@ -584,6 +557,16 @@ def test_create_readonly(client, user, post_data, settings):
assert response.content == b"Site is readonly for maintenance"
def test_search(client, map):
# Very basic search, that do not deal with accent nor case.
# See install.md for how to have a smarter dict + index.
map.name = "Blé dur"
map.save()
url = reverse("search")
response = client.get(url + "?q=Blé")
assert "Blé dur" in response.content.decode()
def test_authenticated_user_can_star_map(client, map, user):
url = reverse("map_star", args=(map.pk,))
client.login(username=user.username, password="123123")
@ -765,9 +748,7 @@ def test_download_multiple_maps_editor(client, map, datalayer):
assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap"
@pytest.mark.parametrize(
"share_status", [Map.PRIVATE, Map.BLOCKED, Map.DRAFT, Map.DELETED]
)
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED])
def test_download_shared_status_map(client, map, datalayer, share_status):
map.share_status = share_status
map.save()
@ -776,9 +757,8 @@ def test_download_shared_status_map(client, map, datalayer, share_status):
assert response.status_code == 403
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
def test_download_my_map(client, map, datalayer, share_status):
map.share_status = share_status
def test_download_my_map(client, map, datalayer):
map.share_status = Map.PRIVATE
map.save()
client.login(username=map.owner.username, password="123123")
url = reverse("map_download", args=(map.pk,))
@ -789,19 +769,7 @@ def test_download_my_map(client, map, datalayer, share_status):
assert j["type"] == "umap"
@pytest.mark.parametrize("share_status", [Map.BLOCKED, Map.DELETED])
def test_download_my_map_blocked_or_deleted(client, map, datalayer, share_status):
map.share_status = share_status
map.save()
client.login(username=map.owner.username, password="123123")
url = reverse("map_download", args=(map.pk,))
response = client.get(url)
assert response.status_code == 403
@pytest.mark.parametrize(
"share_status", [Map.PRIVATE, Map.BLOCKED, Map.OPEN, Map.DRAFT]
)
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED, Map.OPEN])
def test_oembed_shared_status_map(client, map, datalayer, share_status):
map.share_status = share_status
map.save()

View file

@ -0,0 +1,25 @@
import os
import tempfile
from pathlib import Path
from django.core.management import call_command
def test_purge_purgatory(settings):
settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
root = Path(settings.UMAP_PURGATORY_ROOT)
old = root / "old.json"
old.write_text("{}")
stat = old.stat()
os.utime(old, times=(stat.st_mtime - 31 * 86400, stat.st_mtime - 31 * 86400))
recent = root / "recent.json"
recent.write_text("{}")
stat = recent.stat()
os.utime(recent, times=(stat.st_mtime - 8 * 86400, stat.st_mtime - 8 * 86400))
now = root / "now.json"
now.write_text("{}")
assert {f.name for f in root.iterdir()} == {"old.json", "recent.json", "now.json"}
call_command("purge_purgatory")
assert {f.name for f in root.iterdir()} == {"recent.json", "now.json"}
call_command("purge_purgatory", "--days=7")
assert {f.name for f in root.iterdir()} == {"now.json"}

View file

@ -288,28 +288,6 @@ def test_user_dashboard_display_user_maps(client, map):
assert "Owner only" in body
@pytest.mark.django_db
def test_user_dashboard_do_not_display_blocked_user_maps(client, map):
map.share_status = Map.BLOCKED
map.save()
client.login(username=map.owner.username, password="123123")
response = client.get(reverse("user_dashboard"))
assert response.status_code == 200
body = response.content.decode()
assert map.name not in body
@pytest.mark.django_db
def test_user_dashboard_do_not_display_deleted_user_maps(client, map):
map.share_status = Map.DELETED
map.save()
client.login(username=map.owner.username, password="123123")
response = client.get(reverse("user_dashboard"))
assert response.status_code == 200
body = response.content.decode()
assert map.name not in body
@pytest.mark.django_db
def test_user_dashboard_display_user_team_maps(client, map, team, user):
user.teams.add(team)
@ -519,34 +497,3 @@ def test_websocket_token_is_generated_for_editors(client, user, user2, map):
resp = client.get(token_url)
token = resp.json().get("token")
assert TimestampSigner().unsign_object(token, max_age=30)
@pytest.mark.django_db
def test_search(client, map):
# Very basic search, that do not deal with accent nor case.
# See install.md for how to have a smarter dict + index.
map.name = "Blé dur"
map.save()
url = reverse("search")
response = client.get(url + "?q=Blé")
assert "Blé dur" in response.content.decode()
@pytest.mark.django_db
def test_cannot_search_blocked_map(client, map):
map.name = "Blé dur"
map.share_status = Map.BLOCKED
map.save()
url = reverse("search")
response = client.get(url + "?q=Blé")
assert "Blé dur" not in response.content.decode()
@pytest.mark.django_db
def test_cannot_search_deleted_map(client, map):
map.name = "Blé dur"
map.share_status = Map.DELETED
map.save()
url = reverse("search")
response = client.get(url + "?q=Blé")
assert "Blé dur" not in response.content.decode()

View file

@ -373,7 +373,6 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
def get_maps(self):
qs = self.get_search_queryset() or Map.objects.all()
qs = qs.exclude(share_status__in=[Map.DELETED, Map.BLOCKED])
teams = self.object.teams.all()
qs = (
qs.filter(owner=self.object)
@ -602,6 +601,9 @@ class MapDetailMixin(SessionMixin):
"id": self.get_id(),
"starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()),
"share_statuses": [
(i, str(label)) for i, label in Map.SHARE_STATUS if i != Map.BLOCKED
],
"umap_version": VERSION,
"featuresHaveOwner": settings.UMAP_DEFAULT_FEATURES_HAVE_OWNERS,
"websocketEnabled": settings.WEBSOCKET_ENABLED,
@ -611,22 +613,15 @@ class MapDetailMixin(SessionMixin):
}
created = bool(getattr(self, "object", None))
if (created and self.object.owner) or (not created and not user.is_anonymous):
edit_statuses = Map.EDIT_STATUS
map_statuses = Map.EDIT_STATUS
datalayer_statuses = DataLayer.EDIT_STATUS
share_statuses = Map.SHARE_STATUS
else:
edit_statuses = Map.ANONYMOUS_EDIT_STATUS
datalayer_statuses = DataLayer.ANONYMOUS_EDIT_STATUS
share_statuses = Map.ANONYMOUS_SHARE_STATUS
properties["edit_statuses"] = [(i, str(label)) for i, label in edit_statuses]
map_statuses = AnonymousMapPermissionsForm.STATUS
datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS
properties["edit_statuses"] = [(i, str(label)) for i, label in map_statuses]
properties["datalayer_edit_statuses"] = [
(i, str(label)) for i, label in datalayer_statuses
]
properties["share_statuses"] = [
(i, str(label))
for i, label in share_statuses
if i not in [Map.BLOCKED, Map.DELETED]
]
if self.get_short_url():
properties["shortUrl"] = self.get_short_url()
@ -689,9 +684,14 @@ class PermissionsMixin:
permissions["edit_status"] = self.object.edit_status
permissions["share_status"] = self.object.share_status
if self.object.owner:
permissions["owner"] = self.object.owner.get_metadata()
permissions["owner"] = {
"id": self.object.owner.pk,
"name": str(self.object.owner),
"url": self.object.owner.get_url(),
}
permissions["editors"] = [
editor.get_metadata() for editor in self.object.editors.all()
{"id": editor.pk, "name": str(editor)}
for editor in self.object.editors.all()
]
if self.object.team:
permissions["team"] = self.object.team.get_metadata()
@ -847,17 +847,6 @@ class MapViewGeoJSON(MapView):
class MapNew(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html"
def get_map_properties(self):
properties = super().get_map_properties()
properties["permissions"] = {
"edit_status": Map.edit_status.field.default(),
"share_status": Map.share_status.field.default(),
}
if self.request.user.is_authenticated:
user = self.request.user
properties["permissions"]["owner"] = user.get_metadata()
return properties
class MapPreview(MapDetailMixin, TemplateView):
template_name = "umap/map_detail.html"
@ -1022,7 +1011,7 @@ class MapDelete(DeleteView):
self.object = self.get_object()
if not self.object.can_delete(self.request):
return HttpResponseForbidden(_("Only its owner can delete the map."))
self.object.move_to_trash()
self.object.delete()
home_url = reverse("home")
messages.info(self.request, _("Map successfully deleted."))
if is_ajax(self.request):