Provide a link to delete maps from user’s dashboard
This commit is contained in:
parent
46cf432eb4
commit
5d69d3c22f
6 changed files with 148 additions and 62 deletions
|
@ -223,10 +223,10 @@ class Map(NamedModel):
|
||||||
)
|
)
|
||||||
return map_settings
|
return map_settings
|
||||||
|
|
||||||
def generate_geojson(self, request):
|
def generate_umapjson(self, request):
|
||||||
geojson = self.settings
|
umapjson = self.settings
|
||||||
geojson["type"] = "umap"
|
umapjson["type"] = "umap"
|
||||||
geojson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
umapjson["uri"] = request.build_absolute_uri(self.get_absolute_url())
|
||||||
datalayers = []
|
datalayers = []
|
||||||
for datalayer in self.datalayer_set.all():
|
for datalayer in self.datalayer_set.all():
|
||||||
with open(datalayer.geojson.path, "rb") as f:
|
with open(datalayer.geojson.path, "rb") as f:
|
||||||
|
@ -234,8 +234,8 @@ class Map(NamedModel):
|
||||||
if datalayer.settings:
|
if datalayer.settings:
|
||||||
layer["_umap_options"] = datalayer.settings
|
layer["_umap_options"] = datalayer.settings
|
||||||
datalayers.append(layer)
|
datalayers.append(layer)
|
||||||
geojson["layers"] = datalayers
|
umapjson["layers"] = datalayers
|
||||||
return geojson
|
return umapjson
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("map", kwargs={"slug": self.slug or "map", "map_id": self.pk})
|
return reverse("map", kwargs={"slug": self.slug or "map", "map_id": self.pk})
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block maincontent %}
|
{% block maincontent %}
|
||||||
<div class="col wide">
|
<div class="col wide">
|
||||||
<h2 class="section tabs">
|
<h2 class="section tabs">
|
||||||
<a href="{% url "user_dashboard" %}">{% trans "My Dashboard" %}</a> | <a class="selected" href="{% url 'user_profile' %}">{% trans "My Profile" %}</a>
|
<a href="{% url "user_dashboard" %}">{% trans "My Maps" %}</a> | <a class="selected" href="{% url 'user_profile' %}">{% trans "My Profile" %}</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
|
|
@ -37,7 +37,8 @@
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ map_inst.get_absolute_url }}?share">{% translate "Share" %}</a> |
|
<a href="{{ map_inst.get_absolute_url }}?share">{% translate "Share" %}</a> |
|
||||||
<a href="{{ map_inst.get_absolute_url }}?edit">{% translate "Edit" %}</a> |
|
<a href="{{ map_inst.get_absolute_url }}?edit">{% translate "Edit" %}</a> |
|
||||||
<a href="{% url 'map_download' map_inst.pk %}">{% translate "Download" %}</a>
|
<a href="{% url 'map_download' map_inst.pk %}">{% translate "Download" %}</a> |
|
||||||
|
<a href="{% url 'map_delete' map_inst.pk %}">{% translate "Delete" %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -45,13 +46,22 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4"></td>
|
<td colspan="5"></td>
|
||||||
<td colspan="2">
|
<td>
|
||||||
<a href="{% url 'user_download' %}?p={{ request.GET.p }}" class="button">{% translate "Download all these maps" %}</a>
|
<a href="{% url 'user_download' %}?{% spaceless %}
|
||||||
|
{% for map_inst in maps %}map_id={{ map_inst.pk }}{% if not forloop.last %}&{% endif %}{% endfor %}
|
||||||
|
{% endspaceless %}" class="button"
|
||||||
|
>{% blocktranslate count counter=maps.object_list|length %}
|
||||||
|
Download this map
|
||||||
|
{% plural %}
|
||||||
|
Download these {{ counter }} maps
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
{% if maps.has_other_pages %}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if maps.has_previous %}
|
{% if maps.has_previous %}
|
||||||
<a href="?p=1{% if q %}&q={{ q }}{% endif %}">« {% translate "first" %}</a>
|
<a href="?p=1{% if q %}&q={{ q }}{% endif %}">« {% translate "first" %}</a>
|
||||||
|
@ -79,3 +89,32 @@
|
||||||
{# djlint:on #}
|
{# djlint:on #}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
!(function () {
|
||||||
|
for (const deleteLink of document.querySelectorAll("table.maps a[href$='delete/']")) {
|
||||||
|
deleteLink.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const token = document.cookie.replace(
|
||||||
|
/(?:(?:^|.*;\s*)csrftoken\s*\=\s*([^;]*).*$)|^.*$/,
|
||||||
|
'$1'
|
||||||
|
)
|
||||||
|
if (confirm(L._('Are you sure you want to delete this map?'))) {
|
||||||
|
fetch(deleteLink.href, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': token
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
throw response
|
||||||
|
})
|
||||||
|
.catch((error) => console.debug(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
{% trans "Search my maps" as placeholder %}
|
{% trans "Search my maps" as placeholder %}
|
||||||
<div class="col wide">
|
<div class="col wide">
|
||||||
<h2 class="section tabs">
|
<h2 class="section tabs">
|
||||||
<a class="selected" href="{% url 'user_dashboard' %}">{% trans "My Dashboard" %}</a> | <a href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
|
<a class="selected" href="{% url 'user_dashboard' %}"
|
||||||
|
>{% blocktranslate with count=maps.paginator.count %}My Maps ({{ count }}){% endblocktranslate %}
|
||||||
|
</a> | <a href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
|
||||||
</h2>
|
</h2>
|
||||||
{% include "umap/search_bar.html" with action=request.get_full_path placeholder=placeholder %}
|
{% include "umap/search_bar.html" with action=request.get_full_path placeholder=placeholder %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
import zipfile
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -8,7 +10,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
from umap.models import DataLayer, Map, Star
|
from umap.models import DataLayer, Map, Star
|
||||||
|
|
||||||
from .base import login_required
|
from .base import MapFactory, UserFactory, login_required
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
@ -656,6 +658,64 @@ def test_download(client, map, datalayer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_multiple_maps(client, map, datalayer):
|
||||||
|
map.share_status = Map.PRIVATE
|
||||||
|
map.save()
|
||||||
|
another_map = MapFactory(
|
||||||
|
owner=map.owner, name="Another map", share_status=Map.PUBLIC
|
||||||
|
)
|
||||||
|
client.login(username=map.owner.username, password="123123")
|
||||||
|
url = reverse("user_download")
|
||||||
|
response = client.get(f"{url}?map_id={map.id}&map_id={another_map.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
with zipfile.ZipFile(file=BytesIO(response.content), mode="r") as f:
|
||||||
|
assert len(f.infolist()) == 2
|
||||||
|
assert f.infolist()[0].filename == f"umap_backup_test-map_{another_map.id}.umap"
|
||||||
|
assert f.infolist()[1].filename == f"umap_backup_test-map_{map.id}.umap"
|
||||||
|
with f.open(f.infolist()[1]) as umap_file:
|
||||||
|
umapjson = json.loads(umap_file.read().decode())
|
||||||
|
assert list(umapjson.keys()) == [
|
||||||
|
"type",
|
||||||
|
"geometry",
|
||||||
|
"properties",
|
||||||
|
"uri",
|
||||||
|
"layers",
|
||||||
|
]
|
||||||
|
assert umapjson["type"] == "umap"
|
||||||
|
assert umapjson["uri"] == f"http://testserver/en/map/test-map_{map.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_multiple_maps_unauthorized(client, map, datalayer):
|
||||||
|
map.share_status = Map.PRIVATE
|
||||||
|
map.save()
|
||||||
|
user1 = UserFactory(username="user1")
|
||||||
|
another_map = MapFactory(owner=user1, name="Another map", share_status=Map.PUBLIC)
|
||||||
|
client.login(username=map.owner.username, password="123123")
|
||||||
|
url = reverse("user_download")
|
||||||
|
response = client.get(f"{url}?map_id={map.id}&map_id={another_map.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
with zipfile.ZipFile(file=BytesIO(response.content), mode="r") as f:
|
||||||
|
assert len(f.infolist()) == 1
|
||||||
|
assert f.infolist()[0].filename == f"umap_backup_test-map_{map.id}.umap"
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_multiple_maps_editor(client, map, datalayer):
|
||||||
|
map.share_status = Map.PRIVATE
|
||||||
|
map.save()
|
||||||
|
user1 = UserFactory(username="user1")
|
||||||
|
another_map = MapFactory(owner=user1, name="Another map", share_status=Map.PUBLIC)
|
||||||
|
another_map.editors.add(map.owner)
|
||||||
|
another_map.save()
|
||||||
|
client.login(username=map.owner.username, password="123123")
|
||||||
|
url = reverse("user_download")
|
||||||
|
response = client.get(f"{url}?map_id={map.id}&map_id={another_map.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
with zipfile.ZipFile(file=BytesIO(response.content), mode="r") as f:
|
||||||
|
assert len(f.infolist()) == 2
|
||||||
|
assert f.infolist()[0].filename == f"umap_backup_test-map_{another_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])
|
||||||
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
|
||||||
|
|
|
@ -290,46 +290,31 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
||||||
return qs.order_by("-modified_at")
|
return qs.order_by("-modified_at")
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs.update(
|
page = self.paginate(self.get_maps(), settings.UMAP_MAPS_PER_PAGE_OWNER)
|
||||||
{
|
kwargs.update({"q": self.request.GET.get("q"), "maps": page})
|
||||||
"q": self.request.GET.get("q"),
|
|
||||||
"maps": self.paginate(
|
|
||||||
self.get_maps(), settings.UMAP_MAPS_PER_PAGE_OWNER
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
user_dashboard = UserDashboard.as_view()
|
user_dashboard = UserDashboard.as_view()
|
||||||
|
|
||||||
|
|
||||||
class UserDownload(PaginatorMixin, DetailView, SearchMixin):
|
class UserDownload(DetailView, SearchMixin):
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
def is_owner(self):
|
|
||||||
return self.request.user == self.object
|
|
||||||
|
|
||||||
@property
|
|
||||||
def per_page(self):
|
|
||||||
if self.is_owner():
|
|
||||||
return settings.UMAP_MAPS_PER_PAGE_OWNER
|
|
||||||
return settings.UMAP_MAPS_PER_PAGE
|
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.get_queryset().get(pk=self.request.user.pk)
|
return self.get_queryset().get(pk=self.request.user.pk)
|
||||||
|
|
||||||
def get_maps(self):
|
def get_maps(self):
|
||||||
qs = self.get_search_queryset() or Map.objects.all()
|
qs = Map.objects.filter(id__in=self.request.GET.getlist("map_id"))
|
||||||
qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
|
qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
|
||||||
return qs.order_by("-modified_at")
|
return qs.order_by("-modified_at")
|
||||||
|
|
||||||
def render_to_response(self, context, *args, **kwargs):
|
def render_to_response(self, context, *args, **kwargs):
|
||||||
zip_buffer = io.BytesIO()
|
zip_buffer = io.BytesIO()
|
||||||
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
||||||
for map_ in self.paginate(self.get_maps()):
|
for map_ in self.get_maps():
|
||||||
map_geojson = map_.generate_geojson(self.request)
|
umapjson = map_.generate_umapjson(self.request)
|
||||||
geojson_file = io.StringIO(json.dumps(map_geojson))
|
geojson_file = io.StringIO(json.dumps(umapjson))
|
||||||
file_name = f"umap_backup_{map_.slug}_{map_.pk}.umap"
|
file_name = f"umap_backup_{map_.slug}_{map_.pk}.umap"
|
||||||
zip_file.writestr(file_name, geojson_file.getvalue())
|
zip_file.writestr(file_name, geojson_file.getvalue())
|
||||||
|
|
||||||
|
@ -674,8 +659,8 @@ class MapDownload(DetailView):
|
||||||
return reverse("map_download", args=(self.object.pk,))
|
return reverse("map_download", args=(self.object.pk,))
|
||||||
|
|
||||||
def render_to_response(self, context, *args, **kwargs):
|
def render_to_response(self, context, *args, **kwargs):
|
||||||
geojson = self.object.generate_geojson(self.request)
|
umapjson = self.object.generate_umapjson(self.request)
|
||||||
response = simple_json_response(**geojson)
|
response = simple_json_response(**umapjson)
|
||||||
response[
|
response[
|
||||||
"Content-Disposition"
|
"Content-Disposition"
|
||||||
] = f'attachment; filename="umap_backup_{self.object.slug}.umap"'
|
] = f'attachment; filename="umap_backup_{self.object.slug}.umap"'
|
||||||
|
|
Loading…
Reference in a new issue