-
-
Notifications
You must be signed in to change notification settings - Fork 213
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
chore: add clean_tilelayer management command #1670
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import json | ||
import sys | ||
|
||
from django.core.management.base import BaseCommand | ||
from django.db import connection | ||
from psycopg.types.json import Jsonb | ||
|
||
from umap.models import Map, TileLayer | ||
|
||
|
||
class Command(BaseCommand): | ||
help = """Clean tilelayer in database | ||
|
||
This will simply replace the URL in maps settings: | ||
umap clean_tilelayer http://my.old/url/template http://my.new/url/template | ||
|
||
This will replace the whole tilelayer in maps settings by the one with this name: | ||
umap clean_tilelayer http://my.old/url/template "some string" | ||
|
||
This will replace the whole tilelayer in maps settings by the one with this id: | ||
umap clean_tilelayer http://my.old/url/template an_id | ||
|
||
This will delete the whole tilelayer from maps settings: | ||
umap clean_tilelayer http://my.old/url/template | ||
|
||
To get the available tilelayers in db (available for users): | ||
umap clean_tilelayer --available | ||
|
||
To get statistics of tilelayers usage in db (including custom ones): | ||
umap clean_tilelayer --available | ||
""" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument("old", nargs="?", help="url template we want to clean") | ||
parser.add_argument( | ||
"new", help="what to replace this tilelayer with", nargs="?" | ||
) | ||
parser.add_argument( | ||
"--no-input", action="store_true", help="Do not ask for confirm." | ||
) | ||
parser.add_argument( | ||
"--available", action="store_true", help="List known tilelayers." | ||
) | ||
parser.add_argument( | ||
"--stats", action="store_true", help="Display stats on tilelayer usage." | ||
) | ||
|
||
def handle(self, *args, **options): | ||
self.no_input = options["no_input"] | ||
if options["available"]: | ||
self.list_available() | ||
sys.exit() | ||
if options["stats"]: | ||
self.stats() | ||
sys.exit() | ||
old = options["old"] | ||
new = options["new"] | ||
if not old: | ||
sys.exit("⚠ You must define an url_template") | ||
|
||
count = Map.objects.filter( | ||
settings__properties__tilelayer__url_template=old | ||
).count() | ||
if not count: | ||
self.stdout.write("⚠ No map found. Exiting.") | ||
sys.exit() | ||
self.stdout.write(f"{count} maps found.") | ||
if not new: | ||
self.delete(old) | ||
elif new.startswith("http"): | ||
self.replace_url(old, new) | ||
else: | ||
# Let's consider it's a name or an id | ||
self.replace_tilelayer(old, new) | ||
|
||
def confirm(self, message): | ||
if self.no_input: | ||
return True | ||
result = input("%s (y/N) " % message) or "n" | ||
if not result[0].lower() == "y": | ||
self.stdout.write("⚠ Action cancelled.") | ||
sys.exit() | ||
return True | ||
|
||
def delete(self, old): | ||
if self.confirm( | ||
"Are you sure you want to delete the tilelayer key from all those " | ||
"maps settings ?" | ||
): | ||
with connection.cursor() as cursor: | ||
ret = cursor.execute( | ||
"UPDATE umap_map " | ||
"SET settings['properties'] = (settings->'properties') - 'tilelayer'" | ||
"WHERE settings->'properties'->'tilelayer'->'url_template' = %s", | ||
[Jsonb(old)], | ||
) | ||
self.stdout.write(f"✔ Deleted {old} from {ret.rowcount} maps.") | ||
|
||
def replace_url(self, old, new): | ||
if self.confirm( | ||
f"Are you sure you want to replace '{old}'' by '{new}'' from all those " | ||
"map settings ?" | ||
): | ||
with connection.cursor() as cursor: | ||
ret = cursor.execute( | ||
"UPDATE umap_map " | ||
"SET settings['properties']['tilelayer']['url_template'] = %s " | ||
"WHERE settings->'properties'->'tilelayer'->'url_template' = %s", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests seems to be failing because of this line. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, it passes with my local psql (16), but not with the version 13 of the CI. I've tried many syntaxes, but blindly and without success. I'd need to run a version 13 locally I'd say. |
||
[Jsonb(new), Jsonb(old)], | ||
) | ||
self.stdout.write(f"✔ Replaced {old} by {new} in {ret.rowcount} maps.") | ||
|
||
def replace_tilelayer(self, old, new): | ||
try: | ||
tilelayer = TileLayer.objects.get(name=new) | ||
except TileLayer.DoesNotExist: | ||
try: | ||
tilelayer = TileLayer.objects.get(id=new) | ||
except (TileLayer.DoesNotExist, ValueError): | ||
sys.exit(f"⚠ Cannot find a TileLayer with name or id = '{new}'.") | ||
if self.confirm( | ||
f"Are you sure you want to replace {old} by '{tilelayer.name}' " | ||
"from all those map settings ?" | ||
): | ||
with connection.cursor() as cursor: | ||
ret = cursor.execute( | ||
"UPDATE umap_map " | ||
"SET settings['properties']['tilelayer'] = %s " | ||
"WHERE settings->'properties'->'tilelayer'->'url_template' = %s", | ||
[Jsonb(tilelayer.json), Jsonb(old)], | ||
) | ||
self.stdout.write( | ||
f"✔ Replaced {old} by {tilelayer.name} in {ret.rowcount} maps." | ||
) | ||
|
||
def list_available(self): | ||
tilelayers = TileLayer.objects.all() | ||
for tilelayer in tilelayers: | ||
print(f"{tilelayer.pk} '{tilelayer.name}' {tilelayer.url_template}") | ||
|
||
def stats(self): | ||
with connection.cursor() as cursor: | ||
cursor.execute( | ||
"SELECT COUNT(*) as count, " | ||
"settings->'properties'->'tilelayer'->'url_template' as url " | ||
"FROM umap_map " | ||
"GROUP BY settings->'properties'->'tilelayer'->'url_template' " | ||
"ORDER BY count DESC" | ||
) | ||
res = cursor.fetchall() | ||
for count, url in res: | ||
print(f"{count}\t{url}") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import pytest | ||
from django.core.management import call_command | ||
|
||
from umap.models import Map | ||
|
||
pytestmark = pytest.mark.django_db | ||
|
||
|
||
def test_can_delete_tilelayer_from_settings(map): | ||
map.settings["properties"]["tilelayer"] = { | ||
"name": "My TileLayer", | ||
"maxZoom": 18, | ||
"minZoom": 0, | ||
"attribution": "My attribution", | ||
"url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
} | ||
map.save() | ||
# Make sure its saved | ||
map = Map.objects.get(pk=map.pk) | ||
assert "tilelayer" in map.settings["properties"] | ||
call_command( | ||
"clean_tilelayer", "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", "--no-input" | ||
) | ||
map = Map.objects.get(pk=map.pk) | ||
assert "tilelayer" not in map.settings["properties"] | ||
|
||
|
||
def test_can_replace_tilelayer_url_in_map_settings(map): | ||
map.settings["properties"]["tilelayer"] = { | ||
"name": "My TileLayer", | ||
"maxZoom": 18, | ||
"minZoom": 0, | ||
"attribution": "My attribution", | ||
"url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
} | ||
map.save() | ||
new = "https://{s}.foo.bar.baz/{z}/{x}/{y}.png" | ||
call_command( | ||
"clean_tilelayer", | ||
"http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
new, | ||
"--no-input", | ||
) | ||
map = Map.objects.get(pk=map.pk) | ||
assert map.settings["properties"]["tilelayer"]["url_template"] == new | ||
|
||
|
||
def test_can_replace_tilelayer_by_name_in_map_settings(map, tilelayer): | ||
map.settings["properties"]["tilelayer"] = { | ||
"name": "My TileLayer", | ||
"maxZoom": 18, | ||
"minZoom": 0, | ||
"attribution": "My attribution", | ||
"url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
} | ||
map.save() | ||
call_command( | ||
"clean_tilelayer", | ||
"http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
tilelayer.name, | ||
"--no-input", | ||
) | ||
map = Map.objects.get(pk=map.pk) | ||
assert map.settings["properties"]["tilelayer"] == tilelayer.json | ||
|
||
|
||
def test_can_replace_tilelayer_by_id_in_map_settings(map, tilelayer): | ||
map.settings["properties"]["tilelayer"] = { | ||
"name": "My TileLayer", | ||
"maxZoom": 18, | ||
"minZoom": 0, | ||
"attribution": "My attribution", | ||
"url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
} | ||
map.save() | ||
call_command( | ||
"clean_tilelayer", | ||
"http://{s}.foo.bar.baz/{z}/{x}/{y}.png", | ||
tilelayer.pk, | ||
"--no-input", | ||
) | ||
map = Map.objects.get(pk=map.pk) | ||
assert map.settings["properties"]["tilelayer"] == tilelayer.json |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice syntax :-)