From b209ef10c7e2fba82bb4745ae26a1f618efde74b Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Wed, 24 Sep 2025 12:53:26 -0700 Subject: [PATCH 01/11] initial cleanup of the toppage --- viewer/app.py | 3 + viewer/pages/assembly.py | 576 ++++++++++++++++++ viewer/pages/route.py | 2 + viewer/pages/toppage.py | 553 ----------------- .../{ => assembly}/assemble_module.html | 0 viewer/templates/components_table.html | 7 - viewer/templates/scan_table.html | 7 - viewer/templates/sync_statistics.html | 18 + viewer/templates/toppage.html | 20 +- 9 files changed, 613 insertions(+), 573 deletions(-) create mode 100644 viewer/pages/assembly.py rename viewer/templates/{ => assembly}/assemble_module.html (100%) create mode 100644 viewer/templates/sync_statistics.html diff --git a/viewer/app.py b/viewer/app.py index f39dede1c..e4bdd8c6f 100755 --- a/viewer/app.py +++ b/viewer/app.py @@ -29,6 +29,7 @@ from functions.common import ( # noqa: E402 socketio, ) from pages.route import ( # noqa: E402 + assembly_api, checkout_api, component_api, config_api, @@ -127,6 +128,8 @@ with app.app_context(): ### Pages ## Top Page app.register_blueprint(toppage_api) + ## Assembly + app.register_blueprint(assembly_api) ## Dashboard app.register_blueprint(dashboard_bp, url_prefix="/dashboard") ## Component/Result Page diff --git a/viewer/pages/assembly.py b/viewer/pages/assembly.py new file mode 100644 index 000000000..af0811984 --- /dev/null +++ b/viewer/pages/assembly.py @@ -0,0 +1,576 @@ +from __future__ import annotations + +import concurrent.futures +import logging +import pprint +import traceback + +import itkdb +import itksn +from flask import ( + Blueprint, + flash, + redirect, + render_template, + request, + session, + url_for, +) +from functions.common import ( + SN_tokens, + SN_typeinfo, + delim_SN, + get_pd_client, + initPage, + userdb, +) +from functions.itkpd_interface.module import ModuleRegistration +from functions.workers import download_worker + +logger = logging.getLogger("localdb") + +assembly_api = Blueprint("assembly_api", __name__) + + +@assembly_api.route("/assemble_module", methods=["GET", "POST"]) +def assemble_module(): + """Handle module assembly workflow including component validation and registration.""" + initPage() + + if not session.get("logged_in", False): + return render_template("401.html") + + table_docs = {"components": {}} + table_docs["page"] = "module" + table_docs["title"] = "Assemble a new Module on ITkPD" + + stage = request.form.get("stage", "input_module") + moduleinfo = request.form.getlist("moduleinfo") + tripletinfo = request.form.getlist("tripletinfo") + + check_serialNum = "" + module_serial_number = "" + text = "" + config = {} + lookup_table = {} + currentLocation = {} + typeinfos = {} + + if not get_pd_client(): + message = "No valid ITkPD client found. Please login." + return redirect( + url_for( + "user_api.itkdb_authenticate", + message=message, + redirect=url_for("assembly_api.assemble_module"), + ) + ) + + pd_client = get_pd_client() + + user = pd_client.get("getUser", json={"userIdentity": pd_client.user.identity}) + user_institutions = [ + element["name"] for element in (user.get("institutions") or []) + ] + + if session["institution"] not in user_institutions: + _institutions_string = "', '".join(user_institutions) + message = f"LocalDB user institution '{session['institution']}' does not match any any of the authenticated institutions from the authenticated ITkPD user: '{_institutions_string}'. This can be fixed by either using a different production database user account, or changing the institution for the currently logged-in localDB user at the top right." + return redirect( + url_for( + "user_api.itkdb_authenticate", + message=message, + redirect=url_for("assembly_api.assemble_module"), + ) + ) + + if pd_client: + try: + module_config = userdb["QC.module.types"].find_one() + lookup_table = ModuleRegistration(pd_client).make_table(module_config) + except Exception as e: + logger.error(str(e)) + logger.error(traceback.format_exc()) + stage = "download_moduletypes" + + institution = ModuleRegistration(pd_client).user_institutions(pd_client.user) + + logger.info(f"user's institution: {institution}") + + if stage != "input_module": + # moduleinfo[0]: bare_module_SNs + # moduleinfo[1]: PCB_SN + + bare_serial_numbers = [moduleinfo[0]] + pcb_serial_number = moduleinfo[1] + carrier_serial_number = moduleinfo[2] + + assembly_doc = {} + + try: + assembly_doc.update( + { + bare_serial_numbers[0]: check_component( + bare_serial_numbers[0], + "BARE_MODULE", + "Bare Module", + institution, + ) + } + ) + assembly_doc.update( + { + pcb_serial_number: check_component( + pcb_serial_number, "PCB", "Module PCB", institution + ) + } + ) + + bare1_tokens = SN_tokens(bare_serial_numbers[0]) + pcb_tokens = SN_tokens(pcb_serial_number) + + module_type_candidates = { + typename: data.get("XX") + data.get("YY") + for typename, data in lookup_table.get("MODULE_TYPES").items() + if data.get("BARE_MODULE") == bare1_tokens.get("XXYY") + and data.get("PCB") == pcb_tokens.get("XXYY") + } + + module_xxyy_candidates = [ + xxyy for typename, xxyy in module_type_candidates.items() + ] + module_typename_candidates = [ + name for name, xxyy in module_type_candidates.items() + ] + + if len(module_xxyy_candidates) == 0: + msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" are not compatible to deduce a valid Module Type' + raise ValueError(msg) + + if len(module_xxyy_candidates) > 1: + logger.info( + f'Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" do not deduce a unique Module Type: candidates = {module_xxyy_candidates}' + ) + logger.info("Attempting to resolve it using BareModule property...") + + pd_bare_cpt = pd_client.get( + "getComponent", json={"component": bare_serial_numbers[0]} + ) + + bare_subcpts = pd_bare_cpt.get("children") + + sensor_sn = None + for subcpt in bare_subcpts: + if subcpt.get("componentType").get("code") == "SENSOR_TILE": + subcpt_info = subcpt.get("component") + if not subcpt_info: + continue + + sensor_sn = subcpt.get("component").get("serialNumber") + + # if not sensor_sn: + # msg = f"Bare Module {bare_serial_numbers[0]} does not have its sensor tile!" + # raise ValueError(msg) + + if sensor_sn: + logger.info( + "Sensor_tile is found on the bare module. Using SENSOR_TILE properties for module SN resolution" + ) + + sensor_xx = sensor_sn[3:5] + + if sensor_xx == "PG": + module_xxyy_candidates = [ + xxyy for xxyy in module_xxyy_candidates if "PG" in xxyy + ] + module_typename_candidates = [ + name + for name, xxyy in module_type_candidates.items() + if "PG" in xxyy + ] + else: + module_xxyy_candidates = [ + xxyy for xxyy in module_xxyy_candidates if "PI" in xxyy + ] + module_typename_candidates = [ + name + for name, xxyy in module_type_candidates.items() + if "PI" in xxyy + ] + + else: + logger.info( + "No sensor_tile is found on the bare module. As a fallback, attempt to use the PCB_DESIGN_VERSION property for module SN resolution" + ) + + layer_info = itksn.parse( + pcb_serial_number.encode("utf-8") + ).identifier.FE_chip_version + + if "OS" in layer_info: + module_xxyy_candidates = [ + xxyy for xxyy in module_xxyy_candidates if "PG" in xxyy + ] + module_typename_candidates = [ + name + for name, xxyy in module_type_candidates.items() + if "PG" in xxyy + ] + elif "IS" in layer_info: + module_xxyy_candidates = [ + xxyy for xxyy in module_xxyy_candidates if "PI" in xxyy + ] + module_typename_candidates = [ + name + for name, xxyy in module_type_candidates.items() + if "PI" in xxyy + ] + else: + msg = f"Can't determine the layer from PCB_DESIGN_VERSION {layer_info} without a sensor tile!" + raise ValueError(msg) + + if len(module_xxyy_candidates) > 1: + msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" do not deduce a unique Module Type: candidates = {module_xxyy_candidates}' + raise ValueError(msg) + + module_xxyy = module_xxyy_candidates[0] + module_typename = module_typename_candidates[0] + + institution_digit = { + "INFN Genoa": 0, + "Institut de Fisica d'Altes Energies (IFAE)": 1, + "University of Oslo, Department of Physics": 2, + "INFN Milano": 3, + "Lawrence Berkeley National Lab - pixel modules": 4, + } + + if tripletinfo: + if bare1_tokens.get("1st") != "3": + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + bare1_tokens.get("1st"), + str(institution_digit[session["institution"]]), + "0", + pcb_tokens.get("number")[-4:], + ] + ) + else: + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + "4", + str(institution_digit[session["institution"]]), + "0", + pcb_tokens.get("number")[-4:], + ] + ) + # check for duplicate + try: + pd_client.get( + "getComponent", json={"component": module_serial_number} + ) + except itkdb.exceptions.BadRequest as err: + data = err.response.json() + if not data.get("uuAppErrorMap", {}).get( + "ucl-itkpd-main/getComponent/componentDoesNotExist" + ): + raise err + else: + msg = f"Found duplicate serial number {module_serial_number}, assigning 1st SN digit to '5'." + logger.warning(msg) + flash(msg, "warning") + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + "5", + str(institution_digit[session["institution"]]), + "0", + pcb_tokens.get("number")[-4:], + ] + ) + + else: + """ + # Serial Number First Digit Meaning + + ## Yamashita Flex + + | 1st Digit | Meaning | + |-----------|----------------------------------------------------| + | 0 | RD53A FE chip | + | 1 | ITkPix_v1 FE chip | + | 2 | ITkPix_v1.1 FE chip | + | 3 | ITkPix_v2 FE chip | + | 9 | No chip | + + ## Non-Yamashita Flex + + | 1st Digit | Meaning | + |-----------|----------------------------------------------------| + | 0 | RD53A FE chip | + | 1 | ITkPix_v1 FE chip | + | 2 | ITkPix_v1.1 FE chip | + | 3 | ITkPix_v2 FE chip | + | 4 | Production, ITkPix_v2 FE chip | + | 5 | Production, ITkPix_v2 FE chip (if duplicated SN) | + | 9 | No chip | + """ + if ( + "Yamashita" + in itksn.parse( + pcb_serial_number.encode("utf-8") + ).identifier.PCB_manufacturer + ) or (bare1_tokens.get("1st") != "3"): + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + bare1_tokens.get("1st"), + pcb_tokens.get("number")[-6:], + ] + ) + else: + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + "4", + pcb_tokens.get("number")[-6:], + ] + ) + # check for duplicate + try: + pd_client.get( + "getComponent", json={"component": module_serial_number} + ) + except itkdb.exceptions.BadRequest as err: + data = err.response.json() + if not data.get("uuAppErrorMap", {}).get( + "ucl-itkpd-main/getComponent/componentDoesNotExist" + ): + raise err + else: + msg = f"Found duplicate serial number {module_serial_number}, assigning 1st SN digit to '5'." + logger.warning(msg) + flash(msg, "warning") + module_serial_number = "".join( + [ + "20", + "U", + module_xxyy, + "5", + pcb_tokens.get("number")[-6:], + ] + ) + + moduleinfo += [module_typename] + + except Exception as e: + typeinfos = { + "BARE_MODULE": [SN_typeinfo(bare, 1) for bare in bare_serial_numbers], + "PCB": SN_typeinfo(pcb_serial_number, 1), + "MODULE": SN_typeinfo(module_serial_number, 1), + } + + text = str(e) + logger.error(traceback.format_exc()) + stage = "confirm" + return render_template( + "assembly/assemble_module.html", + table_docs=table_docs, + stage=stage, + text=text, + moduleInfo=moduleinfo, + lookup_table=lookup_table, + module_serial_number=module_serial_number, + institution=institution, + tripletInfo=tripletinfo, + check_serialNum=check_serialNum, + currentLocation=currentLocation, + typeinfos=typeinfos, + ) + + if tripletinfo: + # Triplet Check + bare_serial_numbers += tripletinfo + try: + assembly_doc.update( + { + bare_serial_numbers[1]: check_component( + bare_serial_numbers[1], + "BARE_MODULE", + "Bare Module", + institution, + ) + } + ) + assembly_doc.update( + { + bare_serial_numbers[2]: check_component( + bare_serial_numbers[2], + "BARE_MODULE", + "Bare Module", + institution, + ) + } + ) + + bare2_tokens = SN_tokens(bare_serial_numbers[1]) + bare3_tokens = SN_tokens(bare_serial_numbers[2]) + + if bare1_tokens.get("XXYY") != bare2_tokens.get("XXYY"): + msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Types ["{bare1_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}" ] are not compatible to deduce a valid Module Type' + raise ValueError(msg) + + if bare1_tokens.get("XXYY") != bare3_tokens.get("XXYY"): + msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Types ["{bare1_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}" ] are not compatible to deduce a valid Module Type' + raise ValueError(msg) + + except Exception as e: + text = str(e) + logger.error(traceback.format_exc()) + stage = "confirm" + + typeinfos = { + "BARE_MODULE": [ + SN_typeinfo(bare, 1) for bare in bare_serial_numbers + ], + "PCB": SN_typeinfo(pcb_serial_number, 1), + "MODULE": SN_typeinfo(module_serial_number, 1), + } + + return render_template( + "assembly/assemble_module.html", + table_docs=table_docs, + stage=stage, + text=text, + moduleInfo=moduleinfo, + lookup_table=lookup_table, + module_serial_number=module_serial_number, + institution=institution, + tripletInfo=tripletinfo, + check_serialNum=check_serialNum, + currentLocation=currentLocation, + typeinfos=typeinfos, + ) + + logger.info(f"Synthesized Module Serial Number = {module_serial_number}") + + # logger.info( 'assembly_doc = ' + pprint.pformat( assembly_doc ) ) + + current_locations = [doc.get("location") for sn, doc in assembly_doc.items()] + + location = current_locations[0] + + if not all(loc == current_locations[0] for loc in current_locations): + text = "The current location of sub-components are not all identical!" + stage = "confirm" + return render_template( + "assembly/assemble_module.html", + table_docs=table_docs, + stage=stage, + text=text, + moduleInfo=moduleinfo, + lookup_table=lookup_table, + module_serial_number=module_serial_number, + institution=institution, + tripletInfo=tripletinfo, + check_serialNum=check_serialNum, + currentLocation=currentLocation, + typeinfos=typeinfos, + ) + + config = { + "type": module_typename, + "FECHIP": bare1_tokens.get("1st"), + "child": { + "BARE_MODULE": bare_serial_numbers, + "PCB": pcb_serial_number, + "CARRIER": ( + carrier_serial_number if carrier_serial_number != "" else None + ), + }, + "serialNumber": module_serial_number, + } + + # Check the module serial number if available + check_serialNum = ModuleRegistration(pd_client).check_exist( + module_serial_number + ) + + if check_serialNum == "2": + pass + else: + stage = "confirm" + + typeinfos = { + "BARE_MODULE": [SN_typeinfo(bare, 1) for bare in bare_serial_numbers], + "PCB": SN_typeinfo(pcb_serial_number, 1), + "MODULE": SN_typeinfo(module_serial_number, 1), + } + + if stage == "complete": + ModuleRegistration(pd_client).register_Module(config, lookup_table, location) + + with concurrent.futures.ThreadPoolExecutor() as executor: + executor.submit( + download_worker, module_serial_number, None, "MODULE/ASSEMBLY" + ) + + return render_template( + "assembly/assemble_module.html", + table_docs=table_docs, + stage=stage, + text=text, + moduleInfo=moduleinfo, + lookup_table=lookup_table, + module_serial_number=module_serial_number, + institution=institution, + tripletInfo=tripletinfo, + check_serialNum=check_serialNum, + currentLocation=currentLocation, + typeinfos=typeinfos, + ) + + +def check_component(serialNumber, componentType, componentTypeName, user_institution): + """Validate a component exists in ITkPD and matches expected type and location.""" + pd_client = get_pd_client() + + try: + componentInfo = ModuleRegistration(pd_client).get_component(serialNumber) + except Exception as exc: + logger.error(traceback.format_exc()) + text = f"{componentTypeName} {serialNumber} does not exist in ITkPD..." + raise RuntimeError(text) from exc + + checked_component_type = componentInfo.get("componentType")["code"] + checked_component_location = componentInfo["currentLocation"]["code"] + if checked_component_type != componentType: + text = f"{componentTypeName} {serialNumber} component type is not compatible (is a {checked_component_type})..." + raise ValueError(text) + if checked_component_location not in user_institution: + text = f"{componentTypeName} {serialNumber} location is not your institution ({checked_component_location}))..." + raise ValueError(text) + + doc = { + "location": checked_component_location, + "typeInfo": SN_typeinfo(serialNumber), + "delim_SN": delim_SN(serialNumber), + } + + logger.info( + f"check_component(): serialNumber = {serialNumber}, doc = " + + pprint.pformat(doc) + ) + + return doc diff --git a/viewer/pages/route.py b/viewer/pages/route.py index 37ab4c408..54ad544d1 100644 --- a/viewer/pages/route.py +++ b/viewer/pages/route.py @@ -1,6 +1,7 @@ from __future__ import annotations from pages.api import rest_api +from pages.assembly import assembly_api from pages.checkout import checkout_api from pages.component import component_api from pages.config import config_api @@ -21,6 +22,7 @@ from pages.toppage import toppage_api from pages.user import user_api __all__ = ( + "assembly_api", "checkout_api", "component_api", "config_api", diff --git a/viewer/pages/toppage.py b/viewer/pages/toppage.py index f0900b5bd..fff41e706 100644 --- a/viewer/pages/toppage.py +++ b/viewer/pages/toppage.py @@ -1,6 +1,5 @@ from __future__ import annotations -import concurrent.futures import contextlib import logging import os @@ -10,36 +9,26 @@ import traceback from datetime import datetime import arrow -import itkdb -import itksn from bson.objectid import ObjectId from flask import ( Blueprint, - flash, - redirect, render_template, request, session, - url_for, ) from functions.common import ( TMP_DIR, - SN_tokens, SN_typeinfo, args, client, createScanCache, - delim_SN, emit_message, format_messages, - get_pd_client, getScanSummary, initPage, localdb, userdb, ) -from functions.itkpd_interface.module import ModuleRegistration -from functions.workers import download_worker logger = logging.getLogger("localdb") @@ -695,548 +684,6 @@ def show_scans(): # Show test run list -@toppage_api.route("/assemble_module", methods=["GET", "POST"]) -def assemble_module(): - initPage() - - if not session.get("logged_in", False): - return render_template("401.html") - - table_docs = {"components": {}} - table_docs["page"] = "module" - table_docs["title"] = "Assemble a new Module on ITkPD" - - stage = request.form.get("stage", "input_module") - moduleinfo = request.form.getlist("moduleinfo") - tripletinfo = request.form.getlist("tripletinfo") - - check_serialNum = "" - module_serial_number = "" - text = "" - config = {} - lookup_table = {} - currentLocation = {} - typeinfos = {} - - if not get_pd_client(): - message = "No valid ITkPD client found. Please login." - return redirect( - url_for( - "user_api.itkdb_authenticate", - message=message, - redirect=url_for("toppage_api.assemble_module"), - ) - ) - - pd_client = get_pd_client() - - user = pd_client.get("getUser", json={"userIdentity": pd_client.user.identity}) - user_institutions = [ - element["name"] for element in (user.get("institutions") or []) - ] - - if session["institution"] not in user_institutions: - _institutions_string = "', '".join(user_institutions) - message = f"LocalDB user institution '{session['institution']}' does not match any any of the authenticated institutions from the authenticated ITkPD user: '{_institutions_string}'. This can be fixed by either using a different production database user account, or changing the institution for the currently logged-in localDB user at the top right." - return redirect( - url_for( - "user_api.itkdb_authenticate", - message=message, - redirect=url_for("toppage_api.assemble_module"), - ) - ) - - if pd_client: - try: - module_config = userdb["QC.module.types"].find_one() - lookup_table = ModuleRegistration(pd_client).make_table(module_config) - except Exception as e: - logger.error(str(e)) - logger.error(traceback.format_exc()) - stage = "download_moduletypes" - - institution = ModuleRegistration(pd_client).user_institutions(pd_client.user) - - logger.info(f"user's institution: {institution}") - - if stage != "input_module": - # moduleinfo[0]: bare_module_SNs - # moduleinfo[1]: PCB_SN - - bare_serial_numbers = [moduleinfo[0]] - pcb_serial_number = moduleinfo[1] - carrier_serial_number = moduleinfo[2] - - assembly_doc = {} - - try: - assembly_doc.update( - { - bare_serial_numbers[0]: check_component( - bare_serial_numbers[0], - "BARE_MODULE", - "Bare Module", - institution, - ) - } - ) - assembly_doc.update( - { - pcb_serial_number: check_component( - pcb_serial_number, "PCB", "Module PCB", institution - ) - } - ) - - bare1_tokens = SN_tokens(bare_serial_numbers[0]) - pcb_tokens = SN_tokens(pcb_serial_number) - - module_type_candidates = { - typename: data.get("XX") + data.get("YY") - for typename, data in lookup_table.get("MODULE_TYPES").items() - if data.get("BARE_MODULE") == bare1_tokens.get("XXYY") - and data.get("PCB") == pcb_tokens.get("XXYY") - } - - module_xxyy_candidates = [ - xxyy for typename, xxyy in module_type_candidates.items() - ] - module_typename_candidates = [ - name for name, xxyy in module_type_candidates.items() - ] - - if len(module_xxyy_candidates) == 0: - msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" are not compatible to deduce a valid Module Type' - raise ValueError(msg) - - if len(module_xxyy_candidates) > 1: - logger.info( - f'Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" do not deduce a unique Module Type: candidates = {module_xxyy_candidates}' - ) - logger.info("Attempting to resolve it using BareModule property...") - - pd_bare_cpt = pd_client.get( - "getComponent", json={"component": bare_serial_numbers[0]} - ) - - bare_subcpts = pd_bare_cpt.get("children") - - sensor_sn = None - for subcpt in bare_subcpts: - if subcpt.get("componentType").get("code") == "SENSOR_TILE": - subcpt_info = subcpt.get("component") - if not subcpt_info: - continue - - sensor_sn = subcpt.get("component").get("serialNumber") - - # if not sensor_sn: - # msg = f"Bare Module {bare_serial_numbers[0]} does not have its sensor tile!" - # raise ValueError(msg) - - if sensor_sn: - logger.info( - "Sensor_tile is found on the bare module. Using SENSOR_TILE properties for module SN resolution" - ) - - sensor_xx = sensor_sn[3:5] - - if sensor_xx == "PG": - module_xxyy_candidates = [ - xxyy for xxyy in module_xxyy_candidates if "PG" in xxyy - ] - module_typename_candidates = [ - name - for name, xxyy in module_type_candidates.items() - if "PG" in xxyy - ] - else: - module_xxyy_candidates = [ - xxyy for xxyy in module_xxyy_candidates if "PI" in xxyy - ] - module_typename_candidates = [ - name - for name, xxyy in module_type_candidates.items() - if "PI" in xxyy - ] - - else: - logger.info( - "No sensor_tile is found on the bare module. As a fallback, attempt to use the PCB_DESIGN_VERSION property for module SN resolution" - ) - - layer_info = itksn.parse( - pcb_serial_number.encode("utf-8") - ).identifier.FE_chip_version - - if "OS" in layer_info: - module_xxyy_candidates = [ - xxyy for xxyy in module_xxyy_candidates if "PG" in xxyy - ] - module_typename_candidates = [ - name - for name, xxyy in module_type_candidates.items() - if "PG" in xxyy - ] - elif "IS" in layer_info: - module_xxyy_candidates = [ - xxyy for xxyy in module_xxyy_candidates if "PI" in xxyy - ] - module_typename_candidates = [ - name - for name, xxyy in module_type_candidates.items() - if "PI" in xxyy - ] - else: - msg = f"Can't determine the layer from PCB_DESIGN_VERSION {layer_info} without a sensor tile!" - raise ValueError(msg) - - if len(module_xxyy_candidates) > 1: - msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Type "{bare1_tokens.get("XXYY")}" do not deduce a unique Module Type: candidates = {module_xxyy_candidates}' - raise ValueError(msg) - - module_xxyy = module_xxyy_candidates[0] - module_typename = module_typename_candidates[0] - - institution_digit = { - "INFN Genoa": 0, - "Institut de Fisica d'Altes Energies (IFAE)": 1, - "University of Oslo, Department of Physics": 2, - "INFN Milano": 3, - "Lawrence Berkeley National Lab - pixel modules": 4, - } - - if tripletinfo: - if bare1_tokens.get("1st") != "3": - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - bare1_tokens.get("1st"), - str(institution_digit[session["institution"]]), - "0", - pcb_tokens.get("number")[-4:], - ] - ) - else: - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - "4", - str(institution_digit[session["institution"]]), - "0", - pcb_tokens.get("number")[-4:], - ] - ) - # check for duplicate - try: - pd_client.get( - "getComponent", json={"component": module_serial_number} - ) - except itkdb.exceptions.BadRequest as err: - data = err.response.json() - if not data.get("uuAppErrorMap", {}).get( - "ucl-itkpd-main/getComponent/componentDoesNotExist" - ): - raise err - else: - msg = f"Found duplicate serial number {module_serial_number}, assigning 1st SN digit to '5'." - logger.warning(msg) - flash(msg, "warning") - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - "5", - str(institution_digit[session["institution"]]), - "0", - pcb_tokens.get("number")[-4:], - ] - ) - - else: - """ - # Serial Number First Digit Meaning - - ## Yamashita Flex - - | 1st Digit | Meaning | - |-----------|----------------------------------------------------| - | 0 | RD53A FE chip | - | 1 | ITkPix_v1 FE chip | - | 2 | ITkPix_v1.1 FE chip | - | 3 | ITkPix_v2 FE chip | - | 9 | No chip | - - ## Non-Yamashita Flex - - | 1st Digit | Meaning | - |-----------|----------------------------------------------------| - | 0 | RD53A FE chip | - | 1 | ITkPix_v1 FE chip | - | 2 | ITkPix_v1.1 FE chip | - | 3 | ITkPix_v2 FE chip | - | 4 | Production, ITkPix_v2 FE chip | - | 5 | Production, ITkPix_v2 FE chip (if duplicated SN) | - | 9 | No chip | - """ - if ( - "Yamashita" - in itksn.parse( - pcb_serial_number.encode("utf-8") - ).identifier.PCB_manufacturer - ) or (bare1_tokens.get("1st") != "3"): - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - bare1_tokens.get("1st"), - pcb_tokens.get("number")[-6:], - ] - ) - else: - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - "4", - pcb_tokens.get("number")[-6:], - ] - ) - # check for duplicate - try: - pd_client.get( - "getComponent", json={"component": module_serial_number} - ) - except itkdb.exceptions.BadRequest as err: - data = err.response.json() - if not data.get("uuAppErrorMap", {}).get( - "ucl-itkpd-main/getComponent/componentDoesNotExist" - ): - raise err - else: - msg = f"Found duplicate serial number {module_serial_number}, assigning 1st SN digit to '5'." - logger.warning(msg) - flash(msg, "warning") - module_serial_number = "".join( - [ - "20", - "U", - module_xxyy, - "5", - pcb_tokens.get("number")[-6:], - ] - ) - - moduleinfo += [module_typename] - - except Exception as e: - typeinfos = { - "BARE_MODULE": [SN_typeinfo(bare, 1) for bare in bare_serial_numbers], - "PCB": SN_typeinfo(pcb_serial_number, 1), - "MODULE": SN_typeinfo(module_serial_number, 1), - } - - text = str(e) - logger.error(traceback.format_exc()) - stage = "confirm" - return render_template( - "assemble_module.html", - table_docs=table_docs, - stage=stage, - text=text, - moduleInfo=moduleinfo, - lookup_table=lookup_table, - module_serial_number=module_serial_number, - institution=institution, - tripletInfo=tripletinfo, - check_serialNum=check_serialNum, - currentLocation=currentLocation, - typeinfos=typeinfos, - ) - - if tripletinfo: - # Triplet Check - bare_serial_numbers += tripletinfo - try: - assembly_doc.update( - { - bare_serial_numbers[1]: check_component( - bare_serial_numbers[1], - "BARE_MODULE", - "Bare Module", - institution, - ) - } - ) - assembly_doc.update( - { - bare_serial_numbers[2]: check_component( - bare_serial_numbers[2], - "BARE_MODULE", - "Bare Module", - institution, - ) - } - ) - - bare2_tokens = SN_tokens(bare_serial_numbers[1]) - bare3_tokens = SN_tokens(bare_serial_numbers[2]) - - if bare1_tokens.get("XXYY") != bare2_tokens.get("XXYY"): - msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Types ["{bare1_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}" ] are not compatible to deduce a valid Module Type' - raise ValueError(msg) - - if bare1_tokens.get("XXYY") != bare3_tokens.get("XXYY"): - msg = f'ERROR: Specified PCB Type "{pcb_tokens.get("XXYY")}" and BareModule Types ["{bare1_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}", "{bare2_tokens.get("XXYY")}" ] are not compatible to deduce a valid Module Type' - raise ValueError(msg) - - except Exception as e: - text = str(e) - logger.error(traceback.format_exc()) - stage = "confirm" - - typeinfos = { - "BARE_MODULE": [ - SN_typeinfo(bare, 1) for bare in bare_serial_numbers - ], - "PCB": SN_typeinfo(pcb_serial_number, 1), - "MODULE": SN_typeinfo(module_serial_number, 1), - } - - return render_template( - "assemble_module.html", - table_docs=table_docs, - stage=stage, - text=text, - moduleInfo=moduleinfo, - lookup_table=lookup_table, - module_serial_number=module_serial_number, - institution=institution, - tripletInfo=tripletinfo, - check_serialNum=check_serialNum, - currentLocation=currentLocation, - typeinfos=typeinfos, - ) - - logger.info(f"Synthesized Module Serial Number = {module_serial_number}") - - # logger.info( 'assembly_doc = ' + pprint.pformat( assembly_doc ) ) - - current_locations = [doc.get("location") for sn, doc in assembly_doc.items()] - - location = current_locations[0] - - if not all(loc == current_locations[0] for loc in current_locations): - text = "The current location of sub-components are not all identical!" - stage = "confirm" - return render_template( - "assemble_module.html", - table_docs=table_docs, - stage=stage, - text=text, - moduleInfo=moduleinfo, - lookup_table=lookup_table, - module_serial_number=module_serial_number, - institution=institution, - tripletInfo=tripletinfo, - check_serialNum=check_serialNum, - currentLocation=currentLocation, - typeinfos=typeinfos, - ) - - config = { - "type": module_typename, - "FECHIP": bare1_tokens.get("1st"), - "child": { - "BARE_MODULE": bare_serial_numbers, - "PCB": pcb_serial_number, - "CARRIER": ( - carrier_serial_number if carrier_serial_number != "" else None - ), - }, - "serialNumber": module_serial_number, - } - - # Check the module serial number if available - check_serialNum = ModuleRegistration(pd_client).check_exist( - module_serial_number - ) - - if check_serialNum == "2": - pass - else: - stage = "confirm" - - typeinfos = { - "BARE_MODULE": [SN_typeinfo(bare, 1) for bare in bare_serial_numbers], - "PCB": SN_typeinfo(pcb_serial_number, 1), - "MODULE": SN_typeinfo(module_serial_number, 1), - } - - if stage == "complete": - ModuleRegistration(pd_client).register_Module(config, lookup_table, location) - - with concurrent.futures.ThreadPoolExecutor() as executor: - executor.submit( - download_worker, module_serial_number, None, "MODULE/ASSEMBLY" - ) - - return render_template( - "assemble_module.html", - table_docs=table_docs, - stage=stage, - text=text, - moduleInfo=moduleinfo, - lookup_table=lookup_table, - module_serial_number=module_serial_number, - institution=institution, - tripletInfo=tripletinfo, - check_serialNum=check_serialNum, - currentLocation=currentLocation, - typeinfos=typeinfos, - ) - - -def check_component(serialNumber, componentType, componentTypeName, user_institution): - pd_client = get_pd_client() - - try: - componentInfo = ModuleRegistration(pd_client).get_component(serialNumber) - except Exception as exc: - logger.error(traceback.format_exc()) - text = f"{componentTypeName} {serialNumber} does not exist in ITkPD..." - raise RuntimeError(text) from exc - - checked_component_type = componentInfo.get("componentType")["code"] - checked_component_location = componentInfo["currentLocation"]["code"] - if checked_component_type != componentType: - text = f"{componentTypeName} {serialNumber} component type is not compatible (is a {checked_component_type})..." - raise ValueError(text) - if checked_component_location not in user_institution: - text = f"{componentTypeName} {serialNumber} location is not your institution ({checked_component_location}))..." - raise ValueError(text) - - doc = { - "location": checked_component_location, - "typeInfo": SN_typeinfo(serialNumber), - "delim_SN": delim_SN(serialNumber), - } - - logger.info( - f"check_component(): serialNumber = {serialNumber}, doc = " - + pprint.pformat(doc) - ) - - return doc - - ################# ## Query Function def query_docs(Keywords, Match): diff --git a/viewer/templates/assemble_module.html b/viewer/templates/assembly/assemble_module.html similarity index 100% rename from viewer/templates/assemble_module.html rename to viewer/templates/assembly/assemble_module.html diff --git a/viewer/templates/components_table.html b/viewer/templates/components_table.html index e9b47d71a..67f0302eb 100644 --- a/viewer/templates/components_table.html +++ b/viewer/templates/components_table.html @@ -1,12 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} - -
diff --git a/viewer/templates/scan_table.html b/viewer/templates/scan_table.html index 42a009186..3d5f2a6d5 100644 --- a/viewer/templates/scan_table.html +++ b/viewer/templates/scan_table.html @@ -1,12 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} - -
diff --git a/viewer/templates/sync_statistics.html b/viewer/templates/sync_statistics.html new file mode 100644 index 000000000..e1b7e3b31 --- /dev/null +++ b/viewer/templates/sync_statistics.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+

Synchronization Statistics Error

+ + Return to Top Page +
+
+
+{% endblock %} diff --git a/viewer/templates/toppage.html b/viewer/templates/toppage.html index 2e3e6fcb9..e6a329c3f 100644 --- a/viewer/templates/toppage.html +++ b/viewer/templates/toppage.html @@ -1,10 +1,18 @@ - {% extends "layout.html" %} +{% block stylesheet %} + +{% endblock %} + {% block body %}
{% include 'parts/showMessages.html' %} @@ -119,7 +127,7 @@ {% if session['logged_in'] %}

QC Test Dashboard

-   Assemble a new Module @ ITkPD +   Assemble a new Module @ ITkPD

  Upload QC Test RAW Results to LocalDB -- GitLab From e6c7e959baf0fb2f52bc3d2fe70e0b1d715687c0 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Wed, 24 Sep 2025 15:12:08 -0700 Subject: [PATCH 02/11] consolidate more --- viewer/static/assets/css/main.css | 5 + viewer/templates/component.html | 3 - viewer/templates/components_table.html | 138 +--------------- viewer/templates/displayConfig.html | 40 ++--- .../parts/components_pagination.html | 147 ++++++++++++++++++ viewer/templates/parts/scans_pagination.html | 146 +++++++++++++++++ viewer/templates/qc/create_summaryTest.html | 12 +- viewer/templates/qc/input_qcTests.html | 26 ++-- viewer/templates/qc/upload_qcTests.html | 11 +- viewer/templates/qcTests.html | 14 +- viewer/templates/scan_table.html | 138 +--------------- viewer/templates/sitemap.html | 8 +- viewer/templates/toppage.html | 10 -- 13 files changed, 343 insertions(+), 355 deletions(-) create mode 100644 viewer/templates/parts/components_pagination.html create mode 100644 viewer/templates/parts/scans_pagination.html diff --git a/viewer/static/assets/css/main.css b/viewer/static/assets/css/main.css index 295a079f2..0ae368296 100644 --- a/viewer/static/assets/css/main.css +++ b/viewer/static/assets/css/main.css @@ -41,3 +41,8 @@ nav[aria-label="breadcrumb"] { font-size: 0.875rem; vertical-align: middle; } + +.pagination .dropdown-menu { + max-height: 100px; + overflow-y: scroll; +} diff --git a/viewer/templates/component.html b/viewer/templates/component.html index eab93cd33..d458ce845 100644 --- a/viewer/templates/component.html +++ b/viewer/templates/component.html @@ -15,9 +15,6 @@ a.data:active { color: #dc143c; } - th { - background-color: #eeeeff; - } .bg-warning th { background-color: transparent; } diff --git a/viewer/templates/components_table.html b/viewer/templates/components_table.html index 67f0302eb..4011c116d 100644 --- a/viewer/templates/components_table.html +++ b/viewer/templates/components_table.html @@ -144,143 +144,7 @@

- {% if pagingInfo["minPage"] != pagingInfo["maxPage"] %} -
-
- - - -
- - Showing page {{ pagingInfo['thisPage'] }} of {{ pagingInfo['maxPage'] }} ({{ pagingInfo['entries'] }} total results, - {{ pagingInfo['perPage'] }} per page) - -
-
-
- {% endif %} + {% include 'parts/components_pagination.html' %}
diff --git a/viewer/templates/displayConfig.html b/viewer/templates/displayConfig.html index 05346b13f..ef8bb2de8 100644 --- a/viewer/templates/displayConfig.html +++ b/viewer/templates/displayConfig.html @@ -1,24 +1,24 @@ - {% extends "layout.html" %} + +{% block stylesheet %} + +{% endblock %} {% block body %}
diff --git a/viewer/templates/parts/components_pagination.html b/viewer/templates/parts/components_pagination.html new file mode 100644 index 000000000..6fd8d0058 --- /dev/null +++ b/viewer/templates/parts/components_pagination.html @@ -0,0 +1,147 @@ +{# + Components pagination template with dropdown ellipsis + + Required parameters: + - pagingInfo: dict with 'thisPage', 'minPage', 'maxPage', 'entries', 'perPage' + - table_docs: dict with 'view', 'keywords', 'match', 'doShowAll' for URL parameters + - aria_label: string for accessibility (default: "Components") +#} +{% set aria_label = aria_label or "Components" %} + +{% if pagingInfo["minPage"] != pagingInfo["maxPage"] %} +
+
+ + + +
+ + Showing page {{ pagingInfo['thisPage'] }} of {{ pagingInfo['maxPage'] }} ({{ pagingInfo['entries'] }} total results, + {{ pagingInfo['perPage'] }} per page) + +
+
+
+{% endif %} diff --git a/viewer/templates/parts/scans_pagination.html b/viewer/templates/parts/scans_pagination.html new file mode 100644 index 000000000..a1396b964 --- /dev/null +++ b/viewer/templates/parts/scans_pagination.html @@ -0,0 +1,146 @@ +{# + Scans pagination template with dropdown ellipsis + + Required parameters: + - table_docs: dict with 'now_cnt', 'max_cnt', 'total', 'max_num', 'keywords', 'match' + - aria_label: string for accessibility (default: "Scans") +#} +{% set aria_label = aria_label or "Scans" %} + +{% if table_docs['max_cnt'] > 0 %} +
+
+ + + +
+ + Showing page {{ table_docs['now_cnt'] + 1 }} of {{ table_docs['max_cnt'] + 1 }} ({{ table_docs['total'] }} total results, + {{ table_docs['max_num'] }} per page) + +
+
+
+{% endif %} diff --git a/viewer/templates/qc/create_summaryTest.html b/viewer/templates/qc/create_summaryTest.html index 38f01deb3..fa3d47a38 100644 --- a/viewer/templates/qc/create_summaryTest.html +++ b/viewer/templates/qc/create_summaryTest.html @@ -1,16 +1,10 @@ {% import 'widgets/datetime.html' as datetime %} {% import 'widgets/tags.html' as tags_widget %} - - {% extends "layout.html" %} +{% block stylesheet %} +{% endblock %} + {% block body %}
diff --git a/viewer/templates/qc/input_qcTests.html b/viewer/templates/qc/input_qcTests.html index cf9326935..9884db94c 100644 --- a/viewer/templates/qc/input_qcTests.html +++ b/viewer/templates/qc/input_qcTests.html @@ -1,20 +1,16 @@ - {% extends "layout.html" %} +{% block stylesheet %} + +{% endblock %} + {% block body %}
diff --git a/viewer/templates/qc/upload_qcTests.html b/viewer/templates/qc/upload_qcTests.html index e8117dd7a..947778ea8 100644 --- a/viewer/templates/qc/upload_qcTests.html +++ b/viewer/templates/qc/upload_qcTests.html @@ -1,13 +1,8 @@ - {% extends "layout.html" %} +{% block stylesheet %} +{% endblock %} + {% block body %}
diff --git a/viewer/templates/qcTests.html b/viewer/templates/qcTests.html index e3d15b47d..1bf4c9331 100644 --- a/viewer/templates/qcTests.html +++ b/viewer/templates/qcTests.html @@ -1,17 +1,9 @@ {% import 'widgets/datetime.html' as datetime %} - - {% extends "layout.html" %} +{% block stylesheet %} +{% endblock %} + {% block body %}
diff --git a/viewer/templates/sitemap.html b/viewer/templates/sitemap.html index 39a861a41..40e6e7d97 100644 --- a/viewer/templates/sitemap.html +++ b/viewer/templates/sitemap.html @@ -1,10 +1,8 @@ - {% extends "layout.html" %} +{% block stylesheet %} +{% endblock %} + {% block body %}
diff --git a/viewer/templates/toppage.html b/viewer/templates/toppage.html index e6a329c3f..97927316d 100644 --- a/viewer/templates/toppage.html +++ b/viewer/templates/toppage.html @@ -1,16 +1,6 @@ {% extends "layout.html" %} {% block stylesheet %} - {% endblock %} {% block body %} -- GitLab From 243b4921a8d71fb13366fe594d3a85ef762ba751 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Wed, 24 Sep 2025 22:34:17 -0700 Subject: [PATCH 03/11] fix altID search --- viewer/pages/toppage.py | 49 ++++++++------------------ viewer/templates/components_table.html | 6 ++-- 2 files changed, 17 insertions(+), 38 deletions(-) diff --git a/viewer/pages/toppage.py b/viewer/pages/toppage.py index fff41e706..44f7cd4fd 100644 --- a/viewer/pages/toppage.py +++ b/viewer/pages/toppage.py @@ -217,26 +217,14 @@ def show_comps(view_type="module"): }, { "$addFields": { - "altID": { - "$arrayElemAt": [ - { - "$map": { - "input": { - "$filter": { - "input": "$properties", - "cond": { - "$eq": [ - "$$this.code", - "ALTERNATIVE_IDENTIFIER", - ] - }, - } - }, - "in": "$$this.value", - } - }, - 0, - ] + "properties": { + "$arrayToObject": { + "$map": { + "input": "$properties", + "as": "prop", + "in": {"k": "$$prop.code", "v": "$$prop.value"}, + } + } } } }, @@ -245,7 +233,7 @@ def show_comps(view_type="module"): "_id": 1, "serialNumber": 1, "componentType": 1, - "altID": 1, + "properties": 1, "children": 1, "parents": 1, } @@ -272,11 +260,9 @@ def show_comps(view_type="module"): } }, { - "properties": { - "$elemMatch": { - "code": "ALTERNATIVE_IDENTIFIER", - "value": {"$regex": regex_pattern, "$options": "i"}, - } + "properties.ALTERNATIVE_IDENTIFIER": { + "$regex": regex_pattern, + "$options": "i", } }, ] @@ -287,14 +273,7 @@ def show_comps(view_type="module"): {"serialNumber": {"$in": keywords}}, {"children": {"$elemMatch": {"$in": keywords}}}, {"parents": {"$elemMatch": {"$in": keywords}}}, - { - "properties": { - "$elemMatch": { - "code": "ALTERNATIVE_IDENTIFIER", - "value": {"$in": keywords}, - } - } - }, + {"properties.ALTERNATIVE_IDENTIFIER": {"$in": keywords}}, ] ) @@ -480,7 +459,7 @@ def show_comps(view_type="module"): "proDB": component.get("proDB", False), "component_tag_candidate": tag_candidates, "component_tag": tags, - "altID": component.get("altID"), + "properties": component.get("properties", {}), } if qcStatus: diff --git a/viewer/templates/components_table.html b/viewer/templates/components_table.html index 4011c116d..f2fc987ed 100644 --- a/viewer/templates/components_table.html +++ b/viewer/templates/components_table.html @@ -13,7 +13,7 @@
- +
{% if session['logged_in'] %} @@ -69,8 +69,8 @@ title="{{ component['typeinfo'] }}" >{{ component['name'] }} - {% if component['altID'] %} -
{{ component['altID'] }} + {% if component['properties'].get('ALTERNATIVE_IDENTIFIER') %} +
{{ component['properties']['ALTERNATIVE_IDENTIFIER'] }} {% endif %} {% if component['name'].find( '20U' ) != 0 %}
Not ATLAS SN!
-- GitLab From dcdaaa7d6260058fea2f7f35baa46795be44e794 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Wed, 24 Sep 2025 22:44:05 -0700 Subject: [PATCH 04/11] harmonize tables, breadcrumb widget --- viewer/templates/create_tag.html | 13 +++++++------ viewer/templates/dashboard/overview.html | 13 +++++++------ viewer/templates/qcTests.html | 14 +++++++------- viewer/templates/scan_table.html | 2 +- viewer/templates/toppage.html | 2 +- viewer/templates/widgets/breadcrumb.html | 13 +++++++++++++ viewer/templates/yarr_scans/plot.html | 2 +- 7 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 viewer/templates/widgets/breadcrumb.html diff --git a/viewer/templates/create_tag.html b/viewer/templates/create_tag.html index ad472aa00..b72af6fff 100644 --- a/viewer/templates/create_tag.html +++ b/viewer/templates/create_tag.html @@ -1,15 +1,16 @@ +{% import 'widgets/breadcrumb.html' as breadcrumb %} {% extends "layout.html" %} {% block body %}
- + {{ + breadcrumb.widget([ + {'title': 'Top Page', 'url': url_for('toppage_api.show_toppage')}, + {'title': 'Create a new tag'} + ]) + }}
diff --git a/viewer/templates/dashboard/overview.html b/viewer/templates/dashboard/overview.html index 7f4dfeb8f..e79446fa9 100644 --- a/viewer/templates/dashboard/overview.html +++ b/viewer/templates/dashboard/overview.html @@ -1,4 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} +{% import 'widgets/breadcrumb.html' as breadcrumb %} {% extends "layout.html" %} {% block stylesheet %} @@ -7,12 +8,12 @@ {% block body %}
- + {{ + breadcrumb.widget([ + {'title': 'Top Page', 'url': url_for('toppage_api.show_toppage')}, + {'title': 'Module Dashboard'} + ]) + }}

Module Dashboard

diff --git a/viewer/templates/qcTests.html b/viewer/templates/qcTests.html index 1bf4c9331..5faa5afde 100644 --- a/viewer/templates/qcTests.html +++ b/viewer/templates/qcTests.html @@ -1,4 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} +{% import 'widgets/breadcrumb.html' as breadcrumb %} {% extends "layout.html" %} {% block stylesheet %} @@ -6,13 +7,12 @@ {% block body %}
- - + {{ + breadcrumb.widget([ + {'title': 'Top Page', 'url': url_for('toppage_api.show_toppage')}, + {'title': 'QC Tests'} + ]) + }} {% include 'parts/showMessages.html' %}
diff --git a/viewer/templates/scan_table.html b/viewer/templates/scan_table.html index 4df70ad23..de7915e37 100644 --- a/viewer/templates/scan_table.html +++ b/viewer/templates/scan_table.html @@ -6,7 +6,7 @@
-
+
{% if session['logged_in'] %} diff --git a/viewer/templates/toppage.html b/viewer/templates/toppage.html index 97927316d..5721f1564 100644 --- a/viewer/templates/toppage.html +++ b/viewer/templates/toppage.html @@ -56,7 +56,7 @@ name="match" id="gridRadios1" value="partial" - {% if table_docs['match'] == 'partial' %}checked{% endif %} + {% if table_docs['match'] != 'perfect' %}checked{% endif %} /> diff --git a/viewer/templates/widgets/breadcrumb.html b/viewer/templates/widgets/breadcrumb.html new file mode 100644 index 000000000..4805db113 --- /dev/null +++ b/viewer/templates/widgets/breadcrumb.html @@ -0,0 +1,13 @@ +{%- macro widget(items) -%} + +{%- endmacro -%} diff --git a/viewer/templates/yarr_scans/plot.html b/viewer/templates/yarr_scans/plot.html index 8a1281c83..e0a487877 100644 --- a/viewer/templates/yarr_scans/plot.html +++ b/viewer/templates/yarr_scans/plot.html @@ -56,7 +56,7 @@ {% for chip, data in result["chips"].items() %} {% if not data["num"]==0 %}
-
+
-- GitLab From c5fcdf9f7379edbfa9265979e82b5c33c880cb27 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Thu, 25 Sep 2025 00:31:08 -0700 Subject: [PATCH 05/11] =?UTF-8?q?=20=201.=20Alternative=20ID=20search:=20?= =?UTF-8?q?=E2=9C=85=20Updated=20to=20use=20properties.ALTERNATIVE=5FIDENT?= =?UTF-8?q?IFIER=20=20=202.=20Radio=20button=20logic:=20=E2=9C=85=20Fixed?= =?UTF-8?q?=20to=20use=20!=3D=20'perfect'=20instead=20of=20=3D=3D=20'parti?= =?UTF-8?q?al'=20=20=203.=20Pagination:=20=E2=9C=85=20All=20three=20templa?= =?UTF-8?q?tes=20now=20use=20unified=20widget=20with=20harmonized=20variab?= =?UTF-8?q?les=20=20=204.=20Templates/Widgets:=20=E2=9C=85=20Breadcrumb=20?= =?UTF-8?q?properly=20converted=20to=20widget,=20pagination=20is=20now=20a?= =?UTF-8?q?=20widget=20=20=205.=20Table=20classes:=20=E2=9C=85=20Standardi?= =?UTF-8?q?zed=20to=20table=20table-hover=20table-bordered=20table-striped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viewer/pages/toppage.py | 44 +++-- viewer/templates/components_table.html | 15 +- .../parts/components_pagination.html | 147 ---------------- viewer/templates/parts/scans_pagination.html | 146 ---------------- viewer/templates/qcTests.html | 160 +++--------------- viewer/templates/scan_table.html | 13 +- viewer/templates/widgets/pagination.html | 139 +++++++++++++++ 7 files changed, 216 insertions(+), 448 deletions(-) delete mode 100644 viewer/templates/parts/components_pagination.html delete mode 100644 viewer/templates/parts/scans_pagination.html create mode 100644 viewer/templates/widgets/pagination.html diff --git a/viewer/pages/toppage.py b/viewer/pages/toppage.py index 44f7cd4fd..dab59e5b0 100644 --- a/viewer/pages/toppage.py +++ b/viewer/pages/toppage.py @@ -342,17 +342,15 @@ def show_comps(view_type="module"): elif page > maxPage: page = maxPage - minRange = max(1, page - 2) - maxRange = min(maxPage, minRange + 3) - pageRange = list(range(minRange, maxRange + 1)) - - pagingInfo = { - "thisPage": page, - "range": pageRange, - "minPage": 1, - "maxPage": maxPage, - "entries": nModulesTotal, - "perPage": compsPerPage, + pagination_info = { + "page": page, + "total_pages": maxPage, + "total_count": nModulesTotal, + "page_size": compsPerPage, + "has_prev": page > 1, + "has_next": page < maxPage, + "prev_page": page - 1 if page > 1 else None, + "next_page": page + 1 if page < maxPage else None, } cmp_ids = [] @@ -507,7 +505,7 @@ def show_comps(view_type="module"): reverse=True, ) - table_docs.update({"pagingInfo": pagingInfo}) + table_docs.update({"pagination_info": pagination_info}) table_docs["title"] = f"{view.upper()} List" return render_template( @@ -525,8 +523,8 @@ def show_scans(): max_num = 15 sort_cnt = 0 - if request.args.get("p", 0) != "": - sort_cnt = int(request.args.get("p", 0)) + if request.args.get("page", 1) != "": + sort_cnt = int(request.args.get("page", 1)) - 1 # Convert to 0-based indexing table_docs = {"run": []} @@ -639,11 +637,27 @@ def show_scans(): if len(cnt) == max_num: break + total_pages = (nrun_entries - 1) // max_num + current_page = sort_cnt + + pagination_info = { + "page": current_page + 1, # 1-based indexing for display + "total_pages": total_pages + 1, # 1-based indexing for display + "total_count": run_counts, + "page_size": max_num, + "has_prev": current_page > 0, + "has_next": current_page < total_pages, + "prev_page": current_page, # 1-based for URL (current_page + 1 - 1) + "next_page": current_page + 2, # 1-based for URL (current_page + 1 + 1) + } + scans_num = { "total": run_counts, "cnt": cnt, + "pagination_info": pagination_info, + # Keep old variables for backward compatibility during transition "now_cnt": sort_cnt, - "max_cnt": ((nrun_entries - 1) // max_num), + "max_cnt": total_pages, "max_num": max_num, } table_docs.update(scans_num) diff --git a/viewer/templates/components_table.html b/viewer/templates/components_table.html index f2fc987ed..83fa25504 100644 --- a/viewer/templates/components_table.html +++ b/viewer/templates/components_table.html @@ -1,4 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} +{% import 'widgets/pagination.html' as pagination %}
@@ -144,7 +145,19 @@
{{ chip }}
- {% include 'parts/components_pagination.html' %} + {{ + pagination.widget( + table_docs.pagination_info, + 'toppage_api.show_comps', + { + 'view_type': table_docs.view, + 'keywords': table_docs.keywords, + 'match': table_docs.match, + 'showAll': table_docs.doShowAll + }, + 'Components' + ) + }}
diff --git a/viewer/templates/parts/components_pagination.html b/viewer/templates/parts/components_pagination.html deleted file mode 100644 index 6fd8d0058..000000000 --- a/viewer/templates/parts/components_pagination.html +++ /dev/null @@ -1,147 +0,0 @@ -{# - Components pagination template with dropdown ellipsis - - Required parameters: - - pagingInfo: dict with 'thisPage', 'minPage', 'maxPage', 'entries', 'perPage' - - table_docs: dict with 'view', 'keywords', 'match', 'doShowAll' for URL parameters - - aria_label: string for accessibility (default: "Components") -#} -{% set aria_label = aria_label or "Components" %} - -{% if pagingInfo["minPage"] != pagingInfo["maxPage"] %} -
-
- - - -
- - Showing page {{ pagingInfo['thisPage'] }} of {{ pagingInfo['maxPage'] }} ({{ pagingInfo['entries'] }} total results, - {{ pagingInfo['perPage'] }} per page) - -
-
-
-{% endif %} diff --git a/viewer/templates/parts/scans_pagination.html b/viewer/templates/parts/scans_pagination.html deleted file mode 100644 index a1396b964..000000000 --- a/viewer/templates/parts/scans_pagination.html +++ /dev/null @@ -1,146 +0,0 @@ -{# - Scans pagination template with dropdown ellipsis - - Required parameters: - - table_docs: dict with 'now_cnt', 'max_cnt', 'total', 'max_num', 'keywords', 'match' - - aria_label: string for accessibility (default: "Scans") -#} -{% set aria_label = aria_label or "Scans" %} - -{% if table_docs['max_cnt'] > 0 %} -
-
- - - -
- - Showing page {{ table_docs['now_cnt'] + 1 }} of {{ table_docs['max_cnt'] + 1 }} ({{ table_docs['total'] }} total results, - {{ table_docs['max_num'] }} per page) - -
-
-
-{% endif %} diff --git a/viewer/templates/qcTests.html b/viewer/templates/qcTests.html index 5faa5afde..cfd386fcc 100644 --- a/viewer/templates/qcTests.html +++ b/viewer/templates/qcTests.html @@ -1,5 +1,6 @@ {% import 'widgets/datetime.html' as datetime %} {% import 'widgets/breadcrumb.html' as breadcrumb %} +{% import 'widgets/pagination.html' as pagination %} {% extends "layout.html" %} {% block stylesheet %} @@ -121,144 +122,27 @@
- - {% if table_docs['total_pages'] > 1 %} -
-
- - - -
- - Showing page {{ table_docs['page'] }} of {{ table_docs['total_pages'] }} ({{ table_docs['total_count'] }} total results, - {{ table_docs['page_size'] }} per page) - -
-
-
- {% endif %} + {{ + pagination.widget( + { + 'page': table_docs['page'], + 'total_pages': table_docs['total_pages'], + 'total_count': table_docs['total_count'], + 'page_size': table_docs['page_size'], + 'has_prev': table_docs['has_prev'], + 'has_next': table_docs['has_next'], + 'prev_page': table_docs['prev_page'], + 'next_page': table_docs['next_page'] + }, + 'qc_api.qc_tests', + { + 'keyword': table_docs['search_phrase'], + 'stage_filter': table_docs['stage_filter'], + 'test_type_filter': table_docs['test_type_filter'] + }, + 'QC Tests' + ) + }}
diff --git a/viewer/templates/scan_table.html b/viewer/templates/scan_table.html index de7915e37..257074354 100644 --- a/viewer/templates/scan_table.html +++ b/viewer/templates/scan_table.html @@ -1,4 +1,5 @@ {% import 'widgets/datetime.html' as datetime %} +{% import 'widgets/pagination.html' as pagination %}
@@ -159,7 +160,17 @@

- {% include 'parts/scans_pagination.html' %} + {{ + pagination.widget( + table_docs.pagination_info, + 'toppage_api.show_scans', + { + 'keywords': table_docs.keywords, + 'match': table_docs.match + }, + 'Scans' + ) + }}
diff --git a/viewer/templates/widgets/pagination.html b/viewer/templates/widgets/pagination.html new file mode 100644 index 000000000..21b00d65e --- /dev/null +++ b/viewer/templates/widgets/pagination.html @@ -0,0 +1,139 @@ +{%- macro widget(pagination_info, url_for_func, url_params={}, aria_label="Pagination") -%} + {# + Unified pagination widget with dropdown ellipsis + + Required parameters: + - pagination_info: dict with standardized fields: + - page: current page number (1-based) + - total_pages: total number of pages + - total_count: total number of items + - page_size: items per page + - has_prev: boolean if previous page exists + - has_next: boolean if next page exists + - prev_page: previous page number (optional) + - next_page: next page number (optional) + - url_for_func: URL generation function name (e.g., 'qc_api.qc_tests') + - url_params: dict with additional URL parameters + - aria_label: string for accessibility +#} + + {% if pagination_info.total_pages > 1 %} +
+
+ + + +
+ + Showing page {{ pagination_info.page }} of {{ pagination_info.total_pages }} ({{ pagination_info.total_count }} total results, + {{ pagination_info.page_size }} per page) + +
+
+
+ {% endif %} +{%- endmacro -%} -- GitLab From a571fdb734517c9f314819ca6970bb12f1b18857 Mon Sep 17 00:00:00 2001 From: Giordon Stark Date: Thu, 25 Sep 2025 00:50:23 -0700 Subject: [PATCH 06/11] add next/previous --- viewer/templates/widgets/pagination.html | 36 +++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/viewer/templates/widgets/pagination.html b/viewer/templates/widgets/pagination.html index 21b00d65e..5b438dac5 100644 --- a/viewer/templates/widgets/pagination.html +++ b/viewer/templates/widgets/pagination.html @@ -22,18 +22,32 @@