2023-12-25 23:38:23 +01:00
|
|
|
import hashlib
|
2023-12-24 03:38:47 +01:00
|
|
|
from uuid import UUID
|
|
|
|
|
2023-12-22 00:13:11 +01:00
|
|
|
import requests
|
|
|
|
from django.conf import settings
|
|
|
|
from django.core.management.base import BaseCommand
|
2024-02-07 18:34:28 +01:00
|
|
|
from pydantic import BaseModel, Field, PositiveInt
|
2023-12-25 22:48:54 +01:00
|
|
|
from shapely import Point
|
|
|
|
from shapely.geometry import shape
|
2023-12-24 03:38:47 +01:00
|
|
|
|
|
|
|
from c3nav.api.utils import NonEmptyStr
|
2024-12-26 02:20:25 +01:00
|
|
|
from c3nav.editor.models import ChangeSet
|
|
|
|
from c3nav.editor.views.base import within_changeset
|
|
|
|
from c3nav.mapdata.models import Area, LocationGroup, LocationSlug, Space
|
2023-12-25 22:48:54 +01:00
|
|
|
from c3nav.mapdata.models.geometry.space import POI
|
2023-12-25 23:38:23 +01:00
|
|
|
from c3nav.mapdata.models.report import Report
|
2023-12-25 22:48:54 +01:00
|
|
|
from c3nav.mapdata.utils.cache.changes import changed_geometries
|
2023-12-24 03:38:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class HubImportItem(BaseModel):
|
|
|
|
"""
|
|
|
|
Something imported from the hub
|
|
|
|
"""
|
|
|
|
type: NonEmptyStr
|
|
|
|
id: UUID
|
|
|
|
slug: NonEmptyStr = Field(pattern=r'^[-a-zA-Z0-9_]+$')
|
|
|
|
name: NonEmptyStr | dict[NonEmptyStr, NonEmptyStr]
|
|
|
|
is_official: bool
|
|
|
|
description: dict[NonEmptyStr, NonEmptyStr | None]
|
|
|
|
public_url: NonEmptyStr = Field(pattern=r'^https://')
|
|
|
|
parent_id: UUID | None
|
|
|
|
children: list[NonEmptyStr] | None
|
2023-12-25 22:48:54 +01:00
|
|
|
floor: PositiveInt | None
|
|
|
|
location: tuple[float, float] | None
|
2023-12-24 03:38:47 +01:00
|
|
|
polygons: tuple[list[tuple[float, float]]] | None
|
2023-12-22 00:13:11 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
help = 'import from hub'
|
|
|
|
|
|
|
|
def handle(self, *args, **options):
|
2023-12-25 23:41:53 +01:00
|
|
|
r = requests.get(settings.HUB_API_BASE+"/integration/c3nav",
|
2023-12-22 00:13:11 +01:00
|
|
|
headers={"Authorization": "Token "+settings.HUB_API_SECRET})
|
|
|
|
r.raise_for_status()
|
2023-12-24 03:38:47 +01:00
|
|
|
|
2024-12-26 02:20:25 +01:00
|
|
|
changed_geometries.reset()
|
|
|
|
changeset = ChangeSet()
|
2024-12-26 05:41:28 +01:00
|
|
|
changeset.author = self.request.user
|
2024-12-26 02:20:25 +01:00
|
|
|
changeset.title = 'importhub'
|
|
|
|
with within_changeset(changeset=changeset, user=None) as locked_changeset:
|
2023-12-25 22:48:54 +01:00
|
|
|
self.do_import(r.json())
|
2024-12-26 05:41:28 +01:00
|
|
|
with changeset.lock_to_edit() as locked_changeset:
|
|
|
|
locked_changeset.apply(user=None)
|
2023-12-25 22:48:54 +01:00
|
|
|
|
2023-12-25 23:38:23 +01:00
|
|
|
def do_report(self, prefix: str, obj_id: str, obj, report: Report):
|
|
|
|
import_prefix = f"{prefix}:{obj_id}:"
|
|
|
|
import_tag = import_prefix+hashlib.md5(str(obj).encode()).hexdigest()
|
|
|
|
Report.objects.filter(import_tag__startswith=import_prefix, open=True).exclude(import_tag=import_tag).delete()
|
|
|
|
if not Report.objects.filter(import_tag=import_tag).exists():
|
|
|
|
report.import_tag = import_tag
|
|
|
|
report.save()
|
|
|
|
report.notify_reviewers()
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
def do_import(self, data):
|
2023-12-26 02:10:35 +01:00
|
|
|
items: list[HubImportItem] = [HubImportItem.model_validate(item) for item in data if item["type"] == "assembly"]
|
2023-12-24 03:38:47 +01:00
|
|
|
items_by_id = {item.id: item for item in items}
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
spaces_for_level = {}
|
|
|
|
for space in Space.objects.all():
|
|
|
|
spaces_for_level.setdefault(space.level_id, []).append(space)
|
|
|
|
|
|
|
|
locations_so_far = {
|
|
|
|
**{poi.import_tag: poi for poi in POI.objects.filter(import_tag__startswith="hub:")},
|
|
|
|
**{area.import_tag: area for area in Area.objects.filter(import_tag__startswith="hub:")},
|
|
|
|
}
|
|
|
|
|
|
|
|
groups_for_types = {
|
|
|
|
group.hub_import_type: group
|
|
|
|
for group in LocationGroup.objects.filter(hub_import_type__isnull=False)
|
|
|
|
}
|
|
|
|
|
2023-12-24 03:38:47 +01:00
|
|
|
for item in items:
|
2023-12-26 17:19:37 +01:00
|
|
|
item.slug = item.slug.lower().replace('_', '-')
|
2023-12-26 17:14:27 +01:00
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
if item.polygons is None and item.location is None:
|
|
|
|
print(f"SKIPPING: {item.slug} / {item.id} has no polygon or location")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if item.floor is None:
|
|
|
|
print(f"SKIPPING: {item.slug} / {item.id} has no floor")
|
|
|
|
continue
|
|
|
|
|
|
|
|
import_tag = f"hub:{item.id}"
|
|
|
|
|
|
|
|
# determine geometry
|
|
|
|
target_type = Area if item.polygons else POI
|
|
|
|
if target_type == Area:
|
|
|
|
new_geometry = shape({
|
|
|
|
"type": "Polygon",
|
|
|
|
"coordinates": [[[y, x] for x, y in item.polygons[0]]],
|
|
|
|
})
|
|
|
|
elif target_type == POI:
|
|
|
|
new_geometry = shape({
|
|
|
|
"type": "Point",
|
|
|
|
"coordinates": list(reversed(item.location)),
|
|
|
|
})
|
|
|
|
else:
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
# determine space
|
|
|
|
try:
|
|
|
|
possible_spaces = [space for space in spaces_for_level[item.floor]
|
|
|
|
if space.geometry.intersects(new_geometry)]
|
|
|
|
except KeyError:
|
|
|
|
print(f"ERROR: {item.slug} / {item.id} has invalid level ID")
|
|
|
|
continue
|
|
|
|
if not possible_spaces:
|
|
|
|
print(f"ERROR: {item.slug} / {item.id} is not within any space")
|
|
|
|
continue
|
|
|
|
if len(possible_spaces) == 1:
|
|
|
|
new_space = possible_spaces[0]
|
|
|
|
elif target_type == Area:
|
|
|
|
new_space = max(possible_spaces, key=lambda s: s.geometry.intersection(new_geometry).area)
|
|
|
|
elif target_type == POI:
|
|
|
|
print(f"WARNING: {item.slug} / {item.id} could be in multiple spaces, picking one...")
|
|
|
|
new_space = possible_spaces[0]
|
|
|
|
else:
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
# find existing location
|
|
|
|
result = locations_so_far.pop(import_tag, None)
|
|
|
|
result: Area | POI | None
|
|
|
|
|
2023-12-26 11:50:45 +01:00
|
|
|
old_result = None
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
if result is not None:
|
|
|
|
# already exists
|
|
|
|
if not isinstance(result, target_type):
|
|
|
|
# need to change from POI to Area or inverted
|
|
|
|
if result.import_block_geom:
|
2023-12-25 23:38:23 +01:00
|
|
|
self.do_report(
|
|
|
|
prefix='hub:switch_type',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
|
|
|
title=f"importhub: change geometry type for {result.title}, is blocked",
|
|
|
|
description=f"the object has a wrong geometry type and needs to be switched to "
|
|
|
|
f"{target_type} but the geometry is blocked",
|
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print(f"REPORT: {item.slug} / {item.id} needs to be switched to {target_type} but is blocked")
|
2023-12-25 22:48:54 +01:00
|
|
|
continue
|
2023-12-26 11:50:45 +01:00
|
|
|
old_result = result
|
|
|
|
result = target_type(
|
|
|
|
import_tag=import_tag,
|
2023-12-25 23:38:23 +01:00
|
|
|
)
|
2023-12-26 11:50:45 +01:00
|
|
|
result.titles = old_result.other_titles
|
|
|
|
result.icon = old_result.icon
|
|
|
|
result.external_url = old_result.external_url
|
|
|
|
result.can_search = old_result.can_search
|
|
|
|
result.can_describe = old_result.can_describe
|
|
|
|
result.label_overrides = old_result.label_overrides
|
|
|
|
result.label_settings_id = old_result.label_settings_id
|
|
|
|
result.import_block_data = old_result.import_block_data
|
|
|
|
print(f"NOTE: {item.slug} / {item.id} was switched to {target_type}")
|
2023-12-25 22:48:54 +01:00
|
|
|
|
2023-12-24 03:38:47 +01:00
|
|
|
hub_types = [
|
|
|
|
item.type,
|
2023-12-25 22:48:54 +01:00
|
|
|
"%s:%s" % (item.type, "with-children" if item.children else "with-no-children"),
|
|
|
|
"%s:%s" % (item.type, f"parent:{items_by_id[item.parent_id].slug}" if item.parent_id else "no-parent"),
|
2023-12-24 03:38:47 +01:00
|
|
|
]
|
2023-12-25 22:48:54 +01:00
|
|
|
|
|
|
|
# build groups
|
|
|
|
new_groups = [group for hub_type, group in groups_for_types.items() if hub_type in hub_types]
|
|
|
|
|
|
|
|
# build resulting object
|
|
|
|
is_new = False
|
|
|
|
if not result:
|
|
|
|
is_new = True
|
|
|
|
result = target_type(
|
|
|
|
import_tag=import_tag,
|
|
|
|
)
|
|
|
|
|
2023-12-25 23:38:23 +01:00
|
|
|
geometry_needs_change = []
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
if result.space_id != new_space.pk:
|
|
|
|
if result.import_block_geom:
|
2023-12-25 23:38:23 +01:00
|
|
|
geometry_needs_change.append(f"change to space {new_space.title}")
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"NOTE: {item.slug} / {item.id} space has changed but is blocked")
|
|
|
|
else:
|
|
|
|
result.space_id = new_space.pk
|
|
|
|
|
|
|
|
if result.geometry != new_geometry or True:
|
|
|
|
if result.import_block_geom:
|
2024-02-07 18:34:28 +01:00
|
|
|
geometry_needs_change.append("change geometry")
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"NOTE: {item.slug} / {item.id} geometry has changed but is blocked")
|
|
|
|
else:
|
|
|
|
result.geometry = new_geometry
|
|
|
|
|
|
|
|
if target_type == Area:
|
|
|
|
new_main_point = Point(item.location) if item.location else None
|
|
|
|
if result.main_point != new_main_point:
|
|
|
|
if result.import_block_geom:
|
2024-02-07 18:34:28 +01:00
|
|
|
geometry_needs_change.append("change main point")
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"NOTE: {item.slug} / {item.id} main point has changed but is blocked")
|
|
|
|
else:
|
|
|
|
result.main_point = new_main_point
|
|
|
|
|
2023-12-25 23:38:23 +01:00
|
|
|
if geometry_needs_change:
|
|
|
|
self.do_report(
|
|
|
|
prefix='hub:change_geometry',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
2024-02-07 18:34:28 +01:00
|
|
|
title="importhub: geometry is blocked but needs changing",
|
|
|
|
description="changes needed: "+','.join(geometry_needs_change),
|
2023-12-25 23:38:23 +01:00
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
data_needs_change = []
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
if item.slug != result.slug:
|
|
|
|
if result.import_block_data:
|
|
|
|
print(f"NOTE: {item.slug} / {item.id} slug has changed but is blocked")
|
2023-12-25 23:38:23 +01:00
|
|
|
data_needs_change.append(f"change slug to {item.slug}")
|
2023-12-26 11:50:45 +01:00
|
|
|
elif not old_result:
|
2023-12-25 23:38:23 +01:00
|
|
|
slug_occupied = LocationSlug.objects.filter(slug=item.slug).first()
|
|
|
|
if slug_occupied:
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"ERROR: {item.slug} / {item.id} slug {item.slug!r} is already occupied")
|
2023-12-25 23:38:23 +01:00
|
|
|
if is_new:
|
|
|
|
self.do_report(
|
|
|
|
prefix='hub:new_slug_occupied',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
|
|
|
title=f"importhub: want to import item with this slug ({item.slug}), occupied",
|
|
|
|
description=f"object to add {item.id}, for slug '{item.slug}' has name {item.name} "
|
|
|
|
f"and url {item.public_url} and should be in space {new_space.title}",
|
|
|
|
location=slug_occupied,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.do_report(
|
|
|
|
prefix='hub:new_slug_occupied',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
|
|
|
title=f"importhub: want change slug to {item.slug} but it's occupied",
|
|
|
|
description=f"object to add {item.id} for slug '{item.slug}' has name {item.name} "
|
|
|
|
f"and url {item.public_url}",
|
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
2023-12-25 22:48:54 +01:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
result.slug = item.slug
|
|
|
|
|
|
|
|
new_titles = {"en": item.name}
|
|
|
|
if new_titles != result.titles:
|
|
|
|
if result.import_block_data:
|
|
|
|
print(f"NOTE: {item.slug} / {item.id} name has changed but is blocked")
|
2023-12-25 23:38:23 +01:00
|
|
|
data_needs_change.append(f"change name to {item.name}")
|
2023-12-25 22:48:54 +01:00
|
|
|
else:
|
|
|
|
result.titles = new_titles
|
|
|
|
|
|
|
|
if item.public_url != result.external_url:
|
|
|
|
if result.import_block_data:
|
|
|
|
print(f"NOTE: {item.slug} / {item.id} external url has changed but is blocked")
|
2023-12-25 23:38:23 +01:00
|
|
|
data_needs_change.append(f"change external_url to {item.public_url}")
|
2023-12-25 22:48:54 +01:00
|
|
|
else:
|
|
|
|
result.external_url = item.public_url
|
|
|
|
|
2023-12-25 23:38:23 +01:00
|
|
|
if data_needs_change:
|
|
|
|
self.do_report(
|
|
|
|
prefix='hub:change_data',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
2024-02-07 18:34:28 +01:00
|
|
|
title="importhub: data is blocked but needs changing",
|
|
|
|
description="changes needed: "+','.join(data_needs_change),
|
2023-12-25 23:38:23 +01:00
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2023-12-25 22:48:54 +01:00
|
|
|
# time to check the groups
|
|
|
|
new_group_ids = set(group.id for group in new_groups)
|
|
|
|
if is_new:
|
|
|
|
if not new_group_ids:
|
|
|
|
print(f"SKIPPING: {item.slug} / {item.id} has no group IDs, {hub_types}")
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
if not new_group_ids:
|
|
|
|
print(f"SKIPPING: {item.slug} / {item.id} no longer has any group IDs, {hub_types}")
|
2023-12-25 23:38:23 +01:00
|
|
|
self.do_report(
|
|
|
|
prefix='hub:new_groups',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
2024-02-07 18:34:28 +01:00
|
|
|
title="importhub: location no longer has any valid group ids",
|
|
|
|
description="from the hub we would remove all groups, this seems wrong",
|
2023-12-25 23:38:23 +01:00
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
2023-12-25 22:48:54 +01:00
|
|
|
continue
|
|
|
|
|
2023-12-26 11:50:45 +01:00
|
|
|
if old_result:
|
|
|
|
old_group_ids = set(group.pk for group in old_result.groups.all())
|
|
|
|
old_redirect_slugs = set(redirect.slug for redirect in old_result.redirects.all())
|
|
|
|
old_result.delete()
|
2023-12-25 22:48:54 +01:00
|
|
|
result.save()
|
2023-12-26 11:50:45 +01:00
|
|
|
if old_result:
|
|
|
|
result.groups.set(old_group_ids)
|
|
|
|
for slug in old_redirect_slugs:
|
|
|
|
result.redirects.create(slug=slug)
|
2023-12-25 22:48:54 +01:00
|
|
|
if is_new:
|
|
|
|
result.groups.set(new_group_ids)
|
|
|
|
else:
|
|
|
|
old_group_ids = set(group.pk for group in result.groups.all())
|
|
|
|
if new_group_ids != old_group_ids:
|
2023-12-25 23:38:23 +01:00
|
|
|
self.do_report(
|
|
|
|
prefix='hub:new_groups',
|
|
|
|
obj_id=str(item.id),
|
|
|
|
obj=item,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
2024-02-07 18:34:28 +01:00
|
|
|
title="importhub: new groups",
|
|
|
|
description=("hub wants new groups for this, groups are now: " +
|
2023-12-25 23:38:23 +01:00
|
|
|
str([group.title for group in new_groups])),
|
|
|
|
location=result,
|
|
|
|
)
|
|
|
|
)
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"NOTE: {item.slug} / {item.id} groups have changed, was " +
|
|
|
|
str([group.title for group in result.groups.all()]) +
|
|
|
|
", is now"+str([group.title for group in new_groups]), new_group_ids, old_group_ids)
|
|
|
|
|
|
|
|
for import_tag, location in locations_so_far.items():
|
2023-12-25 23:38:23 +01:00
|
|
|
self.do_report(
|
|
|
|
prefix='hub:new_groups',
|
|
|
|
obj_id=import_tag,
|
|
|
|
obj=import_tag,
|
|
|
|
report=Report(
|
|
|
|
category="location-issue",
|
2024-02-07 18:34:28 +01:00
|
|
|
title="importhub: delete this",
|
|
|
|
description="hub wants to delete this",
|
2023-12-25 23:38:23 +01:00
|
|
|
location=location,
|
|
|
|
)
|
|
|
|
)
|
2023-12-25 22:48:54 +01:00
|
|
|
print(f"NOTE: {location.slug} / {import_tag} should be deleted")
|