Compare commits

...

12 commits

Author SHA1 Message Date
Yohan Boniface
5ae83a571b
feat: add DEPRECATED_AUTHENTICATION_PROVIDERS setting (#2461)
Some checks are pending
Test & Docs / tests (postgresql, 3.10) (push) Waiting to run
Test & Docs / tests (postgresql, 3.12) (push) Waiting to run
Test & Docs / lint (push) Waiting to run
Test & Docs / docs (push) Waiting to run
![image](https://github.com/user-attachments/assets/00a55dfa-2e41-40bf-a6aa-bf70903645e4)
2025-01-30 17:15:53 +01:00
Yohan Boniface
be1fa55e9b
fix: allow to set choices for a nullablechoices field (#2462)
fix #2380
2025-01-30 17:14:27 +01:00
David Larlet
ac607370d0
a11y: include site description within page titles (#2455)
This is not the same as the short `SITE_NAME` which is displayed as the
title of the (home)page for instance.

The plan is to set `SITE_DESCRIPTION` as `uMap OpenStreetMap` for the
OSM instance and `uMap agents publics` for the ANCT one.
2025-01-30 10:45:22 -05:00
David Larlet
44aa914658
a11y: include site description within page titles
This is not the same as the short SITE_NAME which is displayed as the title of the (home)page for instance.
2025-01-30 10:36:31 -05:00
Yohan Boniface
c27e675152 fix: allow to set choices for a nullablechoices field
fix #2380
2025-01-30 14:55:11 +01:00
Yohan Boniface
ee1a87cdeb feat: add DEPRECATED_AUTHENTICATION_PROVIDERS setting 2025-01-30 11:04:02 +01:00
Yohan Boniface
96895feea0
fix: display current configured oauth as icon instead of text (#2375)
Before:


![image](https://github.com/user-attachments/assets/b71d78c0-2a11-4d2e-adf5-2f23ccfd6b2e)


After:


![image](https://github.com/user-attachments/assets/35d3494c-275a-42b6-b646-6e025b03d7c5)
2025-01-30 11:03:21 +01:00
Yohan Boniface
359b0b41ca chore: make djlint happy 2025-01-30 09:45:56 +01:00
Yohan Boniface
3aa42d6f0f
feat: soft delete datalayers (#2459)
When deleting a datalayer, it will now be moved to a state "deleted",
and it will only be deleted for real when running the command `umap
empty_trash`.

This is what we already do for the map itself, but until now if a user
deleted a only a datalayer by mistake (not the map itself) it could not
retrieve it.
2025-01-30 09:19:58 +01:00
Yohan Boniface
fd8a1971f8 feat: soft delete datalayers
When deleting a datalayer, it will now be moved to a state "deleted", and
it will only be deleted for real when running the command `umap empty_trash`.

This is what we already do for the map itself, but until now if a user
deleted a only a datalayer by mistake (not the map itself) it could not retrieve
it.
2025-01-29 19:08:59 +01:00
Yohan Boniface
a3baf82b7b chore(templates): use images for oauth providers list
Co-authored-by: David Larlet <david@larlet.fr>
2025-01-27 19:04:13 +01:00
Yohan Boniface
bf631f07de fix: display current configured oauth as icon instead of text 2024-12-17 11:44:13 +01:00
40 changed files with 200 additions and 54 deletions

View file

@ -28,6 +28,14 @@ Can be set through env var too: `ALLOWED_HOSTS=umap.mydomain.org,u.mydomain.org`
Set it to `True` for easier debugging in case of error. Set it to `True` for easier debugging in case of error.
#### DEPRECATED_AUTHENTICATION_PROVIDERS
List of auth providers to deprecate. Defining this will display a message to
all users using this provider, to encourage them to configure another provider to
their account.
DEPRECATED_AUTHENTICATION_PROVIDERS = ["social_core.backends.twitter_oauth2.TwitterOAuth2"]
#### EMAIL_BACKEND #### EMAIL_BACKEND
Must be configured if you want uMap to send emails to anonymous users. Must be configured if you want uMap to send emails to anonymous users.
@ -98,7 +106,12 @@ Eg.: `SHORT_SITE_URL=https://u.umap.org`
#### SITE_NAME #### SITE_NAME
The name of the site, to be used in header and HTML title. The name of the site, to be used in header.
#### SITE_DESCRIPTION
The description of the site, to be used in HTML title.
#### SITE_URL #### SITE_URL

View file

@ -7,6 +7,7 @@ def settings(request):
return { return {
"UMAP_HELP_URL": djsettings.UMAP_HELP_URL, "UMAP_HELP_URL": djsettings.UMAP_HELP_URL,
"SITE_NAME": djsettings.SITE_NAME, "SITE_NAME": djsettings.SITE_NAME,
"SITE_DESCRIPTION": djsettings.SITE_DESCRIPTION,
"SITE_URL": djsettings.SITE_URL, "SITE_URL": djsettings.SITE_URL,
"ENABLE_ACCOUNT_LOGIN": djsettings.ENABLE_ACCOUNT_LOGIN, "ENABLE_ACCOUNT_LOGIN": djsettings.ENABLE_ACCOUNT_LOGIN,
"UMAP_READONLY": djsettings.UMAP_READONLY, "UMAP_READONLY": djsettings.UMAP_READONLY,

View file

@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from umap.models import Map from umap.models import DataLayer, Map
class Command(BaseCommand): class Command(BaseCommand):
@ -33,3 +33,14 @@ class Command(BaseCommand):
if not options["dry_run"]: if not options["dry_run"]:
map.delete() map.delete()
print(f"Deleted map {map_name} ({map_id}), trashed at {trashed_at}") print(f"Deleted map {map_name} ({map_id}), trashed at {trashed_at}")
print(f"Deleting layers in trash since {since}")
layers = DataLayer.objects.filter(
share_status=DataLayer.DELETED, modified_at__lt=since
)
for layer in layers:
layer_id = layer.uuid
layer_name = layer.name
trashed_at = layer.modified_at.date()
if not options["dry_run"]:
layer.delete()
print(f"Deleted layer {layer_name} ({layer_id}), trashed at {trashed_at}")

View file

@ -0,0 +1,26 @@
# Generated by Django 5.1.4 on 2025-01-29 18:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("umap", "0025_alter_datalayer_geojson"),
]
operations = [
migrations.AddField(
model_name="datalayer",
name="modified_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="datalayer",
name="share_status",
field=models.SmallIntegerField(
choices=[(0, "Inherit"), (99, "Deleted")],
default=0,
verbose_name="share status",
),
),
]

View file

@ -247,9 +247,13 @@ class Map(NamedModel):
except KeyError: except KeyError:
return "" return ""
@property
def datalayers(self):
return self.datalayer_set.filter(share_status=DataLayer.INHERIT).all()
@property @property
def preview_settings(self): def preview_settings(self):
layers = self.datalayer_set.all() layers = self.datalayers
datalayer_data = [c.metadata() for c in layers] datalayer_data = [c.metadata() for c in layers]
map_settings = self.settings map_settings = self.settings
if "properties" not in map_settings: if "properties" not in map_settings:
@ -278,6 +282,7 @@ class Map(NamedModel):
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)
# Use datalayer_set so to get also the deleted ones.
for datalayer in self.datalayer_set.all(): for datalayer in self.datalayer_set.all():
datalayer.delete() datalayer.delete()
return super().delete(**kwargs) return super().delete(**kwargs)
@ -287,7 +292,7 @@ class Map(NamedModel):
umapjson["type"] = "umap" umapjson["type"] = "umap"
umapjson["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.datalayers:
with datalayer.geojson.open("rb") as f: with datalayer.geojson.open("rb") as f:
layer = json.loads(f.read()) layer = json.loads(f.read())
if datalayer.settings: if datalayer.settings:
@ -406,7 +411,7 @@ class Map(NamedModel):
new.save() new.save()
for editor in self.editors.all(): for editor in self.editors.all():
new.editors.add(editor) new.editors.add(editor)
for datalayer in self.datalayer_set.all(): for datalayer in self.datalayers:
datalayer.clone(map_inst=new) datalayer.clone(map_inst=new)
return new return new
@ -458,6 +463,11 @@ class DataLayer(NamedModel):
ANONYMOUS = 1 ANONYMOUS = 1
COLLABORATORS = 2 COLLABORATORS = 2
OWNER = 3 OWNER = 3
DELETED = 99
SHARE_STATUS = (
(INHERIT, _("Inherit")),
(DELETED, _("Deleted")),
)
EDIT_STATUS = ( EDIT_STATUS = (
(INHERIT, _("Inherit")), (INHERIT, _("Inherit")),
(ANONYMOUS, _("Everyone")), (ANONYMOUS, _("Everyone")),
@ -490,6 +500,12 @@ class DataLayer(NamedModel):
default=INHERIT, default=INHERIT,
verbose_name=_("edit status"), verbose_name=_("edit status"),
) )
share_status = models.SmallIntegerField(
choices=SHARE_STATUS,
default=INHERIT,
verbose_name=_("share status"),
)
modified_at = models.DateTimeField(auto_now=True)
class Meta: class Meta:
ordering = ("rank",) ordering = ("rank",)
@ -568,6 +584,10 @@ class DataLayer(NamedModel):
can = True can = True
return can return can
def move_to_trash(self):
self.share_status = DataLayer.DELETED
self.save()
class Star(models.Model): class Star(models.Model):
at = models.DateTimeField(auto_now=True) at = models.DateTimeField(auto_now=True)

View file

@ -267,6 +267,7 @@ UMAP_KEEP_VERSIONS = env.int("UMAP_KEEP_VERSIONS", default=10)
SITE_URL = env("SITE_URL", default="http://umap.org") SITE_URL = env("SITE_URL", default="http://umap.org")
SHORT_SITE_URL = env("SHORT_SITE_URL", default=None) SHORT_SITE_URL = env("SHORT_SITE_URL", default=None)
SITE_NAME = "uMap" SITE_NAME = "uMap"
SITE_DESCRIPTION = "Online map creator"
UMAP_DEMO_SITE = env("UMAP_DEMO_SITE", default=False) UMAP_DEMO_SITE = env("UMAP_DEMO_SITE", default=False)
UMAP_EXCLUDE_DEFAULT_MAPS = False UMAP_EXCLUDE_DEFAULT_MAPS = False
UMAP_MAPS_PER_PAGE = 5 UMAP_MAPS_PER_PAGE = 5
@ -305,6 +306,7 @@ LOGIN_URL = "login"
SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/login/popup/end/" SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/login/popup/end/"
AUTHENTICATION_BACKENDS = () AUTHENTICATION_BACKENDS = ()
DEPRECATED_AUTHENTICATION_BACKENDS = []
SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY = env( SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY = env(
"SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY", default="" "SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY", default=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -41,33 +41,14 @@ body.login header {
display: inline-block; display: inline-block;
} }
.login-grid span,
.login-grid a { .login-grid a {
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
padding: 5px;
color: #000; color: #000;
background-position: center bottom;
background-repeat: no-repeat;
background-size: 92px 92px;
height: 92px; height: 92px;
width: 92px; width: 92px;
margin-inline-end: 10px; margin-inline-end: 10px;
} }
.login-grid .login-github {
background-image: url("./github.png");
}
.login-grid .login-bitbucket {
background-image: url("./bitbucket.png");
}
.login-grid .login-twitter-oauth2 {
background-image: url("./twitter.png");
}
.login-grid .login-openstreetmap,
.login-grid .login-openstreetmap-oauth2 {
background-image: url("./openstreetmap.png");
}
.login-grid .login-keycloak {
background-image: url("./keycloak.png");
}
/* **************************** */ /* **************************** */
/* home */ /* home */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

View file

@ -1191,11 +1191,13 @@ Fields.TernaryChoices = class extends Fields.MultiChoice {
Fields.NullableChoices = class extends Fields.TernaryChoices { Fields.NullableChoices = class extends Fields.TernaryChoices {
getChoices() { getChoices() {
return [ return (
this.properties.choices || [
[true, translate('always')], [true, translate('always')],
[false, translate('never')], [false, translate('never')],
['null', translate('hidden')], ['null', translate('hidden')],
] ]
)
} }
} }

View file

@ -447,6 +447,11 @@ export const SCHEMA = {
label: translate('Display label'), label: translate('Display label'),
inheritable: true, inheritable: true,
default: false, default: false,
choices: [
[true, translate('always')],
[false, translate('never')],
['null', translate('on hover')],
],
}, },
slideshow: { slideshow: {
type: Object, type: Object,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% blocktranslate %}{{ current_user }}s maps{% endblocktranslate %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
<div class="col wide"> <div class="col wide">
<h2 class="section"> <h2 class="section">

View file

@ -1,7 +1,10 @@
{% extends "umap/content.html" %} {% extends "umap/content.html" %}
{% load i18n %} {% load i18n static %}
{% block head_title %}
{% translate "My Profile" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% include "umap/dashboard_menu.html" with selected="profile" %} {% include "umap/dashboard_menu.html" with selected="profile" %}
<div class="wrapper"> <div class="wrapper">
@ -28,8 +31,10 @@
</h3> </h3>
<ul> <ul>
{% for name in providers %} {% for name in providers %}
<li> <li class="login-grid">
{{ name|title }} {% with "umap/img/providers/"|add:name|add:".png" as path %}
<img src="{% static path %}" width="92px" height="92px" alt="{{ name }}" />
{% endwith %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -46,9 +51,7 @@
{% for name in backends.backends %} {% for name in backends.backends %}
{% if name not in providers %} {% if name not in providers %}
<li> <li>
<a href="{% url "social:begin" name %}" {% include "umap/components/provider.html" with name=name %}
class="umap-login-popup login-{{ name }}"
title="{{ name|title }}"></a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% blocktranslate %}{{ current_user }}s starred maps{% endblocktranslate %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
<div class="col wide"> <div class="col wide">
<h2 class="section"> <h2 class="section">

View file

@ -5,7 +5,7 @@
<head> <head>
<title> <title>
{% block head_title %} {% block head_title %}
{{ SITE_NAME }} {{ SITE_NAME }} - {{ SITE_DESCRIPTION }}
{% endblock head_title %} {% endblock head_title %}
</title> </title>
<meta charset="utf-8"> <meta charset="utf-8">

View file

@ -3,7 +3,7 @@
{% load i18n %} {% load i18n %}
{% block head_title %} {% block head_title %}
{% trans "Login" %} {% trans "Login" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %} {% endblock head_title %}
{% load umap_tags i18n %} {% load umap_tags i18n %}
@ -55,10 +55,7 @@
<ul class="login-grid block-grid"> <ul class="login-grid block-grid">
{% for name in backends.backends %} {% for name in backends.backends %}
<li> <li>
<a rel="nofollow" {% include "umap/components/provider.html" with name=name %}
href="{% url "social:begin" name %}"
class="umap-login-popup login-{{ name }}"
title="{{ name|title }}"></a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -1,4 +1,9 @@
{% extends "umap/content.html" %} {% extends "umap/content.html" %}
{% load i18n %}
{% block head_title %}
{% translate "About" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% include "umap/about_summary.html" %} {% include "umap/about_summary.html" %}

View file

@ -0,0 +1,8 @@
{% load static %}
<a href="{% url "social:begin" name %}"
class="umap-login-popup"
title="{{ name|title }}">
{% with "umap/img/providers/"|add:name|add:".png" as path %}
<img src="{% static path %}" width="92px" height="92px" alt="{{ name }}" />
{% endwith %}
</a>

View file

@ -3,7 +3,7 @@
{% load umap_tags i18n %} {% load umap_tags i18n %}
{% block head_title %} {% block head_title %}
{{ map.name }} - {{ SITE_NAME }} {{ map.name }} - {{ SITE_NAME }} - {{ SITE_DESCRIPTION }}
{% endblock head_title %} {% endblock head_title %}
{% block body_class %} {% block body_class %}
map_detail map_detail

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "Password change" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block content %} {% block content %}
<h2 class="section"> <h2 class="section">
{% trans "Password change" %} {% trans "Password change" %}

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "Password change successful" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block content %} {% block content %}
<h2 class="section"> <h2 class="section">
{% trans "Password change successful" %} {% trans "Password change successful" %}

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "Explore maps" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block messages %} {% block messages %}
{# We don't want maps from the results list to display errors in the main page. #} {# We don't want maps from the results list to display errors in the main page. #}
{% endblock messages %} {% endblock messages %}

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "Team deletion" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% include "umap/dashboard_menu.html" with selected="teams" %} {% include "umap/dashboard_menu.html" with selected="teams" %}
<div class="wrapper"> <div class="wrapper">

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% blocktranslate %}{{ current_team }}s maps{% endblocktranslate %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
<div class="wrapper"> <div class="wrapper">
<div class="row"> <div class="row">

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "Create or edit a team" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% include "umap/dashboard_menu.html" with selected="teams" %} {% include "umap/dashboard_menu.html" with selected="teams" %}
<div class="wrapper"> <div class="wrapper">

View file

@ -3,7 +3,7 @@
{% load i18n static %} {% load i18n static %}
{% block head_title %} {% block head_title %}
{{ SITE_NAME }} - {% trans "My Dashboard" %} {% translate "My Dashboard" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %} {% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% trans "Search my maps" as placeholder %} {% trans "Search my maps" as placeholder %}

View file

@ -2,6 +2,10 @@
{% load i18n %} {% load i18n %}
{% block head_title %}
{% translate "My Teams" %} - {{ SITE_DESCRIPTION }}
{% endblock head_title %}
{% block maincontent %} {% block maincontent %}
{% include "umap/dashboard_menu.html" with selected="teams" %} {% include "umap/dashboard_menu.html" with selected="teams" %}
<div class="wrapper"> <div class="wrapper">

View file

@ -8,7 +8,7 @@ from umap.models import Map
def test_page_title(page, live_server): def test_page_title(page, live_server):
page.goto(live_server.url) page.goto(live_server.url)
expect(page).to_have_title("uMap") expect(page).to_have_title("uMap - Online map creator")
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -83,7 +83,7 @@ def test_login_from_map_page(live_server, page, tilelayer, settings, user, conte
page.get_by_role("button", name="Save").click() page.get_by_role("button", name="Save").click()
assert Map.objects.count() == 0 assert Map.objects.count() == 0
login_page = login_page_info.value login_page = login_page_info.value
expect(login_page).to_have_title("Login") expect(login_page).to_have_title("Login - Online map creator")
login_page.get_by_placeholder("Username").fill(user.username) login_page.get_by_placeholder("Username").fill(user.username)
login_page.get_by_placeholder("Password").fill("123123") login_page.get_by_placeholder("Password").fill("123123")
with page.expect_response(re.compile(r".*/map/create/")): with page.expect_response(re.compile(r".*/map/create/")):

View file

@ -158,11 +158,14 @@ def test_should_not_be_possible_to_update_with_wrong_map_id_in_url(
def test_delete(client, datalayer, map): def test_delete(client, datalayer, map):
assert map.datalayers.count() == 1
url = reverse("datalayer_delete", args=(map.pk, datalayer.pk)) url = reverse("datalayer_delete", args=(map.pk, datalayer.pk))
client.login(username=map.owner.username, password="123123") client.login(username=map.owner.username, password="123123")
response = client.post(url, {}, follow=True) response = client.post(url, {}, follow=True)
assert response.status_code == 200 assert response.status_code == 200
assert not DataLayer.objects.filter(pk=datalayer.pk).count() assert DataLayer.objects.filter(pk=datalayer.pk).count()
assert map.datalayers.count() == 0
assert DataLayer.objects.get(pk=datalayer.pk).share_status == DataLayer.DELETED
# Check that map has not been impacted # Check that map has not been impacted
assert Map.objects.filter(pk=map.pk).exists() assert Map.objects.filter(pk=map.pk).exists()
# Test response is a json # Test response is a json

View file

@ -4,15 +4,17 @@ from unittest import mock
import pytest import pytest
from django.core.management import call_command from django.core.management import call_command
from umap.models import Map from umap.models import DataLayer, Map
from .base import MapFactory from .base import DataLayerFactory, MapFactory
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
def test_empty_trash(user): def test_empty_trash(user):
recent = MapFactory(owner=user) recent = MapFactory(owner=user)
recent_layer = DataLayerFactory(map=recent)
deleted_layer = DataLayerFactory(map=recent)
recent_deleted = MapFactory(owner=user) recent_deleted = MapFactory(owner=user)
recent_deleted.move_to_trash() recent_deleted.move_to_trash()
recent_deleted.save() recent_deleted.save()
@ -20,15 +22,20 @@ def test_empty_trash(user):
mocked.return_value = datetime.utcnow() - timedelta(days=8) mocked.return_value = datetime.utcnow() - timedelta(days=8)
old_deleted = MapFactory(owner=user) old_deleted = MapFactory(owner=user)
old_deleted.move_to_trash() old_deleted.move_to_trash()
old_deleted.save() deleted_layer.move_to_trash()
old = MapFactory(owner=user) old = MapFactory(owner=user)
assert Map.objects.count() == 4 assert Map.objects.count() == 4
assert DataLayer.objects.count() == 2
call_command("empty_trash", "--days=7", "--dry-run") call_command("empty_trash", "--days=7", "--dry-run")
assert Map.objects.count() == 4 assert Map.objects.count() == 4
assert DataLayer.objects.count() == 2
call_command("empty_trash", "--days=9") call_command("empty_trash", "--days=9")
assert Map.objects.count() == 4 assert Map.objects.count() == 4
assert DataLayer.objects.count() == 2
call_command("empty_trash", "--days=7") call_command("empty_trash", "--days=7")
assert not Map.objects.filter(pk=old_deleted.pk) assert not Map.objects.filter(pk=old_deleted.pk)
assert Map.objects.filter(pk=old.pk) assert Map.objects.filter(pk=old.pk)
assert Map.objects.filter(pk=recent.pk) assert Map.objects.filter(pk=recent.pk)
assert Map.objects.filter(pk=recent_deleted.pk) assert Map.objects.filter(pk=recent_deleted.pk)
assert not DataLayer.objects.filter(pk=deleted_layer.pk)
assert DataLayer.objects.filter(pk=recent_layer.pk)

View file

@ -810,6 +810,17 @@ def test_oembed_shared_status_map(client, map, datalayer, share_status):
assert response.status_code == 403 assert response.status_code == 403
def test_download_does_not_include_delete_datalayers(client, map, datalayer):
datalayer.share_status = DataLayer.DELETED
datalayer.save()
url = reverse("map_download", args=(map.pk,))
response = client.get(url)
assert response.status_code == 200
# Test response is a json
j = json.loads(response.content.decode())
assert j["layers"] == []
def test_oembed_no_url_map(client, map, datalayer): def test_oembed_no_url_map(client, map, datalayer):
url = reverse("map_oembed") url = reverse("map_oembed")
response = client.get(url) response = client.get(url)

View file

@ -15,7 +15,7 @@ from urllib.request import Request, build_opener
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import BACKEND_SESSION_KEY, get_user_model
from django.contrib.auth import logout as do_logout from django.contrib.auth import logout as do_logout
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchVector from django.contrib.postgres.search import SearchQuery, SearchVector
@ -742,14 +742,14 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get_datalayers(self): def get_datalayers(self):
# When initializing datalayers from map, we cannot get the reference version # When initializing datalayers from map, we cannot get the reference version
# the normal way, which is from the header X-Reference-Version # the normal way, which is from the header X-Reference-Version
return [dl.metadata(self.request) for dl in self.object.datalayer_set.all()] return [dl.metadata(self.request) for dl in self.object.datalayers]
@property @property
def edit_mode(self): def edit_mode(self):
edit_mode = "disabled" edit_mode = "disabled"
if self.object.can_edit(self.request): if self.object.can_edit(self.request):
edit_mode = "advanced" edit_mode = "advanced"
elif any(d.can_edit(self.request) for d in self.object.datalayer_set.all()): elif any(d.can_edit(self.request) for d in self.object.datalayers):
edit_mode = "simple" edit_mode = "simple"
return edit_mode return edit_mode
@ -1325,7 +1325,7 @@ class DataLayerDelete(DeleteView):
self.object = self.get_object() self.object = self.get_object()
if self.object.map != self.kwargs["map_inst"]: if self.object.map != self.kwargs["map_inst"]:
return HttpResponseForbidden() return HttpResponseForbidden()
self.object.delete() self.object.move_to_trash()
return simple_json_response(info=_("Layer successfully deleted.")) return simple_json_response(info=_("Layer successfully deleted."))
@ -1419,3 +1419,18 @@ class LoginPopupEnd(TemplateView):
""" """
template_name = "umap/login_popup_end.html" template_name = "umap/login_popup_end.html"
def get(self, *args, **kwargs):
backend = self.request.session[BACKEND_SESSION_KEY]
if backend in settings.DEPRECATED_AUTHENTICATION_BACKENDS:
name = backend.split(".")[-1]
messages.error(
self.request,
_(
"Using “%(name)s” to authenticate is deprecated. "
"Please configure another provider in your profile page."
)
% {"name": name},
)
return HttpResponseRedirect(reverse("user_profile"))
return super().get(*args, **kwargs)