Skip to content

Commit

Permalink
Merge pull request #28 from davewalker5/export-life-list
Browse files Browse the repository at this point in the history
Added ability to export life list
  • Loading branch information
davewalker5 authored Feb 2, 2022
2 parents a5cfa50 + cc1f6ed commit 3baff8f
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 43 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.15.0 /opt/naturerecorderpy-1.0.15.0
COPY naturerecorderpy-1.0.16.0 /opt/naturerecorderpy-1.0.16.0

WORKDIR /opt/naturerecorderpy-1.0.15.0
WORKDIR /opt/naturerecorderpy-1.0.16.0

RUN apt-get update -y
RUN pip install -r requirements.txt
RUN pip install nature_recorder-1.0.15-py3-none-any.whl
RUN pip install nature_recorder-1.0.16-py3-none-any.whl

ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy-1.0.15.0
ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy-1.0.15.0/naturerecorder.db
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

ENTRYPOINT [ "python" ]
CMD [ "-m", "naturerec_web" ]
1 change: 1 addition & 0 deletions docs/source/naturerec_model/data_exchange/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ The data_exchange Package
status_import_helper
sightings_import_helper
sightings_export_helper
life_list_export_helper
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
life_list_export_helper.py
==========================

.. automodule:: naturerec_model.data_exchange.life_list_export_helper
:members:
2 changes: 2 additions & 0 deletions features/export.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Feature: Export Sightings
@export
Scenario: Export unfiltered sightings
Given A set of sightings
| Date | Location | Category | Species | Number | Gender | WithYoung |
Expand All @@ -14,6 +15,7 @@ Feature: Export Sightings
Then The export starts
And There will be 2 sightings in the export file

@export
Scenario: Export filtered sightings
Given A set of sightings
| Date | Location | Category | Species | Number | Gender | WithYoung |
Expand Down
18 changes: 17 additions & 1 deletion features/life_list.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Feature: Life List
And I click on the "List Species" button
Then There will be 1 species in the life list

Scenario: Life list contains no entries
Scenario: Life list contains no entries
Given A set of categories
| Category |
| Birds |
Expand All @@ -22,3 +22,19 @@ Feature: Life List
And I select "Birds" as the "category"
And I click on the "List Species" button
Then The life list will be empty

@export
Scenario: Export life list
Given A set of sightings
| Date | Location | Category | Species | Number | Gender | WithYoung |
| 10/01/2022 | Test Location | Birds | Blackbird | 1 | Male | No |
| 01/01/2022 | Test Location | Amphibians | Frog | 1 | Unknown | No |

When I navigate to the export life list page
And I enter the life list export properties
| Filename | Category |
| birds_life_list.csv | Birds |

And I click on the "Export Life List" button
Then The life list export starts
And There will be 1 entry in the export file
11 changes: 11 additions & 0 deletions features/steps/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def _(context, button_text):
:param context: Behave context
:param button_text: Button text
"""
time.sleep(1)
xpath = f"//*[text()='{button_text}']"
elements = context.browser.find_elements(By.XPATH, xpath)
for element in elements:
Expand Down Expand Up @@ -155,3 +156,13 @@ def _(context, title):
"""
time.sleep(1)
assert title in context.browser.title


@then("There will be {number} {item_type} in the export file")
@then("There will be {number} {item_type} in the export file")
def _(context, number, item_type):
time.sleep(2)
with open(context.export_filepath, mode="rt", encoding="utf-8") as f:
lines = f.readlines()
# Number of lines plus 1 to account for the headers
assert len(lines) == int(number) + 1
11 changes: 0 additions & 11 deletions features/steps/export.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import time
from behave import when, then
from selenium.webdriver.common.by import By
from helpers import select_option, confirm_span_exists, get_export_filepath, delete_export_file
Expand Down Expand Up @@ -32,13 +31,3 @@ def _(context):
@then("The export starts")
def _(context):
confirm_span_exists(context, "Matching sightings are exporting in the background", 1)


@then("There will be {number} sightings in the export file")
@then("There will be {number} sighting in the export file")
def _(context, number):
time.sleep(2)
with open(context.export_filepath, mode="rt", encoding="utf-8") as f:
lines = f.readlines()
# Number of lines plus 1 to account for the headers
assert len(lines) == int(number) + 1
26 changes: 25 additions & 1 deletion features/steps/life_list.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from behave import when, then
from helpers import confirm_table_row_count, confirm_span_exists
from selenium.webdriver.common.by import By
from helpers import confirm_table_row_count, confirm_span_exists, get_export_filepath, delete_export_file, select_option


@when("I navigate to the life list page")
Expand All @@ -9,6 +10,24 @@ def _(context):
assert "Life List" in context.browser.title


@when("I navigate to the export life list page")
def _(context):
url = context.flask_runner.make_url("export/life_list")
context.browser.get(url)
assert "Export Life List" in context.browser.title


@when("I enter the life list export properties")
def _(context):
row = context.table.rows[0]
# We're about to do an export, so if the file already exists then delete it at this stage
context.export_filepath = get_export_filepath(row["Filename"])
delete_export_file(row["Filename"])

context.browser.find_element(By.NAME, "filename").send_keys(row["Filename"])
select_option(context, "category", row["Category"], 0)


@then("There will be {number} species in the life list")
@then("There will be {number} species in the life list")
def _(context, number):
Expand All @@ -18,3 +37,8 @@ def _(context, number):
@then("The life list will be empty")
def _(context):
confirm_span_exists(context, "There are no species in the database for the specified category", 1)


@then("The life list export starts")
def _(context):
confirm_span_exists(context, "The selected life list is exporting in the background", 1)
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.15",
version="1.0.16",
description="Wildlife sightings database",
packages=setuptools.find_packages("src"),
include_package_data=True,
Expand Down
4 changes: 3 additions & 1 deletion src/naturerec_model/data_exchange/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from .status_import_helper import StatusImportHelper
from .sightings_import_helper import SightingsImportHelper
from .sightings_export_helper import SightingsExportHelper
from .life_list_export_helper import LifeListExportHelper


__all__ = [
"StatusImportHelper",
"SightingsImportHelper",
"SightingsExportHelper"
"SightingsExportHelper",
"LifeListExportHelper"
]
49 changes: 49 additions & 0 deletions src/naturerec_model/data_exchange/life_list_export_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
This module implements a helper that will export a life list to a CSV format file on a background thread
"""

import csv
import os
from .data_exchange_helper_base import DataExchangeHelperBase
from ..model import get_data_path
from ..logic.sightings import life_list


class LifeListExportHelper(DataExchangeHelperBase):
JOB_NAME = "Life List export"
COLUMN_NAMES = ["Category", "Species"]

def __init__(self, filename, category_id):
super().__init__(self.export)
self._filename = filename
self._category_id = category_id
self.create_job_status()

def __repr__(self):
return f"{type(self).__name__}(" \
f"filename={self._filename!r}, " \
f"category_id={self._category_id!r})"

def export(self):
"""
Retrieve the life list matching the criteria passed to the init method and write it to file in CSV format
"""
with open(self.get_file_export_path(), mode='wt', newline='', encoding="UTF-8") as f:
writer = csv.writer(f)
writer.writerow(self.COLUMN_NAMES)

species = life_list(self._category_id)
for species in species:
writer.writerow([species.category.name, species.name])

def get_file_export_path(self):
"""
Construct and return the full path to the export file
:return: Full path to the export file
"""
export_folder = os.path.join(get_data_path(), "exports")
if not os.path.exists(export_folder):
os.makedirs(export_folder)

return os.path.join(export_folder, self._filename)
39 changes: 38 additions & 1 deletion src/naturerec_web/export/export_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
from naturerec_model.data_exchange import SightingsExportHelper, LifeListExportHelper
from naturerec_model.model import Sighting


Expand Down Expand Up @@ -49,6 +49,7 @@ def _render_export_filters_page(from_date=None,
:param location_id: Include sightings at this location
:param category_id: Species category for the selected species
:param species_id: Include sightings for this species
:param message: Message to display on the page
:return: The HTML for the rendered sightings export page
"""
return render_template("export/filters.html",
Expand All @@ -65,6 +66,19 @@ def _render_export_filters_page(from_date=None,
edit_enabled=True)


def _render_life_list_export_filters_page(category_id=None, message=None):
"""
:param category_id: Species category for the selected category
:param message: Message to display on the page
:return: The HTML for the rendered life list export page
"""
return render_template("export/life_list.html",
message=message,
categories=list_categories(),
category_id=category_id)


@export_bp.route("/filters", methods=["GET", "POST"])
def export():
"""
Expand All @@ -90,3 +104,26 @@ def export():
return _render_export_filters_page(from_date, to_date, location_id, category_id, species_id, message)
else:
return _render_export_filters_page()


@export_bp.route("/life_list", methods=["GET", "POST"])
def export_life_list():
"""
Show the page that presents filters for exporting life lists
:return: The HTML for the life list export page
"""
if request.method == "POST":
# Get the export parameters
filename = request.form["filename"]
category_id = _get_filter_int("category")

# Kick off the export
exporter = LifeListExportHelper(filename, category_id)
exporter.start()

# Go to the life list export page
message = "The selected life list is exporting in the background"
return _render_life_list_export_filters_page(category_id, message)
else:
return _render_life_list_export_filters_page()
20 changes: 20 additions & 0 deletions src/naturerec_web/export/templates/export/life_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block title %}Export Life List{% endblock %}

{% block content %}
<form method="post">
<h1>Life List</h1>
<div class="filter-criteria">
<div class="form-group">
<label>Filename</label>
<input class="form-control" name="filename" id="filename"
placeholder="CSV file name e.g. birds_life_list.csv" required>
</div>
{% include "category_selector.html" with context %}
<div class="button-bar">
<button type="submit" value="filter" class="btn btn-primary">Export Life List</button>
</div>
</div>
{% include "message.html" with context %}
</form>
{% endblock %}
10 changes: 9 additions & 1 deletion src/naturerec_web/life_list/templates/life_list/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
{% block content %}
<form method="post">
<h1>Life List</h1>
{% include "category_selector.html" with context %}
<div class="filter-criteria">
{% include "category_selector.html" with context %}
<div class="button-bar">
<button type="button" value="import" class="btn btn-light">
<a href="{{ url_for('export.export_life_list') }}">Export Life List</a>
</button>
<button type="submit" value="filter" class="btn btn-primary">List Species</button>
</div>
</div>
{% if species | length > 0 %}
<p>There are {{ species | length }} species in the life list for "{{ category.name }}"</p>
{% include "species.html" with context %}
Expand Down
10 changes: 9 additions & 1 deletion src/naturerec_web/species/templates/species/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
{% block content %}
<form method="post">
<h1>Species</h1>
{% include "category_selector.html" with context %}
<div class="filter-criteria">
{% include "category_selector.html" with context %}
<div class="button-bar">
<button type="submit" value="filter" class="btn btn-light">List Species</button>
<button type="button" class="btn btn-primary">
<a href="{{ url_for('species.edit') }}">Add Species</a>
</button>
</div>
</div>
{% if species | length > 0 %}
{% include "species.html" with context %}
{% elif category_id %}
Expand Down
28 changes: 10 additions & 18 deletions src/naturerec_web/templates/category_selector.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
<div class="filter-criteria">
<div class="form-group">
<label>Category</label>
<select class="form-control" name="category" id="category" required>
<option value="">Please select ...</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == category_id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
<div class="button-bar">
<button type="submit" value="filter" class="btn btn-light">List Species</button>
<button type="button" class="btn btn-primary">
<a href="{{ url_for('species.edit') }}">Add Species</a>
</button>
</div>
<div class="form-group">
<label>Category</label>
<select class="form-control" name="category" id="category" required>
<option value="">Please select ...</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == category_id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</div>
Loading

0 comments on commit 3baff8f

Please sign in to comment.