diff --git a/backend/.gitignore b/backend/.gitignore index e2bd6cf..6cc3cd8 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,4 +4,5 @@ __pycache__ resources right_tree/api/data/fixtures/plants.json -right_tree/staticfiles \ No newline at end of file +right_tree/staticfiles +right_tree/api/data/generated_resources/* \ No newline at end of file diff --git a/backend/right_tree/api/csv_utils.py b/backend/right_tree/api/csv_utils.py deleted file mode 100644 index bf38c3f..0000000 --- a/backend/right_tree/api/csv_utils.py +++ /dev/null @@ -1,127 +0,0 @@ -import csv -from pathlib import Path - -import right_tree.api.data -from .filters import * -from .utils import get_address_from_coordinates, get_point_from_coordinates - - -CSV_FILENAME = 'plants.csv' -HEADER_FIELDS = ['Names', 'Growth Form / Max Height (m) / Spacing (m) / Forest Position', 'Moisture Preferences', - 'Tolerances (Water / Drought / Frost / Salinity)', 'Ecosystem Services', 'Carbon Sequestration Rate', 'Planting Stage'] - - -def get_plant_csv_filepath(): - """ Retrives the filepath for the plant csv file. - """ - return Path(right_tree.api.data.__file__).resolve().parent / 'resources' / CSV_FILENAME - - -def get_location_filters(request): - """ Retrives the selected location data from the request. - """ - filter_rows = [['LOCATION FILTERS:']] - coordinates = request.query_params.get('coordinates') - - if coordinates is not None: - eco_district_layer = ecological_district_coordinate_filter(coordinates).first() - point = get_point_from_coordinates(coordinates) - - filter_rows.append(['Point coordinates:', point]) - filter_rows.append( - ['Ecological region:', eco_district_layer.ecologic_1 or '']) - filter_rows.append( - ['Ecological district:', eco_district_layer.ecologic_2 or '']) - filter_rows.append( - ['Property address:', get_address_from_coordinates(coordinates)['full_address'] or '']) - else: - filter_rows.append(["None specified"]) - - return filter_rows - - -def get_soil_filters(request): - """ Retrives the selected soil type data from the request. - """ - filter_rows = [['SOIL FILTERS:']] - soil_variant = request.query_params.get('soilVariant') - coordinates = request.query_params.get('coordinates') - - if soil_variant is not None and coordinates is not None: - soil_order_obj = soil_order_coordinate_filter(coordinates).first() - - filter_rows.append( - ['Soil Order:', f"{soil_order_obj.name or ''} ({soil_order_obj.code or ''})"]) - filter_rows.append(['Soil Variant:', soil_variant]) - else: - filter_rows.append(["None specified"]) - - return filter_rows - - -def get_site_filters(request): - """ Retrives the selected site data from the request. - """ - filter_rows = [['SITE FILTERS:']] - - habitat = request.query_params.get('habitat') - zone = request.query_params.get('zone') - if zone is not None and habitat is not None: - habitat_json = json.loads(habitat) - zone_json = json.loads(zone) - - filter_rows.append(['Habitat:', habitat_json.get("name", "")]) - filter_rows.append(['Zone Name:', zone_json.get("name", "")]) - filter_rows.append(['Zone Variant:', zone_json.get("variant", "")]) - filter_rows.append( - ['Zone Refined Variant:', zone_json.get("refined_variant", "")]) - else: - filter_rows.append(["None specified"]) - - return filter_rows - - -def get_filter_values(request): - """ Retrives all selected values/filters from the request. - """ - filter_rows = [] - - # Add all the location filters - filter_rows += get_location_filters(request) - filter_rows.append(['']) - - # Add the soil filters - filter_rows += get_soil_filters(request) - filter_rows.append(['']) - - # Add the project site filters - filter_rows += get_site_filters(request) - filter_rows.append(['']) - - return filter_rows - - -def create_plant_csv_file(request, plant_data): - """ Constructs a csv file that contains selected filter values and the resulting plant list. - """ - with open(get_plant_csv_filepath(), 'w', encoding='UTF8') as f: - writer = csv.writer(f) - - # Write filter/selected values - for filter_row in get_filter_values(request): - writer.writerow(filter_row) - - # Write the plant data - writer.writerow(HEADER_FIELDS) - for plant in plant_data: - plant_data_row = [ - plant.display_name, - plant.display_growth_form, - plant.moisture_preferences, - plant.plant_tolerances, - plant.ecosystem_services, - plant.carbon_sequestration, - plant.stage - ] - - writer.writerow(plant_data_row) diff --git a/backend/right_tree/api/filters.py b/backend/right_tree/api/filters.py index ee2cfd4..faf0a8a 100644 --- a/backend/right_tree/api/filters.py +++ b/backend/right_tree/api/filters.py @@ -4,7 +4,7 @@ from django.http import Http404 from django.db.models import Q from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, SoilOrder, SoilVariant -from .utils import get_point_from_coordinates +from .wms_utils import get_point_from_coordinates def coordinate_filter(request, queryset, ignore_soil_order=False): diff --git a/backend/right_tree/api/resource_generation_utils.py b/backend/right_tree/api/resource_generation_utils.py new file mode 100644 index 0000000..1568768 --- /dev/null +++ b/backend/right_tree/api/resource_generation_utils.py @@ -0,0 +1,191 @@ +import csv +from os import write +from pathlib import Path + +import right_tree.api.data +from .filters import * +from .wms_utils import get_address_from_coordinates, get_point_from_coordinates + +import pdfkit +import pandas as pd +from PyPDF2 import PdfFileMerger + + +CSV_FILENAME = 'plants.csv' +PLANTING_GUIDE_PDF_FILENAME = 'planting_guide.pdf' +HEADER_FIELDS = ['Names', 'Growth Form / Max Height (m) / Spacing (m) / Forest Position', 'Moisture Preferences', + 'Tolerances (Water / Drought / Frost / Salinity)', 'Ecosystem Services', 'Carbon Sequestration Rate', 'Planting Stage'] + + +def get_plant_resource_filepath(filename=CSV_FILENAME, resource_dir='generated_resources'): + """ Retrives the filepath for the plant csv file. + """ + return Path(right_tree.api.data.__file__).resolve().parent / resource_dir / filename + + +def get_location_filters(request): + """ Retrives the selected location data from the request. + """ + filter_rows = [['LOCATION FILTERS:']] + coordinates = request.query_params.get('coordinates') + + if coordinates is not None: + eco_district_layer = ecological_district_coordinate_filter( + coordinates).first() + point = get_point_from_coordinates(coordinates) + + filter_rows.append(['Point coordinates:', point]) + filter_rows.append(['Ecological region:', eco_district_layer.ecologic_1 or '']) + filter_rows.append(['Ecological district:', eco_district_layer.ecologic_2 or '']) + filter_rows.append(['Property address:', get_address_from_coordinates(coordinates)['full_address'] or '']) + else: + filter_rows.append(["None specified"]) + + return filter_rows + + +def get_soil_filters(request): + """ Retrives the selected soil type data from the request. + """ + filter_rows = [['SOIL FILTERS:']] + soil_variant = request.query_params.get('soilVariant') + coordinates = request.query_params.get('coordinates') + + if soil_variant is not None and coordinates is not None: + soil_order_obj = soil_order_coordinate_filter(coordinates).first() + + filter_rows.append(['Soil Order:', f"{soil_order_obj.name or ''} ({soil_order_obj.code or ''})"]) + filter_rows.append(['Soil Variant:', soil_variant]) + else: + filter_rows.append(["None specified"]) + + return filter_rows + + +def get_site_filters(request): + """ Retrives the selected site data from the request. + """ + filter_rows = [['SITE FILTERS:']] + + habitat = request.query_params.get('habitat') + zone = request.query_params.get('zone') + if zone is not None and habitat is not None: + habitat_json = json.loads(habitat) + zone_json = json.loads(zone) + + filter_rows.append(['Habitat:', habitat_json.get("name", "")]) + filter_rows.append(['Zone Name:', zone_json.get("name", "")]) + filter_rows.append(['Zone Variant:', zone_json.get("variant", "")]) + filter_rows.append(['Zone Refined Variant:', zone_json.get("refined_variant", "")]) + else: + filter_rows.append(["None specified"]) + + return filter_rows + + +def get_filter_values(request): + """ Retrives all selected values/filters from the request. + """ + filter_rows = [] + + # Add all the location filters + filter_rows += get_location_filters(request) + filter_rows.append(['']) + + # Add the soil filters + filter_rows += get_soil_filters(request) + filter_rows.append(['']) + + # Add the project site filters + filter_rows += get_site_filters(request) + filter_rows.append(['']) + + return filter_rows + + +def write_csv_filter_info(request, writer): + """ Retrieves and writes filter information to a CSV file given a writer. + """ + for filter_row in get_filter_values(request): + writer.writerow(filter_row) + + +def write_csv_plant_info(plant_data, writer): + """ Writes plant list information to a CSV file given a writer. + """ + writer.writerow(HEADER_FIELDS) + for plant in plant_data: + plant_data_row = [ + plant.display_name, + plant.display_growth_form, + plant.moisture_preferences, + plant.plant_tolerances, + plant.ecosystem_services, + plant.carbon_sequestration, + plant.stage + ] + writer.writerow(plant_data_row) + + +def create_plant_csv_file(request, plant_data): + """ Constructs a csv file that contains selected filter values and the resulting plant list. + """ + with open(get_plant_resource_filepath(), 'w', encoding='UTF8') as f: + writer = csv.writer(f) + + # Write filter/selected values + write_csv_filter_info(request, writer) + + # Write the plant data + write_csv_plant_info(plant_data, writer) + + +def generate_pdf(data_name, data, csv_generation_function): + """ Generates a pdf from a csv given data and a csv generation method. + Requires an html file to be generated as an intermediate step. + """ + + # Define filepaths - some are required as intermediate files + csv_filepath = get_plant_resource_filepath(f'{data_name}.csv') + html_filepath = get_plant_resource_filepath(f'{data_name}.html') + pdf_filepath = get_plant_resource_filepath(f'{data_name}.pdf') + + # Create an initial csv file with the data + with open(csv_filepath, 'w', encoding='UTF8') as f: + writer = csv.writer(f) + csv_generation_function(data, writer) + + # Convert the csv to and html file then to a pdf + CSV = pd.read_csv(csv_filepath) + CSV.to_html(html_filepath) + pdfkit.from_file(str(html_filepath), str(pdf_filepath)) + + +def create_planting_guide_pdf(request, plant_data): + """ Creates a planting guide pdf document with a pre-generated planting guide with + filter and plant list tabular informtation appended. + """ + + # Define the names of the resource files as used in multiple places + filter_file_prefix = "filters" + plant_list_file_prefix = "plant_list" + + # Generate the filters and plant list pdf files + generate_pdf(filter_file_prefix, request, write_csv_filter_info) # TODO: space values appear as NaN... this should be fixed + generate_pdf(plant_list_file_prefix, plant_data, write_csv_plant_info) + + # Create a list of pdfs that need to be merged + planting_guide_pdf_filepath = get_plant_resource_filepath(PLANTING_GUIDE_PDF_FILENAME, 'resources') + filter_pdf_filepath = get_plant_resource_filepath(f'{filter_file_prefix}.pdf') + plant_list_pdf_filepath = get_plant_resource_filepath(f'{plant_list_file_prefix}.pdf') + pdfs = [planting_guide_pdf_filepath, filter_pdf_filepath, plant_list_pdf_filepath] + + # Merge pdfs + merger = PdfFileMerger() + for pdf in pdfs: + merger.append(str(pdf)) + + # Create the final output pdf with all merged documents + output_pdf_filepath = get_plant_resource_filepath(PLANTING_GUIDE_PDF_FILENAME) + merger.write(str(output_pdf_filepath)) + merger.close() diff --git a/backend/right_tree/api/views.py b/backend/right_tree/api/views.py index 6bcce23..a239cb9 100644 --- a/backend/right_tree/api/views.py +++ b/backend/right_tree/api/views.py @@ -1,20 +1,19 @@ -from django.http import HttpResponseBadRequest, HttpResponse, HttpRequest -from django.shortcuts import get_object_or_404 - +from django.http import HttpResponseBadRequest, HttpResponse, FileResponse +from django.shortcuts import get_object_or_404 from rest_framework import viewsets from rest_framework.response import Response +from wsgiref.util import FileWrapper from right_tree.api.models import Habitat, HabitatImage, Plant, EcologicalDistrictLayer, SoilOrder from right_tree.api.serializers import HabitatImageSerializer, HabitatSerializer, PlantSerializer, SoilOrderSerializer, EcologicalDistrictLayerSerializer, AddressSerializer from .filters import * -from .utils import get_address_from_coordinates -from .csv_utils import create_plant_csv_file, get_plant_csv_filepath +from .wms_utils import get_address_from_coordinates +from .resource_generation_utils import create_plant_csv_file, get_plant_resource_filepath, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME class PlantViewSet(viewsets.ModelViewSet): - """ - Filtered viewset for plants. + """ Filtered viewset for plants. """ queryset = Plant.objects.all() serializer_class = PlantSerializer @@ -27,8 +26,7 @@ class PlantViewSet(viewsets.ModelViewSet): class SoilOrderViewSet(viewsets.ModelViewSet): - """ - Filtered viewset for soil details. + """ Filtered viewset for soil details. """ serializer_class = SoilOrderSerializer @@ -43,8 +41,7 @@ class SoilOrderViewSet(viewsets.ModelViewSet): class EcologicalDistrictViewSet(viewsets.ModelViewSet): - """ - Filtered viewset for ecological district/region details. + """ Filtered viewset for ecological district/region details. """ serializer_class = EcologicalDistrictLayerSerializer @@ -59,8 +56,7 @@ class EcologicalDistrictViewSet(viewsets.ModelViewSet): class LINZPropertyViewSet(viewsets.ViewSet): - """ - Filtered viewset for ecological district/region details. + """ Filtered viewset for ecological district/region details. """ def list(self, request): @@ -74,16 +70,14 @@ class LINZPropertyViewSet(viewsets.ViewSet): class HabitatViewSet(viewsets.ModelViewSet): - """ - Viewset for all habitats. + """ Viewset for all habitats. """ serializer_class = HabitatSerializer queryset = Habitat.objects.all() class HabitatImageViewSet(viewsets.ViewSet): - """ - Viewset for a habitat image. + """ Viewset for a habitat image. """ def list(self, request): @@ -99,12 +93,27 @@ class HabitatImageViewSet(viewsets.ViewSet): class CSVDownloadView(viewsets.ViewSet): + """ Viewset for a downloading a CSV plant list and filters. + """ def list(self, request, *args, **kwargs): filtered_plants = get_filtered_plants(request) create_plant_csv_file(request, filtered_plants) - csv_file = open(get_plant_csv_filepath(), 'rb') + csv_file = open(get_plant_resource_filepath(), 'rb') response = HttpResponse(csv_file, content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="plants.csv"' return response + +class PDFDownloadView(viewsets.ViewSet): + """ Viewset for a downloading a PDF planting guide with appended filter and plant list info. + """ + + def list(self, request, *args, **kwargs): + filtered_plants = get_filtered_plants(request) + + create_planting_guide_pdf(request, filtered_plants) + pdf_file = open(get_plant_resource_filepath(PLANTING_GUIDE_PDF_FILENAME), 'rb') + + response = HttpResponse(FileWrapper(pdf_file), content_type='application/pdf') + return response diff --git a/backend/right_tree/api/utils.py b/backend/right_tree/api/wms_utils.py similarity index 100% rename from backend/right_tree/api/utils.py rename to backend/right_tree/api/wms_utils.py diff --git a/backend/right_tree/urls.py b/backend/right_tree/urls.py index 694a552..579d76e 100644 --- a/backend/right_tree/urls.py +++ b/backend/right_tree/urls.py @@ -25,8 +25,10 @@ router.register(r'soil', views.SoilOrderViewSet, basename='soil') router.register(r'ecologicaldistrict', views.EcologicalDistrictViewSet, basename='ecologicaldistrict') router.register(r'address', views.LINZPropertyViewSet, basename='address') router.register(r'habitats', views.HabitatViewSet, basename='habitats') -router.register(r'habitatimage', views.HabitatImageViewSet,basename='habitatimage') -router.register(r'download/csv', views.CSVDownloadView ,basename='downloadcsv') +router.register(r'habitatimage', views.HabitatImageViewSet, basename='habitatimage') + +router.register(r'download/csv', views.CSVDownloadView, basename='downloadcsv') +router.register(r'download/pdf', views.PDFDownloadView, basename='downloadpdf') urlpatterns = [ path('admin/', admin.site.urls),