diff --git a/docker/Dockerfile b/docker/Dockerfile index 517560c..005667f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,15 @@ FROM python:3.10-slim-bullseye AS runtime -COPY naturerecorderpy-1.0.16.0 /opt/naturerecorderpy-1.0.16.0 +COPY naturerecorderpy-1.0.17.0 /opt/naturerecorderpy-1.0.17.0 -WORKDIR /opt/naturerecorderpy-1.0.16.0 +WORKDIR /opt/naturerecorderpy-1.0.17.0 RUN apt-get update -y RUN pip install -r requirements.txt -RUN pip install nature_recorder-1.0.16-py3-none-any.whl +RUN pip install nature_recorder-1.0.17-py3-none-any.whl -ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy-1.0.16.0 -ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy-1.0.16.0/naturerecorder.db +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 ENTRYPOINT [ "python" ] CMD [ "-m", "naturerec_web" ] diff --git a/docs/source/naturerec_model/logic/index.rst b/docs/source/naturerec_model/logic/index.rst index 79ef7f0..298a2d9 100644 --- a/docs/source/naturerec_model/logic/index.rst +++ b/docs/source/naturerec_model/logic/index.rst @@ -12,4 +12,5 @@ The logic Package status_schemes status_ratings species_status_ratings + reports job_statuses diff --git a/docs/source/naturerec_model/logic/reports.rst b/docs/source/naturerec_model/logic/reports.rst new file mode 100644 index 0000000..9dfd806 --- /dev/null +++ b/docs/source/naturerec_model/logic/reports.rst @@ -0,0 +1,5 @@ +reports.py +========== + +.. automodule:: naturerec_model.logic.reports + :members: diff --git a/docs/source/naturerec_web/index.rst b/docs/source/naturerec_web/index.rst index 1b541b0..004edd1 100644 --- a/docs/source/naturerec_web/index.rst +++ b/docs/source/naturerec_web/index.rst @@ -14,3 +14,5 @@ The naturerec_web Package species_ratings_blueprint export_blueprint jobs_blueprint + reports_blueprint + request_utils diff --git a/docs/source/naturerec_web/reports_blueprint.rst b/docs/source/naturerec_web/reports_blueprint.rst new file mode 100644 index 0000000..97bd74e --- /dev/null +++ b/docs/source/naturerec_web/reports_blueprint.rst @@ -0,0 +1,5 @@ +reports_blueprint.py +==================== + +.. automodule:: naturerec_web.reports.reports_blueprint + :members: diff --git a/docs/source/naturerec_web/request_utils.rst b/docs/source/naturerec_web/request_utils.rst new file mode 100644 index 0000000..c99d83e --- /dev/null +++ b/docs/source/naturerec_web/request_utils.rst @@ -0,0 +1,5 @@ +request_utils.py +================ + +.. automodule:: naturerec_web.request_utils + :members: diff --git a/features/reports.feature b/features/reports.feature new file mode 100644 index 0000000..0ee7d00 --- /dev/null +++ b/features/reports.feature @@ -0,0 +1,30 @@ +Feature: Reporting + Scenario: Report on numbers of individuals by location + Given A set of sightings + | Date | Location | Category | Species | Number | Gender | WithYoung | + | TODAY | Test Location | Birds | Woodpigeon | 1 | Unknown | No | + | TODAY | Test Location | Birds | Blackbird | 1 | Male | No | + | TODAY | Test Location | Birds | Robin | 1 | Unknown | No | + | TODAY | Test Location | Mammals | Grey Squirrel | 1 | Unknown | No | + + When I navigate to the individuals by location report page + And I fill in the individuals by location report details + | Location | Category | From | + | Test Location | Birds | TODAY | + + And I click on the "Generate Report" button + Then There will be 3 results in the report table + + Scenario: Report on numbers of individuals by location + Given A set of sightings + | Date | Location | Category | Species | Number | Gender | WithYoung | + | TODAY | Test Location | Birds | Woodpigeon | 1 | Unknown | No | + | TODAY | Test Location | Mammals | Grey Squirrel | 1 | Unknown | No | + + When I navigate to the sightings by location report page + And I fill in the sightings by location report details + | Location | Category | From | + | Test Location | Birds | TODAY | + + And I click on the "Generate Report" button + Then There will be 1 result in the report table diff --git a/features/steps/reports.py b/features/steps/reports.py new file mode 100644 index 0000000..f282ebf --- /dev/null +++ b/features/steps/reports.py @@ -0,0 +1,35 @@ +from behave import when, then +from selenium.webdriver.common.by import By + +from features.steps.helpers import select_option, get_date_from_string, confirm_table_row_count +from naturerec_model.model import Sighting + + +@when("I navigate to the individuals by location report page") +def _(context): + url = context.flask_runner.make_url("reports/location/individuals") + context.browser.get(url) + assert "Individuals by Species & Location" in context.browser.title + + +@when("I navigate to the sightings by location report page") +def _(context): + url = context.flask_runner.make_url("reports/location/sightings") + context.browser.get(url) + assert "Sightings by Species & Location" in context.browser.title + + +@when("I fill in the individuals by location report details") +@when("I fill in the sightings by location report details") +def _(context): + row = context.table.rows[0] + select_option(context, "location", row["Location"], None) + select_option(context, "category", row["Category"], None) + from_date = get_date_from_string(row["From"]).strftime(Sighting.DATE_DISPLAY_FORMAT) + context.browser.find_element(By.NAME, "from_date").send_keys(from_date) + + +@then("There will be {number} results in the report table") +@then("There will be {number} result in the report table") +def _(context, number): + confirm_table_row_count(context, number, 1) diff --git a/setup.py b/setup.py index 4abd48b..86d5d5a 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.16", + version="1.0.17", 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 fd3a61d..ce16898 100644 --- a/src/naturerec_model/logic/__init__.py +++ b/src/naturerec_model/logic/__init__.py @@ -7,6 +7,7 @@ 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 __all__ = [ @@ -40,5 +41,7 @@ "list_species_status_ratings", "create_job_status", "complete_job_status", - "list_job_status" + "list_job_status", + "location_individuals_report", + "location_days_report" ] diff --git a/src/naturerec_model/logic/reports.py b/src/naturerec_model/logic/reports.py new file mode 100644 index 0000000..4a8fc9d --- /dev/null +++ b/src/naturerec_model/logic/reports.py @@ -0,0 +1,71 @@ +""" +This module contains the business logic for the pre-defined reports +""" + +import datetime +import pandas as pd +from ..model import Engine, Sighting + + +def location_individuals_report(from_date, location_id, category_id, to_date=None): + """ + Report on the total number of individuals seen, filtering by location, category and date range + + :param from_date: Start date for reporting + :param location_id: Location id + :param category_id: Category id + :param to_date: End-date for reporting or None for today + :return: A Pandas Dataframe containing the results + """ + # If the "to" date isn't set, make it today + if not to_date: + to_date = datetime.datetime.today() + + # Format the dates in the format required in a SQL query + from_date_string = from_date.strftime(Sighting.DATE_FORMAT) + to_date_string = to_date.strftime(Sighting.DATE_FORMAT) + + # Construct the query + sql_query = f"SELECT sp.Name AS 'Species', SUM( IFNULL( s.Number, 1 ) ) AS 'Count' " \ + f"FROM SIGHTINGS s " \ + f"INNER JOIN LOCATIONS l ON l.Id = s.LocationId " \ + f"INNER JOIN SPECIES sp ON sp.Id = s.SpeciesId " \ + f"INNER JOIN CATEGORIES c ON c.Id = sp.CategoryId " \ + f"WHERE Date BETWEEN '{from_date_string}' AND '{to_date_string}' " \ + f"AND l.Id = {location_id} " \ + f"AND c.Id = {category_id} " \ + f"GROUP BY sp.Name" + + return pd.read_sql(sql_query, Engine).set_index("Species") + + +def location_days_report(from_date, location_id, category_id, to_date=None): + """ + Report on the number of days on which a given species was seen, filtering by location, category and date range + + :param from_date: Start date for reporting + :param location_id: Location id + :param category_id: Category id + :param to_date: End-date for reporting or None for today + :return: A Pandas Dataframe containing the results + """ + # If the "to" date isn't set, make it today + if not to_date: + to_date = datetime.datetime.today() + + # Format the dates in the format required in a SQL query + from_date_string = from_date.strftime(Sighting.DATE_FORMAT) + to_date_string = to_date.strftime(Sighting.DATE_FORMAT) + + # Construct the query + sql_query = f"SELECT sp.Name AS 'Species', COUNT( s.Id ) AS 'Count' " \ + f"FROM SIGHTINGS s " \ + f"INNER JOIN LOCATIONS l ON l.Id = s.LocationId " \ + f"INNER JOIN SPECIES sp ON sp.Id = s.SpeciesId " \ + f"INNER JOIN CATEGORIES c ON c.Id = sp.CategoryId " \ + f"WHERE Date BETWEEN '{from_date_string}' AND '{to_date_string}' " \ + f"AND l.Id = {location_id} " \ + f"AND c.Id = {category_id} " \ + f"GROUP BY sp.Name" + + return pd.read_sql(sql_query, Engine).set_index("Species") diff --git a/src/naturerec_web/export/export_blueprint.py b/src/naturerec_web/export/export_blueprint.py index 49e71c1..646fbe0 100644 --- a/src/naturerec_web/export/export_blueprint.py +++ b/src/naturerec_web/export/export_blueprint.py @@ -2,39 +2,16 @@ The export blueprint supplies view functions and templates for exporting sightings """ -import datetime from flask import Blueprint, render_template, request from naturerec_model.logic import list_locations from naturerec_model.logic import list_categories from naturerec_model.data_exchange import SightingsExportHelper, LifeListExportHelper from naturerec_model.model import Sighting - +from naturerec_web.request_utils import get_posted_date, get_posted_int export_bp = Blueprint("export", __name__, template_folder='templates') -def _get_filter_int(key): - """ - Retrieve a named integer value from the POSTed filtering form - - :param key: Value key - :return: Value or None if not specified - """ - value = request.form[key] if key in request.form else None - return int(value) if value else None - - -def _get_filter_date(key): - """ - Retrieve a named date value from the POSTed filtering form - - :param key: Value key - :return: Value or None if not specified - """ - date_string = request.form[key] if key in request.form else None - return datetime.datetime.strptime(date_string, Sighting.DATE_DISPLAY_FORMAT).date() if date_string else None - - def _render_export_filters_page(from_date=None, to_date=None, location_id=None, @@ -89,11 +66,11 @@ def export(): if request.method == "POST": # Get the export parameters filename = request.form["filename"] - from_date = _get_filter_date("from_date") - to_date = _get_filter_date("to_date") - location_id = _get_filter_int("location") - category_id = _get_filter_int("category") - species_id = _get_filter_int("species") + from_date = get_posted_date("from_date") + to_date = get_posted_date("to_date") + location_id = get_posted_int("location") + category_id = get_posted_int("category") + species_id = get_posted_int("species") # Kick off the export exporter = SightingsExportHelper(filename, from_date, to_date, location_id, species_id) @@ -116,7 +93,7 @@ def export_life_list(): if request.method == "POST": # Get the export parameters filename = request.form["filename"] - category_id = _get_filter_int("category") + category_id = get_posted_int("category") # Kick off the export exporter = LifeListExportHelper(filename, category_id) diff --git a/src/naturerec_web/jobs/templates/jobs/list.html b/src/naturerec_web/jobs/templates/jobs/list.html index 05e5575..643ffb2 100644 --- a/src/naturerec_web/jobs/templates/jobs/list.html +++ b/src/naturerec_web/jobs/templates/jobs/list.html @@ -1,6 +1,5 @@ {% extends "layout.html" %} {% block title %}Background Job Status{% endblock %} -{% set import_enabled = True %} {% block content %}
+{% endblock %} diff --git a/src/naturerec_web/reports/templates/reports/report.html b/src/naturerec_web/reports/templates/reports/report.html new file mode 100644 index 0000000..8efd05c --- /dev/null +++ b/src/naturerec_web/reports/templates/reports/report.html @@ -0,0 +1,22 @@ +{% if report.index | length > 0 %} +{{ index_column_name }} | + {% for column in report.columns %} +{{ column }} | + {% endfor %} +
---|---|
{{ index }} | + {% for column in report.columns %} +{{ report.loc[index, column] }} | + {% endfor %} +