Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 71 additions & 1 deletion backend/docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ paths:
/paths/{id}/:
get:
operationId: paths_retrieve
description: 指定されたIDのPathの詳細情報を取得
description: 指定されたIDのPathの詳細情報を取得(標高グラフデータ付き)
parameters:
- in: path
name: id
Expand All @@ -310,6 +310,76 @@ paths:
schema:
$ref: '#/components/schemas/Path'
description: ''
/paths/bulk-delete/:
post:
operationId: paths_bulk_delete_create
description: 指定された境界ボックス内の複数のPathを一括削除
parameters:
- in: query
name: maxlat
schema:
type: number
format: double
description: 検索範囲の最大緯度(bboxフィルタ用)
required: true
- in: query
name: maxlon
schema:
type: number
format: double
description: 検索範囲の最大経度(bboxフィルタ用)
required: true
- in: query
name: minlat
schema:
type: number
format: double
description: 検索範囲の最小緯度(bboxフィルタ用)
required: true
- in: query
name: minlon
schema:
type: number
format: double
description: 検索範囲の最小経度(bboxフィルタ用)
required: true
tags:
- paths
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Path'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Path'
multipart/form-data:
schema:
$ref: '#/components/schemas/Path'
required: true
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/json:
schema:
type: object
properties:
deleted_count:
type: integer
description: ''
'400':
content:
application/json:
schema:
type: object
properties:
error:
type: string
description: ''
components:
schemas:
Mountain:
Expand Down
132 changes: 111 additions & 21 deletions backend/paths/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.gis.geos import Polygon
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import viewsets
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.response import Response

Expand All @@ -10,7 +11,7 @@


class PathViewSet(viewsets.ReadOnlyModelViewSet):
"""Path API ViewSet (Read-only)"""
"""Path API ViewSet (Read-only with custom delete action)"""

queryset = Path.objects.all()
serializer_class = PathSerializer
Expand Down Expand Up @@ -49,15 +50,17 @@ def _get_elevation_data(self, path: Path) -> dict:
print(f"Fetched DEM data for {len(dem_data)} tiles")

# 各ジオメトリポイントの標高と累積距離を計算
geometries = list(path.geometries.order_by('sequence'))
geometries = list(path.geometries.order_by("sequence"))
if not geometries:
return {
'id': path.id,
'path_id': path.id,
'osm_id': path.osm_id,
'type': path.type,
'difficulty': path.tags.first().difficulty if path.tags.exists() else None,
'path_graphic': [],
"id": path.id,
"path_id": path.id,
"osm_id": path.osm_id,
"type": path.type,
"difficulty": path.tags.first().difficulty
if path.tags.exists()
else None,
"path_graphic": [],
}

base_lon = geometries[0].lon
Expand All @@ -68,22 +71,24 @@ def _get_elevation_data(self, path: Path) -> dict:
for geom in geometries:
elevation_value = get_nearest_elevation(geom.lat, geom.lon, dem_data)
distance += int(local_distance_m(base_lat, base_lon, geom.lat, geom.lon))
points.append({
'x': distance,
'y': elevation_value,
'lon': geom.lon,
'lat': geom.lat,
})
points.append(
{
"x": distance,
"y": elevation_value,
"lon": geom.lon,
"lat": geom.lat,
}
)
base_lon = geom.lon
base_lat = geom.lat

return {
'id': path.id,
'path_id': path.id,
'osm_id': path.osm_id,
'type': path.type,
'difficulty': path.tags.first().difficulty if path.tags.exists() else None,
'path_graphic': points,
"id": path.id,
"path_id": path.id,
"osm_id": path.osm_id,
"type": path.type,
"difficulty": path.tags.first().difficulty if path.tags.exists() else None,
"path_graphic": points,
}

@extend_schema(
Expand Down Expand Up @@ -171,3 +176,88 @@ def list(self, request):
"results": serializer.data,
}
)

@extend_schema(
description="指定された境界ボックス内の複数のPathを一括削除",
parameters=[
OpenApiParameter(
name="minlat",
type=float,
description="検索範囲の最小緯度(bboxフィルタ用)",
required=True,
location=OpenApiParameter.QUERY,
),
OpenApiParameter(
name="maxlat",
type=float,
description="検索範囲の最大緯度(bboxフィルタ用)",
required=True,
location=OpenApiParameter.QUERY,
),
OpenApiParameter(
name="minlon",
type=float,
description="検索範囲の最小経度(bboxフィルタ用)",
required=True,
location=OpenApiParameter.QUERY,
),
OpenApiParameter(
name="maxlon",
type=float,
description="検索範囲の最大経度(bboxフィルタ用)",
required=True,
location=OpenApiParameter.QUERY,
),
],
responses={
200: {
"type": "object",
"properties": {
"deleted_count": {"type": "integer"},
},
},
400: {
"type": "object",
"properties": {
"error": {"type": "string"},
},
},
},
)
@action(detail=False, methods=["post"], url_path="bulk-delete")
def bulk_delete(self, request):
"""指定された境界ボックス内の複数のPathを一括削除"""
# クエリパラメータから取得
minlat = request.query_params.get("minlat")
minlon = request.query_params.get("minlon")
maxlat = request.query_params.get("maxlat")
maxlon = request.query_params.get("maxlon")

# バリデーション: すべてのbboxパラメータが必須
if not all([minlat, minlon, maxlat, maxlon]):
return Response(
{
"error": "All bbox parameters (minlat, minlon, maxlat, maxlon) are required"
},
status=status.HTTP_400_BAD_REQUEST,
)

try:
minlat = float(minlat)
minlon = float(minlon)
maxlat = float(maxlat)
maxlon = float(maxlon)
except (ValueError, TypeError):
return Response(
{"error": "All bbox parameters must be valid numbers"},
status=status.HTTP_400_BAD_REQUEST,
)

# bbox検索(PostGIS)
search_bbox = Polygon.from_bbox((minlon, minlat, maxlon, maxlat))
search_bbox.srid = 4326

queryset = self.get_queryset().filter(bbox__intersects=search_bbox)
deleted_count, _ = queryset.delete()

Copilot AI Oct 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bulk delete endpoint lacks authentication and authorization checks. Any user can delete paths without permission verification. Add appropriate permission classes (e.g., IsAuthenticated, IsAdminUser) or implement custom permission logic to restrict this destructive operation.

Copilot uses AI. Check for mistakes.

return Response({"deleted_count": deleted_count}, status=status.HTTP_200_OK)
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ dependencies = [
"inflection>=0.5.1",
"uritemplate>=4.1.1",
"drf-spectacular>=0.28.0",
"ipykernel>=7.1.0",

Copilot AI Oct 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding ipykernel as a production dependency is unusual. This package is typically used for Jupyter notebook development and shouldn't be required for the Django application runtime. Consider moving this to a development dependencies section or removing it if not needed.

Suggested change
"ipykernel>=7.1.0",

Copilot uses AI. Check for mistakes.
]
Loading
Loading