Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Added 'supports gender' category flag

Revision ID: 2cdd34308ad3
Revises: b8960906cfcb
Create Date: 2025-02-20 09:27:58.112087

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '2cdd34308ad3'
down_revision = 'b8960906cfcb'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column('Categories', sa.Column('Supports_Gender', sa.Integer, nullable=False, server_default="1"))


def downgrade() -> None:
op.drop_column('Categories', 'Supports_Gender')
9 changes: 6 additions & 3 deletions alembic/versions/5ce1dfff9cd4_add_rbac_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,22 @@ def upgrade() -> None:
user_ids = [user.id for user in users]
user_ids.sort()

# Identify the "default" user ID, accounting for the fact that there may not be any users yet
user_id = user_ids[0] if len(user_ids) > 0 else 0

# Create the roles, using the first user as the creator
for name in ["Administrator", "Reporter", "Reader"]:
session.add(Role(name=name,
created_by=user_ids[0],
updated_by=user_ids[0],
created_by=user_id,
updated_by=user_id,
date_created=datetime.now(),
date_updated=datetime.now()))

# Apply the admin role to existing users (avoids loss of functionality due to access rights)
admin_role = session.query(Role).filter(Role.name=="Administrator").first()
for user_id in user_ids:
bind.execute(f"INSERT INTO UserRoles ( user_id, role_id, created_by, date_created ) "
f"VALUES ( {user_id}, {admin_role.id}, {user_ids[0]}, '{str(datetime.now())}')")
f"VALUES ( {user_id}, {admin_role.id}, {user_id}, '{str(datetime.now())}')")
session.commit()


Expand Down
8 changes: 8 additions & 0 deletions alembic/versions/version_history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
| File | Revision | Down |
| ------------------------------------------------ | ------------ | ------------ |
| 5a2e355711a1_initial_migration | 5a2e355711a1 | - |
| 560b0df5ff2a_add_user_audit_columns | 560b0df5ff2a | 5a2e355711a1 |
| e95d3cceae06_add_date_audit_columns | e95d3cceae06 | 560b0df5ff2a |
| 5ce1dfff9cd4_add_rbac_support | 5ce1dfff9cd4 | e95d3cceae06 |
| b8960906cfcb_add_species_scientific_name | b8960906cfcb | 5ce1dfff9cd4 |
| 2cdd34308ad3_added_supports_gender_category_flag | 2cdd34308ad3 | b8960906cfcb |
10 changes: 10 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh -f

export PROJECT_ROOT=$( cd "$( dirname "$0" )" && pwd )
. $PROJECT_ROOT/venv/bin/activate
export PYTHONPATH=$PROJECT_ROOT/src

echo "Project root = $PROJECT_ROOT"
echo "Python Path = $PYTHONPATH"

python -m unittest
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def create_species(self, category_name, species_name, scientific_name):
try:
category = get_category(tidied_category_name)
except ValueError:
category = create_category(tidied_category_name, self._user)
category = create_category(tidied_category_name, True, self._user)
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
Expand Down
8 changes: 6 additions & 2 deletions src/naturerec_model/logic/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ def _check_for_existing_records(session, name):
return [category.id for category in categories]


def create_category(name, user):
def create_category(name, supports_gender, user):
"""
Create a new species category

:param name: Category name
:param supports_gender: True if the category supports entry of gender against sightings
:param user: Current user
:returns: An instance of the Category class for the created record
:raises ValueError: If the specified name is None, an empty string or consists solely of whitespace
Expand All @@ -41,6 +42,7 @@ def create_category(name, user):
raise ValueError("Duplicate category found")

category = Category(name=tidied,
supports_gender=supports_gender,
created_by=user.id,
updated_by=user.id,
date_created=dt.utcnow(),
Expand All @@ -52,12 +54,13 @@ def create_category(name, user):
return category


def update_category(category_id, name, user):
def update_category(category_id, name, supports_gender, user):
"""
Update an existing species category

:param category_id: ID for the category record to update
:param name: Category name
:param supports_gender: True if the category supports entry of gender against sightings
:param user: Current user
:returns: An instance of the Category class for the updated record
:raises ValueError: If the specified name is None, an empty string or consists solely of whitespace
Expand Down Expand Up @@ -85,6 +88,7 @@ def update_category(category_id, name, user):
raise ValueError("Category not found")

category.name = tidied
category.supports_gender = supports_gender
category.updated_by = user.id
category.date_updated = dt.utcnow()
except IntegrityError as e:
Expand Down
6 changes: 4 additions & 2 deletions src/naturerec_model/model/category.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, UniqueConstraint, CheckConstraint, DateTime
from sqlalchemy import Column, Integer, String, UniqueConstraint, CheckConstraint, DateTime, Boolean
from sqlalchemy.orm import relationship
from .base import Base

Expand All @@ -13,6 +13,8 @@ class Category(Base):
id = Column(Integer, primary_key=True)
#: Category name
name = Column(String, nullable=False, unique=True)
#: Whether the category supports entry of gender via the UI
supports_gender = Column(Integer, nullable=False, default=1)
#: Audit columns
created_by = Column(Integer, nullable=False)
updated_by = Column(Integer, nullable=False)
Expand All @@ -29,4 +31,4 @@ class Category(Base):
CheckConstraint("LENGTH(TRIM(name)) > 0"))

def __repr__(self):
return f"{type(self).__name__}(Id={self.id!r}, name={self.name!r})"
return f"{type(self).__name__}(Id={self.id!r}, name={self.name!r}, supports_gender={self.supports_gender!r})"
8 changes: 6 additions & 2 deletions src/naturerec_web/categories/categories_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,15 @@ def edit(category_id):
:return: The HTML for the category entry page or a response object redirecting to the category list page
"""
if request.method == "POST":
# The "supports gender" flag is a check box and is only POSTed if it's checked. If it's unchecked, it
# won't appear in the form data
supports_gender = True if "supports_gender" in request.form else False

try:
if category_id:
_ = update_category(category_id, request.form["name"], current_user)
_ = update_category(category_id, request.form["name"], supports_gender, current_user)
else:
_ = create_category(request.form["name"], current_user)
_ = create_category(request.form["name"], supports_gender, current_user)
return redirect("/categories/list")
except ValueError as e:
return _render_category_editing_page(category_id, e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<table class="striped">
<thead>
<th>Name</th>
<th>Supports Gender Recording</th>
{% if edit_enabled %}
<th></th>
<th></th>
Expand All @@ -11,6 +12,13 @@
{% for category in categories %}
<tr>
<td>{{ category.name }}</td>
<td>
{% if category.supports_gender %}
<span>Yes</span>
{% else %}
<span>No</span>
{% endif %}
</td>
{% if edit_enabled %}
<td>
<a href="{{ url_for('categories.edit', category_id=category.id) }}">
Expand Down
6 changes: 6 additions & 0 deletions src/naturerec_web/categories/templates/categories/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
{% set title = "Edit Category" %}
{% set submit_title = "Save Category" %}
{% set name = category.name %}
{% set supports_gender = "checked" if category.supports_gender else "" %}
{% else %}
{% set title = "Add Category" %}
{% set submit_title = "Add Category" %}
{% set name = "" %}
{% set supports_gender = "" %}
{% endif %}

{% extends "layout.html" %}
Expand All @@ -21,6 +23,10 @@ <h1>{{ title }}</h1>
<input class="form-control" name="name" placeholder="Category name e.g. Insects" value="{{ name }}"
required>
</div>
<div class="form-check mb-4">
<input class="form-check-input" type="checkbox" value="" name="supports_gender" id="supports_gender" {{ supports_gender }}/>
<label class="form-check-label" for="supports_gender">Display gender recording options for this category</label>
</div>
<div class="button-bar">
<button type="button" class="btn btn-light">
<a href="{{ url_for('categories.list_all') }}">Cancel</a>
Expand Down
22 changes: 20 additions & 2 deletions src/naturerec_web/sightings/sightings_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import datetime
import html
from flask import Blueprint, render_template, request, session, redirect, abort
from flask import Blueprint, render_template, request, session, redirect, abort, jsonify
from flask_login import login_required, current_user
from naturerec_model.logic import list_sightings, get_sighting, create_sighting, update_sighting, delete_sighting
from naturerec_model.logic import list_locations
from naturerec_model.logic import list_categories
from naturerec_model.logic import list_categories, get_category
from naturerec_model.logic import list_species
from naturerec_model.model import Gender, Sighting
from naturerec_model.data_exchange import SightingsImportHelper
Expand Down Expand Up @@ -179,6 +179,24 @@ def list_species_for_category(category_id, selected_species_id):
species_id=selected_species_id)


@sightings_bp.route("/supports_gender/<int:category_id>")
@login_required
def get_supports_gender_flag_for_category(category_id):
"""
Return a JSON object indicating whether the specified category supports gender reporting

:param category_id: ID for the category for which to return the flag
:return: JSON object containing the value of the 'supports gender' flag
"""
try:
category = get_category(category_id)
supports_gender = category.supports_gender
except ValueError:
supports_gender = False

return jsonify(supports_gender=supports_gender)


@sightings_bp.route("/edit", defaults={"sighting_id": None}, methods=["GET", "POST"])
@sightings_bp.route("/edit/<int:sighting_id>", methods=["GET", "POST"])
@login_required
Expand Down
26 changes: 19 additions & 7 deletions src/naturerec_web/sightings/templates/sightings/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,25 @@ <h1>{{ title }}</h1>
<script type="text/javascript" src="{{ url_for( 'static', filename='script/category-selection.js') }}"></script>
<script type="text/javascript">
function show_hide_default_fields() {
const show_categories = ["Birds", "Mammals"];
var category_name = $("#category").find(":selected").text().trim();
if (show_categories.indexOf(category_name) > -1) {
$("#defaulted-fields").show();
} else {
$("#defaulted-fields").hide();
}
var category_id = $("#category").val();
$.ajax({
url: "/sightings/supports_gender/" + convert_none_to_zero(category_id),
type: "GET",
cache: false,
dataType: "json",
success: function(data, _textStatus, _jqXHR) {
console.log("JSON = " + show_defaulted_fields);
var show_defaulted_fields = data.supports_gender;
if (show_defaulted_fields) {
$("#defaulted-fields").show();
} else {
$("#defaulted-fields").hide();
}
},
error: function(_jqXHR, textStatus, _errorThrown) {
// Sink the error, for now
}
});
}

$(document).ready(function() {
Expand Down
Loading