# Buggy code
def extract_steps(
    trans,
    history=None,
    job_ids=None,
    dataset_ids=None,
    dataset_collection_ids=None,
    dataset_names=None,
    dataset_collection_names=None,
):
    # Ensure job_ids and dataset_ids are lists (possibly empty)
    if job_ids is None:
        job_ids = []
    elif type(job_ids) is not list:
        job_ids = [job_ids]
    if dataset_ids is None:
        dataset_ids = []
    elif type(dataset_ids) is not list:
        dataset_ids = [dataset_ids]
    if dataset_collection_ids is None:
        dataset_collection_ids = []
    elif type(dataset_collection_ids) is not list:
        dataset_collection_ids = [dataset_collection_ids]
    # Convert both sets of ids to integers
    job_ids = [int(_) for _ in job_ids]
    dataset_ids = [int(_) for _ in dataset_ids]
    dataset_collection_ids = [int(_) for _ in dataset_collection_ids]
    # Find each job, for security we (implicitly) check that they are
    # associated with a job in the current history.
    summary = WorkflowSummary(trans, history)
    jobs = summary.jobs
    steps = []
    hid_to_output_pair = {}
    # Input dataset steps
    for i, hid in enumerate(dataset_ids):
        step = model.WorkflowStep()
        step.type = "data_input"
        if dataset_names:
            name = dataset_names[i]
        else:
            name = "Input Dataset"
        step.tool_inputs = dict(name=name)
        hid_to_output_pair[hid] = (step, "output")
        steps.append(step)
    for i, hid in enumerate(dataset_collection_ids):
        step = model.WorkflowStep()
        step.type = "data_collection_input"
        if hid not in summary.collection_types:
            raise exceptions.RequestParameterInvalidException(
                "hid %s does not appear to be a collection" % hid
            )
        collection_type = summary.collection_types[hid]
        if dataset_collection_names:
            name = dataset_collection_names[i]
        else:
            name = "Input Dataset Collection"
        step.tool_inputs = dict(name=name, collection_type=collection_type)
        hid_to_output_pair[hid] = (step, "output")
        steps.append(step)
    # Tool steps
    for job_id in job_ids:
        if job_id not in summary.job_id2representative_job:
            log.warning(
                "job_id %s not found in job_id2representative_job %s"
                % (job_id, summary.job_id2representative_job)
            )
            raise AssertionError(
                "Attempt to create workflow with job not connected to current history"
            )
        job = summary.job_id2representative_job[job_id]
        tool_inputs, associations = step_inputs(trans, job)
        step = model.WorkflowStep()
        step.type = "tool"
        step.tool_id = job.tool_id
        step.tool_version = job.tool_version
        step.tool_inputs = tool_inputs
        # NOTE: We shouldn't need to do two passes here since only
        #       an earlier job can be used as an input to a later
        #       job.
        for other_hid, input_name in associations:
            if job in summary.implicit_map_jobs:
                an_implicit_output_collection = jobs[job][0][1]
                input_collection = (
                    an_implicit_output_collection.find_implicit_input_collection(
                        input_name
                    )
                )
                if input_collection:
                    other_hid = input_collection.hid
                else:
                    log.info(
                        "Cannot find implicit input collection for %s" % input_name
                    )
            if other_hid in hid_to_output_pair:
                step_input = step.get_or_add_input(input_name)
                other_step, other_name = hid_to_output_pair[other_hid]
                conn = model.WorkflowStepConnection()
                conn.input_step_input = step_input
                # Should always be connected to an earlier step
                conn.output_step = other_step
                conn.output_name = other_name
        steps.append(step)
        # Store created dataset hids
        for assoc in job.output_datasets + job.output_dataset_collection_instances:
            assoc_name = assoc.name
            if ToolOutputCollectionPart.is_named_collection_part_name(assoc_name):
                continue
            if job in summary.implicit_map_jobs:
                hid = None
                for implicit_pair in jobs[job]:
                    query_assoc_name, dataset_collection = implicit_pair
                    if query_assoc_name == assoc_name or assoc_name.startswith(
                        "__new_primary_file_%s|" % query_assoc_name
                    ):
                        hid = dataset_collection.hid
                if hid is None:
                    template = "Failed to find matching implicit job - job id is %s, implicit pairs are %s, assoc_name is %s."
                    message = template % (job.id, jobs[job], assoc_name)
                    log.warning(message)
                    raise Exception("Failed to extract job.")
            else:
                if hasattr(assoc, "dataset"):
                    hid = assoc.dataset.hid
                else:
                    hid = assoc.dataset_collection_instance.hid
            hid_to_output_pair[hid] = (step, assoc.name)
    return steps


# Traceback (AttributeError)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: 80.138.30.147 - - [18/Oct/2020:23:43:09 +0200] "POST /workflow/build_from_current_history HTTP/1.1" 500 - "https://usegalaxy.eu/workflow/build_from_current_history" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0"
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: Traceback (most recent call last):
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/web/framework/middleware/error.py", line 154, in __call__
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: app_iter = self.application(environ, sr_checker)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "/opt/galaxy/venv/lib64/python3.6/site-packages/paste/recursive.py", line 85, in __call__
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: return self.application(environ, start_response)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/web/framework/middleware/statsd.py", line 34, in __call__
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: req = self.application(environ, start_response)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "/opt/galaxy/venv/lib64/python3.6/site-packages/paste/httpexceptions.py", line 640, in __call__
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: return self.application(environ, start_response)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/web/framework/base.py", line 145, in __call__
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: return self.handle_request(environ, start_response)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/web/framework/base.py", line 224, in handle_request
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: body = method(trans, **kwargs)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/webapps/galaxy/controllers/workflow.py", line 854, in build_from_current_history
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: dataset_collection_names=dataset_collection_names
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 29, in extract_workflow
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: steps = extract_steps(trans, history=history, job_ids=job_ids, dataset_ids=dataset_ids, dataset_collection_ids=dataset_collection_ids, dataset_names=dataset_names, dataset_collection_names=None)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 109, in extract_steps
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: tool_inputs, associations = step_inputs(trans, job)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 310, in step_inputs
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: associations = __cleanup_param_values(tool.inputs, param_values)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 372, in __cleanup_param_values
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: cleanup("", inputs, values)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 368, in cleanup
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: cleanup("%s%s|" % (prefix, key), input.cases[current_case].inputs, group_values)
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 342, in cleanup
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: associations.extend([(t.hid, prefix + key) for t in tmp])
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: File "lib/galaxy/workflow/extract.py", line 342, in <listcomp>
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: associations.extend([(t.hid, prefix + key) for t in tmp])
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: AttributeError: 'DatasetCollectionElement' object has no attribute 'hid'
Oct 18 23:43:10 sn04.bi.uni-freiburg.de uwsgi[1008399]: The above exception was the direct cause of the

# Fixed code
def extract_steps(
    trans,
    history=None,
    job_ids=None,
    dataset_ids=None,
    dataset_collection_ids=None,
    dataset_names=None,
    dataset_collection_names=None,
):
    # Ensure job_ids and dataset_ids are lists (possibly empty)
    job_ids = listify(job_ids)
    dataset_ids = listify(dataset_ids)
    dataset_collection_ids = listify(dataset_collection_ids)
    # Convert both sets of ids to integers
    job_ids = [int(_) for _ in job_ids]
    dataset_ids = [int(_) for _ in dataset_ids]
    dataset_collection_ids = [int(_) for _ in dataset_collection_ids]
    # Find each job, for security we (implicitly) check that they are
    # associated with a job in the current history.
    summary = WorkflowSummary(trans, history)
    jobs = summary.jobs
    steps = []
    hid_to_output_pair = {}
    # Input dataset steps
    for i, hid in enumerate(dataset_ids):
        step = model.WorkflowStep()
        step.type = "data_input"
        if dataset_names:
            name = dataset_names[i]
        else:
            name = "Input Dataset"
        step.tool_inputs = dict(name=name)
        hid_to_output_pair[hid] = (step, "output")
        steps.append(step)
    for i, hid in enumerate(dataset_collection_ids):
        step = model.WorkflowStep()
        step.type = "data_collection_input"
        if hid not in summary.collection_types:
            raise exceptions.RequestParameterInvalidException(
                "hid %s does not appear to be a collection" % hid
            )
        collection_type = summary.collection_types[hid]
        if dataset_collection_names:
            name = dataset_collection_names[i]
        else:
            name = "Input Dataset Collection"
        step.tool_inputs = dict(name=name, collection_type=collection_type)
        hid_to_output_pair[hid] = (step, "output")
        steps.append(step)
    # Tool steps
    for job_id in job_ids:
        if job_id not in summary.job_id2representative_job:
            log.warning(
                "job_id %s not found in job_id2representative_job %s"
                % (job_id, summary.job_id2representative_job)
            )
            raise AssertionError(
                "Attempt to create workflow with job not connected to current history"
            )
        job = summary.job_id2representative_job[job_id]
        tool_inputs, associations = step_inputs(trans, job)
        step = model.WorkflowStep()
        step.type = "tool"
        step.tool_id = job.tool_id
        step.tool_version = job.tool_version
        step.tool_inputs = tool_inputs
        # NOTE: We shouldn't need to do two passes here since only
        #       an earlier job can be used as an input to a later
        #       job.
        for other_hid, input_name in associations:
            if job in summary.implicit_map_jobs:
                an_implicit_output_collection = jobs[job][0][1]
                input_collection = (
                    an_implicit_output_collection.find_implicit_input_collection(
                        input_name
                    )
                )
                if input_collection:
                    other_hid = input_collection.hid
                else:
                    log.info(
                        "Cannot find implicit input collection for %s" % input_name
                    )
            if other_hid in hid_to_output_pair:
                step_input = step.get_or_add_input(input_name)
                other_step, other_name = hid_to_output_pair[other_hid]
                conn = model.WorkflowStepConnection()
                conn.input_step_input = step_input
                # Should always be connected to an earlier step
                conn.output_step = other_step
                conn.output_name = other_name
        steps.append(step)
        # Store created dataset hids
        for assoc in job.output_datasets + job.output_dataset_collection_instances:
            assoc_name = assoc.name
            if ToolOutputCollectionPart.is_named_collection_part_name(assoc_name):
                continue
            if job in summary.implicit_map_jobs:
                hid = None
                for implicit_pair in jobs[job]:
                    query_assoc_name, dataset_collection = implicit_pair
                    if query_assoc_name == assoc_name or assoc_name.startswith(
                        "__new_primary_file_%s|" % query_assoc_name
                    ):
                        hid = dataset_collection.hid
                if hid is None:
                    template = "Failed to find matching implicit job - job id is %s, implicit pairs are %s, assoc_name is %s."
                    message = template % (job.id, jobs[job], assoc_name)
                    log.warning(message)
                    raise Exception("Failed to extract job.")
            else:
                if hasattr(assoc, "dataset"):
                    hid = assoc.dataset.hid
                else:
                    hid = assoc.dataset_collection_instance.hid
            hid_to_output_pair[hid] = (step, assoc.name)
    return steps