SG Ground Truth

undo task template merge

Undo a task template merge, returning an entity's Tasks, fields and dependencies to their state before it

API

A merge (recipe 015) cannot be undone from what is left after it: the apply overwrote sg_sort_order, sg_description and duration on every Task linked to the new template (probe 096), and every other field the template sets (probe 102). Take a snapshot before the merge. The undo writes the old template_task on each Task first, then the old task_template, then deletes what the merge made and writes the snapshot's fields back. Entity first makes duplicates.

Call

import sys

sys.path.insert(0, "src")                       # or PYTHONPATH=src
from sg_groundtruth.client import FPT
from sg_groundtruth.env import load

c = FPT.from_env(load("."))
ARR = {"Content-Type": "application/vnd+shotgun.api3_array+json"}
SLUG = {"Shot": "shots", "Asset": "assets", "Sequence": "sequences"}
# The fields probe 096 saw the apply overwrite, and content, which the re-sync renames (probes 102,
# 112). Probe 102 adds step, est_in_mins, task_reviewers, milestone and a custom field (a list and a
# checkbox field probed), when the template sets them: add the ones yours set.
KEEP = ["template_task", "sg_sort_order", "sg_description", "duration", "content"]


def search(slug, filters, fields):
    r = c.post(f"/entity/{slug}/_search", headers=ARR,
               json={"filters": filters, "fields": fields, "page": {"size": 500}})
    r.raise_for_status()
    return r.json()["data"]


def link(row, field):
    return (row["relationships"].get(field) or {}).get("data")


def snapshot(entity):
    """Read before the merge: the entity's template, each Task's fields, the dependency ids."""
    sh = c.get(f"/entity/{SLUG[entity['type']]}/{entity['id']}",
               params={"fields": "task_template"}).json()["data"]
    tasks = search("tasks", [["entity", "is", entity]], KEEP)
    ids = [{"type": "Task", "id": t["id"]} for t in tasks]
    deps = search("task_dependencies", [["task", "in", ids]], ["task"]) if ids else []
    return {"task_template": link(sh, "task_template"),
            "tasks": {t["id"]: {**{k: v for k, v in t["attributes"].items() if k in KEEP},
                                "template_task": link(t, "template_task")} for t in tasks},
            "deps": {d["id"] for d in deps}}


def undo_merge(entity, snap):
    slug = SLUG[entity["type"]]
    before = snap["tasks"]
    now = search("tasks", [["entity", "is", entity]], ["template_task"])

    # 1. Tasks first. An entity write while they still point at the new template's tasks makes the
    #    old template re-create them and move its edges onto the copies (probe 096).
    for t in now:
        old = before.get(t["id"])
        if old and (link(t, "template_task") or {}).get("id") != (old["template_task"] or {}).get("id"):
            c.put(f"/entity/tasks/{t['id']}", json={"template_task": old["template_task"]}).raise_for_status()

    # 2. The old template. A non-null value re-syncs its Tasks and drops the new template's edges
    #    between them; null does nothing.
    c.put(f"/entity/{slug}/{entity['id']}", json={"task_template": snap["task_template"]}).raise_for_status()

    # 3. What the merge made. A Task delete retires its edges (probe 089).
    for t in now:
        if t["id"] not in before:
            c.delete(f"/entity/tasks/{t['id']}").raise_for_status()
    ids = [{"type": "Task", "id": i} for i in before]
    # Every edge left after a non-null step 2 whose downstream Task is on the old template is one of the
    # template's own; the write can re-create it under a new id (probe 111), so do not sweep it.
    for d in search("task_dependencies", [["task", "in", ids]], ["task"]) if ids else []:
        back = snap["task_template"] and before[link(d, "task")["id"]]["template_task"]
        if d["id"] not in snap["deps"] and not back:
            c.delete(f"/entity/task_dependencies/{d['id']}").raise_for_status()

    # 4. The fields both applies overwrote.
    for i, old in before.items():
        data = {k: v for k, v in old.items() if k != "template_task"}
        c.put(f"/entity/tasks/{i}", json=data).raise_for_status()


shot = {"type": "Shot", "id": 1234}
snap = snapshot(shot)
# ... apply_template(shot, 57) from recipe 015 ...
undo_merge(shot, snap)

Response

A: comp, roto, lay; roto on comp.  B: comp, lay, paint; lay on comp SS+1, paint on lay
before the merge   3 Tasks, 1 edge   comp ip "hand", lay order 99, roto "hand" 1440
after the merge    4 Tasks, 3 edges  comp, lay: B's order/desc/dur
step 1  PUT comp, lay template_task A.*  -> nothing else changes
step 2  PUT Shot task_template A         -> B's lay-on-comp edge removed, fields reset to A's
step 3  DELETE paint                     -> 3 Tasks, 1 edge (roto on comp)
step 4  PUT the snapshot's fields        -> "hand", 99, 1440 back; comp still ip

Notes

  • Revive before step 2. A Task retired since the snapshot does not count as linked: step 2 re-creates its template task and wires the copy, and a later revive leaves two Tasks on one template task, both upstream of the same Task. Revive restores template_task (probe 110).

  • Step 4 is what makes it an undo rather than a reapply of A. Status survives both applies, so it is not in KEEP. Assignees and dates are only filled where empty (probe 102): add them when the Task had none before. snapshot reads attributes only; read step and task_reviewers with link if you add them.

  • When the entity had no template before, step 2 writes null and the server does nothing. Step 3's dependency sweep is then the only thing that removes the new template's edges between old Tasks.

  • The dependency sweep compares ids, so an edge the merge deleted is not restored. A merge deletes an edge between two claimed Tasks that the template lacks, or holds with another type, offset or direction, and erases the row (probes 101, 102): revive cannot bring it back. Snapshot each edge's ends, type and offset and re-create the missing ones (recipe 016).

  • Step 2 erases every edge whose downstream Task is on the old template and that the template lacks, pre-merge edges the merge kept included (probe 111). Snapshot ends, type and offset to re-create them.

  • The step 3 skip of edges downstream on the old template follows probe 111's read after step 2, where the template's own edge came back under a new id and the unskipped sweep deleted it. Re-run as written, with content in KEEP, on three sandbox Shots merged by recipe 015:

    Shot undo Tasks, links, fields, status edges after
    the Response case ok equal roto on comp, same id
    no template before, comp and lay by hand ok, undo to null equal none, as before
    probe 111's W, 3 hand edges before and 3 after the merge ok equal roto on comp, new id

    The third lost roto on x, roto on lay (step 2) and comp on x (the merge), as the notes above say.

  • In one _batch (recipe 022), leave out the edges step 2 removes. A batch takes its ids before it runs, so it cannot read after step 2 as step 3 does here. A DELETE of an edge the non-null task_template write already removed is 404 Entity of type [TaskDependency] with id=... does not exist. and rolls back the whole batch (probe 104).

  • Undo to null removes nothing, so a batch undo to null keeps those DELETEs (probe 104).

  • Events: each write logs like any other (probe 090). The undo leaves Shotgun_Task_Change rows for template_task behind; the history is not rewritten.

Every entry on this site is the output of a probe in probes/. The corpus is generated by running those probes against a live Flow Production Tracking site, not written from memory.

Not affiliated with or endorsed by Autodesk. Flow Production Tracking is their product; this is an independent record of how its REST API answers.