Skip to content

Commit

Permalink
Add scientific name for species
Browse files Browse the repository at this point in the history
  • Loading branch information
davewalker5 committed Oct 29, 2024
1 parent f57597d commit 92a6bd9
Show file tree
Hide file tree
Showing 33 changed files with 339 additions and 249 deletions.
24 changes: 24 additions & 0 deletions alembic/versions/b8960906cfcb_add_species_scientific_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""add species scientific name
Revision ID: b8960906cfcb
Revises: 5ce1dfff9cd4
Create Date: 2024-10-29 03:04:18.463075
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'b8960906cfcb'
down_revision = '5ce1dfff9cd4'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column('Species', sa.Column('Scientific_Name', sa.Text, nullable=True))


def downgrade() -> None:
op.drop_column('Species', 'Scientific_Name')
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
FROM python:3.10-slim-bullseye AS runtime

COPY naturerecorderpy-1.6.0.0 /opt/naturerecorderpy
COPY naturerecorderpy-1.7.0.0 /opt/naturerecorderpy

WORKDIR /opt/naturerecorderpy

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

ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy
ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy/naturerecorder.db
Expand Down
Binary file modified docs/source/naturerec_model/images/schema.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.6.0",
version="1.7.0",
description="Wildlife sightings database",
packages=setuptools.find_packages("src"),
include_package_data=True,
Expand Down
10 changes: 6 additions & 4 deletions src/naturerec_model/data_exchange/data_exchange_helper_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,30 +57,32 @@ def join(self, timeout: Optional[float] = ...) -> None:
if self._exception:
raise self._exception

def create_species(self, category_name, species_name):
def create_species(self, category_name, species_name, scientific_name):
"""
Ensure the species with the specified name exists in the specified category
:param category_name: Name of the category to which the species belongs
:param species_name: Name of the species
:param species_name: Common name of the species
:param scientific_name: Scientific name of the species
"""
tidied_category_name = " ".join(category_name.split()).title()
tidied_species_name = " ".join(species_name.split()).title()
tidied_scientific_name = " ".join(scientific_name.split()).title() if scientific_name != None else None

# See if the category exists and, if not, create it and the species
try:
category = get_category(tidied_category_name)
except ValueError:
category = create_category(tidied_category_name, self._user)
return create_species(category.id, tidied_species_name, self._user).id
return create_species(category.id, tidied_species_name, tidied_scientific_name, self._user).id

# See if the species exists against the existing category. If so, just return its ID
species_ids = [species.id for species in category.species if species.name == tidied_species_name]
if len(species_ids):
return species_ids[0]

# Doesn't exist so create it and return its ID
return create_species(category.id, tidied_species_name, self._user).id
return create_species(category.id, tidied_species_name, tidied_scientific_name, self._user).id

def create_job_status(self):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,39 @@
This module defines a base class for CSV data exchange helpers that complete on a background thread. The
CSV files have the following columns:
+-----------+-----------------------------------------------------------------------------+
| Column | Contents |
+-----------+-----------------------------------------------------------------------------+
| Species | Name of the species |
+-----------+-----------------------------------------------------------------------------+
| Category | Category to which the species belongs |
+-----------+-----------------------------------------------------------------------------+
| Number | Number of individuals seen or 0 if not counted |
+-----------+-----------------------------------------------------------------------------+
| Gender | Gender of the individuals seen - Unkown, Male, Female or Both |
+-----------+-----------------------------------------------------------------------------+
| WithYoung | Yes or No, indicating whether young were also seen |
+-----------+-----------------------------------------------------------------------------+
| Date | Date of the sighting in DD/MM/YYYY format |
+-----------+-----------------------------------------------------------------------------+
| Location | Name of the location where the sighting was made |
+-----------+-----------------------------------------------------------------------------+
| Address | Street address for the location |
+-----------+-----------------------------------------------------------------------------+
| City | City for the location |
+-----------+-----------------------------------------------------------------------------+
| County | County for the location |
+-----------+-----------------------------------------------------------------------------+
| Postcode | Postcode for the location |
+-----------+-----------------------------------------------------------------------------+
| Latitude | Latitude for the location in decimal format |
+-----------+-----------------------------------------------------------------------------+
| Longitude | Longitude for the location in decimal format |
+-----------+-----------------------------------------------------------------------------+
| Notes | Sighting notes |
+-----------+-----------------------------------------------------------------------------+
+-------------------+--------------------------------------------------------------------------+
| Column | Contents |
+-------------------+--------------------------------------------------------------------------+
| Species | Common name of the species |
+-------------------+--------------------------------------------------------------------------+
| Scientific Name | Scientific name of the species |
+-------------------+--------------------------------------------------------------------------+
| Category | Category to which the species belongs |
+-------------------+--------------------------------------------------------------------------+
| Number | Number of individuals seen or 0 if not counted |
+-------------------+--------------------------------------------------------------------------+
| Gender | Gender of the individuals seen - Unkown, Male, Female or Both |
+-------------------+--------------------------------------------------------------------------+
| WithYoung | Yes or No, indicating whether young were also seen |
+-------------------+--------------------------------------------------------------------------+
| Date | Date of the sighting in DD/MM/YYYY format |
+-------------------+--------------------------------------------------------------------------+
| Location | Name of the location where the sighting was made |
+-------------------+--------------------------------------------------------------------------+
| Address | Street address for the location |
+-------------------+--------------------------------------------------------------------------+
| City | City for the location |
+-------------------+--------------------------------------------------------------------------+
| County | County for the location |
+-------------------+--------------------------------------------------------------------------+
| Postcode | Postcode for the location |
+-------------------+--------------------------------------------------------------------------+
| Latitude | Latitude for the location in decimal format |
+-------------------+--------------------------------------------------------------------------+
| Longitude | Longitude for the location in decimal format |
+-------------------+--------------------------------------------------------------------------+
| Notes | Sighting notes |
+-------------------+--------------------------------------------------------------------------+
"""

from .data_exchange_helper_base import DataExchangeHelperBase
Expand All @@ -41,6 +43,7 @@
class SightingsDataExchangeHelperBase(DataExchangeHelperBase):
COLUMN_NAMES = [
'Species',
'Scientific Name',
'Category',
'Number',
'Gender',
Expand Down
42 changes: 21 additions & 21 deletions src/naturerec_model/data_exchange/sightings_import_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ def import_sightings(self):
"""
self._read_csv_rows()
for row in self._rows:
species_id = self.create_species(row[1], row[0])
species_id = self.create_species(row[2], row[0], row[1])
location_id = self._create_location(row)
date = datetime.datetime.strptime(row[5], Sighting.DATE_IMPORT_FORMAT).date()
number = int(row[2]) if row[2].strip() else None
gender = [key for key, value in Gender.gender_map().items() if value == row[3].strip().title()][0]
with_young = 1 if row[4].strip().title() == "Yes" else 0
notes = row[14] if row[14] else None
date = datetime.datetime.strptime(row[6], Sighting.DATE_IMPORT_FORMAT).date()
number = int(row[3]) if row[3].strip() else None
gender = [key for key, value in Gender.gender_map().items() if value == row[4].strip().title()][0]
with_young = 1 if row[5].strip().title() == "Yes" else 0
notes = row[14] if row[15] else None
_ = create_sighting(location_id, species_id, date, number, gender, with_young, notes, self._user)

def _read_csv_rows(self):
Expand Down Expand Up @@ -76,25 +76,25 @@ def _validate_row(cls, row, row_number):
:param row: CSV row (collection of fields)
:param row_number: Row number for error reporting
"""
if len(row) != 15:
if len(row) != 16:
raise ValueError(f"Malformed data at row {row_number}")

cls._check_not_empty(row, 0, row_number) # Species
cls._check_not_empty(row, 1, row_number) # Category
cls._check_valid_int(row, 2, row_number, True) # Number
cls._check_not_empty(row, 2, row_number) # Category
cls._check_valid_int(row, 3, row_number, True) # Number

# Gender
cls._check_valid_value(row, 3, row_number, Gender.gender_map().values())
cls._check_valid_value(row, 4, row_number, Gender.gender_map().values())

# With Young
cls._check_valid_value(row, 4, row_number, ["Yes", "No"])
cls._check_valid_value(row, 5, row_number, ["Yes", "No"])

cls._check_valid_date(row, 5, row_number) # Date
cls._check_not_empty(row, 6, row_number) # Location
cls._check_not_empty(row, 9, row_number) # County
cls._check_not_empty(row, 11, row_number) # Country
cls._check_valid_float(row, 12, row_number, True) # Latitude
cls._check_valid_float(row, 13, row_number, True) # Longitude
cls._check_valid_date(row, 6, row_number) # Date
cls._check_not_empty(row, 7, row_number) # Location
cls._check_not_empty(row, 10, row_number) # County
cls._check_not_empty(row, 12, row_number) # Country
cls._check_valid_float(row, 13, row_number, True) # Latitude
cls._check_valid_float(row, 14, row_number, True) # Longitude

@classmethod
def _check_not_empty(cls, row, index, row_number):
Expand Down Expand Up @@ -179,11 +179,11 @@ def _check_valid_date(cls, row, index, row_number):
raise ValueError(f"Invalid value for {cls.get_field_name(index)} on row {row_number}")

def _create_location(self, row):
tidied_name = " ".join(row[6].split()).title()
tidied_name = " ".join(row[7].split()).title()
try:
location = get_location(tidied_name)
except ValueError:
latitude = float(row[12]) if row[12].strip() else None
longitude = float(row[13]) if row[12].strip() else None
location = create_location(tidied_name, row[9], row[11], self._user, row[7], row[8], row[10], latitude, longitude)
latitude = float(row[13]) if row[13].strip() else None
longitude = float(row[14]) if row[14].strip() else None
location = create_location(tidied_name, row[10], row[12], self._user, row[8], row[9], row[11], latitude, longitude)
return location.id
2 changes: 1 addition & 1 deletion src/naturerec_model/data_exchange/status_import_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def import_ratings(self):
"""
self._read_csv_rows()
for row in self._rows:
species_id = self.create_species(row[1], row[0])
species_id = self.create_species(row[1], row[0], None)
rating_id = self._create_rating(row[2], row[3])
start = datetime.datetime.strptime(row[5], SpeciesStatusRating.IMPORT_DATE_FORMAT).date()
end = datetime.datetime.strptime(row[6], SpeciesStatusRating.IMPORT_DATE_FORMAT).date() \
Expand Down
1 change: 1 addition & 0 deletions src/naturerec_model/logic/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def location_species_report(from_date, to_date, location_id, category_id):

# Construct the query
sql_query = f"SELECT sp.Name AS 'Species', " \
f"IFNULL( sp.Scientific_Name, '' ) AS 'Scientific Name', " \
f"COUNT( sp.Id ) AS 'Sightings', " \
f"SUM( IFNULL( s.Number, 1 ) ) AS 'Total Individuals', " \
f"MIN( IFNULL( s.Number, 1 ) ) AS 'Minimum Seen', " \
Expand Down
22 changes: 14 additions & 8 deletions src/naturerec_model/logic/species.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ def _check_for_existing_records(session, category_id, name):
return [s.id for s in species]


def create_species(category_id, name, user):
def create_species(category_id, name, scientific_name, user):
"""
Create a new species for a specified category
:param category_id: Category ID
:param name: Species name
:param name: Species scientific name
:param user: Current user
:returns: An instance of the Species class for the created record
:raises ValueError: If the species is a duplicate or has an invalid name
Expand All @@ -40,12 +41,14 @@ def create_species(category_id, name, user):
with Session.begin() as session:
# There is a check constraint to prevent duplicates in the Python model but the pre-existing database
# does not have that constraint so explicitly check for duplicates before adding a new record
tidied = " ".join(name.split()).title() if name else None
if len(_check_for_existing_records(session, category_id, tidied)):
tidied_name = " ".join(name.split()).title() if name else None
tidied_scientific_name = " ".join(scientific_name.split()).title() if scientific_name else None
if len(_check_for_existing_records(session, category_id, tidied_name)):
raise ValueError("Duplicate category found")

species = Species(categoryId=category_id,
name=tidied,
name=tidied_name,
scientific_name=tidied_scientific_name,
created_by=user.id,
updated_by=user.id,
date_created=dt.utcnow(),
Expand All @@ -58,13 +61,14 @@ def create_species(category_id, name, user):
return species


def update_species(species_id, category_id, name, user):
def update_species(species_id, category_id, name, scientific_name, user):
"""
Update an existing species
:param species_id: ID of the species record to updated
:param category_id: Category ID
:param name: Species name
:param name: Species scientific name
:param user: Current user
:returns: An instance of the Species class for the updated record
:raises ValueError: If the species is a duplicate or has an invalid name
Expand All @@ -73,8 +77,9 @@ def update_species(species_id, category_id, name, user):
with Session.begin() as session:
# There is a check constraint to prevent duplicates in the Python model but the pre-existing database
# does not have that constraint so explicitly check for duplicates before adding a new record
tidied = " ".join(name.split()).title() if name else None
species_ids = _check_for_existing_records(session, category_id, tidied)
tidied_name = " ".join(name.split()).title() if name else None
tidied_scientific_name = " ".join(scientific_name.split()).title() if scientific_name else None
species_ids = _check_for_existing_records(session, category_id, tidied_name)

# Remove the current category from the list, if it's there
if species_id in species_ids:
Expand All @@ -89,7 +94,8 @@ def update_species(species_id, category_id, name, user):
raise ValueError("Species not found")

species.categoryId = category_id
species.name = tidied
species.name = tidied_name
species.scientific_name = tidied_scientific_name
species.updated_by = user.id
species.date_updated = dt.utcnow()
except IntegrityError as e:
Expand Down
1 change: 1 addition & 0 deletions src/naturerec_model/model/sighting.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def with_young_name(self):
def csv_columns(self):
return [
self.species.name,
self.species.scientific_name,
self.species.category.name,
self.number,
self.gender_name,
Expand Down
2 changes: 2 additions & 0 deletions src/naturerec_model/model/species.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Species(Base):
categoryId = Column(Integer, ForeignKey("Categories.id"), nullable=False)
#: Species name
name = Column(String, nullable=False, unique=True)
#: Scientific (Latin) name for the species
scientific_name = Column(String, nullable=True)
#: Audit columns
created_by = Column(Integer, nullable=False)
updated_by = Column(Integer, nullable=False)
Expand Down
4 changes: 3 additions & 1 deletion src/naturerec_web/reports/reports_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ def _render_species_report_page(from_date=None, to_date=None, location_id=None,
# Get the location and species details and use them to construct a sub-title
location = get_location(location_id)
species = get_species(species_id)
print(f"LATIN = {species.scientific_name}")
scientific_name = f" ({species.scientific_name})" if species.scientific_name != None else ""
subtitle = f"Location: {location.name}\n" \
f"Species: {species.name} " \
f"Species: {species.name}{scientific_name}\n" \
f"From: {from_date_string} " \
f"To: {to_date_string}"

Expand Down
Loading

0 comments on commit 92a6bd9

Please sign in to comment.