Skip to content

Instantly share code, notes, and snippets.

@aurthurm
Last active June 10, 2025 11:52
Show Gist options
  • Save aurthurm/368371649508d68a95099bac4a9c6ff1 to your computer and use it in GitHub Desktop.
Save aurthurm/368371649508d68a95099bac4a9c6ff1 to your computer and use it in GitHub Desktop.
Fix invalid categories and reassign analysis
from nmrl.lims.scripts import setup_script_environment
from bika.lims import api
import transaction
from senaite.core.catalog import SETUP_CATALOG
import re
def create_category(parent, title):
category = api.create(parent, "AnalysisCategory", title=title)
category.reindexObject()
return category
def search_category(title):
brains = api.search({"portal_type": "AnalysisCategory", "title": title}, SETUP_CATALOG)
if not brains:
return None
return [api.get_object(brain) for brain in brains][0]
def validate_categories(app):
print("πŸ” Scanning for valid and invalid Analysis Categories...")
ac_brains = api.search({"portal_type": "AnalysisCategory"}, SETUP_CATALOG)
all_cats = [api.get_object(brain) for brain in ac_brains]
print("Total categories found: {}".format(len(all_cats)))
valid_cats = [c for c in all_cats if re.match(r'^analysiscategory-\d+$', c.getId())]
invalid_cats = [c for c in all_cats if not re.match(r'^analysiscategory-\d+$', c.getId())]
# Senaite application-level configured categories (often includes wrong ones)
# Senaite setup categories
setup_cats = app.senaite.setup.analysiscategories
setup_cats = [setup_cats[cid] for cid in setup_cats]
setup_cat_ids = [c.getId() for c in setup_cats]
print("\nπŸ”§ Categories in `app.senaite.setup.analysiscategories` ({}): {}".format(
len(setup_cat_ids),
setup_cat_ids
))
# βœ… Assert all valid categories are present in setup
missing_from_setup = [c for c in valid_cats if c.getId() not in setup_cat_ids]
if missing_from_setup:
print("\n🚫 ERROR: Valid categories missing from Senaite setup:")
for c in missing_from_setup:
print(" - {}: {}".format(c.getId(), c.Title()))
raise AssertionError("Some valid categories are missing from `app.senaite.setup.analysiscategories`.")
# ⚠️ Add setup-level invalids to invalid_cats
invalid_cats = list(set(invalid_cats + [
c for c in setup_cats
if not re.match(r'^analysiscategory-\d+$', c.getId())
]))
print("βœ… Valid categories: {} -> {}".format(len(valid_cats), [c.getId() for c in valid_cats]))
print("⚠️ Invalid categories: {} -> {}".format(len(invalid_cats), [c.getId() for c in invalid_cats]))
return valid_cats, invalid_cats
def fix_services(app):
print("πŸ” Fixing services ...")
valid_cats, invalid_cats = validate_categories(app)
service_brains = api.search({"portal_type": "AnalysisService"})
analysis_services = [api.get_object(x) for x in service_brains]
fixed = []
for invalid_cat in invalid_cats:
title = invalid_cat.Title()
services = list(
filter(lambda a: a.getCategory().getId() == invalid_cat.getId(), analysis_services)
)
print(" - AnalysisServices using this category: {}".format(len(services)))
# get a valid category with the same title or creare a new one
valid_cat = search_category(title)
if not valid_cat:
print(" -> Creating new valid category for: {}".format(title))
valid_cat = create_category(valid_cats[0].aq_parent, title=title)
fixed.append(valid_cat)
for s in services:
s.setCategory(valid_cat)
s.reindexObject()
transaction.commit()
print("βœ… Done fixing services: {} -> {}".format(len(fixed), [c.getId() for c in fixed]))
return fixed, valid_cats, invalid_cats
def fix_(app):
print("πŸ” Fixing categories related issues ...")
fixed, valid_, invalid_cats = fix_services(app)
valid_cats = fixed + valid_
reassigned_count = 0
batch_counter = 0
for invalid_cat in invalid_cats:
title = invalid_cat.Title()
print("\nInvalid ID found: {} ({})".format(invalid_cat.getId(), title))
match = next((c for c in valid_cats if c.Title() == title), None)
analyses = api.search({"portal_type": "Analysis", "getCategoryUID": invalid_cat.UID()})
print(" - Analyses using this category: {}".format(len(analyses)))
if match:
print(" -> Found valid category with same title: {}".format(match.getId()))
else:
# this must not happen ideally since all must be fixed by fix_services at the top
print(" -> [Not expected] Creating new valid category for: {}".format(title))
match = create_category(valid_cats[0].aq_parent, title=title)
valid_cats.append(match)
for brain in analyses:
a = api.get_object(brain)
a.setCategory(match)
a.reindexObject()
reassigned_count += 1
batch_counter += 1
if batch_counter >= 10:
transaction.commit()
print(" πŸ”„ Intermediate commit after 10 updates")
batch_counter = 0
print("\nπŸ” Reassigned {} analyses from invalid categories.".format(reassigned_count))
# Clean up invalid categories
print("\n🧹 Deleting {} invalid categories...".format(len(invalid_cats)))
for cat in invalid_cats:
print(" - Deleting: {} ({})".format(cat.getId(), cat.Title()))
api.delete(cat, False)
transaction.commit()
print("βœ… Cleanup and reassignment complete.\n")
return valid_cats, invalid_cats
def main(app):
setup_script_environment(app)
fix_(app)
if __name__ == "__main__":
main(app)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment