Make steps possible to hide/show
authorSawyer Bergeron <sbergeron@iol.unh.edu>
Wed, 22 May 2019 14:13:03 +0000 (10:13 -0400)
committerSawyer Bergeron <sbergeron@iol.unh.edu>
Wed, 22 May 2019 17:34:13 +0000 (13:34 -0400)
Change-Id: Ice5036ea9801655032cb080537fbd471fb3fda3e
Signed-off-by: Sawyer Bergeron <sbergeron@iol.unh.edu>
src/templates/workflow/viewport-base.html
src/workflow/booking_workflow.py
src/workflow/models.py
src/workflow/opnfv_workflow.py
src/workflow/resource_bundle_workflow.py
src/workflow/snapshot_workflow.py
src/workflow/sw_bundle_workflow.py
src/workflow/views.py
src/workflow/workflow_factory.py
src/workflow/workflow_manager.py

index 1329595..beea7d2 100644 (file)
     }
 
     #breadcrumbs {
-        padding: 4px;
+        margin-bottom: 0;
     }
+
+    .btn_wrapper {
+        margin: 0;
+    }
+
     .step{
-        background: #DEEED3;
         display: inline;
-        padding: 5px;
+        padding: 7px;
         margin: 1px;
+        font-size: 14pt;
+        cursor: default;
+    }
+    .step:active {
+        -webkit-box-shadow: inherit;
+        box-shadow: inherit;
+    }
+    .step_active:active {
+        -webkit-box-shadow: inherit;
+        box-shadow: inherit;
     }
 
     .step_active{
-        background: #5EC392;
         display: inline;
-        padding: 5px;
+        padding: 7px;
         margin: 1px;
-        font-weight: bold;
+        cursor: default;
+        font-size: 14pt;
+        padding-bottom: 4px !important;
+        border-bottom: 4px solid #41ba78 !important;
     }
 
-    .step_untouched
+    .step_hidden
     {
-        background: #DDDDDD;
+        background: #EFEFEF;
+        color: #999999;
     }
 
-    .step_invalid
+    .step_invalid::after
     {
-        background: #CC3300;
+        content: " \2612";
+        color: #CC3300;
     }
 
-    .step_valid
+    .step_valid::after
     {
-        background: #0FD57D;
+        content: " \2611";
+        color: #41ba78;
     }
 
-    .iframe_div {
+    .step_untouched::after
+    {
+        content: " \2610";
+    }
 
+    .iframe_div {
         width: calc(100% - 450px);
         margin-left: 70px;
         height: calc(100vh - 155px);
         position: absolute;
         border: none;
     }
+
     .iframe_elem {
         width: 100%;
         height: calc(100vh - 155px);
         border: none;
     }
+
+    #breadcrumbs {
+        background-color: inherit;
+    }
+
+    #breadcrumbs.breadcrumb > li {
+        border: 1px solid #cccccc;
+        border-left: none;
+    }
+    #breadcrumbs.breadcrumb > li:first-child {
+        border-left: 1px solid #cccccc;
+    }
+    #breadcrumbs.breadcrumb > li + li:before {
+        content: "";
+        width: 0;
+        margin: 0;
+        padding: 0;
+    }
 </style>
 
-<button id="gof" onclick="go(step+1)" class="btn go_btn go_forward">Go Forward</button>
-<button id="gob" onclick="go(step-1)" class="btn go_btn go_back">Go Back</button>
+<button id="gof" onclick="go('next')" class="btn go_btn go_forward">Go Forward</button>
+<button id="gob" onclick="go('prev')" class="btn go_btn go_back">Go Back</button>
 
 <div class="options">
     <button id="cancel_btn" class="btn btn-primary" onclick="cancel_wf()">Cancel</button>
 </div>
 <div class="btn_wrapper">
-<div id="breadcrumbs" class="btn-group">
-    <div class="btn-group" id="breadcrumb-wrapper">
-    </div>
-</div>
+<ol id="breadcrumbs" class="btn-group breadcrumb">
+</ol>
 </div>
 {% csrf_token %}
 
                 return;
             }
         }
-        if( to >= page_count )
-        {
-            to = page_count-1;
-        }
-        else if( to < 0 )
-        {
-            to = 0;
-        }
+
         var problem = function() {
             alert("There was a problem");
         }
     }
 
     function create_step(step_json, active){
-        var step_dom = document.createElement("DIV");
+        var step_dom = document.createElement("li");
         if(active){
             step_dom.className = "step_active";
 
             stat = "valid";
             msg = step_json['message'];
         }
+        if( step_json['enabled'] == false )
+        {
+            step_dom.classList.add("step_hidden");
+        }
         if(active)
         {
             update_message(msg, stat);
         step_dom.classList.add("btn");
 
         var step_number = step_json['index'];
-        step_dom.onclick = function(){ go(step_number); }
         return step_dom;
     }
 
index 8be7720..eb87728 100644 (file)
@@ -59,11 +59,11 @@ class Resource_Select(WorkflowStep):
             data = form.cleaned_data['generic_resource_bundle']
             data = data[2:-2]
             if not data:
-                self.metastep.set_invalid("Please select a valid bundle")
+                self.set_invalid("Please select a valid bundle")
                 return render(request, self.template, context)
             selected_bundle = json.loads(data)
             if len(selected_bundle) < 1:
-                self.metastep.set_invalid("Please select a valid bundle")
+                self.set_invalid("Please select a valid bundle")
                 return render(request, self.template, context)
             selected_id = selected_bundle[0]['id']
             gresource_bundle = None
@@ -86,11 +86,11 @@ class Resource_Select(WorkflowStep):
             confirm[self.confirm_key]["resource name"] = gresource_bundle.name
             self.repo_put(self.repo.CONFIRMATION, confirm)
             messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
             return render(request, self.template, context)
         else:
             messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
-            self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+            self.set_invalid("Please complete the fields highlighted in red to continue")
             return render(request, self.template, context)
 
 
@@ -135,11 +135,11 @@ class SWConfig_Select(WorkflowStep):
             bundle_json = form.cleaned_data['software_bundle']
             bundle_json = bundle_json[2:-2]  # Stupid django string bug
             if not bundle_json:
-                self.metastep.set_invalid("Please select a valid config")
+                self.set_invalid("Please select a valid config")
                 return self.render(request)
             bundle_json = json.loads(bundle_json)
             if len(bundle_json) < 1:
-                self.metastep.set_invalid("Please select a valid config")
+                self.set_invalid("Please select a valid config")
                 return self.render(request)
             bundle = None
             id = int(bundle_json[0]['id'])
@@ -148,7 +148,7 @@ class SWConfig_Select(WorkflowStep):
             grb = self.repo_get(self.repo.SELECTED_GRESOURCE_BUNDLE)
 
             if grb and bundle.bundle != grb:
-                self.metastep.set_invalid("Incompatible config selected for resource bundle")
+                self.set_invalid("Incompatible config selected for resource bundle")
                 return self.render(request)
             if not grb:
                 self.repo_set(self.repo.SELECTED_GRESOURCE_BUNDLE, bundle.bundle)
@@ -163,10 +163,10 @@ class SWConfig_Select(WorkflowStep):
                 confirm['booking'] = {}
             confirm['booking']["configuration name"] = bundle.name
             self.repo_put(self.repo.CONFIRMATION, confirm)
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
             messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
         else:
-            self.metastep.set_invalid("Please select or create a valid config")
+            self.set_invalid("Please select or create a valid config")
             messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
 
         return self.render(request)
@@ -270,9 +270,9 @@ class Booking_Meta(WorkflowStep):
             self.repo_put(self.repo.BOOKING_MODELS, models)
             self.repo_put(self.repo.CONFIRMATION, confirm)
             messages.add_message(request, messages.SUCCESS, 'Form Validated', fail_silently=True)
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
         else:
             messages.add_message(request, messages.ERROR, "Form didn't validate", fail_silently=True)
-            self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+            self.set_invalid("Please complete the fields highlighted in red to continue")
             context['form'] = form  # TODO: store this form
         return render(request, self.template, context)
index bf5751d..25d7e84 100644 (file)
@@ -158,12 +158,52 @@ class BookingAuthManager():
         return False
 
 
+class WorkflowStepStatus(object):
+    UNTOUCHED = 0
+    INVALID = 100
+    VALID = 200
+
+
 class WorkflowStep(object):
     template = 'bad_request.html'
     title = "Generic Step"
     description = "You were led here by mistake"
     short_title = "error"
     metastep = None
+    # phasing out metastep:
+
+    valid = WorkflowStepStatus.UNTOUCHED
+    message = ""
+
+    enabled = True
+
+    def cleanup(self):
+        raise Exception("WorkflowStep subclass of type " + str(type(self)) + " has no concrete implemented cleanup() method")
+
+    def enable(self):
+        if not self.enabled:
+            self.enabled = True
+
+    def disable(self):
+        if self.enabled:
+            self.cleanup()
+            self.enabled = False
+
+    def set_invalid(self, message, code=WorkflowStepStatus.INVALID):
+        self.valid = code
+        self.message = message
+
+    def set_valid(self, message, code=WorkflowStepStatus.VALID):
+        self.valid = code
+        self.message = message
+
+    def to_json(self):
+        return {
+            'title': self.short_title,
+            'enabled': self.enabled,
+            'valid': self.valid,
+            'message': self.message,
+        }
 
     def __init__(self, id, repo=None):
         self.repo = repo
@@ -205,6 +245,8 @@ class Confirmation_Step(WorkflowStep):
     title = "Confirm Changes"
     description = "Does this all look right?"
 
+    short_title = "confirm"
+
     def get_context(self):
         context = super(Confirmation_Step, self).get_context()
         context['form'] = ConfirmationForm()
@@ -245,12 +287,6 @@ class Confirmation_Step(WorkflowStep):
             pass
 
 
-class Workflow():
-
-    steps = []
-    active_index = 0
-
-
 class Repository():
 
     EDIT = "editing"
@@ -271,6 +307,7 @@ class Repository():
     CONFIG_MODELS = "configuration bundle models"
     OPNFV_MODELS = "opnfv configuration models"
     SESSION_USER = "session owner user account"
+    SESSION_MANAGER = "session manager for current session"
     VALIDATED_MODEL_GRB = "valid grb config model instance in db"
     VALIDATED_MODEL_CONFIG = "valid config model instance in db"
     VALIDATED_MODEL_BOOKING = "valid booking model instance in db"
index 26e1d7c..490d2f0 100644 (file)
@@ -41,11 +41,11 @@ class OPNFV_Resource_Select(WorkflowStep):
             bundle_json = form.cleaned_data['software_bundle']
             bundle_json = bundle_json[2:-2]  # Stupid django string bug
             if not bundle_json:
-                self.metastep.set_invalid("Please select a valid config")
+                self.set_invalid("Please select a valid config")
                 return self.render(request)
             bundle_json = json.loads(bundle_json)
             if len(bundle_json) < 1:
-                self.metastep.set_invalid("Please select a valid config")
+                self.set_invalid("Please select a valid config")
                 return self.render(request)
             bundle = None
             id = int(bundle_json[0]['id'])
@@ -53,11 +53,11 @@ class OPNFV_Resource_Select(WorkflowStep):
 
             models['configbundle'] = bundle
             self.repo_put(self.repo.OPNFV_MODELS, models)
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
             messages.add_message(request, messages.SUCCESS, 'Form Validated Successfully', fail_silently=True)
             self.update_confirmation()
         else:
-            self.metastep.set_invalid("Please select or create a valid config")
+            self.set_invalid("Please select or create a valid config")
             messages.add_message(request, messages.ERROR, "Form Didn't Validate", fail_silently=True)
 
         return self.render(request)
@@ -111,9 +111,9 @@ class Pick_Installer(WorkflowStep):
             models['scenario_chosen'] = scenario
             self.repo_put(self.repo.OPNFV_MODELS, models)
             self.update_confirmation()
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
         else:
-            self.metastep.set_invalid("Please select an Installer and Scenario")
+            self.set_invalid("Please select an Installer and Scenario")
 
         return self.render(request)
 
@@ -190,11 +190,11 @@ class Assign_Network_Roles(WorkflowStep):
                     "network": form.cleaned_data['network']
                 })
             models['network_roles'] = results
-            self.metastep.set_valid("Completed")
+            self.set_valid("Completed")
             self.repo_put(self.repo.OPNFV_MODELS, models)
             self.update_confirmation()
         else:
-            self.metastep.set_invalid("Please complete all fields")
+            self.set_invalid("Please complete all fields")
         return self.render(request)
 
 
@@ -276,11 +276,11 @@ class Assign_Host_Roles(WorkflowStep):  # taken verbatim from Define_Software in
             self.update_confirmation()
 
             if not has_jumphost:
-                self.metastep.set_invalid('Must have at least one "Jumphost" per POD')
+                self.set_invalid('Must have at least one "Jumphost" per POD')
             else:
-                self.metastep.set_valid("Completed")
+                self.set_valid("Completed")
         else:
-            self.metastep.set_invalid("Please complete all fields")
+            self.set_invalid("Please complete all fields")
 
         return self.render(request)
 
@@ -319,9 +319,9 @@ class MetaInfo(WorkflowStep):
             models['meta'] = info
             self.repo_put(self.repo.OPNFV_MODELS, models)
             self.update_confirmation()
-            self.metastep.set_valid("Complete")
+            self.set_valid("Complete")
         else:
-            self.metastep.set_invalid("Please correct the errors shown below")
+            self.set_invalid("Please correct the errors shown below")
 
         self.repo_put(self.repo.OPNFV_MODELS, models)
         return self.render(request)
index 536187f..ced355f 100644 (file)
@@ -134,16 +134,16 @@ class Define_Hardware(WorkflowStep):
             self.form = HardwareDefinitionForm(request.POST)
             if self.form.is_valid():
                 if len(json.loads(self.form.cleaned_data['filter_field'])['labs']) != 1:
-                    self.metastep.set_invalid("Please select one lab")
+                    self.set_invalid("Please select one lab")
                 else:
                     self.update_models(self.form.cleaned_data)
                     self.update_confirmation()
-                    self.metastep.set_valid("Step Completed")
+                    self.set_valid("Step Completed")
             else:
-                self.metastep.set_invalid("Please complete the fields highlighted in red to continue")
+                self.set_invalid("Please complete the fields highlighted in red to continue")
                 pass
         except Exception as e:
-            self.metastep.set_invalid(str(e))
+            self.set_invalid(str(e))
         self.context = self.get_context()
         return render(request, self.template, self.context)
 
@@ -239,11 +239,11 @@ class Define_Nets(WorkflowStep):
             xmlData = request.POST.get("xml")
             self.updateModels(xmlData)
             # update model with xml
-            self.metastep.set_valid("Networks applied successfully")
+            self.set_valid("Networks applied successfully")
         except ResourceAvailabilityException:
-            self.metastep.set_invalid("Public network not availble")
+            self.set_invalid("Public network not availble")
         except Exception as e:
-            self.metastep.set_invalid("An error occurred when applying networks: " + str(e))
+            self.set_invalid("An error occurred when applying networks: " + str(e))
         return self.render(request)
 
     def updateModels(self, xmlData):
@@ -425,10 +425,10 @@ class Resource_Meta_Info(WorkflowStep):
                 tmp = tmp[:60] + "..."
             confirm_info["description"] = tmp
             self.repo_put(self.repo.CONFIRMATION, confirm)
-            self.metastep.set_valid("Step Completed")
+            self.set_valid("Step Completed")
 
         else:
-            self.metastep.set_invalid("Please correct the fields highlighted in red to continue")
+            self.set_invalid("Please correct the fields highlighted in red to continue")
             pass
         return self.render(request)
 
index 34ac3a5..5414784 100644 (file)
@@ -52,11 +52,11 @@ class Select_Host_Step(WorkflowStep):
     def post_render(self, request):
         host_data = request.POST.get("host")
         if not host_data:
-            self.metastep.set_invalid("Please select a host")
+            self.set_invalid("Please select a host")
             return self.render(request)
         host = json.loads(host_data)
         if 'name' not in host or 'booking' not in host:
-            self.metastep.set_invalid("Invalid host selected")
+            self.set_invalid("Invalid host selected")
             return self.render(request)
         name = host['name']
         booking_id = host['booking']
@@ -75,7 +75,7 @@ class Select_Host_Step(WorkflowStep):
         snap_confirm['host'] = name
         confirm['snapshot'] = snap_confirm
         self.repo_put(self.repo.CONFIRMATION, confirm)
-        self.metastep.set_valid("Success")
+        self.set_valid("Success")
         return self.render(request)
 
 
@@ -112,8 +112,8 @@ class Image_Meta_Step(WorkflowStep):
             confirm['snapshot'] = snap_confirm
             self.repo_put(self.repo.CONFIRMATION, confirm)
 
-            self.metastep.set_valid("Success")
+            self.set_valid("Success")
         else:
-            self.metastep.set_invalid("Please Fill out the Form")
+            self.set_invalid("Please Fill out the Form")
 
         return self.render(request)
index a6a7464..329b716 100644 (file)
@@ -113,7 +113,7 @@ class Define_Software(WorkflowStep):
             context['headnode'] = self.repo_get(self.repo.CONFIG_MODELS, {}).get("headnode_index", 1)
         else:
             context["error"] = "Please select a resource first"
-            self.metastep.set_invalid("Step requires information that is not yet provided by previous step")
+            self.set_invalid("Step requires information that is not yet provided by previous step")
 
         return context
 
@@ -152,7 +152,7 @@ class Define_Software(WorkflowStep):
                 })
 
             if not has_headnode:
-                self.metastep.set_invalid('Must have one "Headnode" per POD')
+                self.set_invalid('Must have one "Headnode" per POD')
                 return self.render(request)
 
             self.repo_put(self.repo.CONFIG_MODELS, models)
@@ -160,9 +160,9 @@ class Define_Software(WorkflowStep):
                 confirm['configuration'] = {}
             confirm['configuration']['hosts'] = confirm_hosts
             self.repo_put(self.repo.CONFIRMATION, confirm)
-            self.metastep.set_valid("Completed")
+            self.set_valid("Completed")
         else:
-            self.metastep.set_invalid("Please complete all fields")
+            self.set_invalid("Please complete all fields")
 
         return self.render(request)
 
@@ -201,9 +201,9 @@ class Config_Software(WorkflowStep):
 
             confirm['configuration']['name'] = form.cleaned_data['name']
             confirm['configuration']['description'] = form.cleaned_data['description']
-            self.metastep.set_valid("Complete")
+            self.set_valid("Complete")
         else:
-            self.metastep.set_invalid("Please correct the errors shown below")
+            self.set_invalid("Please correct the errors shown below")
 
         self.repo_put(self.repo.CONFIG_MODELS, models)
         self.repo_put(self.repo.CONFIRMATION, confirm)
index 6d59b1c..f2e37ef 100644 (file)
@@ -54,7 +54,12 @@ def step_view(request):
         # no manager found, redirect to "lost" page
         return no_workflow(request)
     if request.GET.get('step') is not None:
-        manager.goto(int(request.GET.get('step')))
+        if request.GET.get('step') == 'next':
+            manager.go_next()
+        elif request.GET.get('step') == 'prev':
+            manager.go_prev()
+        else:
+            raise Exception("requested action for new step had malformed contents: " + request.GET.get('step'))
     return manager.render(request)
 
 
index db2bba1..08cf296 100644 (file)
@@ -21,27 +21,6 @@ import logging
 logger = logging.getLogger(__name__)
 
 
-class BookingMetaWorkflow(object):
-    workflow_type = 0
-    color = "#0099ff"
-    is_child = False
-
-
-class ResourceMetaWorkflow(object):
-    workflow_type = 1
-    color = "#ff6600"
-
-
-class ConfigMetaWorkflow(object):
-    workflow_type = 2
-    color = "#00ffcc"
-
-
-class OPNFVMetaWorkflow(object):
-    workflow_type = 3
-    color = "000000"
-
-
 class MetaStep(object):
 
     UNTOUCHED = 0
@@ -60,6 +39,7 @@ class MetaStep(object):
         self.short_title = "error"
         self.skip_step = 0
         self.valid = 0
+        self.hidden = False
         self.message = ""
         self.id = uuid.uuid4()
 
@@ -85,10 +65,9 @@ class MetaStep(object):
 
 
 class Workflow(object):
-    def __init__(self, steps, metasteps, repository):
+    def __init__(self, steps, repository):
         self.repository = repository
         self.steps = steps
-        self.metasteps = metasteps
         self.active_index = 0
 
 
@@ -134,18 +113,13 @@ class WorkflowFactory():
         ]
 
         steps = self.make_steps(workflow_types[workflow_type], repository=repo)
-        meta_steps = self.metaize(steps=steps, wf_type=workflow_type)
-        return steps, meta_steps
+        return steps
 
     def create_workflow(self, workflow_type=None, repo=None):
-        steps, meta_steps = self.conjure(workflow_type, repo)
+        steps = self.conjure(workflow_type, repo)
         c_step = self.make_step(Confirmation_Step, repo)
-        metaconfirm = MetaStep()
-        metaconfirm.short_title = "confirm"
-        metaconfirm.index = len(steps)
         steps.append(c_step)
-        meta_steps.append(metaconfirm)
-        return Workflow(steps, meta_steps, repo)
+        return Workflow(steps, repo)
 
     def make_steps(self, step_types, repository):
         steps = []
@@ -157,13 +131,3 @@ class WorkflowFactory():
     def make_step(self, step_type, repository):
         iden = step_type.description + step_type.title + step_type.template
         return step_type(iden, repository)
-
-    def metaize(self, steps, wf_type):
-        meta_dict = []
-        for step in steps:
-            meta_step = MetaStep()
-            meta_step.short_title = step.short_title
-            meta_dict.append(meta_step)
-            step.metastep = meta_step
-
-        return meta_dict
index 89a9d96..525aa6f 100644 (file)
@@ -35,6 +35,16 @@ class SessionManager():
 
         self.factory = WorkflowFactory()
 
+    def set_step_statuses(self, superclass_type, desired_enabled=True):
+        workflow = self.active_workflow()
+        steps = workflow.steps
+        for step in steps:
+            if isinstance(step, superclass_type):
+                if desired_enabled:
+                    step.enable()
+                else:
+                    step.disable()
+
     def add_workflow(self, workflow_type=None, target_id=None, **kwargs):
         if target_id is not None:
             self.prefill_repo(target_id, workflow_type)
@@ -45,6 +55,7 @@ class SessionManager():
             repo.set_defaults(defaults)
             repo.el[repo.HAS_RESULT] = False
         repo.el[repo.SESSION_USER] = self.owner
+        repo.el[repo.SESSION_MANAGER] = self
         self.workflows.append(
             self.factory.create_workflow(
                 workflow_type=workflow_type,
@@ -65,11 +76,11 @@ class SessionManager():
 
     def status(self, request):
         try:
-            meta_steps = []
-            for step in self.active_workflow().metasteps:
-                meta_steps.append(step.to_json())
+            meta_json = []
+            for step in self.active_workflow().steps:
+                meta_json.append(step.to_json())
             responsejson = {}
-            responsejson["steps"] = meta_steps
+            responsejson["steps"] = meta_json
             responsejson["active"] = self.active_workflow().repository.el['active_step']
             responsejson["workflow_count"] = len(self.workflows)
             return JsonResponse(responsejson, safe=False)
@@ -86,10 +97,23 @@ class SessionManager():
     def post_render(self, request):
         return self.active_workflow().steps[self.active_workflow().active_index].post_render(request)
 
-    def goto(self, num, **kwargs):
-        self.active_workflow().repository.el['active_step'] = int(num)
-        self.active_workflow().active_index = int(num)
-        # TODO: change to include some checking
+    def go_next(self, **kwargs):
+        next_step = self.active_workflow().active_index + 1
+        if next_step >= len(self.active_workflow().steps):
+            raise Exception("Out of bounds request for step")
+        while not self.active_workflow().steps[next_step].enabled:
+            next_step += 1
+        self.active_workflow().repository.el['active_step'] = next_step
+        self.active_workflow().active_index = next_step
+
+    def go_prev(self, **kwargs):
+        prev_step = self.active_workflow().active_index - 1
+        if prev_step < 0:
+            raise Exception("Out of bounds request for step")
+        while not self.active_workflow().steps[prev_step].enabled:
+            prev_step -= 1
+        self.active_workflow().repository.el['active_step'] = prev_step
+        self.active_workflow().active_index = prev_step
 
     def prefill_repo(self, target_id, workflow_type):
         self.repository.el[self.repository.EDIT] = True