mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 11:52:38 +02:00
feat: introduce Map.share_status=DRAFT and DELETED (#2357)
This PR introduce two new share_status: `DRAFT` and `DELETED`. So all status are now: (DRAFT, _("Draft (private)")), (PUBLIC, _("Everyone (public)")), (OPEN, _("Anyone with link")), (PRIVATE, _("Editors and team only")), (BLOCKED, _("Blocked")), (DELETED, _("Deleted")), Here are the impact of such introduction, on the draft side: - by default maps are now create in draft status, and they are not visible from others than owner (or collaborators if any); this can be changed for a given instance with the setting `UMAP_DEFAULT_SHARE_STATUS` - now even anonymous owned maps have a share status, given one of the goals is to make a better distinction between maps ready to be shared and other, this also apply to maps without logged in owners  - when the map in in draft mode, the "Save" button on the frontend says "Save draft", so to make the state clear  - now the visibility of the map is immediately visible on the top bar (while before this was only displayed after first save)  Note: add the end `DRAFT` and `PRIVATE` are very similar, but I made the choice to keep the two, so one can still distinguish their draft maps from their ready map they want to keep private. On the delete side: - when deleting a map, it's now set as `share_status=DELETED`, which act as a sort of trash; so it become easier to recover a map, with it's full datalayers, etc. (the only thing which will not be restored is the previous share_status, which should be draft again after a restore; the restore function itself is not implemented) - there is a new command `empty_trash` which delete for real maps in DELETED status and with last_modified > 30 days (by default, can be changed with a command line argument) - deleted maps disappear from all views: home, search, dashboard… - in the future, we could create a new view "My Trash", where one could see their deleted map not yet deleted for real (and that should be the opportunity to also add the `restore` function, which for now can be done by simply changing the share_status from the shell or the admin) - all the purgatory related code has been removed fix #2207
This commit is contained in:
commit
73e7f60cdf
20 changed files with 333 additions and 182 deletions
|
@ -287,14 +287,6 @@ How many total maps to return in the search.
|
||||||
|
|
||||||
How many maps to show in the user "my maps" page.
|
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
|
#### UMAP_SEARCH_CONFIGURATION
|
||||||
|
|
||||||
Use it if you take control over the search configuration.
|
Use it if you take control over the search configuration.
|
||||||
|
|
|
@ -40,16 +40,12 @@ class UpdateMapPermissionsForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class AnonymousMapPermissionsForm(forms.ModelForm):
|
class AnonymousMapPermissionsForm(forms.ModelForm):
|
||||||
STATUS = (
|
edit_status = forms.ChoiceField(choices=Map.ANONYMOUS_EDIT_STATUS)
|
||||||
(Map.OWNER, _("Only editable with secret edit link")),
|
share_status = forms.ChoiceField(choices=Map.ANONYMOUS_SHARE_STATUS)
|
||||||
(Map.ANONYMOUS, _("Everyone can edit")),
|
|
||||||
)
|
|
||||||
|
|
||||||
edit_status = forms.ChoiceField(choices=STATUS)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Map
|
model = Map
|
||||||
fields = ("edit_status",)
|
fields = ("edit_status", "share_status")
|
||||||
|
|
||||||
|
|
||||||
class DataLayerForm(forms.ModelForm):
|
class DataLayerForm(forms.ModelForm):
|
||||||
|
@ -65,13 +61,7 @@ class DataLayerPermissionsForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class AnonymousDataLayerPermissionsForm(forms.ModelForm):
|
class AnonymousDataLayerPermissionsForm(forms.ModelForm):
|
||||||
STATUS = (
|
edit_status = forms.ChoiceField(choices=DataLayer.ANONYMOUS_EDIT_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:
|
class Meta:
|
||||||
model = DataLayer
|
model = DataLayer
|
||||||
|
|
32
umap/management/commands/empty_trash.py
Normal file
32
umap/management/commands/empty_trash.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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}")
|
|
@ -1,28 +0,0 @@
|
||||||
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}")
|
|
|
@ -38,13 +38,22 @@ def get_user_stars_url(self):
|
||||||
return reverse("user_stars", kwargs={"identifier": identifier})
|
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("__str__", display_name)
|
||||||
User.add_to_class("get_url", get_user_url)
|
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_stars_url", get_user_stars_url)
|
||||||
|
User.add_to_class("get_metadata", get_user_metadata)
|
||||||
|
|
||||||
|
|
||||||
def get_default_share_status():
|
def get_default_share_status():
|
||||||
return settings.UMAP_DEFAULT_SHARE_STATUS or Map.PUBLIC
|
return settings.UMAP_DEFAULT_SHARE_STATUS or Map.DRAFT
|
||||||
|
|
||||||
|
|
||||||
def get_default_edit_status():
|
def get_default_edit_status():
|
||||||
|
@ -161,20 +170,30 @@ class Map(NamedModel):
|
||||||
ANONYMOUS = 1
|
ANONYMOUS = 1
|
||||||
COLLABORATORS = 2
|
COLLABORATORS = 2
|
||||||
OWNER = 3
|
OWNER = 3
|
||||||
|
DRAFT = 0
|
||||||
PUBLIC = 1
|
PUBLIC = 1
|
||||||
OPEN = 2
|
OPEN = 2
|
||||||
PRIVATE = 3
|
PRIVATE = 3
|
||||||
BLOCKED = 9
|
BLOCKED = 9
|
||||||
|
DELETED = 99
|
||||||
|
ANONYMOUS_EDIT_STATUS = (
|
||||||
|
(OWNER, _("Only editable with secret edit link")),
|
||||||
|
(ANONYMOUS, _("Everyone can edit")),
|
||||||
|
)
|
||||||
EDIT_STATUS = (
|
EDIT_STATUS = (
|
||||||
(ANONYMOUS, _("Everyone")),
|
(ANONYMOUS, _("Everyone")),
|
||||||
(COLLABORATORS, _("Editors and team only")),
|
(COLLABORATORS, _("Editors and team only")),
|
||||||
(OWNER, _("Owner only")),
|
(OWNER, _("Owner only")),
|
||||||
)
|
)
|
||||||
SHARE_STATUS = (
|
ANONYMOUS_SHARE_STATUS = (
|
||||||
|
(DRAFT, _("Draft (private)")),
|
||||||
(PUBLIC, _("Everyone (public)")),
|
(PUBLIC, _("Everyone (public)")),
|
||||||
|
)
|
||||||
|
SHARE_STATUS = ANONYMOUS_SHARE_STATUS + (
|
||||||
(OPEN, _("Anyone with link")),
|
(OPEN, _("Anyone with link")),
|
||||||
(PRIVATE, _("Editors and team only")),
|
(PRIVATE, _("Editors and team only")),
|
||||||
(BLOCKED, _("Blocked")),
|
(BLOCKED, _("Blocked")),
|
||||||
|
(DELETED, _("Deleted")),
|
||||||
)
|
)
|
||||||
slug = models.SlugField(db_index=True)
|
slug = models.SlugField(db_index=True)
|
||||||
center = models.PointField(geography=True, verbose_name=_("center"))
|
center = models.PointField(geography=True, verbose_name=_("center"))
|
||||||
|
@ -257,6 +276,10 @@ class Map(NamedModel):
|
||||||
)
|
)
|
||||||
return map_settings
|
return map_settings
|
||||||
|
|
||||||
|
def move_to_trash(self):
|
||||||
|
self.share_status = Map.DELETED
|
||||||
|
self.save()
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
# Explicitely call datalayers.delete, so we can deal with removing files
|
# Explicitely call datalayers.delete, so we can deal with removing files
|
||||||
# (the cascade delete would not call the model delete method)
|
# (the cascade delete would not call the model delete method)
|
||||||
|
@ -352,19 +375,20 @@ class Map(NamedModel):
|
||||||
return can
|
return can
|
||||||
|
|
||||||
def can_view(self, request):
|
def can_view(self, request):
|
||||||
if self.share_status == self.BLOCKED:
|
if self.share_status in [Map.BLOCKED, Map.DELETED]:
|
||||||
can = False
|
can = False
|
||||||
|
elif self.share_status in [Map.PUBLIC, Map.OPEN]:
|
||||||
|
can = True
|
||||||
elif self.owner is None:
|
elif self.owner is None:
|
||||||
can = True
|
can = settings.UMAP_ALLOW_ANONYMOUS and self.is_anonymous_owner(request)
|
||||||
elif self.share_status in [self.PUBLIC, self.OPEN]:
|
|
||||||
can = True
|
|
||||||
elif not request.user.is_authenticated:
|
elif not request.user.is_authenticated:
|
||||||
can = False
|
can = False
|
||||||
elif request.user == self.owner:
|
elif request.user == self.owner:
|
||||||
can = True
|
can = True
|
||||||
else:
|
else:
|
||||||
|
restricted = self.share_status in [Map.PRIVATE, Map.DRAFT]
|
||||||
can = not (
|
can = not (
|
||||||
self.share_status == self.PRIVATE
|
restricted
|
||||||
and request.user not in self.editors.all()
|
and request.user not in self.editors.all()
|
||||||
and self.team not in request.user.teams.all()
|
and self.team not in request.user.teams.all()
|
||||||
)
|
)
|
||||||
|
@ -444,6 +468,11 @@ class DataLayer(NamedModel):
|
||||||
(COLLABORATORS, _("Editors and team only")),
|
(COLLABORATORS, _("Editors and team only")),
|
||||||
(OWNER, _("Owner 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)
|
uuid = models.UUIDField(unique=True, primary_key=True, editable=False)
|
||||||
old_id = models.IntegerField(null=True, blank=True)
|
old_id = models.IntegerField(null=True, blank=True)
|
||||||
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
map = models.ForeignKey(Map, on_delete=models.CASCADE)
|
||||||
|
@ -484,21 +513,13 @@ class DataLayer(NamedModel):
|
||||||
force_insert=force_insert, force_update=force_update, **kwargs
|
force_insert=force_insert, force_update=force_update, **kwargs
|
||||||
)
|
)
|
||||||
self.purge_gzip()
|
self.purge_gzip()
|
||||||
self.purge_old_versions()
|
self.purge_old_versions(keep=settings.UMAP_KEEP_VERSIONS)
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
def delete(self, **kwargs):
|
||||||
self.purge_gzip()
|
self.purge_gzip()
|
||||||
self.to_purgatory()
|
self.purge_old_versions(keep=None)
|
||||||
return super().delete(**kwargs)
|
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):
|
def upload_to(self):
|
||||||
root = self.storage_root()
|
root = self.storage_root()
|
||||||
name = "%s_%s.geojson" % (self.pk, int(time.time() * 1000))
|
name = "%s_%s.geojson" % (self.pk, int(time.time() * 1000))
|
||||||
|
@ -576,14 +597,16 @@ class DataLayer(NamedModel):
|
||||||
def get_version_path(self, name):
|
def get_version_path(self, name):
|
||||||
return "{root}/{name}".format(root=self.storage_root(), name=name)
|
return "{root}/{name}".format(root=self.storage_root(), name=name)
|
||||||
|
|
||||||
def purge_old_versions(self):
|
def purge_old_versions(self, keep=None):
|
||||||
root = self.storage_root()
|
root = self.storage_root()
|
||||||
versions = self.versions[settings.UMAP_KEEP_VERSIONS :]
|
versions = self.versions
|
||||||
|
if keep is not None:
|
||||||
|
versions = versions[keep:]
|
||||||
for version in versions:
|
for version in versions:
|
||||||
name = version["name"]
|
name = version["name"]
|
||||||
# Should not be in the list, but ensure to not delete the file
|
# Should not be in the list, but ensure to not delete the file
|
||||||
# currently used in database
|
# currently used in database
|
||||||
if self.geojson.name.endswith(name):
|
if keep is not None and self.geojson.name.endswith(name):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
self.geojson.storage.delete(os.path.join(root, name))
|
self.geojson.storage.delete(os.path.join(root, name))
|
||||||
|
|
|
@ -272,7 +272,6 @@ UMAP_DEFAULT_FEATURES_HAVE_OWNERS = False
|
||||||
UMAP_HOME_FEED = "latest"
|
UMAP_HOME_FEED = "latest"
|
||||||
UMAP_IMPORTERS = {}
|
UMAP_IMPORTERS = {}
|
||||||
UMAP_HOST_INFOS = {}
|
UMAP_HOST_INFOS = {}
|
||||||
UMAP_PURGATORY_ROOT = "/tmp/umappurgatory"
|
|
||||||
UMAP_LABEL_KEYS = ["name", "title"]
|
UMAP_LABEL_KEYS = ["name", "title"]
|
||||||
|
|
||||||
UMAP_READONLY = env("UMAP_READONLY", default=False)
|
UMAP_READONLY = env("UMAP_READONLY", default=False)
|
||||||
|
|
|
@ -50,6 +50,14 @@ export class MapPermissions extends ServerStored {
|
||||||
selectOptions: this._umap.properties.edit_statuses,
|
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 builder = new U.FormBuilder(this, fields)
|
||||||
const form = builder.build()
|
const form = builder.build()
|
||||||
container.appendChild(form)
|
container.appendChild(form)
|
||||||
|
@ -184,11 +192,11 @@ export class MapPermissions extends ServerStored {
|
||||||
}
|
}
|
||||||
if (this.isOwner() || this.isAnonymousMap()) {
|
if (this.isOwner() || this.isAnonymousMap()) {
|
||||||
formData.append('edit_status', this.properties.edit_status)
|
formData.append('edit_status', this.properties.edit_status)
|
||||||
|
formData.append('share_status', this.properties.share_status)
|
||||||
}
|
}
|
||||||
if (this.isOwner()) {
|
if (this.isOwner()) {
|
||||||
formData.append('owner', this.properties.owner?.id)
|
formData.append('owner', this.properties.owner?.id)
|
||||||
formData.append('team', this.properties.team?.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(
|
const [data, response, error] = await this._umap.server.post(
|
||||||
this.getUrl(),
|
this.getUrl(),
|
||||||
|
@ -228,6 +236,10 @@ export class MapPermissions extends ServerStored {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDraft() {
|
||||||
|
return this.properties.share_status === 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataLayerPermissions extends ServerStored {
|
export class DataLayerPermissions extends ServerStored {
|
||||||
|
|
|
@ -31,7 +31,8 @@ const TOP_BAR_TEMPLATE = `
|
||||||
<button class="edit-save button round" type="button" data-ref="save">
|
<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"></i>
|
||||||
<i class="icon icon-16 icon-save-disabled"></i>
|
<i class="icon icon-16 icon-save-disabled"></i>
|
||||||
<span class="">${translate('Save')}</span>
|
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
||||||
|
<span hidden data-ref="saveDraftLabel">${translate('Save draft')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
@ -145,6 +146,8 @@ export class TopBar extends WithTemplate {
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
this.elements.peers.hidden = !this._umap.getProperty('syncEnabled')
|
this.elements.peers.hidden = !this._umap.getProperty('syncEnabled')
|
||||||
|
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||||
|
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1321,6 +1321,7 @@ export default class Umap extends ServerStored {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.topBar.redraw()
|
||||||
},
|
},
|
||||||
numberOfConnectedPeers: () => {
|
numberOfConnectedPeers: () => {
|
||||||
Utils.eachElement('.connected-peers span', (el) => {
|
Utils.eachElement('.connected-peers span', (el) => {
|
||||||
|
|
|
@ -102,6 +102,7 @@ class MapFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
licence = factory.SubFactory(LicenceFactory)
|
licence = factory.SubFactory(LicenceFactory)
|
||||||
owner = factory.SubFactory(UserFactory)
|
owner = factory.SubFactory(UserFactory)
|
||||||
|
share_status = Map.PUBLIC
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _adjust_kwargs(cls, **kwargs):
|
def _adjust_kwargs(cls, **kwargs):
|
||||||
|
|
|
@ -76,8 +76,6 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
|
||||||
edit_permissions = owner_session.get_by_title("Update permissions and editors")
|
edit_permissions = owner_session.get_by_title("Update permissions and editors")
|
||||||
expect(edit_permissions).to_be_visible()
|
expect(edit_permissions).to_be_visible()
|
||||||
edit_permissions.click()
|
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")
|
owner_field = owner_session.locator(".umap-field-owner")
|
||||||
expect(owner_field).to_be_hidden()
|
expect(owner_field).to_be_hidden()
|
||||||
editors_field = owner_session.locator(".umap-field-editors input")
|
editors_field = owner_session.locator(".umap-field-editors input")
|
||||||
|
@ -92,8 +90,15 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
|
||||||
".datalayer-permissions select[name='edit_status'] option:checked"
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
)
|
)
|
||||||
expect(option).to_have_text("Inherit")
|
expect(option).to_have_text("Inherit")
|
||||||
# Those fields should not be present in anonymous maps
|
expect(owner_session.locator(".umap-field-share_status select")).to_be_visible()
|
||||||
expect(owner_session.locator(".umap-field-share_status select")).to_be_hidden()
|
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
|
||||||
expect(owner_session.locator(".umap-field-owner")).to_be_hidden()
|
expect(owner_session.locator(".umap-field-owner")).to_be_hidden()
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,15 +140,15 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
|
||||||
page.get_by_title("Manage layers").click()
|
page.get_by_title("Manage layers").click()
|
||||||
page.get_by_title("Add a layer").click()
|
page.get_by_title("Add a layer").click()
|
||||||
page.locator("input[name=name]").fill("Layer 1")
|
page.locator("input[name=name]").fill("Layer 1")
|
||||||
save = page.get_by_role("button", name="Save")
|
expect(
|
||||||
expect(save).to_be_visible()
|
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()
|
||||||
with page.expect_response(re.compile(r".*/datalayer/create/.*")):
|
with page.expect_response(re.compile(r".*/datalayer/create/.*")):
|
||||||
save.click()
|
page.get_by_role("button", name="Save draft", exact=True).click()
|
||||||
edit_permissions = page.get_by_title("Update permissions and editors")
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
expect(edit_permissions).to_be_visible()
|
expect(edit_permissions).to_be_visible()
|
||||||
edit_permissions.click()
|
edit_permissions.click()
|
||||||
select = page.locator(".umap-field-share_status select")
|
|
||||||
expect(select).to_be_hidden()
|
|
||||||
owner_field = page.locator(".umap-field-owner")
|
owner_field = page.locator(".umap-field-owner")
|
||||||
expect(owner_field).to_be_hidden()
|
expect(owner_field).to_be_hidden()
|
||||||
editors_field = page.locator(".umap-field-editors input")
|
editors_field = page.locator(".umap-field-editors input")
|
||||||
|
@ -157,6 +162,9 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
|
||||||
)
|
)
|
||||||
expect(option).to_have_text("Inherit")
|
expect(option).to_have_text("Inherit")
|
||||||
expect(page.get_by_label("Secret edit link:")).to_be_visible()
|
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(
|
def test_alert_message_after_create(
|
||||||
|
@ -232,7 +240,7 @@ def test_anonymous_owner_can_delete_the_map(anonymap, live_server, owner_session
|
||||||
owner_session.get_by_role("button", name="Delete").click()
|
owner_session.get_by_role("button", name="Delete").click()
|
||||||
with owner_session.expect_response(re.compile(r".*/update/delete/.*")):
|
with owner_session.expect_response(re.compile(r".*/update/delete/.*")):
|
||||||
owner_session.get_by_role("button", name="OK").click()
|
owner_session.get_by_role("button", name="OK").click()
|
||||||
assert not Map.objects.count()
|
assert Map.objects.get(pk=anonymap.pk).share_status == Map.DELETED
|
||||||
|
|
||||||
|
|
||||||
def test_non_owner_cannot_see_delete_button(anonymap, live_server, page):
|
def test_non_owner_cannot_see_delete_button(anonymap, live_server, page):
|
||||||
|
|
|
@ -22,7 +22,7 @@ def test_owner_can_delete_map_after_confirmation(map, live_server, login):
|
||||||
with page.expect_navigation():
|
with page.expect_navigation():
|
||||||
delete_button.click()
|
delete_button.click()
|
||||||
assert dialog_shown
|
assert dialog_shown
|
||||||
assert Map.objects.all().count() == 0
|
assert Map.objects.get(pk=map.pk).share_status == Map.DELETED
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_map_preview(map, live_server, datalayer, login):
|
def test_dashboard_map_preview(map, live_server, datalayer, login):
|
||||||
|
|
|
@ -61,8 +61,12 @@ def test_owner_permissions_form(map, datalayer, live_server, login):
|
||||||
edit_permissions = page.get_by_title("Update permissions and editors")
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
expect(edit_permissions).to_be_visible()
|
expect(edit_permissions).to_be_visible()
|
||||||
edit_permissions.click()
|
edit_permissions.click()
|
||||||
select = page.locator(".umap-field-share_status select")
|
expect(page.locator(".umap-field-share_status select")).to_be_visible()
|
||||||
expect(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]
|
||||||
# expect(select).to_have_value(Map.PUBLIC) # Does not work
|
# expect(select).to_have_value(Map.PUBLIC) # Does not work
|
||||||
owner_field = page.locator(".umap-field-owner")
|
owner_field = page.locator(".umap-field-owner")
|
||||||
expect(owner_field).to_be_visible()
|
expect(owner_field).to_be_visible()
|
||||||
|
@ -137,7 +141,7 @@ def test_owner_has_delete_map_button(map, live_server, login):
|
||||||
delete.click()
|
delete.click()
|
||||||
with page.expect_navigation():
|
with page.expect_navigation():
|
||||||
page.get_by_role("button", name="OK").click()
|
page.get_by_role("button", name="OK").click()
|
||||||
assert Map.objects.all().count() == 0
|
assert Map.objects.get(pk=map.pk).share_status == Map.DELETED
|
||||||
|
|
||||||
|
|
||||||
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
|
def test_editor_do_not_have_delete_map_button(map, live_server, login, user):
|
||||||
|
@ -181,29 +185,31 @@ def test_can_change_perms_after_create(tilelayer, live_server, login, user):
|
||||||
page.get_by_title("Manage layers").click()
|
page.get_by_title("Manage layers").click()
|
||||||
page.get_by_title("Add a layer").click()
|
page.get_by_title("Add a layer").click()
|
||||||
page.locator("input[name=name]").fill("Layer 1")
|
page.locator("input[name=name]").fill("Layer 1")
|
||||||
save = page.get_by_role("button", name="Save")
|
expect(
|
||||||
expect(save).to_be_visible()
|
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()
|
||||||
with page.expect_response(re.compile(r".*/map/create/")):
|
with page.expect_response(re.compile(r".*/map/create/")):
|
||||||
save.click()
|
page.get_by_role("button", name="Save draft", exact=True).click()
|
||||||
edit_permissions = page.get_by_title("Update permissions and editors")
|
edit_permissions = page.get_by_title("Update permissions and editors")
|
||||||
expect(edit_permissions).to_be_visible()
|
expect(edit_permissions).to_be_visible()
|
||||||
edit_permissions.click()
|
edit_permissions.click()
|
||||||
select = page.locator(".umap-field-share_status select")
|
expect(page.locator(".umap-field-share_status select")).to_be_visible()
|
||||||
expect(select).to_be_visible()
|
expect(page.locator("select[name='share_status'] option:checked")).to_have_text(
|
||||||
option = page.locator("select[name='share_status'] option:checked")
|
"Draft (private)"
|
||||||
expect(option).to_have_text("Everyone (public)")
|
)
|
||||||
owner_field = page.locator(".umap-field-owner")
|
expect(page.locator(".umap-field-owner")).to_be_visible()
|
||||||
expect(owner_field).to_be_visible()
|
expect(page.locator(".umap-field-editors input")).to_be_visible()
|
||||||
editors_field = page.locator(".umap-field-editors input")
|
expect(page.get_by_text('Who can edit "Layer 1"')).to_be_visible()
|
||||||
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")
|
options = page.locator(".datalayer-permissions select[name='edit_status'] option")
|
||||||
expect(options).to_have_count(4)
|
expect(options).to_have_count(4)
|
||||||
option = page.locator(
|
option = page.locator(
|
||||||
".datalayer-permissions select[name='edit_status'] option:checked"
|
".datalayer-permissions select[name='edit_status'] option:checked"
|
||||||
)
|
)
|
||||||
expect(option).to_have_text("Inherit")
|
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):
|
def test_can_change_owner(map, live_server, login, user):
|
||||||
|
|
|
@ -273,7 +273,6 @@ def test_anonymous_can_edit_in_inherit_mode_and_map_in_public_mode(
|
||||||
|
|
||||||
|
|
||||||
def test_should_remove_all_versions_on_delete(map, settings):
|
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)
|
datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
|
||||||
root = Path(datalayer.storage_root())
|
root = Path(datalayer.storage_root())
|
||||||
before = len(datalayer.geojson.storage.listdir(root)[1])
|
before = len(datalayer.geojson.storage.listdir(root)[1])
|
||||||
|
@ -292,4 +291,3 @@ def test_should_remove_all_versions_on_delete(map, settings):
|
||||||
datalayer.delete()
|
datalayer.delete()
|
||||||
found = datalayer.geojson.storage.listdir(root)[1]
|
found = datalayer.geojson.storage.listdir(root)[1]
|
||||||
assert found == [other, f"{other}.gz"]
|
assert found == [other, f"{other}.gz"]
|
||||||
assert len(list(Path(settings.UMAP_PURGATORY_ROOT).iterdir())) == 4 + before
|
|
||||||
|
|
34
umap/tests/test_empty_trash.py
Normal file
34
umap/tests/test_empty_trash.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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)
|
|
@ -2,6 +2,7 @@ import pytest
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from umap.forms import DEFAULT_CENTER
|
||||||
from umap.models import Map
|
from umap.models import Map
|
||||||
|
|
||||||
from .base import MapFactory
|
from .base import MapFactory
|
||||||
|
@ -160,8 +161,16 @@ def test_can_change_default_edit_status(user, settings):
|
||||||
|
|
||||||
|
|
||||||
def test_can_change_default_share_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)
|
map = MapFactory(owner=user)
|
||||||
assert map.share_status == Map.PUBLIC
|
assert map.share_status == Map.PUBLIC
|
||||||
settings.UMAP_DEFAULT_SHARE_STATUS = Map.PRIVATE
|
|
||||||
map = MapFactory(owner=user)
|
|
||||||
assert map.share_status == Map.PRIVATE
|
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
|
||||||
|
|
|
@ -42,7 +42,7 @@ def test_create(client, user, post_data):
|
||||||
assert created_map.center.y == 48.94415123418794
|
assert created_map.center.y == 48.94415123418794
|
||||||
assert j["permissions"] == {
|
assert j["permissions"] == {
|
||||||
"edit_status": 3,
|
"edit_status": 3,
|
||||||
"share_status": 1,
|
"share_status": 0,
|
||||||
"owner": {"id": user.pk, "name": "Joe", "url": "/en/user/Joe/"},
|
"owner": {"id": user.pk, "name": "Joe", "url": "/en/user/Joe/"},
|
||||||
"editors": [],
|
"editors": [],
|
||||||
}
|
}
|
||||||
|
@ -114,8 +114,10 @@ def test_delete(client, map, datalayer):
|
||||||
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert not Map.objects.filter(pk=map.pk).exists()
|
assert Map.objects.filter(pk=map.pk).exists()
|
||||||
assert not DataLayer.objects.filter(pk=datalayer.pk).exists()
|
assert DataLayer.objects.filter(pk=datalayer.pk).exists()
|
||||||
|
reloaded = Map.objects.get(pk=map.pk)
|
||||||
|
assert reloaded.share_status == Map.DELETED
|
||||||
# Check that user has not been impacted
|
# Check that user has not been impacted
|
||||||
assert User.objects.filter(pk=map.owner.pk).exists()
|
assert User.objects.filter(pk=map.owner.pk).exists()
|
||||||
# Test response is a json
|
# Test response is a json
|
||||||
|
@ -241,42 +243,46 @@ def test_map_creation_should_allow_unicode_names(client, map, post_data):
|
||||||
assert created_map.slug == "map"
|
assert created_map.slug == "map"
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_can_access_map_with_share_status_public(client, map):
|
@pytest.mark.parametrize("share_status", [Map.PUBLIC, Map.OPEN])
|
||||||
|
def test_anonymous_can_access_map_with_share_status_accessible(
|
||||||
|
client, map, share_status
|
||||||
|
):
|
||||||
url = reverse("map", args=(map.slug, map.pk))
|
url = reverse("map", args=(map.slug, map.pk))
|
||||||
map.share_status = map.PUBLIC
|
map.share_status = share_status
|
||||||
map.save()
|
map.save()
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_can_access_map_with_share_status_open(client, map):
|
@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
|
||||||
|
):
|
||||||
url = reverse("map", args=(map.slug, map.pk))
|
url = reverse("map", args=(map.slug, map.pk))
|
||||||
map.share_status = map.OPEN
|
map.share_status = share_status
|
||||||
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()
|
map.save()
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_owner_can_access_map_with_share_status_private(client, map):
|
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
|
||||||
|
def test_owner_can_access_map_with_share_status_restricted(client, map, share_status):
|
||||||
url = reverse("map", args=(map.slug, map.pk))
|
url = reverse("map", args=(map.slug, map.pk))
|
||||||
map.share_status = map.PRIVATE
|
map.share_status = share_status
|
||||||
map.save()
|
map.save()
|
||||||
client.login(username=map.owner.username, password="123123")
|
client.login(username=map.owner.username, password="123123")
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_editors_can_access_map_with_share_status_private(client, map, user):
|
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
|
||||||
|
def test_editors_can_access_map_with_share_status_resricted(
|
||||||
|
client, map, user, share_status
|
||||||
|
):
|
||||||
url = reverse("map", args=(map.slug, map.pk))
|
url = reverse("map", args=(map.slug, map.pk))
|
||||||
map.share_status = map.PRIVATE
|
map.share_status = share_status
|
||||||
map.editors.add(user)
|
map.editors.add(user)
|
||||||
map.save()
|
map.save()
|
||||||
client.login(username=user.username, password="123123")
|
client.login(username=user.username, password="123123")
|
||||||
|
@ -284,10 +290,11 @@ def test_editors_can_access_map_with_share_status_private(client, map, user):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_anonymous_cannot_access_map_with_share_status_blocked(client, map):
|
def test_owner_cannot_access_map_with_share_status_deleted(client, map):
|
||||||
url = reverse("map", args=(map.slug, map.pk))
|
url = reverse("map", args=(map.slug, map.pk))
|
||||||
map.share_status = map.BLOCKED
|
map.share_status = map.DELETED
|
||||||
map.save()
|
map.save()
|
||||||
|
client.login(username=map.owner.username, password="123123")
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
@ -401,14 +408,16 @@ def test_anonymous_delete(cookieclient, anonymap):
|
||||||
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert not Map.objects.filter(pk=anonymap.pk).count()
|
assert Map.objects.filter(pk=anonymap.pk).exists()
|
||||||
|
reloaded = Map.objects.get(pk=anonymap.pk)
|
||||||
|
assert reloaded.share_status == Map.DELETED
|
||||||
# Test response is a json
|
# Test response is a json
|
||||||
j = json.loads(response.content.decode())
|
j = json.loads(response.content.decode())
|
||||||
assert "redirect" in j
|
assert "redirect" in j
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("allow_anonymous")
|
@pytest.mark.usefixtures("allow_anonymous")
|
||||||
def test_no_cookie_cant_delete(client, anonymap):
|
def test_no_cookie_cannot_delete(client, anonymap):
|
||||||
url = reverse("map_delete", args=(anonymap.pk,))
|
url = reverse("map_delete", args=(anonymap.pk,))
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
url, headers={"X-Requested-With": "XMLHttpRequest"}, follow=True
|
||||||
|
@ -416,6 +425,24 @@ def test_no_cookie_cant_delete(client, anonymap):
|
||||||
assert response.status_code == 403
|
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")
|
@pytest.mark.usefixtures("allow_anonymous")
|
||||||
def test_anonymous_edit_url(cookieclient, anonymap):
|
def test_anonymous_edit_url(cookieclient, anonymap):
|
||||||
url = anonymap.get_anonymous_edit_url()
|
url = anonymap.get_anonymous_edit_url()
|
||||||
|
@ -557,16 +584,6 @@ def test_create_readonly(client, user, post_data, settings):
|
||||||
assert response.content == b"Site is readonly for maintenance"
|
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):
|
def test_authenticated_user_can_star_map(client, map, user):
|
||||||
url = reverse("map_star", args=(map.pk,))
|
url = reverse("map_star", args=(map.pk,))
|
||||||
client.login(username=user.username, password="123123")
|
client.login(username=user.username, password="123123")
|
||||||
|
@ -748,7 +765,9 @@ def test_download_multiple_maps_editor(client, map, datalayer):
|
||||||
assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap"
|
assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED])
|
@pytest.mark.parametrize(
|
||||||
|
"share_status", [Map.PRIVATE, Map.BLOCKED, Map.DRAFT, Map.DELETED]
|
||||||
|
)
|
||||||
def test_download_shared_status_map(client, map, datalayer, share_status):
|
def test_download_shared_status_map(client, map, datalayer, share_status):
|
||||||
map.share_status = share_status
|
map.share_status = share_status
|
||||||
map.save()
|
map.save()
|
||||||
|
@ -757,8 +776,9 @@ def test_download_shared_status_map(client, map, datalayer, share_status):
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_download_my_map(client, map, datalayer):
|
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.DRAFT])
|
||||||
map.share_status = Map.PRIVATE
|
def test_download_my_map(client, map, datalayer, share_status):
|
||||||
|
map.share_status = share_status
|
||||||
map.save()
|
map.save()
|
||||||
client.login(username=map.owner.username, password="123123")
|
client.login(username=map.owner.username, password="123123")
|
||||||
url = reverse("map_download", args=(map.pk,))
|
url = reverse("map_download", args=(map.pk,))
|
||||||
|
@ -769,7 +789,19 @@ def test_download_my_map(client, map, datalayer):
|
||||||
assert j["type"] == "umap"
|
assert j["type"] == "umap"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED, Map.OPEN])
|
@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]
|
||||||
|
)
|
||||||
def test_oembed_shared_status_map(client, map, datalayer, share_status):
|
def test_oembed_shared_status_map(client, map, datalayer, share_status):
|
||||||
map.share_status = share_status
|
map.share_status = share_status
|
||||||
map.save()
|
map.save()
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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"}
|
|
|
@ -288,6 +288,28 @@ def test_user_dashboard_display_user_maps(client, map):
|
||||||
assert "Owner only" in body
|
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
|
@pytest.mark.django_db
|
||||||
def test_user_dashboard_display_user_team_maps(client, map, team, user):
|
def test_user_dashboard_display_user_team_maps(client, map, team, user):
|
||||||
user.teams.add(team)
|
user.teams.add(team)
|
||||||
|
@ -497,3 +519,34 @@ def test_websocket_token_is_generated_for_editors(client, user, user2, map):
|
||||||
resp = client.get(token_url)
|
resp = client.get(token_url)
|
||||||
token = resp.json().get("token")
|
token = resp.json().get("token")
|
||||||
assert TimestampSigner().unsign_object(token, max_age=30)
|
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()
|
||||||
|
|
|
@ -373,6 +373,7 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
||||||
|
|
||||||
def get_maps(self):
|
def get_maps(self):
|
||||||
qs = self.get_search_queryset() or Map.objects.all()
|
qs = self.get_search_queryset() or Map.objects.all()
|
||||||
|
qs = qs.exclude(share_status__in=[Map.DELETED, Map.BLOCKED])
|
||||||
teams = self.object.teams.all()
|
teams = self.object.teams.all()
|
||||||
qs = (
|
qs = (
|
||||||
qs.filter(owner=self.object)
|
qs.filter(owner=self.object)
|
||||||
|
@ -601,9 +602,6 @@ class MapDetailMixin(SessionMixin):
|
||||||
"id": self.get_id(),
|
"id": self.get_id(),
|
||||||
"starred": self.is_starred(),
|
"starred": self.is_starred(),
|
||||||
"licences": dict((l.name, l.json) for l in Licence.objects.all()),
|
"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,
|
"umap_version": VERSION,
|
||||||
"featuresHaveOwner": settings.UMAP_DEFAULT_FEATURES_HAVE_OWNERS,
|
"featuresHaveOwner": settings.UMAP_DEFAULT_FEATURES_HAVE_OWNERS,
|
||||||
"websocketEnabled": settings.WEBSOCKET_ENABLED,
|
"websocketEnabled": settings.WEBSOCKET_ENABLED,
|
||||||
|
@ -613,15 +611,22 @@ class MapDetailMixin(SessionMixin):
|
||||||
}
|
}
|
||||||
created = bool(getattr(self, "object", None))
|
created = bool(getattr(self, "object", None))
|
||||||
if (created and self.object.owner) or (not created and not user.is_anonymous):
|
if (created and self.object.owner) or (not created and not user.is_anonymous):
|
||||||
map_statuses = Map.EDIT_STATUS
|
edit_statuses = Map.EDIT_STATUS
|
||||||
datalayer_statuses = DataLayer.EDIT_STATUS
|
datalayer_statuses = DataLayer.EDIT_STATUS
|
||||||
|
share_statuses = Map.SHARE_STATUS
|
||||||
else:
|
else:
|
||||||
map_statuses = AnonymousMapPermissionsForm.STATUS
|
edit_statuses = Map.ANONYMOUS_EDIT_STATUS
|
||||||
datalayer_statuses = AnonymousDataLayerPermissionsForm.STATUS
|
datalayer_statuses = DataLayer.ANONYMOUS_EDIT_STATUS
|
||||||
properties["edit_statuses"] = [(i, str(label)) for i, label in map_statuses]
|
share_statuses = Map.ANONYMOUS_SHARE_STATUS
|
||||||
|
properties["edit_statuses"] = [(i, str(label)) for i, label in edit_statuses]
|
||||||
properties["datalayer_edit_statuses"] = [
|
properties["datalayer_edit_statuses"] = [
|
||||||
(i, str(label)) for i, label in datalayer_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():
|
if self.get_short_url():
|
||||||
properties["shortUrl"] = self.get_short_url()
|
properties["shortUrl"] = self.get_short_url()
|
||||||
|
|
||||||
|
@ -684,14 +689,9 @@ class PermissionsMixin:
|
||||||
permissions["edit_status"] = self.object.edit_status
|
permissions["edit_status"] = self.object.edit_status
|
||||||
permissions["share_status"] = self.object.share_status
|
permissions["share_status"] = self.object.share_status
|
||||||
if self.object.owner:
|
if self.object.owner:
|
||||||
permissions["owner"] = {
|
permissions["owner"] = self.object.owner.get_metadata()
|
||||||
"id": self.object.owner.pk,
|
|
||||||
"name": str(self.object.owner),
|
|
||||||
"url": self.object.owner.get_url(),
|
|
||||||
}
|
|
||||||
permissions["editors"] = [
|
permissions["editors"] = [
|
||||||
{"id": editor.pk, "name": str(editor)}
|
editor.get_metadata() for editor in self.object.editors.all()
|
||||||
for editor in self.object.editors.all()
|
|
||||||
]
|
]
|
||||||
if self.object.team:
|
if self.object.team:
|
||||||
permissions["team"] = self.object.team.get_metadata()
|
permissions["team"] = self.object.team.get_metadata()
|
||||||
|
@ -847,6 +847,17 @@ class MapViewGeoJSON(MapView):
|
||||||
class MapNew(MapDetailMixin, TemplateView):
|
class MapNew(MapDetailMixin, TemplateView):
|
||||||
template_name = "umap/map_detail.html"
|
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):
|
class MapPreview(MapDetailMixin, TemplateView):
|
||||||
template_name = "umap/map_detail.html"
|
template_name = "umap/map_detail.html"
|
||||||
|
@ -1011,7 +1022,7 @@ class MapDelete(DeleteView):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
if not self.object.can_delete(self.request):
|
if not self.object.can_delete(self.request):
|
||||||
return HttpResponseForbidden(_("Only its owner can delete the map."))
|
return HttpResponseForbidden(_("Only its owner can delete the map."))
|
||||||
self.object.delete()
|
self.object.move_to_trash()
|
||||||
home_url = reverse("home")
|
home_url = reverse("home")
|
||||||
messages.info(self.request, _("Map successfully deleted."))
|
messages.info(self.request, _("Map successfully deleted."))
|
||||||
if is_ajax(self.request):
|
if is_ajax(self.request):
|
||||||
|
|
Loading…
Reference in a new issue