Skip to content

Commit

Permalink
Merge pull request #30 from davewalker5/add-chart-to-reports
Browse files Browse the repository at this point in the history
Add chart to reports
  • Loading branch information
davewalker5 authored Feb 5, 2022
2 parents ea0f53e + fca18a2 commit 87dfae5
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 36 deletions.
10 changes: 5 additions & 5 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/naturerec_model/logic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -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"
]
77 changes: 77 additions & 0 deletions src/naturerec_model/logic/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
40 changes: 28 additions & 12 deletions src/naturerec_web/reports/reports_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"])
Expand All @@ -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)

Expand All @@ -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)
30 changes: 16 additions & 14 deletions src/naturerec_web/reports/templates/reports/location_report.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@
{% set index_column_name = "Species" %}

{% block content %}
<form method="post">
<h1>{{ title }}</h1>
<div class="filter-criteria">
{% include "location_selector.html" with context %}
{% include "category_selector.html" with context %}
{% include "date_range_selector.html" with context %}
<div class="button-bar">
<button type="submit" value="generate" class="btn btn-primary">Generate Report</button>
</div>
</div>
{% if report.index | length > 0 %}
<p>{{ report.index | length }} record{% if report.index | length > 1 %}s{% endif %} found</p>
<img src="data:image/png;base64,{{chart}}" alt="{{ title }}" width="100%" />
{% include "reports/report.html" with context %}
{% elif report %}
<span>The report contains no results</span>
{% else %}
<form method="post">
<h1>{{ title }}</h1>
<div class="filter-criteria">
{% include "location_selector.html" with context %}
{% include "category_selector.html" with context %}
{% include "date_range_selector.html" with context %}
<div class="button-bar">
<button type="submit" value="generate" class="btn btn-primary">Generate Report</button>
</div>
</div>
{% if report.empty %}
<span>The report contains no results</span>
{% endif %}
</form>
{% endif %}
</form>
{% endblock %}
5 changes: 5 additions & 0 deletions src/naturerec_web/reports/templates/reports/report.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{% if report.index | length > 0 %}
<div align="center">
Location = {{ location.name }}, Category = {{ category.name }}<br/>
{{ report.index | length }} record{% if report.index | length > 1 %}s{% endif %} found<br/><br/>

<table class="striped">
<thead>
<tr>
Expand All @@ -19,4 +23,5 @@
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
23 changes: 21 additions & 2 deletions tests/naturerec_model/logic/test_reports.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import unittest
import datetime
from src.naturerec_model.model import create_database, Gender
import uuid
import os
from src.naturerec_model.model import create_database, Gender, get_data_path
from src.naturerec_model.logic import create_category, create_species, create_location, create_sighting
from src.naturerec_model.logic import location_individuals_report, location_days_report
from src.naturerec_model.logic import location_individuals_report, location_days_report, save_report_barchart, \
get_report_barchart_base64


class TestLocations(unittest.TestCase):
Expand All @@ -24,3 +27,19 @@ def test_can_get_location_sightings_report(self):
self.assertEqual(1, len(report_df.index))
self.assertTrue("Black-Headed Gull" in report_df.index)
self.assertEqual(1, report_df.loc["Black-Headed Gull", "Count"])

def test_can_export_report_barchart(self):
report_df = location_individuals_report(datetime.date(2021, 12, 1), self._location.id, self._category.id)
image_name = f"{str(uuid.uuid4())}.png"
image_path = os.path.join(get_data_path(), "exports", image_name)
self.assertFalse(os.path.exists(image_path))
save_report_barchart(report_df, "Count", "Species", "Individuals", "Individuals by Species and Location",
image_path, None)
self.assertTrue(os.path.exists(image_path))
os.unlink(image_path)

def test_can_get_report_barchart_base64(self):
report_df = location_individuals_report(datetime.date(2021, 12, 1), self._location.id, self._category.id)
base64 = get_report_barchart_base64(report_df, "Count", "Species", "Individuals",
"Individuals by Species and Location", None)
self.assertTrue(len(base64) > 0)

0 comments on commit 87dfae5

Please sign in to comment.