From 20463e30b60147b84d71dd08611d00720a06865b Mon Sep 17 00:00:00 2001 From: Dave Walker Date: Mon, 19 Jan 2026 10:43:12 +0000 Subject: [PATCH] Dependabot updates --- docker/Dockerfile | 4 +- reports/requirements.txt | 57 ++--- requirements.txt | 4 +- setup.py | 2 +- src/naturerec_model/logic/categories.py | 8 +- src/naturerec_model/logic/job_statuses.py | 8 +- src/naturerec_model/logic/locations.py | 8 +- src/naturerec_model/logic/sightings.py | 8 +- src/naturerec_model/logic/species.py | 8 +- .../logic/species_status_ratings.py | 6 +- src/naturerec_model/logic/status_ratings.py | 8 +- src/naturerec_model/logic/status_schemes.py | 8 +- src/naturerec_model/logic/users.py | 6 +- src/naturerec_web/__init__.py | 198 +++++++++--------- src/naturerec_web/templates/menu.html | 110 +++++----- 15 files changed, 216 insertions(+), 227 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 335f204..5cda188 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,12 @@ FROM python:3.10-slim-bullseye AS runtime -COPY naturerecorderpy-1.19.0.0 /opt/naturerecorderpy +COPY naturerecorderpy-1.20.0.0 /opt/naturerecorderpy WORKDIR /opt/naturerecorderpy RUN apt-get update -y RUN pip install -r requirements.txt -RUN pip install nature_recorder-1.19.0-py3-none-any.whl +RUN pip install nature_recorder-1.20.0-py3-none-any.whl ENV NATURE_RECORDER_DATA_FOLDER=/var/opt/naturerecorderpy ENV NATURE_RECORDER_DB=/var/opt/naturerecorderpy/naturerecorder.db diff --git a/reports/requirements.txt b/reports/requirements.txt index 920e708..9c7c01b 100644 --- a/reports/requirements.txt +++ b/reports/requirements.txt @@ -1,24 +1,21 @@ -ansicolors==1.1.8 -anyio==4.11.0 +anyio==4.12.0 appnope==0.1.4 argon2-cffi==25.1.0 argon2-cffi-bindings==25.1.0 -arrow==1.3.0 -asttokens==3.0.0 +arrow==1.4.0 +asttokens==3.0.1 async-lru==2.0.5 attrs==25.4.0 babel==2.17.0 -beautifulsoup4==4.14.2 -bleach==6.2.0 -certifi==2025.10.5 +beautifulsoup4==4.14.3 +bleach==6.3.0 +certifi==2025.11.12 cffi==2.0.0 charset-normalizer==3.4.4 -click==8.3.0 comm==0.2.3 -debugpy==1.8.17 +debugpy==1.8.18 decorator==5.2.1 defusedxml==0.7.1 -entrypoints==0.4 executing==2.2.1 fastjsonschema==2.21.2 fqdn==1.5.1 @@ -26,8 +23,8 @@ h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.11 -ipykernel==7.0.1 -ipython==9.6.0 +ipykernel==7.1.0 +ipython==9.8.0 ipython_pygments_lexers==1.1.1 isoduration==20.11.0 jedi==0.19.2 @@ -38,41 +35,38 @@ jsonschema==4.25.1 jsonschema-specifications==2025.9.1 jupyter-events==0.12.0 jupyter-lsp==2.3.0 -jupyter_client==8.6.3 -jupyter_core==5.8.1 +jupyter_client==8.7.0 +jupyter_core==5.9.1 jupyter_server==2.17.0 jupyter_server_terminals==0.5.3 -jupyterlab==4.4.9 +jupyterlab==4.5.0 jupyterlab_pygments==0.3.0 -jupyterlab_server==2.27.3 -lark==1.3.0 +jupyterlab_server==2.28.0 +lark==1.3.1 MarkupSafe==3.0.3 -matplotlib-inline==0.1.7 +matplotlib-inline==0.2.1 mistune==3.1.4 +narwhals==2.14.0 nbclient==0.10.2 nbconvert==7.16.6 nbformat==5.10.4 nest-asyncio==1.6.0 -notebook==7.4.7 +notebook==7.5.0 notebook_shim==0.2.4 -numpy==2.3.3 packaging==25.0 -pandas==2.3.3 pandocfilters==1.5.1 -papermill==2.6.0 parso==0.8.5 pexpect==4.9.0 -platformdirs==4.5.0 +platformdirs==4.5.1 prometheus_client==0.23.1 prompt_toolkit==3.0.52 -psutil==7.1.0 +psutil==7.1.3 ptyprocess==0.7.0 pure_eval==0.2.3 pycparser==2.23 Pygments==2.19.2 python-dateutil==2.9.0.post0 python-json-logger==4.0.0 -pytz==2025.2 PyYAML==6.0.3 pyzmq==27.1.0 referencing==0.37.0 @@ -80,26 +74,21 @@ requests==2.32.5 rfc3339-validator==0.1.4 rfc3986-validator==0.1.1 rfc3987-syntax==1.1.0 -rpds-py==0.27.1 +rpds-py==0.30.0 Send2Trash==1.8.3 setuptools==80.9.0 six==1.17.0 -sniffio==1.3.1 soupsieve==2.8 -sqlparse==0.5.3 stack-data==0.6.3 -tenacity==9.1.2 terminado==0.18.1 tinycss2==1.4.0 -tornado==6.5.2 -tqdm==4.67.1 +tornado==6.5.3 traitlets==5.14.3 -types-python-dateutil==2.9.0.20251008 typing_extensions==4.15.0 tzdata==2025.2 uri-template==1.3.0 -urllib3==2.5.0 +urllib3==2.6.1 wcwidth==0.2.14 -webcolors==24.11.1 +webcolors==25.10.0 webencodings==0.5.1 websocket-client==1.9.0 diff --git a/requirements.txt b/requirements.txt index 727cd67..e7ca34a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,9 +48,9 @@ trio==0.30.0 trio-websocket==0.12.2 typing_extensions==4.14.1 tzdata==2025.2 -urllib3==2.5.0 +urllib3 urllib3-secure-extra==0.1.0 websocket-client==1.8.0 -Werkzeug==3.1.3 +Werkzeug wsproto==1.2.0 WTForms==3.2.1 diff --git a/setup.py b/setup.py index d0bfb8d..a5b8ff0 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.19.0", + version="1.20.0", description="Wildlife sightings database", packages=setuptools.find_packages("src"), include_package_data=True, diff --git a/src/naturerec_model/logic/categories.py b/src/naturerec_model/logic/categories.py index a78ba1a..e4625f0 100644 --- a/src/naturerec_model/logic/categories.py +++ b/src/naturerec_model/logic/categories.py @@ -4,7 +4,7 @@ from functools import singledispatch import sqlalchemy as db -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError, NoResultFound from ..model import Session, Category, Species from .naming import tidy_string, Casing @@ -45,8 +45,8 @@ def create_category(name, supports_gender, user): supports_gender=supports_gender, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(category) except IntegrityError as e: raise ValueError("Invalid or duplicate category name") from e @@ -90,7 +90,7 @@ def update_category(category_id, name, supports_gender, user): category.name = tidied category.supports_gender = supports_gender category.updated_by = user.id - category.date_updated = dt.utcnow() + category.date_updated = dt.now(UTC) except IntegrityError as e: raise ValueError("Invalid or duplicate category name") from e diff --git a/src/naturerec_model/logic/job_statuses.py b/src/naturerec_model/logic/job_statuses.py index bb402a3..a0c2282 100644 --- a/src/naturerec_model/logic/job_statuses.py +++ b/src/naturerec_model/logic/job_statuses.py @@ -3,7 +3,7 @@ """ import sqlalchemy as db -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError from ..model import Session, JobStatus @@ -25,8 +25,8 @@ def create_job_status(name, parameters, start, user): start_date=start, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(job_status) except IntegrityError as e: raise ValueError("Invalid job status properties") from e @@ -53,7 +53,7 @@ def complete_job_status(job_status_id, end, error, user): job_status.end_date = end job_status.error = error job_status.updated_by = user.id - job_status.date_updated = dt.utcnow() + job_status.date_updated = dt.now(UTC) except IntegrityError as e: raise ValueError("Invalid job status properties") from e diff --git a/src/naturerec_model/logic/locations.py b/src/naturerec_model/logic/locations.py index 5a1a497..dcf47c9 100644 --- a/src/naturerec_model/logic/locations.py +++ b/src/naturerec_model/logic/locations.py @@ -6,7 +6,7 @@ import pandas as pd import pgeocode import pycountry -from datetime import datetime as dt +from datetime import datetime as dt, UTC from functools import singledispatch from sqlalchemy.exc import IntegrityError, NoResultFound from ..model import Session, Location, Sighting @@ -57,8 +57,8 @@ def create_location(name, county, country, user, address=None, city=None, postco longitude=longitude, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(location) except IntegrityError as e: raise ValueError("Invalid location properties or duplicate name") from e @@ -111,7 +111,7 @@ def update_location(location_id, name, county, country, user, address=None, city location.latitude = latitude location.longitude = longitude location.updated_by = user.id - location.date_updated = dt.utcnow() + location.date_updated = dt.now(UTC) except IntegrityError as e: raise ValueError("Invalid location properties or duplicate name") from e diff --git a/src/naturerec_model/logic/sightings.py b/src/naturerec_model/logic/sightings.py index 31c746f..8cf686f 100644 --- a/src/naturerec_model/logic/sightings.py +++ b/src/naturerec_model/logic/sightings.py @@ -3,7 +3,7 @@ """ import sqlalchemy as db -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError from ..model import Session, Sighting, Species @@ -58,8 +58,8 @@ def create_sighting(location_id, species_id, date, number, gender, with_young, n notes=notes, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(sighting) except IntegrityError as e: raise ValueError("Invalid sighting properties") from e @@ -109,7 +109,7 @@ def update_sighting(sighting_id, location_id, species_id, date, number, gender, sighting.withYoung = with_young sighting.notes = notes sighting.updated_by = user.id - sighting.date_updated = dt.utcnow() + sighting.date_updated = dt.now(UTC) session.add(sighting) except IntegrityError as e: raise ValueError("Invalid sighting properties") from e diff --git a/src/naturerec_model/logic/species.py b/src/naturerec_model/logic/species.py index 6629b74..3f24820 100644 --- a/src/naturerec_model/logic/species.py +++ b/src/naturerec_model/logic/species.py @@ -4,7 +4,7 @@ from functools import singledispatch import sqlalchemy as db -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError, NoResultFound from ..model import Session, Species, Sighting, SpeciesStatusRating from .naming import tidy_string, Casing @@ -52,8 +52,8 @@ def create_species(category_id, name, scientific_name, user): scientific_name=tidied_scientific_name, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(species) except IntegrityError as e: raise ValueError("Missing category or invalid or duplicate species name") \ @@ -98,7 +98,7 @@ def update_species(species_id, category_id, name, scientific_name, user): species.name = tidied_name species.scientific_name = tidied_scientific_name species.updated_by = user.id - species.date_updated = dt.utcnow() + species.date_updated = dt.now(UTC) except IntegrityError as e: raise ValueError("Missing category or invalid or duplicate species name") \ from e diff --git a/src/naturerec_model/logic/species_status_ratings.py b/src/naturerec_model/logic/species_status_ratings.py index a8b80ca..e19859b 100644 --- a/src/naturerec_model/logic/species_status_ratings.py +++ b/src/naturerec_model/logic/species_status_ratings.py @@ -55,8 +55,8 @@ def create_species_status_rating(species_id, status_rating_id, region, start, us end_date=end, created_by=user.id, updated_by=user.id, - date_created=datetime.datetime.utcnow(), - date_updated=datetime.datetime.utcnow()) + date_created=datetime.datetime.now(datetime.UTC), + date_updated=datetime.datetime.now(datetime.UTC)) session.add(species_rating) except IntegrityError as e: raise ValueError("Invalid species conservation status rating properties") from e @@ -78,7 +78,7 @@ def close_species_status_rating(species_status_rating_id, user): rating.end_date = datetime.datetime.today().date() rating.updated_by = user.id - rating.date_updated = datetime.datetime.utcnow() + rating.date_updated = datetime.datetime.now(datetime.UTC) def get_species_status_rating(species_status_rating_id): diff --git a/src/naturerec_model/logic/status_ratings.py b/src/naturerec_model/logic/status_ratings.py index e991f99..3f9c92c 100644 --- a/src/naturerec_model/logic/status_ratings.py +++ b/src/naturerec_model/logic/status_ratings.py @@ -2,7 +2,7 @@ Conservation status scheme rating business logic """ -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError from ..model import Session, StatusRating, SpeciesStatusRating @@ -47,8 +47,8 @@ def create_status_rating(status_scheme_id, name, user): name=tidied, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(scheme) except IntegrityError as e: raise ValueError("Invalid or duplicate conservation status scheme rating") from e @@ -89,7 +89,7 @@ def update_status_rating(status_rating_id, name, user): rating.name = tidied rating.updated_by = user.id - rating.date_updated = dt.utcnow() + rating.date_updated = dt.now(UTC) except IntegrityError as e: raise ValueError("Invalid or duplicate conservation status scheme rating") from e diff --git a/src/naturerec_model/logic/status_schemes.py b/src/naturerec_model/logic/status_schemes.py index d02f561..f646a5d 100644 --- a/src/naturerec_model/logic/status_schemes.py +++ b/src/naturerec_model/logic/status_schemes.py @@ -4,7 +4,7 @@ from functools import singledispatch import sqlalchemy as db -from datetime import datetime as dt +from datetime import datetime as dt, UTC from sqlalchemy.exc import IntegrityError, NoResultFound from ..model import Session, StatusScheme, SpeciesStatusRating @@ -43,8 +43,8 @@ def create_status_scheme(name, user): scheme = StatusScheme(name=tidied, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(scheme) except IntegrityError as e: raise ValueError("Invalid or duplicate conservation status scheme name") from e @@ -85,7 +85,7 @@ def update_status_scheme(status_scheme_id, name, user): scheme.name = tidied scheme.updated_by = user.id - scheme.date_updated=dt.utcnow() + scheme.date_updated=dt.now(UTC) except IntegrityError as e: raise ValueError("Invalid or duplicate conservation status scheme name") from e diff --git a/src/naturerec_model/logic/users.py b/src/naturerec_model/logic/users.py index 4fe2f50..3eee103 100644 --- a/src/naturerec_model/logic/users.py +++ b/src/naturerec_model/logic/users.py @@ -5,7 +5,7 @@ import hashlib import os import base64 -from datetime import datetime as dt +from datetime import datetime as dt, UTC from functools import singledispatch from sqlalchemy.exc import IntegrityError, NoResultFound from ..model import Session, User @@ -60,8 +60,8 @@ def create_user(username, password, user): password=b64password, created_by=user.id, updated_by=user.id, - date_created=dt.utcnow(), - date_updated=dt.utcnow()) + date_created=dt.now(UTC), + date_updated=dt.now(UTC)) session.add(user) except IntegrityError as e: raise ValueError("Invalid or duplicate user") from e diff --git a/src/naturerec_web/__init__.py b/src/naturerec_web/__init__.py index 6a6ae25..4e52058 100644 --- a/src/naturerec_web/__init__.py +++ b/src/naturerec_web/__init__.py @@ -1,99 +1,99 @@ -import os -from flask import Flask -from flask_login import LoginManager -from flask_wtf.csrf import CSRFProtect -from .home import home_bp -from .sightings import sightings_bp -from .export import export_bp -from .locations import locations_bp -from .categories import categories_bp -from .species import species_bp -from .status import status_bp -from .species_ratings import species_ratings_bp -from .jobs import jobs_bp -from .auth import auth_bp, unauthorised, has_roles -from naturerec_model.logic import get_user - - -csrf = CSRFProtect() - - -def create_app(environment="production"): - """ - Flask Application Factory - - :return: An instance of the Flask application - """ - app = Flask("Nature Recorder", - static_folder=os.path.join(os.path.dirname(__file__), "static"), - template_folder=os.path.join(os.path.dirname(__file__), "templates")) - - config_object = f"naturerec_web.config.{'ProductionConfig' if environment == 'production' else 'DevelopmentConfig'}" - app.config.from_object(config_object) - app.config.update( - SESSION_COOKIE_SAMESITE="Strict", - SESSION_COOKIE_HTTPONLY=True, - PERMANENT_SESSION_LIFETIME=600 - ) - - # Register the blueprints - app.secret_key = os.environ["SECRET_KEY"] - app.register_blueprint(home_bp, url_prefix="") - app.register_blueprint(sightings_bp, url_prefix='/sightings') - app.register_blueprint(export_bp, url_prefix='/export') - app.register_blueprint(locations_bp, url_prefix='/locations') - app.register_blueprint(categories_bp, url_prefix='/categories') - app.register_blueprint(species_bp, url_prefix='/species') - app.register_blueprint(status_bp, url_prefix='/status') - app.register_blueprint(species_ratings_bp, url_prefix='/species_ratings') - app.register_blueprint(jobs_bp, url_prefix='/jobs') - app.register_blueprint(auth_bp, url_prefix='/auth') - - # Register the 401 Unathorised error handler - app.register_error_handler(401, unauthorised) - - # Create the flask-login user manager - login_manager = LoginManager() - login_manager.login_view = 'auth.login' - login_manager.init_app(app) - - # Enable CSRF protection - csrf.init_app(app) - - @login_manager.user_loader - def load_user(user_id): - """ - Method that returns a user given their ID - - :param user_id: ID of the user to retrieve - :return: Instance of the User class for the specified user - """ - return get_user(int(user_id)) - - @app.context_processor - def inject_roles(): - """ - Make role membership available to all templates to allow the layout view to configure the menu - bar based on those permissions - """ - is_admin = has_roles(["Administrator"]) - is_reporter = has_roles(["Reporter"]) - return dict(is_admin=is_admin, is_reporter=is_reporter) - - @app.after_request - def add_security_headers(response): - """ - Enforce security-related response headers - - :param response: Response object - :return: Response object with headers set - """ - # response.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none'; form-action 'self'" - response.headers["X-Frame-Options"] = "DENY" - response.headers["X-Content-Type-Options"] = "nosniff" - response.headers["X-XSS-Protection"] = "1; mode=block" - return response - - return app - - +import os +from flask import Flask +from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect +from .home import home_bp +from .sightings import sightings_bp +from .export import export_bp +from .locations import locations_bp +from .categories import categories_bp +from .species import species_bp +from .status import status_bp +from .species_ratings import species_ratings_bp +from .jobs import jobs_bp +from .auth import auth_bp, unauthorised, has_roles +from naturerec_model.logic import get_user + + +csrf = CSRFProtect() + + +def create_app(environment="production"): + """ + Flask Application Factory + + :return: An instance of the Flask application + """ + app = Flask("Nature Recorder", + static_folder=os.path.join(os.path.dirname(__file__), "static"), + template_folder=os.path.join(os.path.dirname(__file__), "templates")) + + config_object = f"naturerec_web.config.{'ProductionConfig' if environment == 'production' else 'DevelopmentConfig'}" + app.config.from_object(config_object) + app.config.update( + SESSION_COOKIE_SAMESITE="Strict", + SESSION_COOKIE_HTTPONLY=True, + PERMANENT_SESSION_LIFETIME=600 + ) + + # Register the blueprints + app.secret_key = os.environ["SECRET_KEY"] + app.register_blueprint(home_bp, url_prefix="") + app.register_blueprint(sightings_bp, url_prefix='/sightings') + app.register_blueprint(export_bp, url_prefix='/export') + app.register_blueprint(locations_bp, url_prefix='/locations') + app.register_blueprint(categories_bp, url_prefix='/categories') + app.register_blueprint(species_bp, url_prefix='/species') + app.register_blueprint(status_bp, url_prefix='/status') + app.register_blueprint(species_ratings_bp, url_prefix='/species_ratings') + app.register_blueprint(jobs_bp, url_prefix='/jobs') + app.register_blueprint(auth_bp, url_prefix='/auth') + + # Register the 401 Unathorised error handler + app.register_error_handler(401, unauthorised) + + # Create the flask-login user manager + login_manager = LoginManager() + login_manager.login_view = 'auth.login' + login_manager.init_app(app) + + # Enable CSRF protection + csrf.init_app(app) + + @login_manager.user_loader + def load_user(user_id): + """ + Method that returns a user given their ID + + :param user_id: ID of the user to retrieve + :return: Instance of the User class for the specified user + """ + return get_user(int(user_id)) + + @app.context_processor + def inject_roles(): + """ + Make role membership available to all templates to allow the layout view to configure the menu + bar based on those permissions + """ + is_admin = has_roles(["Administrator"]) + is_reporter = has_roles(["Reporter"]) + return dict(is_admin=is_admin, is_reporter=is_reporter) + + @app.after_request + def add_security_headers(response): + """ + Enforce security-related response headers + + :param response: Response object + :return: Response object with headers set + """ + # response.headers["Content-Security-Policy"] = "default-src 'self'; frame-ancestors 'none'; form-action 'self'" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-XSS-Protection"] = "1; mode=block" + return response + + return app + + diff --git a/src/naturerec_web/templates/menu.html b/src/naturerec_web/templates/menu.html index 297978c..407a150 100644 --- a/src/naturerec_web/templates/menu.html +++ b/src/naturerec_web/templates/menu.html @@ -1,56 +1,56 @@ -