diff --git a/docker/Dockerfile b/docker/Dockerfile index 005667f..71c1bb5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,15 @@ FROM python:3.10-slim-bullseye AS runtime -COPY naturerecorderpy-1.0.17.0 /opt/naturerecorderpy-1.0.17.0 +COPY naturerecorderpy-1.0.18.0 /opt/naturerecorderpy-1.0.18.0 -WORKDIR /opt/naturerecorderpy-1.0.17.0 +WORKDIR /opt/naturerecorderpy-1.0.18.0 RUN apt-get update -y RUN pip install -r requirements.txt -RUN pip install nature_recorder-1.0.17-py3-none-any.whl +RUN pip install nature_recorder-1.0.18-py3-none-any.whl -ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy-1.0.17.0 -ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy-1.0.17.0/naturerecorder.db +ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy-1.0.18.0 +ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy-1.0.18.0/naturerecorder.db ENTRYPOINT [ "python" ] CMD [ "-m", "naturerec_web" ] diff --git a/requirements.txt b/requirements.txt index c443907..501b978 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,10 +11,12 @@ click==8.0.3 ConfigArgParse==1.5.3 coverage==6.2 cryptography==36.0.1 +cycler==0.11.0 docutils==0.17.1 Flask==2.0.2 Flask-BasicAuth==0.2.0 Flask-Cors==3.0.10 +fonttools==4.29.1 gevent==21.12.0 geventhttpclient==1.5.3 greenlet==1.1.2 @@ -23,8 +25,10 @@ idna==3.3 imagesize==1.3.0 itsdangerous==2.0.1 Jinja2==3.0.3 +kiwisolver==1.3.2 locust==2.5.1 MarkupSafe==2.0.1 +matplotlib==3.5.1 msgpack==1.0.3 numpy==1.21.5 outcome==1.1.0 @@ -34,6 +38,7 @@ parse==1.19.0 parse-type==0.5.2 pdfkit==1.0.0 pgeocode==0.3.0 +Pillow==9.0.1 psutil==5.9.0 pycountry==20.7.3 pycparser==2.21 diff --git a/setup.py b/setup.py index 86d5d5a..a862428 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def find_package_files(directory, remove_root): setuptools.setup( name="nature_recorder", - version="1.0.17", + version="1.0.18", description="Wildlife sightings database", packages=setuptools.find_packages("src"), include_package_data=True, diff --git a/src/naturerec_model/logic/__init__.py b/src/naturerec_model/logic/__init__.py index ce16898..40facb6 100644 --- a/src/naturerec_model/logic/__init__.py +++ b/src/naturerec_model/logic/__init__.py @@ -7,7 +7,8 @@ from .species_status_ratings import create_species_status_rating, get_species_status_rating, \ list_species_status_ratings, close_species_status_rating from .job_statuses import create_job_status, complete_job_status, list_job_status -from .reports import location_individuals_report, location_days_report +from .reports import location_individuals_report, location_days_report, save_report_barchart, \ + get_report_barchart_base64 __all__ = [ @@ -43,5 +44,7 @@ "complete_job_status", "list_job_status", "location_individuals_report", - "location_days_report" + "location_days_report", + "save_report_barchart", + "get_report_barchart_base64" ] diff --git a/src/naturerec_model/logic/reports.py b/src/naturerec_model/logic/reports.py index 4a8fc9d..22a25cc 100644 --- a/src/naturerec_model/logic/reports.py +++ b/src/naturerec_model/logic/reports.py @@ -3,9 +3,20 @@ """ import datetime +import base64 +import uuid +import tempfile +import os import pandas as pd from ..model import Engine, Sighting +# Need to do this to prevent it from attempting to start the Matplotlib GUI +import matplotlib + +matplotlib.use('Agg') + +import matplotlib.pyplot as plt + def location_individuals_report(from_date, location_id, category_id, to_date=None): """ @@ -69,3 +80,69 @@ def location_days_report(from_date, location_id, category_id, to_date=None): f"GROUP BY sp.Name" return pd.read_sql(sql_query, Engine).set_index("Species") + + +def save_report_barchart(report_df, y_column_name, x_label, y_label, title, image_path, x_column_name=None): + """ + Export a PNG image containing a report barchart + + :param report_df: Report dataframe + :param y_column_name: Name of the column containing the Y-axis values + :param x_label: X-axis label + :param y_label: Y-axis label + :param title: TItle + :param image_path: Output image path + :param x_column_name: Name of the column containing the X-axis labels or None to use the index + """ + + # Set up the X and Y axes + x = report_df[x_column_name] if x_column_name else report_df.index + y = report_df[y_column_name] + x_pos = [i for i, _ in enumerate(x)] + + # Configure the style and plot type + plt.style.use('ggplot') + plt.bar(x_pos, y, color='green') + + # Set up the axes and title + plt.xlabel(x_label) + plt.xticks(x_pos, x) + plt.xticks(rotation=90) + plt.ylabel(y_label) + plt.title(title) + + # This prevents the X-labels from going over the edge of the plot + plt.tight_layout() + + # Save to the specified file in PNG format + plt.savefig(image_path, format='png', dpi=300) + + +def get_image_base64(image_path): + """ + Given the path to an image file, read it and return the base-64 representation of its contents + + :param image_path: Path to the image file to read + :return: Base-64 representation of the image + """ + with open(image_path, mode="rb") as f: + return base64.b64encode(f.read()) + + +def get_report_barchart_base64(report_df, y_column_name, x_label, y_label, title, x_column_name=None): + """ + Return the base-64 representation of a PNG image containing a report barchart + + :param report_df: Report dataframe + :param y_column_name: Name of the column containing the Y-axis values + :param x_label: X-axis label + :param y_label: Y-axis label + :param title: TItle + :param x_column_name: Name of the column containing the X-axis labels or None to use the index + :return: String containing the Base-64 representation of the barchart + """ + image_path = os.path.join(tempfile.gettempdir(), f"{str(uuid.uuid4())}.png") + save_report_barchart(report_df, y_column_name, x_label, y_label, title, image_path, x_column_name) + barchart_base64 = get_image_base64(image_path).decode("utf-8") + os.unlink(image_path) + return barchart_base64 diff --git a/src/naturerec_web/reports/reports_blueprint.py b/src/naturerec_web/reports/reports_blueprint.py index 59c91ce..7a276be 100644 --- a/src/naturerec_web/reports/reports_blueprint.py +++ b/src/naturerec_web/reports/reports_blueprint.py @@ -3,21 +3,22 @@ """ from flask import Blueprint, render_template, request -from naturerec_model.logic import list_locations -from naturerec_model.logic import list_categories -from naturerec_model.logic import location_individuals_report, location_days_report +from naturerec_model.logic import list_locations, get_location +from naturerec_model.logic import list_categories, get_category +from naturerec_model.logic import location_individuals_report, location_days_report, get_report_barchart_base64 from naturerec_model.model import Sighting from naturerec_web.request_utils import get_posted_date, get_posted_int reports_bp = Blueprint("reports", __name__, template_folder='templates') -def _render_location_report_page(title, report_generator=None, from_date=None, to_date=None, location_id=None, - category_id=None): +def _render_location_report_page(title, y_label=None, report_generator=None, from_date=None, to_date=None, + location_id=None, category_id=None): """ Helper to show a location-based reporting page :param title: Title of the report + :param y_lable: Y-axis label of the report barchart :param report_generator: Report generator method :param from_date: From date for the reporting period :param to_date: To date for the reporting period @@ -29,20 +30,34 @@ def _render_location_report_page(title, report_generator=None, from_date=None, t to_date_string = to_date.strftime(Sighting.DATE_DISPLAY_FORMAT) if to_date else "" if report_generator and from_date and location_id and category_id: - report = report_generator(from_date=from_date, to_date=to_date, location_id=location_id, - category_id=category_id) + # Generate the report + report_df = report_generator(from_date=from_date, to_date=to_date, location_id=location_id, + category_id=category_id) + + # Create a barchart from the report + barchart_base64 = get_report_barchart_base64(report_df, "Count", "Species", y_label, title, None) + + # Get the location and category details + location = get_location(location_id) + category = get_category(category_id) else: - report = None + report_df = None + barchart_base64 = None + location = None + category = None return render_template("reports/location_report.html", title=title, locations=list_locations(), categories=list_categories(), category_id=category_id, + category=category, location_id=location_id, + location=location, from_date=from_date_string, to_date=to_date_string, - report=report) + report=report_df, + chart=barchart_base64) @reports_bp.route("/location/individuals", methods=["GET", "POST"]) @@ -59,8 +74,8 @@ def individuals_by_species_and_location(): to_date = get_posted_date("to_date") location_id = get_posted_int("location") category_id = get_posted_int("category") - return _render_location_report_page(title, location_individuals_report, from_date, to_date, location_id, - category_id) + return _render_location_report_page(title, "Individuals", location_individuals_report, from_date, to_date, + location_id, category_id) else: return _render_location_report_page(title) @@ -78,6 +93,7 @@ def sightings_by_species_and_location(): to_date = get_posted_date("to_date") location_id = get_posted_int("location") category_id = get_posted_int("category") - return _render_location_report_page(title, location_days_report, from_date, to_date, location_id, category_id) + return _render_location_report_page(title, "Sightings", location_days_report, from_date, to_date, location_id, + category_id) else: return _render_location_report_page(title) diff --git a/src/naturerec_web/reports/templates/reports/location_report.html b/src/naturerec_web/reports/templates/reports/location_report.html index 700324d..cb369d5 100644 --- a/src/naturerec_web/reports/templates/reports/location_report.html +++ b/src/naturerec_web/reports/templates/reports/location_report.html @@ -5,21 +5,23 @@ {% set index_column_name = "Species" %} {% block content %} -
{% endif %} - {% endblock %} diff --git a/src/naturerec_web/reports/templates/reports/report.html b/src/naturerec_web/reports/templates/reports/report.html index 8efd05c..4656771 100644 --- a/src/naturerec_web/reports/templates/reports/report.html +++ b/src/naturerec_web/reports/templates/reports/report.html @@ -1,4 +1,8 @@ {% if report.index | length > 0 %} +