Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce Map.share_status=DRAFT and DELETED #2357

Merged
merged 6 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions docs/config/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,6 @@ 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.
Expand Down
18 changes: 4 additions & 14 deletions umap/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,12 @@ class Meta:


class AnonymousMapPermissionsForm(forms.ModelForm):
STATUS = (
(Map.OWNER, _("Only editable with secret edit link")),
(Map.ANONYMOUS, _("Everyone can edit")),
)

edit_status = forms.ChoiceField(choices=STATUS)
edit_status = forms.ChoiceField(choices=Map.ANONYMOUS_EDIT_STATUS)
share_status = forms.ChoiceField(choices=Map.ANONYMOUS_SHARE_STATUS)

class Meta:
model = Map
fields = ("edit_status",)
fields = ("edit_status", "share_status")


class DataLayerForm(forms.ModelForm):
Expand All @@ -65,13 +61,7 @@ class Meta:


class AnonymousDataLayerPermissionsForm(forms.ModelForm):
STATUS = (
(DataLayer.INHERIT, _("Inherit")),
(DataLayer.OWNER, _("Only editable with secret edit link")),
(DataLayer.ANONYMOUS, _("Everyone can edit")),
)

edit_status = forms.ChoiceField(choices=STATUS)
edit_status = forms.ChoiceField(choices=DataLayer.ANONYMOUS_EDIT_STATUS)

class Meta:
model = DataLayer
Expand Down
32 changes: 32 additions & 0 deletions umap/management/commands/empty_trash.py
Original file line number Diff line number Diff line change
@@ -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}")
28 changes: 0 additions & 28 deletions umap/management/commands/purge_purgatory.py

This file was deleted.

63 changes: 43 additions & 20 deletions umap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ 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.PUBLIC
return settings.UMAP_DEFAULT_SHARE_STATUS or Map.DRAFT


def get_default_edit_status():
Expand Down Expand Up @@ -161,20 +170,30 @@ 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")),
)
SHARE_STATUS = (
ANONYMOUS_SHARE_STATUS = (
(DRAFT, _("Draft (private)")),
(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"))
Expand Down Expand Up @@ -257,6 +276,10 @@ def preview_settings(self):
)
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)
Expand Down Expand Up @@ -352,19 +375,20 @@ def can_edit(self, request=None):
return can

def can_view(self, request):
if self.share_status == self.BLOCKED:
if self.share_status in [Map.BLOCKED, Map.DELETED]:
can = False
elif self.owner is None:
can = True
elif self.share_status in [self.PUBLIC, self.OPEN]:
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)
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 (
self.share_status == self.PRIVATE
restricted
and request.user not in self.editors.all()
and self.team not in request.user.teams.all()
)
Expand Down Expand Up @@ -444,6 +468,11 @@ 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)
Expand Down Expand Up @@ -484,21 +513,13 @@ def save(self, force_insert=False, force_update=False, **kwargs):
force_insert=force_insert, force_update=force_update, **kwargs
)
self.purge_gzip()
self.purge_old_versions()
self.purge_old_versions(keep=settings.UMAP_KEEP_VERSIONS)

def delete(self, **kwargs):
self.purge_gzip()
self.to_purgatory()
self.purge_old_versions(keep=None)
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))
Expand Down Expand Up @@ -576,14 +597,16 @@ def get_version(self, name):
def get_version_path(self, 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()
versions = self.versions[settings.UMAP_KEEP_VERSIONS :]
versions = self.versions
if keep is not None:
versions = versions[keep:]
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 self.geojson.name.endswith(name):
if keep is not None and self.geojson.name.endswith(name):
continue
try:
self.geojson.storage.delete(os.path.join(root, name))
Expand Down
1 change: 0 additions & 1 deletion umap/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@
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)
Expand Down
14 changes: 13 additions & 1 deletion umap/static/umap/js/modules/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ 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)
Expand Down Expand Up @@ -184,11 +192,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(),
Expand Down Expand Up @@ -228,6 +236,10 @@ export class MapPermissions extends ServerStored {
]
}
}

isDraft() {
return this.properties.share_status === 0
}
}

export class DataLayerPermissions extends ServerStored {
Expand Down
5 changes: 4 additions & 1 deletion umap/static/umap/js/modules/ui/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ 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 class="">${translate('Save')}</span>
<span hidden data-ref="saveLabel">${translate('Save')}</span>
<span hidden data-ref="saveDraftLabel">${translate('Save draft')}</span>
</button>
</div>
</div>`
Expand Down Expand Up @@ -145,6 +146,8 @@ 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()
}
}

Expand Down
1 change: 1 addition & 0 deletions umap/static/umap/js/modules/umap.js
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,7 @@ export default class Umap extends ServerStored {
})
})
}
this.topBar.redraw()
},
numberOfConnectedPeers: () => {
Utils.eachElement('.connected-peers span', (el) => {
Expand Down
1 change: 1 addition & 0 deletions umap/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class MapFactory(factory.django.DjangoModelFactory):

licence = factory.SubFactory(LicenceFactory)
owner = factory.SubFactory(UserFactory)
share_status = Map.PUBLIC

@classmethod
def _adjust_kwargs(cls, **kwargs):
Expand Down
Loading
Loading