diff --git a/names.txt b/names.txt index 6545bdc..42c01cb 100644 --- a/names.txt +++ b/names.txt @@ -1 +1,2 @@ Recall +Centerline diff --git a/src/geo-backend/accounts.txt b/src/geo-backend/accounts.txt index 7e639de..151401b 100644 --- a/src/geo-backend/accounts.txt +++ b/src/geo-backend/accounts.txt @@ -1,3 +1,3 @@ Test Accounts: -admin:hei8iWae \ No newline at end of file +admin1:hei8iWae \ No newline at end of file diff --git a/src/geo-backend/data/urls.py b/src/geo-backend/data/urls.py index e742054..a3989ca 100644 --- a/src/geo-backend/data/urls.py +++ b/src/geo-backend/data/urls.py @@ -1,10 +1,11 @@ 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 = [ path('item/import/upload/', upload_item, name='upload_file'), - path('item/import/get/', fetch_import_queue, name='fetch_import_queue'), + path('item/import/get/', fetch_import_queue, name='fetch_import_queue'), path('item/import/get/mine', fetch_queued, name='fetch_queued'), path('item/import/delete/', delete_import_queue, name='delete_import_queue'), + path('item/import/update/', update_imported_item), ] diff --git a/src/geo-backend/data/views/import_item.py b/src/geo-backend/data/views/import_item.py index 1ea3b9a..9cc8f37 100644 --- a/src/geo-backend/data/views/import_item.py +++ b/src/geo-backend/data/views/import_item.py @@ -6,10 +6,14 @@ from django import forms from django.core.serializers.json import DjangoJSONEncoder from django.db import IntegrityError 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 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.tagging import generate_auto_tags +from geo_lib.types.feature import GeoPoint, GeoLineString, GeoPolygon from geo_lib.website.auth import login_required_401 @@ -63,12 +67,12 @@ def upload_item(request): @login_required_401 -def fetch_import_queue(request, id): - if id is None: +def fetch_import_queue(request, item_id): + if item_id is None: return JsonResponse({'success': False, 'msg': 'ID not provided', 'code': 400}, status=400) lock_manager = DBLockManager() try: - queue = ImportQueue.objects.get(id=id) + queue = ImportQueue.objects.get(id=item_id) if queue.user_id != request.user.id: 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)): @@ -80,7 +84,7 @@ def fetch_import_queue(request, id): @login_required_401 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)) lock_manager = DBLockManager() for i, item in enumerate(data): @@ -103,6 +107,50 @@ def delete_import_queue(request, id): 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): if not isinstance(b, bytes): b = b.encode() diff --git a/src/geo-backend/geo_lib/daemon/workers/importer.py b/src/geo-backend/geo_lib/daemon/workers/importer.py index caa69aa..55d5362 100644 --- a/src/geo-backend/geo_lib/daemon/workers/importer.py +++ b/src/geo-backend/geo_lib/daemon/workers/importer.py @@ -10,7 +10,6 @@ from geo_lib.daemon.database.connection import CursorFromConnectionFromPool 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.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.time import get_time_ms from geo_lib.types.feature import geojson_to_geofeature @@ -49,8 +48,6 @@ def import_worker(): messages.extend(kml_conv_messages) geofetures, typing_messages = geojson_to_geofeature(geojson_data) messages.extend(typing_messages) - for feature in geofetures: - feature.tags = generate_auto_tags(feature) success = True except Exception as e: err_name = e.__class__.__name__ diff --git a/src/geo-backend/geo_lib/daemon/workers/workers_lib/importer/tagging.py b/src/geo-backend/geo_lib/daemon/workers/workers_lib/importer/tagging.py index ca8fdf3..35f7095 100644 --- a/src/geo-backend/geo_lib/daemon/workers/workers_lib/importer/tagging.py +++ b/src/geo-backend/geo_lib/daemon/workers/workers_lib/importer/tagging.py @@ -10,6 +10,6 @@ def generate_auto_tags(feature: GeoFeatureSupported) -> List[str]: ] now = datetime.now() - tags.append(f'year:{now.year}') - tags.append(f'month:{now.strftime("%B")}') + tags.append(f'import-year:{now.year}') + tags.append(f'import-month:{now.strftime("%B")}') return [str(x) for x in tags] diff --git a/src/geo-backend/geo_lib/geo_backend.py b/src/geo-backend/geo_lib/geo_backend.py new file mode 100644 index 0000000..b610230 --- /dev/null +++ b/src/geo-backend/geo_lib/geo_backend.py @@ -0,0 +1,2 @@ +SOFTWARE_NAME = 'geo-backend' +SOFTWARE_VERSION = '0.0.0' diff --git a/src/geo-backend/geo_lib/types/feature.py b/src/geo-backend/geo_lib/types/feature.py index 27e9356..97f84ee 100644 --- a/src/geo-backend/geo_lib/types/feature.py +++ b/src/geo-backend/geo_lib/types/feature.py @@ -1,9 +1,12 @@ +from datetime import datetime from enum import Enum from typing import Optional, List, Union, Tuple +import pytz from pydantic import Field, BaseModel 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): @@ -12,6 +15,13 @@ class GeoFeatureType(str, Enum): 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): """ A thing that's shown on the map. @@ -21,8 +31,8 @@ class GeoFeature(BaseModel): id: int # From the database type: GeoFeatureType description: Optional[str] = None - tags: List[str] = Field(default_factory=list) geometry: List + properties: GeoFeatureProperties = Field(default_factory=GeoFeatureProperties) class GeoPoint(GeoFeature): diff --git a/src/geo-backend/requirements.txt b/src/geo-backend/requirements.txt index 071970a..94e011f 100644 --- a/src/geo-backend/requirements.txt +++ b/src/geo-backend/requirements.txt @@ -8,4 +8,5 @@ geojson==3.1.0 pydantic==2.7.3 sqlalchemy==2.0.30 redis==5.0.5 -async_timeout==4.0.3 \ No newline at end of file +async_timeout==4.0.3 +pytz \ No newline at end of file diff --git a/src/geo-frontend/package-lock.json b/src/geo-frontend/package-lock.json index 28f659c..0cc9654 100644 --- a/src/geo-frontend/package-lock.json +++ b/src/geo-frontend/package-lock.json @@ -11,8 +11,10 @@ "@types/geojson": "^7946.0.14", "axios": "^1.7.2", "dropzone-vue": "^0.1.11", + "flatpickr": "^4.6.13", "geojson": "^0.5.0", "vue": "^3.4.21", + "vue-flatpickr-component": "^11.0.5", "vue-router": "^4.3.2", "vuex": "^4.1.0" }, @@ -943,9 +945,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -1330,6 +1333,12 @@ "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": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -1606,10 +1615,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "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": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", diff --git a/src/geo-frontend/package.json b/src/geo-frontend/package.json index 03073bc..f05d629 100644 --- a/src/geo-frontend/package.json +++ b/src/geo-frontend/package.json @@ -12,8 +12,10 @@ "@types/geojson": "^7946.0.14", "axios": "^1.7.2", "dropzone-vue": "^0.1.11", + "flatpickr": "^4.6.13", "geojson": "^0.5.0", "vue": "^3.4.21", + "vue-flatpickr-component": "^11.0.5", "vue-router": "^4.3.2", "vuex": "^4.1.0" }, diff --git a/src/geo-frontend/src/assets/js/types/geofeature-types.ts b/src/geo-frontend/src/assets/js/types/geofeature-types.ts index 7240923..4b3582e 100644 --- a/src/geo-frontend/src/assets/js/types/geofeature-types.ts +++ b/src/geo-frontend/src/assets/js/types/geofeature-types.ts @@ -4,13 +4,20 @@ enum GeoFeatureType { POLYGON = 'Polygon' } +interface GeoFeatureProperties { + created: Date; + software: string; + software_version: string; + tags: string[]; +} + interface GeoFeatureProps { name: string; id: number; type: GeoFeatureType; description?: string; - tags?: string[]; geometry: any[]; + properties: GeoFeatureProperties; } class GeoFeature { @@ -20,40 +27,29 @@ class GeoFeature { description?: string; tags: string[] = []; geometry: any[]; + properties: GeoFeatureProperties; constructor(props: GeoFeatureProps) { this.name = props.name; this.id = props.id; this.type = props.type; this.description = props.description; - this.tags = props.tags || []; this.geometry = props.geometry || []; + this.properties = props.properties; } } export class GeoPoint extends GeoFeature { type: GeoFeatureType = GeoFeatureType.POINT; geometry: number[]; - - constructor(props: GeoFeatureProps) { - super({...props, type: GeoFeatureType.POINT}); - } } export class GeoLineString extends GeoFeature { type: GeoFeatureType = GeoFeatureType.LINESTRING; geometry: number[][]; - - constructor(props: GeoFeatureProps) { - super({...props, type: GeoFeatureType.LINESTRING}); - } } export class GeoPolygon extends GeoFeature { type: GeoFeatureType = GeoFeatureType.POLYGON; geometry: number[][][]; - - constructor(props: GeoFeatureProps) { - super({...props, type: GeoFeatureType.POLYGON}); - } } diff --git a/src/geo-frontend/src/components/import/Process.vue b/src/geo-frontend/src/components/import/Process.vue index a937d95..3e53f6a 100644 --- a/src/geo-frontend/src/components/import/Process.vue +++ b/src/geo-frontend/src/components/import/Process.vue @@ -11,15 +11,72 @@

{{ item }}

+
-
  • -

    {{ item.name }}

    -
    -        {{ item }}
    -      
    -
  • +
      +
    • +
      + +
      + + +
      +
      +
      + +
      + + +
      +
      +
      + +
      + + +
      +
      + +
      +
      + + +
      +
      +
      +
      + + +
      +
      +
    • +
    + + +