Skip to content

Commit

Permalink
Improve validation workflow (#4233)
Browse files Browse the repository at this point in the history
* Fix and improve validation workflow

* User taxon group id from current taxa when editing
  • Loading branch information
dimasciput committed Sep 9, 2024
1 parent afed0f8 commit ba5add1
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 60 deletions.
16 changes: 8 additions & 8 deletions bims/api_views/taxon.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ def post(self, request, *args):
get_vernacular=True
)
elif taxon_name and rank:
if rank.lower() == 'species' and parent and parent.rank.lower() == 'genus':
if parent.canonical_name not in taxon_name:
taxon_name = parent.canonical_name + ' ' + taxon_name
elif rank.lower() == 'subspecies' and parent and parent.rank.lower() == 'species':
species_name = parent.species_name
if species_name not in taxon_name:
taxon_name = species_name + ' ' + taxon_name
taxon_name = taxon_name.strip()
try:
taxonomy, created = Taxonomy.objects.get_or_create(
scientific_name=taxon_name,
Expand Down Expand Up @@ -549,14 +557,6 @@ def get_taxa_by_parameters(request):
taxongrouptaxonomy__is_validated=False,
taxongrouptaxonomy__taxongroup__in=taxon_group_ids
)

rejected_taxa = list(TaxonomyUpdateProposal.objects.filter(
taxon_group_id__in=taxon_group_ids,
status='rejected'
).values_list('original_taxonomy_id', flat=True))

if rejected_taxa:
taxon_list = taxon_list.exclude(id__in=rejected_taxa)
else:
taxon_list = taxon_list.filter(
taxongrouptaxonomy__is_validated=True,
Expand Down
8 changes: 7 additions & 1 deletion bims/api_views/taxon_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def create_taxon_proposal(
accepted_taxonomy = accepted_taxonomy[0]

additional_data = taxon.additional_data
if not additional_data:
additional_data = {}
additional_data_to_check = [
'Taxonomic Comments',
'Conservation Comments',
Expand All @@ -54,9 +56,13 @@ def create_taxon_proposal(
additional_data[additional_key] = data.get(additional_key)

canonical_name = data.get('canonical_name', taxon.canonical_name)
if taxon.is_species:
if taxon.rank.lower() == 'species':
if taxon.genus_name not in canonical_name:
canonical_name = taxon.genus_name + ' ' + canonical_name
elif taxon.rank.lower() == 'subspecies':
if taxon.full_species_name not in canonical_name:
canonical_name = taxon.full_species_name + ' ' + canonical_name
canonical_name = canonical_name.strip()

proposal, created = TaxonomyUpdateProposal.objects.get_or_create(
original_taxonomy=taxon,
Expand Down
10 changes: 10 additions & 0 deletions bims/models/taxonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,14 @@ def genus_name(self):
def sub_genus_name(self):
return self.get_taxon_rank_name(TaxonomicRank.SUBGENUS.name)

@cached_property
def full_species_name(self):
genus_name = self.get_taxon_rank_name(TaxonomicRank.GENUS.name)
species_name = self.species_name
if genus_name not in species_name:
return genus_name + ' ' + species_name
return species_name

@property
def species_name(self):
return self.get_taxon_rank_name(TaxonomicRank.SPECIES.name)
Expand All @@ -371,6 +379,8 @@ def variety_name(self):

@property
def taxon_name(self):
if self.rank.lower() == 'subspecies':
return self.canonical_name.split(self.full_species_name)[-1].strip()
if self.is_species and self.genus_name:
return self.canonical_name.split(self.genus_name)[-1].strip()
return self.canonical_name
Expand Down
164 changes: 115 additions & 49 deletions bims/models/taxonomy_update_proposal.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from django.contrib.auth import get_user_model
from django.db import models
from django.conf import settings
Expand Down Expand Up @@ -254,11 +256,7 @@ def approve(self, reviewer: settings.AUTH_USER_MODEL):
)
top_level_taxon_group = self.taxon_group.get_top_level_parent()

if self.taxon_group_under_review:
self.taxon_group_under_review = top_level_taxon_group
self.save()

if top_level_taxon_group != self.taxon_group:
if top_level_taxon_group != self.taxon_group_under_review:
parent_expert = list(top_level_taxon_group.experts.values_list(
'email', flat=True
))
Expand All @@ -272,59 +270,127 @@ def approve(self, reviewer: settings.AUTH_USER_MODEL):
from_email = settings.DEFAULT_FROM_EMAIL
message = (f"Dear Validator,\n\nThe taxon '{self.original_taxonomy.canonical_name}' "
f"has been validated by the current expert.\n\nIt now requires "
f"validation to be added to the taxon group '{self.taxon_group.name}'.")
f"validation to be added to the taxon group '{top_level_taxon_group.name}'.")
send_mail_notification.delay(
subject,
message,
from_email,
recipients
)

# Only top level experts can approve data
if top_level_taxon_group.experts.filter(
id=reviewer.id
).exists() or reviewer.is_superuser:
fields_to_update = [
'scientific_name',
'canonical_name',
'legacy_canonical_name',
'rank',
'taxonomic_status',
'endemism',
'iucn_status',
'accepted_taxonomy',
'parent',
'tags',
'biographic_distributions',
'additional_data',
'vernacular_names',
'origin']
for field in fields_to_update:
if field == 'tags':
self.original_taxonomy.tags.clear()
self.original_taxonomy.tags.set(getattr(self, field).all())
elif field == 'biographic_distributions':
self.original_taxonomy.biographic_distributions.clear()
self.original_taxonomy.biographic_distributions.set(
getattr(self, field).all())
elif field == 'vernacular_names':
self.original_taxonomy.vernacular_names.clear()
self.original_taxonomy.vernacular_names.set(
getattr(self, field).all())
else:
setattr(
self.original_taxonomy,
field, getattr(self, field))
self.original_taxonomy.hierarchical_data = {}
self.original_taxonomy.save()
self.status = 'approved'
self.taxon_group_under_review = top_level_taxon_group
self.save()
else:
# Only top level experts can approve data
if top_level_taxon_group.experts.filter(
id=reviewer.id
).exists() or reviewer.is_superuser:
fields_to_update = [
'scientific_name',
'canonical_name',
'legacy_canonical_name',
'rank',
'taxonomic_status',
'endemism',
'iucn_status',
'accepted_taxonomy',
'parent',
'tags',
'biographic_distributions',
'additional_data',
'vernacular_names',
'origin']
for field in fields_to_update:
if field == 'tags':
self.original_taxonomy.tags.clear()
self.original_taxonomy.tags.set(getattr(self, field).all())
elif field == 'biographic_distributions':
self.original_taxonomy.biographic_distributions.clear()
self.original_taxonomy.biographic_distributions.set(
getattr(self, field).all())
elif field == 'vernacular_names':
self.original_taxonomy.vernacular_names.clear()
self.original_taxonomy.vernacular_names.set(
getattr(self, field).all())
else:
setattr(
self.original_taxonomy,
field, getattr(self, field))
self.original_taxonomy.hierarchical_data = {}
self.original_taxonomy.save()
self.status = 'approved'
self.save()

self.validate_taxon(
self.taxon_group,
self.original_taxonomy
)

self.send_success_emails(reviewer)
return

self.validate_taxon(
self.taxon_group,
self.original_taxonomy
def send_success_emails(self, reviewer, comments: str = "",):
"""
Send notification emails to the collector and staff upon successful validation.
Args:
reviewer (User, optional): User reviewing the taxon
comments (str, optional): Comments regarding the approval.
"""
current_site = get_current_domain()
from_email = settings.DEFAULT_FROM_EMAIL

# Email to the collector
if self.collector_user:
submission_type = 'Addition' if self.new_data else 'Update'
collector_subject = f'[{current_site}] Taxon {submission_type} Submission Approved'
collector_message = (
f"Dear {self.collector_user.get_full_name()},\n\n"
f"Your submission for the taxon '{self.canonical_name}' "
f"has been successfully approved by "
f"the experts at {current_site}.\n\n"
)
return
send_mail_notification.delay(
collector_subject,
collector_message,
from_email,
[self.collector_user.email]
)

# Email to staff and experts
staff_subject = (
f'[{current_site}] Taxon '
f'{"Addition" if self.new_data else "Update"} Submission Approved'
)

# Gather all expert emails
recipients = []
experts = self.taxon_group.get_all_experts()
for expert in experts:
if expert.email not in recipients:
recipients.append(expert.email)
superusers = list(
get_user_model().objects.filter(
is_superuser=True).values_list('email', flat=True)
)
recipients = list(set(superusers + recipients))

staff_message = (
f"Dear Staff/Expert,\n\n"
f"The taxon '{self.canonical_name}' has been successfully validated and "
f"approved at {current_site}.\n\n"
f"Submission Details:\n"
f"Taxon Name: {self.canonical_name}\n\n"
f"Taxon Group: {self.taxon_group.name}\n\n"
f"Approved by: {reviewer.get_full_name()}\n"
f"Date: {datetime.now().strftime('%Y-%m-%d')}\n\n"
)

send_mail_notification.delay(
staff_subject,
staff_message,
from_email,
recipients
)


class TaxonomyUpdateReviewer(models.Model):
Expand Down
10 changes: 9 additions & 1 deletion bims/serializers/taxon_detail_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ def get_species_group(self, obj: Taxonomy):
return ''

def get_subspecies(self, obj: Taxonomy):
return obj.sub_species_name
sub_species_name = obj.sub_species_name
if sub_species_name:
genus_name = obj.genus_name
if genus_name:
sub_species_name = sub_species_name.replace(genus_name, '', 1).strip()
species_name = self.get_species(obj)
if species_name:
sub_species_name = sub_species_name.replace(species_name, '', 1).strip()
return sub_species_name

def get_subgenus(self, obj: Taxonomy):
return obj.sub_genus_name
14 changes: 14 additions & 0 deletions bims/serializers/taxon_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,28 @@ def get_taxon_group(self, obj: Taxonomy):
taxon_group_id = self.context.get('taxon_group_id', None)
if taxon_group_id:
taxon_group = TaxonGroup.objects.get(id=taxon_group_id)
if not TaxonGroupTaxonomy.objects.filter(
taxongroup_id=taxon_group_id,
taxonomy=obj
).exists():
children = taxon_group.get_all_children()
children_ids = [child.id for child in children]
taxon_group_taxa = TaxonGroupTaxonomy.objects.filter(
taxongroup_id__in=children_ids,
taxonomy=obj
)
if taxon_group_taxa.exists():
taxon_group = taxon_group_taxa.first().taxongroup
return {
'id': taxon_group.id,
'logo': taxon_group.logo.name,
'name': taxon_group.name
}
taxonomy_obj = self.taxonomy_obj(obj)
taxon_module = taxonomy_obj.taxongroup_set.first()
if taxon_module:
return {
'id': taxon_module.id,
'logo': taxon_module.logo.name,
'name': taxon_module.name
}
Expand Down
2 changes: 1 addition & 1 deletion bims/static/js/taxa_management/taxa_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export const taxaManagement = (() => {
if (!taxaData) return false;
let data = taxaData.find(taxon => taxon.id === $(this).parent().data('id'));
if (data) {
window.location.href = `/taxonomy/edit/${selectedTaxonGroup}/${data.id}/?next=${encodeURIComponent(location.href)}`;
window.location.href = `/taxonomy/edit/${data.taxon_group.id}/${data.id}/?next=${encodeURIComponent(location.href)}`;
}
});

Expand Down
1 change: 1 addition & 0 deletions bims/views/edit_taxon_view.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db import transaction
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.views.generic import UpdateView
Expand Down

0 comments on commit ba5add1

Please sign in to comment.