Revision Key Schedule (Building Comments) + AddRevision.KEY values

Building comments dynamo- Uses fixed parameters to store building comments and record replies and statuses. I fear there is no ‘legit’ way in the API to fill in place holders. We do have ideate- which can read and write the key schedule but cannot save the key schedule definition :face_with_symbols_on_mouth: which I am chatting with them about now.

The attached DYN

View+Revision-Schedule-Building-Dept-Comments-Create.dyn (69.8 KB)

Uses the dictionary value at key and a slider to select the category to store using Forge_ID. (could be a DYF)

This then binds in the shared params to the Key Schedule.

These get passed to the Check.Key.Create which creates the Key Schedules if it can find the params and associated with revisions (cat = Category.GetCategory(doc, BuiltInCategory.OST_RevisionClouds)

then I wanted to populate some key values in the DYN with the output of the create node and I cannot populate the values.:collision::brick:

Each run creates a new line but reporting it cannot modify the content.

Just need to verify that is indeed the case- if not is there a work around?

Py.KeySched.Add.Populate:

# Dynamo Player-safe Key Schedule row adder with duplicate check and built-in Yes/No handling
# IN[0]=schedule_name (str)                      # exact schedule view name
# IN[1]=param_names (list[str])                  # headers OR field/parameter names; "Key Name" optional
# IN[2]=rows (list[list[Any]])                   # each inner list aligns to param_names order
# IN[3]=case_sensitive (bool, optional)          # default False
# IN[4]=trim_whitespace (bool, optional)         # default True
# OUT: dict with ok, message, scheduleId, isKeySchedule, results, insertedRowIndices

from typing import List, Tuple

import clr
clr.AddReference("RevitAPI")
clr.AddReference("RevitServices")

from Autodesk.Revit.DB import (
    FilteredElementCollector, ViewSchedule, SectionType, TableSectionData,
    LabelUtils, BuiltInParameter, ElementId, ParameterElement
)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

# -------- inputs --------
schedule_name = IN[0] if len(IN) > 0 else None
param_names: List[str] = IN[1] if len(IN) > 1 and IN[1] else []
rows_in: List[List[object]] = IN[2] if len(IN) > 2 and IN[2] else []
case_sensitive = bool(IN[3]) if len(IN) > 3 and IN[3] is not None else False
trim_whitespace = True if len(IN) <= 4 or IN[4] is None else bool(IN[4])

# -------- helpers --------
def _safe_str(v):
    try:
        return "" if v is None else str(v)
    except:
        return ""

def _norm(s):
    if s is None:
        s = ""
    if trim_whitespace:
        s = s.strip()
    if not case_sensitive:
        s = s.lower()
    return s

def _to_cell_text(v):
    # Convert values to display strings that Revit table accepts
    if isinstance(v, bool):
        return "Yes" if v else "No"
    if isinstance(v, (int, float)):
        # numbers become their string form
        return str(v)
    if isinstance(v, str):
        low = v.strip().lower()
        if low in ["true", "t", "1"]:  # accept truthy strings
            return "Yes"
        if low in ["false", "f", "0"]:
            return "No"
        return v
    return _safe_str(v)

def _get_schedule_by_name(name):
    if not name:
        return None
    for vs in FilteredElementCollector(doc).OfClass(ViewSchedule):
        if vs.Name == name:
            return vs
    return None

def _build_header_map(vs):
    """
    Returns (name_to_col, body, header, key_col_idx).
    Accepts for each column:
      - header text, field name, column heading override,
      - schedulable field name, ParameterElement.Name,
      - BuiltInParameter label. Also detects 'Key Name'.
    """
    table = vs.GetTableData()
    body = table.GetSectionData(SectionType.Body)
    header = table.GetSectionData(SectionType.Header)

    sd = vs.Definition
    visible_fields = []
    try:
        for i in range(sd.GetFieldCount()):
            f = sd.GetField(i)
            if not f.IsHidden:
                visible_fields.append(f)
    except:
        pass

    name_to_col = {}
    key_col_idx = None
    ncols = body.NumberOfColumns
    for c in range(ncols):
        f = visible_fields[c] if c < len(visible_fields) else None
        keys = set()

        # header cell
        try:
            ht = header.GetCellText(0, c)
            if ht:
                keys.add(_norm(ht))
                if _norm(ht) == _norm("Key Name"):
                    key_col_idx = c
        except:
            pass

        # field and overrides
        if f:
            try:
                fn = f.GetName()
                if fn:
                    keys.add(_norm(fn))
            except:
                pass
            try:
                ch = getattr(f, "ColumnHeading", None) or f.GetColumnHeading()
                if ch:
                    keys.add(_norm(ch))
                    if _norm(ch) == _norm("Key Name"):
                        key_col_idx = c
            except:
                pass
            try:
                sf = f.GetSchedulableField()
                if sf:
                    try:
                        sfn = sf.GetName()
                        if sfn:
                            keys.add(_norm(sfn))
                            if _norm(sfn) == _norm("Key Name"):
                                key_col_idx = c
                    except:
                        pass
                    try:
                        pid = sf.ParameterId
                        if isinstance(pid, ElementId) and pid.IntegerValue > 0:
                            pe = doc.GetElement(pid)
                            if isinstance(pe, ParameterElement) and pe.Name:
                                keys.add(_norm(pe.Name))
                                if _norm(pe.Name) == _norm("Key Name"):
                                    key_col_idx = c
                    except:
                        pass
                    try:
                        pid = sf.ParameterId
                        if isinstance(pid, ElementId) and pid.IntegerValue < 0:
                            bip = BuiltInParameter(pid.IntegerValue)
                            label = LabelUtils.GetLabelFor(bip)
                            if label:
                                keys.add(_norm(label))
                                if _norm(label) == _norm("Key Name"):
                                    key_col_idx = c
                    except:
                        pass
            except:
                pass

        for k in keys:
            if k and k not in name_to_col:
                name_to_col[k] = c

    # fallback: assume column 0 is Key Name if not found
    if key_col_idx is None and ncols > 0:
        key_col_idx = 0

    return name_to_col, body, header, key_col_idx

def _read_row_signature(body, row_idx, cols):
    vals = []
    for c in cols:
        try:
            vals.append(_norm(body.GetCellText(row_idx, c)))
        except:
            vals.append(_norm(""))
    return tuple(vals)

# -------- main --------
results, inserted_indices = [], []
ok = True
msgs = []
schedule_id = None
is_key = None

if not schedule_name:
    ok = False
    msgs.append("No schedule name provided.")
else:
    vs = _get_schedule_by_name(schedule_name)
    if not vs:
        ok = False
        msgs.append("Schedule not found: '{}'".format(schedule_name))
    else:
        schedule_id = vs.Id.IntegerValue
        try:
            is_key = bool(vs.Definition.IsKeySchedule)
        except:
            is_key = False

        if not is_key:
            ok = False
            msgs.append("'{}' is not a Key Schedule.".format(schedule_name))
        elif not param_names:
            ok = False
            msgs.append("No parameter (column) names supplied.")
        else:
            name_to_col, body, header, key_col = _build_header_map(vs)

            # Ensure "Key Name" is part of our mapping; if not provided, we will infer from first value
            norm_param_names = [_norm(p or "") for p in param_names]
            user_has_keyname = _norm("Key Name") in norm_param_names
            if not user_has_keyname:
                norm_param_names = [_norm("Key Name")] + norm_param_names
                prepend_key = True
            else:
                prepend_key = False

            # Validate columns exist
            missing = [("Key Name" if p == _norm("Key Name") else param_names[i - (0 if user_has_keyname else 1)])
                       for i, p in enumerate(norm_param_names) if p not in name_to_col]
            if missing:
                ok = False
                msgs.append("Missing schedule columns: " + ", ".join(missing))
            else:
                cols = [name_to_col[p] for p in norm_param_names]

                # Existing signatures (including Key Name)
                existing = {}
                try:
                    first, last = body.FirstRowNumber, body.LastRowNumber
                except:
                    first, last = 0, -1
                if last >= first:
                    for r in range(first, last + 1):
                        sig = _read_row_signature(body, r, cols)
                        if sig not in existing:
                            existing[sig] = r

                # Plan inserts with duplicate check
                to_insert = []
                for r in rows_in:
                    row_list = list(r) if isinstance(r, (list, tuple)) else [r]

                    # Determine a full row vector aligned with norm_param_names (maybe adding Key Name)
                    if prepend_key:
                        if not row_list:
                            results.append({"row": row_list, "status": "Skipped: no Key Name", "rowIndex": None})
                            continue
                        key_text = _to_cell_text(row_list[0])
                        row_for_sig = [key_text] + row_list
                    else:
                        # user provided "Key Name" among param_names
                        idx_key = param_names.index("Key Name") if "Key Name" in param_names else None
                        if idx_key is None or idx_key >= len(row_list) or row_list[idx_key] in [None, ""]:
                            results.append({"row": row_list, "status": "Skipped: no Key Name", "rowIndex": None})
                            continue
                        row_for_sig = list(row_list)

                    # Build signature
                    norm_vals = []
                    for i in range(len(norm_param_names)):
                        v = row_for_sig[i] if i < len(row_for_sig) else None
                        norm_vals.append(_norm(_to_cell_text(v)))
                    sig = tuple(norm_vals)

                    if sig in existing:
                        results.append({"row": row_list, "status": "Already present", "rowIndex": existing[sig]})
                    else:
                        to_insert.append((row_list, row_for_sig, sig))

                # Insert in one transaction
                if to_insert:
                    TransactionManager.Instance.EnsureInTransaction(doc)
                    try:
                        for row_list, row_for_sig, sig in to_insert:
                            insert_at = body.LastRowNumber + 1
                            body.InsertRow(insert_at)
                            for i in range(len(norm_param_names)):
                                if i >= len(row_for_sig):
                                    break
                                text = _to_cell_text(row_for_sig[i])
                                body.SetCellText(insert_at, cols[i], text)
                            inserted_indices.append(insert_at)
                            existing[sig] = insert_at
                            results.append({"row": row_list, "status": "Inserted", "rowIndex": insert_at})
                    except Exception as ex:
                        ok = False
                        msgs.append("Insert failed: {}".format(_safe_str(ex)))
                    finally:
                        TransactionManager.Instance.TransactionTaskDone()
                else:
                    msgs.append("No new rows to insert (all already present).")

OUT = {
    "ok": ok,
    "message": "; ".join(msgs) if msgs else "Completed.",
    "scheduleId": schedule_id,
    "isKeySchedule": is_key,
    "results": results,
    "insertedRowIndices": inserted_indices
}

2 Likes