diff --git a/.gitignore b/.gitignore index 75cf1aa..dac0034 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Vagrantfile .vagrant *local_settings.py landmapper/app/testing_files +landmapper/app/static/landmapper/shapefiles/ \ No newline at end of file diff --git a/landmapper/app/static/landmapper/css/report.css b/landmapper/app/static/landmapper/css/report.css index 66f7974..fd27987 100644 --- a/landmapper/app/static/landmapper/css/report.css +++ b/landmapper/app/static/landmapper/css/report.css @@ -128,7 +128,7 @@ p { .icon { display: block; margin: .25em auto; - width: 2em; + width: 1.75em; /* vertical-align: text-bottom; */ } diff --git a/landmapper/app/static/landmapper/img/icon/icon-share.svg b/landmapper/app/static/landmapper/img/icon/icon-share.svg new file mode 100644 index 0000000..454579f --- /dev/null +++ b/landmapper/app/static/landmapper/img/icon/icon-share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/landmapper/app/static/landmapper/img/icon/icon-shp.svg b/landmapper/app/static/landmapper/img/icon/icon-shp.svg new file mode 100644 index 0000000..79e6673 --- /dev/null +++ b/landmapper/app/static/landmapper/img/icon/icon-shp.svg @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/landmapper/app/static/landmapper/js/report.js b/landmapper/app/static/landmapper/js/report.js index c664dbe..b6a8cf4 100644 --- a/landmapper/app/static/landmapper/js/report.js +++ b/landmapper/app/static/landmapper/js/report.js @@ -29,4 +29,47 @@ let newPropertyID = documentPropertyIdSplit.join('%7C'); if (copyToAccountBtn) { copyToAccountBtn.href = `/landmapper/report/${newPropertyID}`; } + + + /** + * Add event listener to the export layer button + */ + function exportLayerHandler() { + const propertyId = this.getAttribute('data-property-id'); + fetch(`/export_layer/${propertyId}/shp`, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.ok ? response.blob() : response.text().then(text => { throw new Error(text); })) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `${propertyId}.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while exporting the layer.'); + }); + } + + function setupExportLayerButton() { + const exportLayerButton = document.getElementById('export-layer-button'); + if (exportLayerButton) { + exportLayerButton.addEventListener('click', exportLayerHandler); + } + } + + function init() { + setupExportLayerButton(); + } + + document.addEventListener('DOMContentLoaded', init); + })(); \ No newline at end of file diff --git a/landmapper/app/templates/landmapper/report/report-overview.html b/landmapper/app/templates/landmapper/report/report-overview.html index e4543c4..6a5d868 100644 --- a/landmapper/app/templates/landmapper/report/report-overview.html +++ b/landmapper/app/templates/landmapper/report/report-overview.html @@ -67,6 +67,12 @@

All maps of your property

PDF + + + {% else %} diff --git a/landmapper/app/templates/landmapper/report/report-share.html b/landmapper/app/templates/landmapper/report/report-share.html index 86ce0bd..8d8410a 100644 --- a/landmapper/app/templates/landmapper/report/report-share.html +++ b/landmapper/app/templates/landmapper/report/report-share.html @@ -1,6 +1,6 @@ {% load static %} \ No newline at end of file diff --git a/landmapper/app/urls.py b/landmapper/app/urls.py index 40e17ed..a75b8ad 100644 --- a/landmapper/app/urls.py +++ b/landmapper/app/urls.py @@ -46,6 +46,7 @@ path('admin_export_property_records/', exportPropertyRecords, name='export_property_records'), path('accounts/profile/', homeRedirect, name='account_confirm_email'), path('auth/email/', homeRedirect, name='auth_email'), + path('export_layer//shp', export_layer, name='export_layer'), # path('tinymce/', include('tinymce.urls')), re_path(r'^tinymce/', include('tinymce.urls')), ] diff --git a/landmapper/app/views.py b/landmapper/app/views.py index 094dee3..1497d64 100644 --- a/landmapper/app/views.py +++ b/landmapper/app/views.py @@ -27,13 +27,18 @@ from django.views.decorators.csrf import csrf_protect from django.views.generic.edit import FormView from flatblocks.models import FlatBlock +import os from PIL import Image import requests import ssl +import subprocess import sys from urllib.error import URLError from urllib.parse import quote import urllib.request, urllib.parse +import logging + +logger = logging.getLogger(__name__) def unstable_request_wrapper(url, params=False, retries=0): # """ @@ -714,8 +719,40 @@ def get_property_pdf_georef(request, property_id, map_type="aerial"): pass return response -## BELONGS IN VIEWS.py -def export_layer(request): +#* +#* Export shapefile +#* +@login_required(login_url='/auth/login/') +def export_shapefile(db_user, db_pw_command, database_name, shpdir, filename, query): + """ + Helper function to export a shapefile using pgsql2shp. + """ + export_command = f"pgsql2shp -u {db_user}{db_pw_command} -f {shpdir}/{filename} {database_name} \"{query}\"" + subprocess.run(export_command, shell=True, check=True) + +#* +#* Zip Shapefile +#* +def zip_shapefile(shpdir, filename): + """ + Helper function to zip the shapefile. + """ + import io + import zipfile + + os.chdir(shpdir) + files = [f for f in os.listdir(shpdir) if f.startswith(filename)] + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + for file in files: + zf.write(file) + zip_buffer.seek(0) + return zip_buffer + +#* +#* Export Layer +#* +def export_layer(request, property_id): ''' (called on request for download GIS data) IN: @@ -723,11 +760,43 @@ def export_layer(request): Format (default: zipped .shp, leave modular to support json & others) property OUT: - layer file + property layer in requested format USES: pgsql2shp (OGR/PostGIS built-in) ''' - return render(request, 'landmapper/base.html', {}) + if not request.user.is_authenticated: + return HttpResponse('User not authenticated. Please log in.', status=401) + + try: + property_record = properties.get_property_by_id(property_id, request.user) + except PropertyRecord.DoesNotExist: + return HttpResponse('Property not found or you do not have permission to access it.', status=404) + + db_user = settings.DATABASES['default']['USER'] + db_pw_command = f" -P {settings.DATABASES['default']['PASSWORD']}" if settings.DATABASES['default']['PASSWORD'] else "" + database_name = settings.DATABASES['default']['NAME'] + filename = f"{property_record.name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + shpdir = os.path.join(settings.SHAPEFILE_EXPORT_DIR, filename) + os.makedirs(shpdir, exist_ok=True) + + try: + query = f"SELECT * FROM app_propertyrecord WHERE id = {property_record.id};" + export_shapefile(db_user, db_pw_command, database_name, shpdir, filename, query) + zip_buffer = zip_shapefile(shpdir, filename) + + # Return zipped shapefile + response = HttpResponse(zip_buffer.read(), content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename={filename}.zip' + return response + except subprocess.CalledProcessError as e: + logger.error(f"Error exporting shapefile: {e}") + return HttpResponse('Error exporting shapefile.', status=500) + finally: + # Clean up temporary files + for file in os.listdir(shpdir): + os.remove(os.path.join(shpdir, file)) + os.rmdir(shpdir) + ### REGISTRATION/ACCOUNT MANAGEMENT ### def accountsRedirect(request): diff --git a/landmapper/landmapper/settings.py b/landmapper/landmapper/settings.py index 31dc580..ec8b1a6 100644 --- a/landmapper/landmapper/settings.py +++ b/landmapper/landmapper/settings.py @@ -1509,6 +1509,11 @@ TESTING_DIR = os.path.join(APP_DIR, 'testing_files') IMAGE_TEST_DIR = os.path.join(TESTING_DIR, 'image_test') +########################################### +## Shapefile Export ### +########################################### +SHAPEFILE_EXPORT_DIR = os.path.join(APP_DIR, 'static/landmapper/shapefiles/') + ########################################### ## PDF Files ### ###########################################