implement import editing
This commit is contained in:
parent
2d5525ccce
commit
613210beaa
|
@ -1,3 +1,3 @@
|
||||||
Test Accounts:
|
Test Accounts:
|
||||||
|
|
||||||
admin:hei8iWae
|
admin1:hei8iWae
|
|
@ -1,10 +1,11 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from data.views.import_item import upload_item, fetch_import_queue, fetch_queued, delete_import_queue
|
from data.views.import_item import upload_item, fetch_import_queue, fetch_queued, delete_import_queue, update_imported_item
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('item/import/upload/', upload_item, name='upload_file'),
|
path('item/import/upload/', upload_item, name='upload_file'),
|
||||||
path('item/import/get/<int:id>', fetch_import_queue, name='fetch_import_queue'),
|
path('item/import/get/<int:item_id>', fetch_import_queue, name='fetch_import_queue'),
|
||||||
path('item/import/get/mine', fetch_queued, name='fetch_queued'),
|
path('item/import/get/mine', fetch_queued, name='fetch_queued'),
|
||||||
path('item/import/delete/<int:id>', delete_import_queue, name='delete_import_queue'),
|
path('item/import/delete/<int:id>', delete_import_queue, name='delete_import_queue'),
|
||||||
|
path('item/import/update/<int:item_id>', update_imported_item),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,10 +6,14 @@ from django import forms
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from data.models import ImportQueue
|
from data.models import ImportQueue
|
||||||
from geo_lib.daemon.database.locking import DBLockManager
|
from geo_lib.daemon.database.locking import DBLockManager
|
||||||
from geo_lib.daemon.workers.workers_lib.importer.kml import kmz_to_kml
|
from geo_lib.daemon.workers.workers_lib.importer.kml import kmz_to_kml
|
||||||
|
from geo_lib.daemon.workers.workers_lib.importer.tagging import generate_auto_tags
|
||||||
|
from geo_lib.types.feature import GeoPoint, GeoLineString, GeoPolygon
|
||||||
from geo_lib.website.auth import login_required_401
|
from geo_lib.website.auth import login_required_401
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,12 +67,12 @@ def upload_item(request):
|
||||||
|
|
||||||
|
|
||||||
@login_required_401
|
@login_required_401
|
||||||
def fetch_import_queue(request, id):
|
def fetch_import_queue(request, item_id):
|
||||||
if id is None:
|
if item_id is None:
|
||||||
return JsonResponse({'success': False, 'msg': 'ID not provided', 'code': 400}, status=400)
|
return JsonResponse({'success': False, 'msg': 'ID not provided', 'code': 400}, status=400)
|
||||||
lock_manager = DBLockManager()
|
lock_manager = DBLockManager()
|
||||||
try:
|
try:
|
||||||
queue = ImportQueue.objects.get(id=id)
|
queue = ImportQueue.objects.get(id=item_id)
|
||||||
if queue.user_id != request.user.id:
|
if queue.user_id != request.user.id:
|
||||||
return JsonResponse({'success': False, 'msg': 'not authorized to view this item', 'code': 403}, status=400)
|
return JsonResponse({'success': False, 'msg': 'not authorized to view this item', 'code': 403}, status=400)
|
||||||
if not lock_manager.is_locked('data_importqueue', queue.id) and (len(queue.geofeatures) or len(queue.log)):
|
if not lock_manager.is_locked('data_importqueue', queue.id) and (len(queue.geofeatures) or len(queue.log)):
|
||||||
|
@ -80,7 +84,7 @@ def fetch_import_queue(request, id):
|
||||||
|
|
||||||
@login_required_401
|
@login_required_401
|
||||||
def fetch_queued(request):
|
def fetch_queued(request):
|
||||||
user_items = ImportQueue.objects.filter(user=request.user).values('id', 'geofeatures', 'original_filename', 'raw_kml_hash', 'data', 'log', 'timestamp')
|
user_items = ImportQueue.objects.exclude(geofeatures__len=0).filter(user=request.user).values('id', 'geofeatures', 'original_filename', 'raw_kml_hash', 'data', 'log', 'timestamp')
|
||||||
data = json.loads(json.dumps(list(user_items), cls=DjangoJSONEncoder))
|
data = json.loads(json.dumps(list(user_items), cls=DjangoJSONEncoder))
|
||||||
lock_manager = DBLockManager()
|
lock_manager = DBLockManager()
|
||||||
for i, item in enumerate(data):
|
for i, item in enumerate(data):
|
||||||
|
@ -103,6 +107,50 @@ def delete_import_queue(request, id):
|
||||||
return HttpResponse(status=405)
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required_401
|
||||||
|
@csrf_protect # TODO: put this on all routes
|
||||||
|
@require_http_methods(["PUT"])
|
||||||
|
def update_imported_item(request, item_id):
|
||||||
|
try:
|
||||||
|
queue = ImportQueue.objects.get(id=item_id)
|
||||||
|
except ImportQueue.DoesNotExist:
|
||||||
|
return JsonResponse({'success': False, 'msg': 'ID does not exist', 'code': 404}, status=400)
|
||||||
|
if queue.user_id != request.user.id:
|
||||||
|
return JsonResponse({'success': False, 'msg': 'not authorized to edit this item', 'code': 403}, status=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise ValueError('Invalid data format. Expected a list.')
|
||||||
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
|
return JsonResponse({'success': False, 'msg': str(e), 'code': 400}, status=400)
|
||||||
|
|
||||||
|
parsed_data = []
|
||||||
|
for feature in data:
|
||||||
|
match feature['type'].lower():
|
||||||
|
case 'point':
|
||||||
|
c = GeoPoint(**feature)
|
||||||
|
case 'linestring':
|
||||||
|
c = GeoLineString(**feature)
|
||||||
|
case 'polygon':
|
||||||
|
c = GeoPolygon(**feature)
|
||||||
|
case _:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate the tags after the user has made their changes.
|
||||||
|
c.properties.tags = generate_auto_tags(c)
|
||||||
|
parsed_data.append(json.loads(c.model_dump_json()))
|
||||||
|
|
||||||
|
# Erase the geofeatures column
|
||||||
|
queue.geofeatures = []
|
||||||
|
|
||||||
|
# Update the data column with the new data
|
||||||
|
queue.data = parsed_data
|
||||||
|
|
||||||
|
queue.save()
|
||||||
|
return JsonResponse({'success': True, 'msg': 'Item updated successfully'})
|
||||||
|
|
||||||
|
|
||||||
def _hash_kml(b: str):
|
def _hash_kml(b: str):
|
||||||
if not isinstance(b, bytes):
|
if not isinstance(b, bytes):
|
||||||
b = b.encode()
|
b = b.encode()
|
||||||
|
|
|
@ -10,7 +10,6 @@ from geo_lib.daemon.database.connection import CursorFromConnectionFromPool
|
||||||
from geo_lib.daemon.database.locking import DBLockManager
|
from geo_lib.daemon.database.locking import DBLockManager
|
||||||
from geo_lib.daemon.workers.workers_lib.importer.kml import kml_to_geojson
|
from geo_lib.daemon.workers.workers_lib.importer.kml import kml_to_geojson
|
||||||
from geo_lib.daemon.workers.workers_lib.importer.logging import create_import_log_msg
|
from geo_lib.daemon.workers.workers_lib.importer.logging import create_import_log_msg
|
||||||
from geo_lib.daemon.workers.workers_lib.importer.tagging import generate_auto_tags
|
|
||||||
from geo_lib.logging.database import log_to_db, DatabaseLogLevel, DatabaseLogSource
|
from geo_lib.logging.database import log_to_db, DatabaseLogLevel, DatabaseLogSource
|
||||||
from geo_lib.time import get_time_ms
|
from geo_lib.time import get_time_ms
|
||||||
from geo_lib.types.feature import geojson_to_geofeature
|
from geo_lib.types.feature import geojson_to_geofeature
|
||||||
|
@ -49,8 +48,6 @@ def import_worker():
|
||||||
messages.extend(kml_conv_messages)
|
messages.extend(kml_conv_messages)
|
||||||
geofetures, typing_messages = geojson_to_geofeature(geojson_data)
|
geofetures, typing_messages = geojson_to_geofeature(geojson_data)
|
||||||
messages.extend(typing_messages)
|
messages.extend(typing_messages)
|
||||||
for feature in geofetures:
|
|
||||||
feature.tags = generate_auto_tags(feature)
|
|
||||||
success = True
|
success = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_name = e.__class__.__name__
|
err_name = e.__class__.__name__
|
||||||
|
|
|
@ -10,6 +10,6 @@ def generate_auto_tags(feature: GeoFeatureSupported) -> List[str]:
|
||||||
]
|
]
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
tags.append(f'year:{now.year}')
|
tags.append(f'import-year:{now.year}')
|
||||||
tags.append(f'month:{now.strftime("%B")}')
|
tags.append(f'import-month:{now.strftime("%B")}')
|
||||||
return [str(x) for x in tags]
|
return [str(x) for x in tags]
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
SOFTWARE_NAME = 'geo-backend'
|
||||||
|
SOFTWARE_VERSION = '0.0.0'
|
|
@ -1,9 +1,12 @@
|
||||||
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, List, Union, Tuple
|
from typing import Optional, List, Union, Tuple
|
||||||
|
|
||||||
|
import pytz
|
||||||
from pydantic import Field, BaseModel
|
from pydantic import Field, BaseModel
|
||||||
|
|
||||||
from geo_lib.daemon.workers.workers_lib.importer.logging import create_import_log_msg
|
from geo_lib.daemon.workers.workers_lib.importer.logging import create_import_log_msg
|
||||||
|
from geo_lib.geo_backend import SOFTWARE_NAME, SOFTWARE_VERSION
|
||||||
|
|
||||||
|
|
||||||
class GeoFeatureType(str, Enum):
|
class GeoFeatureType(str, Enum):
|
||||||
|
@ -12,6 +15,13 @@ class GeoFeatureType(str, Enum):
|
||||||
POLYGON = 'Polygon'
|
POLYGON = 'Polygon'
|
||||||
|
|
||||||
|
|
||||||
|
class GeoFeatureProperties(BaseModel):
|
||||||
|
tags: List[str] = Field(default_factory=list)
|
||||||
|
created: datetime = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||||
|
software: str = Field(SOFTWARE_NAME, frozen=True)
|
||||||
|
software_version: str = Field(SOFTWARE_VERSION, frozen=True)
|
||||||
|
|
||||||
|
|
||||||
class GeoFeature(BaseModel):
|
class GeoFeature(BaseModel):
|
||||||
"""
|
"""
|
||||||
A thing that's shown on the map.
|
A thing that's shown on the map.
|
||||||
|
@ -21,8 +31,8 @@ class GeoFeature(BaseModel):
|
||||||
id: int # From the database
|
id: int # From the database
|
||||||
type: GeoFeatureType
|
type: GeoFeatureType
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: List[str] = Field(default_factory=list)
|
|
||||||
geometry: List
|
geometry: List
|
||||||
|
properties: GeoFeatureProperties = Field(default_factory=GeoFeatureProperties)
|
||||||
|
|
||||||
|
|
||||||
class GeoPoint(GeoFeature):
|
class GeoPoint(GeoFeature):
|
||||||
|
|
|
@ -8,4 +8,5 @@ geojson==3.1.0
|
||||||
pydantic==2.7.3
|
pydantic==2.7.3
|
||||||
sqlalchemy==2.0.30
|
sqlalchemy==2.0.30
|
||||||
redis==5.0.5
|
redis==5.0.5
|
||||||
async_timeout==4.0.3
|
async_timeout==4.0.3
|
||||||
|
pytz
|
|
@ -11,8 +11,10 @@
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"dropzone-vue": "^0.1.11",
|
"dropzone-vue": "^0.1.11",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
"vue-flatpickr-component": "^11.0.5",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.3.2",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
@ -943,9 +945,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.7",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
@ -1330,6 +1333,12 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flatpickr": {
|
||||||
|
"version": "4.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
|
||||||
|
"integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.6",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
|
@ -1606,10 +1615,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
|
@ -2434,6 +2444,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-flatpickr-component": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-flatpickr-component/-/vue-flatpickr-component-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-Vfwg5uVU+sanKkkLzUGC5BUlWd5wlqAMq/UpQ6lI2BCZq0DDrXhOMX7hrevt8bEgglIq2QUv0K2Nl84Me/VnlA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"flatpickr": "^4.6.13"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz",
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"dropzone-vue": "^0.1.11",
|
"dropzone-vue": "^0.1.11",
|
||||||
|
"flatpickr": "^4.6.13",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
"vue-flatpickr-component": "^11.0.5",
|
||||||
"vue-router": "^4.3.2",
|
"vue-router": "^4.3.2",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,13 +4,20 @@ enum GeoFeatureType {
|
||||||
POLYGON = 'Polygon'
|
POLYGON = 'Polygon'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GeoFeatureProperties {
|
||||||
|
created: Date;
|
||||||
|
software: string;
|
||||||
|
software_version: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface GeoFeatureProps {
|
interface GeoFeatureProps {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
type: GeoFeatureType;
|
type: GeoFeatureType;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags?: string[];
|
|
||||||
geometry: any[];
|
geometry: any[];
|
||||||
|
properties: GeoFeatureProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoFeature {
|
class GeoFeature {
|
||||||
|
@ -20,40 +27,29 @@ class GeoFeature {
|
||||||
description?: string;
|
description?: string;
|
||||||
tags: string[] = [];
|
tags: string[] = [];
|
||||||
geometry: any[];
|
geometry: any[];
|
||||||
|
properties: GeoFeatureProperties;
|
||||||
|
|
||||||
constructor(props: GeoFeatureProps) {
|
constructor(props: GeoFeatureProps) {
|
||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.id = props.id;
|
this.id = props.id;
|
||||||
this.type = props.type;
|
this.type = props.type;
|
||||||
this.description = props.description;
|
this.description = props.description;
|
||||||
this.tags = props.tags || [];
|
|
||||||
this.geometry = props.geometry || [];
|
this.geometry = props.geometry || [];
|
||||||
|
this.properties = props.properties;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoPoint extends GeoFeature {
|
export class GeoPoint extends GeoFeature {
|
||||||
type: GeoFeatureType = GeoFeatureType.POINT;
|
type: GeoFeatureType = GeoFeatureType.POINT;
|
||||||
geometry: number[];
|
geometry: number[];
|
||||||
|
|
||||||
constructor(props: GeoFeatureProps) {
|
|
||||||
super({...props, type: GeoFeatureType.POINT});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoLineString extends GeoFeature {
|
export class GeoLineString extends GeoFeature {
|
||||||
type: GeoFeatureType = GeoFeatureType.LINESTRING;
|
type: GeoFeatureType = GeoFeatureType.LINESTRING;
|
||||||
geometry: number[][];
|
geometry: number[][];
|
||||||
|
|
||||||
constructor(props: GeoFeatureProps) {
|
|
||||||
super({...props, type: GeoFeatureType.LINESTRING});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeoPolygon extends GeoFeature {
|
export class GeoPolygon extends GeoFeature {
|
||||||
type: GeoFeatureType = GeoFeatureType.POLYGON;
|
type: GeoFeatureType = GeoFeatureType.POLYGON;
|
||||||
geometry: number[][][];
|
geometry: number[][][];
|
||||||
|
|
||||||
constructor(props: GeoFeatureProps) {
|
|
||||||
super({...props, type: GeoFeatureType.POLYGON});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,72 @@
|
||||||
<p class="font-bold">{{ item }}</p>
|
<p class="font-bold">{{ item }}</p>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<li v-for="(item, index) in itemsForUser" :key="`item-${index}`">
|
<ul class="space-y-4">
|
||||||
<h2>{{ item.name }}</h2>
|
<li v-for="(item, index) in itemsForUser" :key="`item-${index}`" class="bg-white shadow-md rounded-md p-4">
|
||||||
<pre>
|
<div class="mb-4">
|
||||||
{{ item }}
|
<label class="block text-gray-700 font-bold mb-2">Name:</label>
|
||||||
</pre>
|
<div class="flex items-center">
|
||||||
</li>
|
<input v-model="item.name" :placeholder="originalItems[index].name"
|
||||||
|
class="border border-gray-300 rounded-md px-3 py-2 w-full"/>
|
||||||
|
<button class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold py-2 px-4 rounded"
|
||||||
|
@click="resetField(index, 'name')">Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 font-bold mb-2">Description:</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input v-model="item.description" :placeholder="originalItems[index].description"
|
||||||
|
class="border border-gray-300 rounded-md px-3 py-2 w-full"/>
|
||||||
|
<button class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold py-2 px-4 rounded"
|
||||||
|
@click="resetField(index, 'description')">Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<label class="block text-gray-700 font-bold mb-2">Created:</label>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<flat-pickr :config="flatpickrConfig" :value="item.properties.created"
|
||||||
|
class="border border-gray-300 rounded-md px-3 py-2 w-full"
|
||||||
|
@on-change="updateDate(index, $event)"></flat-pickr>
|
||||||
|
<button class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold py-2 px-4 rounded"
|
||||||
|
@click="resetNestedField(index, 'properties', 'created')">Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-bold mb-2">Tags:</label>
|
||||||
|
<div v-for="(tag, tagIndex) in item.properties.tags" :key="`tag-${tagIndex}`" class="mb-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input v-model="item.properties.tags[tagIndex]" :placeholder="getTagPlaceholder(index, tag)"
|
||||||
|
class="border rounded-md px-3 py-2 w-full bg-white"/>
|
||||||
|
<button class="ml-2 bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded"
|
||||||
|
@click="removeTag(index, tagIndex)">Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mt-2">
|
||||||
|
<button :class="{ 'opacity-50 cursor-not-allowed': isLastTagEmpty(index) }"
|
||||||
|
:disabled="isLastTagEmpty(index)"
|
||||||
|
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
|
||||||
|
@click="addTag(index)">Add Tag
|
||||||
|
</button>
|
||||||
|
<button class="ml-2 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold py-2 px-4 rounded"
|
||||||
|
@click="resetTags(index)">Reset Tags
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="m-2 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded"
|
||||||
|
@click="saveChanges">Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
<!-- Load the queue to populate it. -->
|
<!-- Load the queue to populate it. -->
|
||||||
<Importqueue/>
|
<Importqueue/>
|
||||||
|
@ -34,6 +91,9 @@ import {capitalizeFirstLetter} from "@/assets/js/string.js";
|
||||||
import Importqueue from "@/components/import/parts/importqueue.vue";
|
import Importqueue from "@/components/import/parts/importqueue.vue";
|
||||||
import {GeoFeatureTypeStrings} from "@/assets/js/types/geofeature-strings";
|
import {GeoFeatureTypeStrings} from "@/assets/js/types/geofeature-strings";
|
||||||
import {GeoPoint, GeoLineString, GeoPolygon} from "@/assets/js/types/geofeature-types";
|
import {GeoPoint, GeoLineString, GeoPolygon} from "@/assets/js/types/geofeature-types";
|
||||||
|
import {getCookie} from "@/assets/js/auth.js";
|
||||||
|
import flatPickr from 'vue-flatpickr-component';
|
||||||
|
import 'flatpickr/dist/flatpickr.css';
|
||||||
|
|
||||||
// TODO: for each feature, query the DB and check if there is a duplicate. For points that's duplicate coords, for linestrings and polygons that's duplicate points
|
// TODO: for each feature, query the DB and check if there is a duplicate. For points that's duplicate coords, for linestrings and polygons that's duplicate points
|
||||||
// TODO: auto-refresh if still processing
|
// TODO: auto-refresh if still processing
|
||||||
|
@ -42,13 +102,20 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["userInfo"]),
|
...mapState(["userInfo"]),
|
||||||
},
|
},
|
||||||
components: {Importqueue},
|
components: {Importqueue, flatPickr},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
msg: "",
|
msg: "",
|
||||||
currentId: null,
|
currentId: null,
|
||||||
itemsForUser: [],
|
itemsForUser: [],
|
||||||
workerLog: []
|
originalItems: [],
|
||||||
|
workerLog: [],
|
||||||
|
flatpickrConfig: {
|
||||||
|
enableTime: true,
|
||||||
|
time_24hr: true,
|
||||||
|
dateFormat: 'Y-m-d H:i',
|
||||||
|
timezone: 'UTC',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [authMixin],
|
mixins: [authMixin],
|
||||||
|
@ -69,14 +136,63 @@ export default {
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid feature type: ${item.type}`);
|
throw new Error(`Invalid feature type: ${item.type}`);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
resetField(index, fieldName) {
|
||||||
|
this.itemsForUser[index][fieldName] = this.originalItems[index][fieldName];
|
||||||
|
},
|
||||||
|
resetNestedField(index, nestedField, fieldName) {
|
||||||
|
this.itemsForUser[index][nestedField][fieldName] = this.originalItems[index][nestedField][fieldName];
|
||||||
|
},
|
||||||
|
addTag(index) {
|
||||||
|
if (!this.isLastTagEmpty(index)) {
|
||||||
|
this.itemsForUser[index].tags.push('');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTagPlaceholder(index, tag) {
|
||||||
|
const originalTagIndex = this.originalItems[index].tags.indexOf(tag);
|
||||||
|
return originalTagIndex !== -1 ? this.originalItems[index].tags[originalTagIndex] : '';
|
||||||
|
},
|
||||||
|
isLastTagEmpty(index) {
|
||||||
|
const tags = this.itemsForUser[index].tags;
|
||||||
|
return tags.length > 0 && tags[tags.length - 1].trim().length === 0;
|
||||||
|
},
|
||||||
|
resetTags(index) {
|
||||||
|
this.itemsForUser[index].tags = [...this.originalItems[index].tags];
|
||||||
|
},
|
||||||
|
removeTag(index, tagIndex) {
|
||||||
|
this.itemsForUser[index].tags.splice(tagIndex, 1);
|
||||||
|
},
|
||||||
|
updateDate(index, selectedDates) {
|
||||||
|
this.itemsForUser[index].properties.created = selectedDates[0];
|
||||||
|
},
|
||||||
|
saveChanges() {
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
axios.put('/api/data/item/import/update/' + this.id, this.itemsForUser, {
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
this.msg = 'Changes saved successfully';
|
||||||
|
window.alert(this.msg);
|
||||||
|
} else {
|
||||||
|
this.msg = 'Error saving changes: ' + response.data.msg;
|
||||||
|
window.alert(this.msg);
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.msg = 'Error saving changes: ' + error.message;
|
||||||
|
window.alert(this.msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
,
|
||||||
beforeRouteEnter(to, from, next) {
|
beforeRouteEnter(to, from, next) {
|
||||||
next(async vm => {
|
next(async vm => {
|
||||||
if (vm.currentId !== vm.id) {
|
if (vm.currentId !== vm.id) {
|
||||||
vm.msg = ""
|
vm.msg = ""
|
||||||
vm.messages = []
|
vm.messages = []
|
||||||
vm.itemsForUser = []
|
vm.itemsForUser = []
|
||||||
|
vm.originalItems = []
|
||||||
vm.currentId = null
|
vm.currentId = null
|
||||||
axios.get('/api/data/item/import/get/' + vm.id).then(response => {
|
axios.get('/api/data/item/import/get/' + vm.id).then(response => {
|
||||||
if (!response.data.success) {
|
if (!response.data.success) {
|
||||||
|
@ -87,6 +203,7 @@ export default {
|
||||||
response.data.geofeatures.forEach((item) => {
|
response.data.geofeatures.forEach((item) => {
|
||||||
vm.itemsForUser.push(vm.parseGeoJson(item))
|
vm.itemsForUser.push(vm.parseGeoJson(item))
|
||||||
})
|
})
|
||||||
|
vm.originalItems = JSON.parse(JSON.stringify(vm.itemsForUser))
|
||||||
}
|
}
|
||||||
vm.msg = response.data.msg
|
vm.msg = response.data.msg
|
||||||
vm.workerLog = response.data.log
|
vm.workerLog = response.data.log
|
||||||
|
@ -96,8 +213,10 @@ export default {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
};
|
,
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
Loading…
Reference in New Issue