diff --git a/opentech/apply/activity/migrations/0002_activity_type.py b/opentech/apply/activity/migrations/0002_activity_type.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0688ecb048114d572c479d534d4632264d7f36d
--- /dev/null
+++ b/opentech/apply/activity/migrations/0002_activity_type.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-03-01 15:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('activity', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='activity',
+            name='type',
+            field=models.CharField(choices=[('comment', 'Comment'), ('activity', 'Activity')], default='comment', max_length=30),
+            preserve_default=False,
+        ),
+    ]
diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
index c01132e720799eeb537f136b087ec7ed7ed764bb..ae0acc1145982ecaa2a44d76c5c9a88153b17a2b 100644
--- a/opentech/apply/activity/models.py
+++ b/opentech/apply/activity/models.py
@@ -1,12 +1,45 @@
 from django.conf import settings
 from django.db import models
 
+COMMENT = 'comment'
+ACTION = 'action'
+
+ACTIVITY_TYPES = {
+    COMMENT: 'Comment',
+    ACTION: 'Action',
+}
+
+
+class ActivityBaseManager(models.Manager):
+    def create(self, **kwargs):
+        kwargs.update(type=self.type)
+        return super().create(**kwargs)
+
+    def get_queryset(self):
+        return super().get_queryset().filter(type=self.type)
+
+
+class CommentManger(ActivityBaseManager):
+    type = COMMENT
+
+
+class ActionManager(ActivityBaseManager):
+    type = ACTION
+
 
 class Activity(models.Model):
     timestamp = models.DateTimeField(auto_now_add=True)
+    type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30)
     user = models.ForeignKey(settings.AUTH_USER_MODEL)
     submission = models.ForeignKey('funds.ApplicationSubmission', related_name='activities')
     message = models.TextField()
 
+    objects = models.Manager()
+    comments = CommentManger()
+    actions = ActionManager()
+
     class Meta:
         ordering = ['-timestamp']
+
+    def __str__(self):
+        return '{}: for "{}"'.format(self.get_type_display(), self.submission)
diff --git a/opentech/apply/activity/templates/activity/include/action_list.html b/opentech/apply/activity/templates/activity/include/action_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..ebd8bbaa11232a7312f180a2ec98681b9ec50958
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/action_list.html
@@ -0,0 +1,3 @@
+{% for action in actions %}
+    {% include "activity/include/listing_base.html" with activity=action %}
+{% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/all_activity_list.html b/opentech/apply/activity/templates/activity/include/all_activity_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..312a0b3294fa6438dbb81bb3da4973e7151f152f
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/all_activity_list.html
@@ -0,0 +1,3 @@
+{% for activity in all_activity %}
+    {% include "activity/include/listing_base.html" with activity=activity %}
+{% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/comment_form.html b/opentech/apply/activity/templates/activity/include/comment_form.html
index 06a11b9a96a12198ba7d7702bc2722cb92941837..c20300aec2bd4315b05c7d672885b34c9c9e602d 100644
--- a/opentech/apply/activity/templates/activity/include/comment_form.html
+++ b/opentech/apply/activity/templates/activity/include/comment_form.html
@@ -1,5 +1,5 @@
-<form method="post">
+<form method="post" id="comment-form">
     {% csrf_token %}
     {{ comment_form }}
-    <input type="submit" value="Comment">
+    <input id="comment-form-submit" name="form-submitted" type="submit" form="comment-form" value="Comment">
 </form>
diff --git a/opentech/apply/activity/templates/activity/include/comment_list.html b/opentech/apply/activity/templates/activity/include/comment_list.html
index 9217e30137f2b12b4fc33ddc9fccac8d5a409260..cfd58fa44049d4fd9544e96d4aa58ddb506e2de1 100644
--- a/opentech/apply/activity/templates/activity/include/comment_list.html
+++ b/opentech/apply/activity/templates/activity/include/comment_list.html
@@ -1,7 +1,3 @@
 {% for comment in comments %}
-<div>
-    <p>{{ comment.timestamp }}</p>
-    <p>{{ comment.user }}</p>
-    <p>{{ comment.message }}</p>
-</div>
+    {% include "activity/include/listing_base.html" with activity=comment %}
 {% endfor %}
diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html
new file mode 100644
index 0000000000000000000000000000000000000000..8d44760d741a3ec26f32fc3f51b9dc0ba634d1dc
--- /dev/null
+++ b/opentech/apply/activity/templates/activity/include/listing_base.html
@@ -0,0 +1,5 @@
+<div>
+    <p>{{ activity.timestamp }}</p>
+    <p>{{ activity.user }}</p>
+    <p>{{ activity.message }}</p>
+</div>
diff --git a/opentech/apply/activity/views.py b/opentech/apply/activity/views.py
index 5a2c82c874f3569a6dc4359f4669921c16f103de..c680bda9d570f291388d6425936e9c4182aa7fc9 100644
--- a/opentech/apply/activity/views.py
+++ b/opentech/apply/activity/views.py
@@ -1,20 +1,33 @@
-from django.views.generic import CreateView
+from django.views.generic import CreateView, View
 
 from .forms import CommentForm
-from .models import Activity
+from .models import Activity, COMMENT
 
 
-class CommentContextMixin:
+ACTIVITY_LIMIT = 50
+
+
+class AllActivityContextMixin:
+    def get_context_data(self, **kwargs):
+        extra = {
+            'actions': Activity.actions.filter(submission__in=self.object_list)[:ACTIVITY_LIMIT],
+            'comments': Activity.comments.filter(submission__in=self.object_list[:ACTIVITY_LIMIT]),
+            'all_activity': Activity.objects.filter(submission__in=self.object_list)[:ACTIVITY_LIMIT],
+        }
+        return super().get_context_data(**extra, **kwargs)
+
+
+class ActivityContextMixin:
     def get_context_data(self, **kwargs):
         extra = {
-            'comments': Activity.objects.filter(submission=self.object),
-            CommentFormView.context_name: CommentFormView.form_class(),
+            'actions': Activity.actions.filter(submission=self.object),
+            'comments': Activity.comments.filter(submission=self.object),
         }
 
         return super().get_context_data(**extra, **kwargs)
 
 
-class DelegatedCreateView(CreateView):
+class DelegatedViewMixin(View):
     """For use on create views accepting forms from another view"""
     def get_template_names(self):
         return self.kwargs['template_names']
@@ -26,15 +39,25 @@ class DelegatedCreateView(CreateView):
         kwargs.update(**{self.context_name: form})
         return super().get_context_data(**kwargs)
 
+    @classmethod
+    def contribute_form(cls, submission):
+        return cls.context_name, cls.form_class(instance=submission)
 
-class CommentFormView(DelegatedCreateView):
+
+class CommentFormView(DelegatedViewMixin, CreateView):
     form_class = CommentForm
     context_name = 'comment_form'
 
     def form_valid(self, form):
         form.instance.user = self.request.user
         form.instance.submission = self.kwargs['submission']
+        form.instance.type = COMMENT
         return super().form_valid(form)
 
     def get_success_url(self):
-        return self.object.application.get_absolute_url()
+        return self.object.submission.get_absolute_url()
+
+    @classmethod
+    def contribute_form(cls, submission):
+        # We dont want to pass the submission as the instance
+        return super().contribute_form(None)
diff --git a/opentech/apply/funds/admin_forms.py b/opentech/apply/funds/admin_forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..643ab0d9012941d53390c04c154df894918814d5
--- /dev/null
+++ b/opentech/apply/funds/admin_forms.py
@@ -0,0 +1,34 @@
+from wagtail.wagtailadmin.forms import WagtailAdminPageForm
+
+
+class WorkflowFormAdminForm(WagtailAdminPageForm):
+    def clean(self):
+        cleaned_data = super().clean()
+        model = self._meta.model
+
+        workflow = model.workflow_class_from_name(cleaned_data['workflow_name'])
+        application_forms = self.formsets['forms']
+
+        self.validate_stages_equal_forms(workflow, application_forms)
+
+        return cleaned_data
+
+    def validate_stages_equal_forms(self, workflow, application_forms):
+        if application_forms.is_valid():
+            valid_forms = [form for form in application_forms if not form.cleaned_data['DELETE']]
+            number_of_forms = len(valid_forms)
+            plural_form = 's' if number_of_forms > 1 else ''
+
+            number_of_stages = len(workflow.stage_classes)
+            plural_stage = 's' if number_of_stages > 1 else ''
+
+            if number_of_forms != number_of_stages:
+                self.add_error(
+                    None,
+                    'Number of forms does not match number of stages: '
+                    f'{number_of_stages} stage{plural_stage} and {number_of_forms} '
+                    f'form{plural_form} provided',
+                )
+
+                for form in valid_forms[number_of_stages:]:
+                    form.add_error('form', 'Exceeds required number of forms for stage, please remove.')
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 643ab0d9012941d53390c04c154df894918814d5..e74a7449e5a9b4aa3fea1ace29af62a6b96b9e8e 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -1,34 +1,34 @@
-from wagtail.wagtailadmin.forms import WagtailAdminPageForm
+from django import forms
 
+from .models import ApplicationSubmission
 
-class WorkflowFormAdminForm(WagtailAdminPageForm):
-    def clean(self):
-        cleaned_data = super().clean()
-        model = self._meta.model
 
-        workflow = model.workflow_class_from_name(cleaned_data['workflow_name'])
-        application_forms = self.formsets['forms']
+class ProgressSubmissionForm(forms.ModelForm):
+    action = forms.ChoiceField()
 
-        self.validate_stages_equal_forms(workflow, application_forms)
+    class Meta:
+        model = ApplicationSubmission
+        fields: list = []
 
-        return cleaned_data
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        choices = [(action, action) for action in self.instance.phase.action_names]
+        self.fields['action'].choices = choices
+        self.fields['action'].label = self.instance.phase.name
+        self.should_show = bool(choices)
 
-    def validate_stages_equal_forms(self, workflow, application_forms):
-        if application_forms.is_valid():
-            valid_forms = [form for form in application_forms if not form.cleaned_data['DELETE']]
-            number_of_forms = len(valid_forms)
-            plural_form = 's' if number_of_forms > 1 else ''
+    def save(self, *args, **kwargs):
+        new_phase = self.instance.workflow.process(self.instance.phase, self.cleaned_data['action'])
+        self.instance.status = str(new_phase)
+        return super().save(*args, **kwargs)
 
-            number_of_stages = len(workflow.stage_classes)
-            plural_stage = 's' if number_of_stages > 1 else ''
 
-            if number_of_forms != number_of_stages:
-                self.add_error(
-                    None,
-                    'Number of forms does not match number of stages: '
-                    f'{number_of_stages} stage{plural_stage} and {number_of_forms} '
-                    f'form{plural_form} provided',
-                )
+class UpdateSubmissionLeadForm(forms.ModelForm):
+    class Meta:
+        model = ApplicationSubmission
+        fields = ('lead',)
 
-                for form in valid_forms[number_of_stages:]:
-                    form.add_error('form', 'Exceeds required number of forms for stage, please remove.')
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        qs = self.fields['lead'].queryset
+        self.fields['lead'].queryset = qs.exclude(id=self.instance.lead.id)
diff --git a/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py b/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d68ac6179b435e8d6a2187c814cb6e72bed0d53
--- /dev/null
+++ b/opentech/apply/funds/migrations/0026_add_leads_to_submission_and_lab.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2018-03-02 17:08
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('funds', '0025_update_with_file_blocks'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='applicationsubmission',
+            name='lead',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='submission_lead', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='labtype',
+            name='lead',
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='lab_lead', to=settings.AUTH_USER_MODEL),
+            preserve_default=False,
+        ),
+    ]
diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py
index f1eb0b1a2937842f3a231d09df58206b8e9de718..c36510831779e8db5128b3da927b7a8b58fbf82e 100644
--- a/opentech/apply/funds/models.py
+++ b/opentech/apply/funds/models.py
@@ -38,7 +38,7 @@ from opentech.apply.users.groups import STAFF_GROUP_NAME
 
 from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES
 from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
-from .forms import WorkflowFormAdminForm
+from .admin_forms import WorkflowFormAdminForm
 from .workflow import SingleStage, DoubleStage
 
 
@@ -382,11 +382,17 @@ class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ig
     class Meta:
         verbose_name = _("Lab")
 
+    lead = models.ForeignKey(settings.AUTH_USER_MODEL, limit_choices_to={'groups__name': STAFF_GROUP_NAME}, related_name='lab_lead')
+
     parent_page_types = ['apply_home.ApplyHomePage']
     subpage_types = []  # type: ignore
 
+    content_panels = WorkflowStreamForm.content_panels + [
+        FieldPanel('lead')
+    ]
+
     edit_handler = TabbedInterface([
-        ObjectList(WorkflowStreamForm.content_panels, heading='Content'),
+        ObjectList(content_panels, heading='Content'),
         EmailForm.email_tab,
         ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
     ])
@@ -434,6 +440,7 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
     form_fields = StreamField(CustomFormFieldsBlock())
     page = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT)
     round = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT, related_name='submissions', null=True)
+    lead = models.ForeignKey(settings.AUTH_USER_MODEL, limit_choices_to={'groups__name': STAFF_GROUP_NAME}, related_name='submission_lead')
     user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
     search_data = models.TextField()
 
@@ -515,6 +522,12 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
                 self.workflow_name = self.page.workflow_name
             self.status = str(self.workflow.first())
 
+            try:
+                self.lead = self.round.specific.lead
+            except AttributeError:
+                # Its a lab
+                self.lead = self.page.specific.lead
+
         # add a denormed version of the answer for searching
         self.search_data = ' '.join(self.prepare_search_values())
 
@@ -568,4 +581,7 @@ class ApplicationSubmission(WorkflowHelpers, AbstractFormSubmission):
         raise AttributeError('{} has no attribute "{}"'.format(repr(self), item))
 
     def __str__(self):
-        return str(super().__str__())
+        return f'{self.title} from {self.full_name} for {self.page.title}'
+
+    def __repr__(self):
+        return f'<{self.__class__.__name__}: {str(self.form_data)}>'
diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
index fd87b677c8e282c0d8d45c3e6ba37663f777e38f..d6a2e2805a0d886ee8e88541807e7b2cf971073e 100644
--- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
+++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
@@ -9,9 +9,9 @@
             <span>{{ object.stage }}</span>
             <span>{{ object.page }}</span>
             <span>{{ object.round }}</span>
-            <span>Lead: {{ object.round.specific.lead }}</span>
+            <span>Lead: {{ object.lead }}</span>
         </h5>
-        {% include "funds/includes/status_bar.html" with workflow=object.workflow status=object.status %}
+        {% include "funds/includes/status_bar.html" with workflow=object.workflow status=object.phase %}
     </div>
 </div>
 
@@ -50,6 +50,8 @@
         </div>
 
         <aside class="sidebar">
+            {% include "funds/includes/progress_form.html" %}
+            {% include "funds/includes/update_lead_form.html" %}
             {% if other_submissions %}
                 <div class="sidebar__inner">
                 <h6 class="heading heading--light-grey heading--small heading--uppercase">Past Submissions</h6>
@@ -61,6 +63,7 @@
             {% endif %}
             {% include "activity/include/comment_form.html" %}
             {% include "activity/include/comment_list.html" %}
+            {% include "activity/include/action_list.html" %}
         </aside>
     </div>
 </div>
diff --git a/opentech/apply/funds/templates/funds/includes/progress_form.html b/opentech/apply/funds/templates/funds/includes/progress_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..8d4b54481406eb93bf14c6ba74062824fbea05c7
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/progress_form.html
@@ -0,0 +1,7 @@
+{% if progress_form.should_show %}
+<form method="post" id="progress-form">
+    {% csrf_token %}
+    {{ progress_form }}
+    <input id="progress-form-submit" name="form-submitted" type="submit" form="progress-form" value="Progress">
+</form>
+{% endif %}
diff --git a/opentech/apply/funds/templates/funds/includes/status_bar.html b/opentech/apply/funds/templates/funds/includes/status_bar.html
index d99ed4c6819bdd39b36cc41f6cb3fa29f41c9bde..9dfbdb9409a9d53f9c7c41ddd4d36b50fc6e64f9 100644
--- a/opentech/apply/funds/templates/funds/includes/status_bar.html
+++ b/opentech/apply/funds/templates/funds/includes/status_bar.html
@@ -1,17 +1,26 @@
 <div class="status-bar">
-    {# '.status-bar__item--is-complete' needs to be added to each 'complete' step #}
-    {# '.status-bar__item--is-current' needs to be added to the current step #}
-    {% for phase in workflow %}
-        <div class="status-bar__item {% if phase == status %}status-bar__item--is-current{% endif %}">
-            <span class="status-bar__tooltip" data-title="{{ phase.name }}" aria-label="{{ phase.name }}"></span>
+    {% for phase in status.stage %}
+        <div class="status-bar__item
+                    {% if phase == status %}
+                        status-bar__item--is-current
+                    {% elif phase.step < status.step %}
+                        status-bar__item--is-complete
+                    {% endif %}">
+            <span class="status-bar__tooltip"
+                {% if phase == status %}
+                    data-title="{{ status.name }}" aria-label="{{ status.name }}"
+                {% else %}
+                    data-title="{{ phase.name }}" aria-label="{{ phase.name }}"
+                {% endif %}
+            ></span>
             <svg class="status-bar__icon"><use xlink:href="#tick-alt"></use></svg>
         </div>
     {% endfor %}
 </div>
 <div class="status-bar--mobile">
-    {% for phase in workflow %}
+    {% for phase in status.stage %}
         {% if phase == status %}
-            <h6 class="status-bar__subheading">{{ phase.name }}</h6>
+            <h6 class="status-bar__subheading">{{ status.name }}</h6>
         {% endif %}
     {% endfor %}
 </div>
diff --git a/opentech/apply/funds/templates/funds/includes/update_lead_form.html b/opentech/apply/funds/templates/funds/includes/update_lead_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..b6ad97907a33f678a11a2560a5927cfc96755aaf
--- /dev/null
+++ b/opentech/apply/funds/templates/funds/includes/update_lead_form.html
@@ -0,0 +1,5 @@
+<form method="post" id="update-lead-form">
+    {% csrf_token %}
+    {{ lead_form }}
+    <input id="update-form-submit" name="form-submitted" type="submit" form="update-lead-form" value="Update">
+</form>
diff --git a/opentech/apply/funds/templates/funds/submissions.html b/opentech/apply/funds/templates/funds/submissions.html
index 3428cc9a7d1cc291ff9c14e714f62d167a7d14c8..f039d269e2cac92256acb8b8ece3c1a753a740aa 100644
--- a/opentech/apply/funds/templates/funds/submissions.html
+++ b/opentech/apply/funds/templates/funds/submissions.html
@@ -39,6 +39,10 @@
     {% render_table table %}
 </div>
 
+{% include "activity/include/comment_list.html" %}
+{% include "activity/include/action_list.html" %}
+{% include "activity/include/all_activity_list.html" %}
+
 {% endblock %}
 
 {% block extra_js %}
diff --git a/opentech/apply/funds/tests/test_workflow.py b/opentech/apply/funds/tests/test_workflow.py
index ad1898c37ed551200ad44aa9a6449716b6df4cd0..7fd8a08ed07478d85b1d314010143294d10c1605 100644
--- a/opentech/apply/funds/tests/test_workflow.py
+++ b/opentech/apply/funds/tests/test_workflow.py
@@ -58,6 +58,28 @@ class TestStageCreation(SimpleTestCase):
         self.assertEqual(stage.name, name)
         self.assertEqual(stage.form, form)
 
+    def test_can_create_with_multi_phase_step(self):
+        first_phase, second_phase = Phase(name='first'), Phase(name='second')
+        change_first = ChangePhaseAction(first_phase, 'first')
+        change_second = ChangePhaseAction(second_phase, 'second')
+
+        class PhaseSwitch(Phase):
+            actions = [change_first, change_second]
+
+        class MultiPhaseStep(Stage):
+            name = 'stage'
+            phases = [
+                PhaseSwitch(),
+                [first_phase, second_phase],
+            ]
+
+        stage = MultiPhaseStep(None)
+        self.assertEqual(stage.steps, 2)
+
+        current_phase = stage.phases[0]
+        self.assertEqual(current_phase.process(change_first.name), stage.phases[1])  # type: ignore
+        self.assertEqual(current_phase.process(change_second.name), stage.phases[2])  # type: ignore
+
     def test_can_get_next_phase(self):
         stage = StageFactory.build(num_phases=2)
         self.assertEqual(stage.next(stage.phases[0]), stage.phases[1])
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 27781e88163dd5250f14de6db72ed4cdc511dcf8..31b4536c2fada2df244a4cbcf9f25967c15379d5 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -1,18 +1,25 @@
 from django import forms
 from django.template.response import TemplateResponse
-from django.views.generic import DetailView
+from django.views.generic import DetailView, UpdateView, View
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
 
-from opentech.apply.activity.views import CommentContextMixin, CommentFormView
+from opentech.apply.activity.views import (
+    AllActivityContextMixin,
+    ActivityContextMixin,
+    CommentFormView,
+    DelegatedViewMixin,
+)
+from opentech.apply.activity.models import Activity
 
+from .forms import ProgressSubmissionForm, UpdateSubmissionLeadForm
 from .models import ApplicationSubmission
 from .tables import SubmissionsTable, SubmissionFilter, SubmissionFilterAndSearch
 from .workflow import SingleStage, DoubleStage
 
 
-class SubmissionListView(SingleTableMixin, FilterView):
+class SubmissionListView(AllActivityContextMixin, SingleTableMixin, FilterView):
     template_name = 'funds/submissions.html'
     table_class = SubmissionsTable
 
@@ -42,13 +49,54 @@ class SubmissionSearchView(SingleTableMixin, FilterView):
         )
 
 
-class SubmissionDetailView(CommentContextMixin, DetailView):
+class ProgressSubmissionView(DelegatedViewMixin, UpdateView):
     model = ApplicationSubmission
+    form_class = ProgressSubmissionForm
+    context_name = 'progress_form'
+
+    def form_valid(self, form):
+        old_phase = form.instance.phase.name
+        response = super().form_valid(form)
+        new_phase = form.instance.phase.name
+        Activity.actions.create(
+            user=self.request.user,
+            submission=self.kwargs['submission'],
+            message=f'Progressed from {old_phase} to {new_phase}'
+        )
+        return response
+
+
+class UpdateLeadView(DelegatedViewMixin, UpdateView):
+    model = ApplicationSubmission
+    form_class = UpdateSubmissionLeadForm
+    context_name = 'lead_form'
+
+    def form_valid(self, form):
+        old_lead = form.instance.lead
+        response = super().form_valid(form)
+        new_lead = form.instance.lead
+        Activity.actions.create(
+            user=self.request.user,
+            submission=self.kwargs['submission'],
+            message=f'Lead changed from {old_lead} to {new_lead}'
+        )
+        return response
+
+
+class SubmissionDetailView(ActivityContextMixin, DetailView):
+    model = ApplicationSubmission
+    form_views = {
+        'progress': ProgressSubmissionView,
+        'comment': CommentFormView,
+        'update': UpdateLeadView,
+    }
 
     def get_context_data(self, **kwargs):
+        forms = dict(form_view.contribute_form(self.object) for form_view in self.form_views.values())
         return super().get_context_data(
             other_submissions=self.model.objects.filter(user=self.object.user).exclude(id=self.object.id),
-            **kwargs
+            **forms,
+            **kwargs,
         )
 
     def post(self, request, *args, **kwargs):
@@ -60,7 +108,8 @@ class SubmissionDetailView(CommentContextMixin, DetailView):
         kwargs['template_names'] = self.get_template_names()
         kwargs['context'] = self.get_context_data()
 
-        view = CommentFormView.as_view()
+        form_submitted = request.POST['form-submitted'].lower()
+        view = self.form_views[form_submitted].as_view()
 
         return view(request, *args, **kwargs)
 
diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py
index b356dd158876933ec3ab865c374fea8839389a40..36dbc2ec84b58cf47fc7d98675c5811fade3ef54 100644
--- a/opentech/apply/funds/workflow.py
+++ b/opentech/apply/funds/workflow.py
@@ -1,6 +1,7 @@
+from collections import defaultdict
 import copy
 
-from typing import Iterable, Iterator, List, Sequence, Type, Union
+from typing import Dict, Iterable, Iterator, List, Sequence, Type, Union
 
 from django.forms import Form
 from django.utils.text import slugify
@@ -12,14 +13,14 @@ Workflow -> Stage -> Phase -> Action
 """
 
 
-def phase_name(stage: 'Stage', phase: Union['Phase', str], occurrence: int) -> str:
+def phase_name(stage: 'Stage', phase: Union['Phase', str], step: int) -> str:
     # Build the identifiable name for a phase
     if not isinstance(phase, str):
         phase_name = phase._internal
     else:
         phase_name = phase
 
-    return '__'.join([stage.name, phase_name, str(occurrence)])
+    return '__'.join([stage.name, phase_name, str(step)])
 
 
 class Workflow(Iterable):
@@ -113,27 +114,43 @@ class Stage(Iterable):
         # TODO: consider removing form from stage as the stage is generic and
         # shouldn't care about forms.
         self.form = form
+        self.steps = len(self.phases)
         # Make the phases new instances to prevent errors with mutability
-        existing_phases: set = set()
-        new_phases: list = list()
-        for phase in self.phases:
-            phase.stage = self
-            while str(phase) in existing_phases:
-                phase.occurrence += 1
-            existing_phases.add(str(phase))
-            new_phases.append(copy.copy(phase))
-        self.phases = new_phases
-
-    def __iter__(self) -> Iterator['Phase']:
-        yield from self.phases
+        self.phases = self.copy_phases(self.phases)
+
+    def copy_phases(self, phases: List['Phase']) -> List['Phase']:
+        new_phases = list()
+        for step, phase in enumerate(self.phases):
+            try:
+                new_phases.append(self.copy_phase(phase, step))
+            except AttributeError:
+                # We have a step with multiple equivalent phases
+                for sub_phase in phase:
+                    new_phases.append(self.copy_phase(sub_phase, step))
+        return new_phases
+
+    def copy_phase(self, phase: 'Phase', step: int) -> 'Phase':
+        phase.stage = self
+        phase.step = step
+        return copy.copy(phase)
+
+    def __iter__(self) -> 'PhaseIterator':
+        return PhaseIterator(self.phases, self.steps)
 
     def __str__(self) -> str:
         return self.name
 
     def get_phase(self, phase_name: str) -> 'Phase':
         for phase in self.phases:
-            if str(phase) == phase_name:
+            if phase == phase_name:
+                return phase
+
+        # We don't have the exact name
+        for phase in self.phases:
+            if phase._internal == phase_name:
+                # Grab the first phase to match the name
                 return phase
+
         return None
 
     def first(self) -> 'Phase':
@@ -152,6 +169,45 @@ class Stage(Iterable):
         return None
 
 
+class PhaseIterator(Iterator):
+    class Step:
+        """Allow handling phases which are equivalent e.g. outcomes (accepted/rejected)
+        Delegates to the underlying phases except where naming is concerned
+        """
+        def __init__(self, phases: List['Phase']) -> None:
+            self.phases = phases
+
+        @property
+        def step(self) -> int:
+            return self.phases[0].step
+
+        @property
+        def name(self) -> str:
+            # Hardcode a name for multi-phased step - always outcome at the moment
+            if len(self.phases) > 1:
+                return 'Outcome'
+            return self.phases[0].name
+
+        def __eq__(self, other: object) -> bool:
+            return any(phase == other for phase in self.phases)
+
+    def __init__(self, phases: List['Phase'], steps: int) -> None:
+        self.current = 0
+        self.phases: Dict[int, List['Phase']] = defaultdict(list)
+        for phase in phases:
+            self.phases[phase.step].append(phase)
+        self.steps = steps
+
+    def __iter__(self) -> 'PhaseIterator':
+        return self
+
+    def __next__(self) -> 'Step':
+        self.current += 1
+        if self.current > self.steps:
+            raise StopIteration
+        return self.Step(self.phases[self.current - 1])
+
+
 class Phase:
     """
     Holds the Actions which a user can perform at each stage. A Phase with no actions is
@@ -173,12 +229,12 @@ class Phase:
         self._internal = slugify(self.name)
         self.stage: Union['Stage', None] = None
         self._actions = {action.name: action for action in self.actions}
-        self.occurrence: int = 0
+        self.step: int = 0
 
     def __eq__(self, other: Union[object, str]) -> bool:
         if isinstance(other, str):
             return str(self) == other
-        to_match = ['name', 'occurrence']
+        to_match = ['name', 'step']
         return all(getattr(self, attr) == getattr(other, attr) for attr in to_match)
 
     @property
@@ -186,7 +242,7 @@ class Phase:
         return list(self._actions.keys())
 
     def __str__(self) -> str:
-        return phase_name(self.stage, self, self.occurrence)
+        return phase_name(self.stage, self, self.step)
 
     def __getitem__(self, value: str) -> 'Action':
         return self._actions[value]
@@ -218,7 +274,7 @@ class ChangePhaseAction(Action):
 
     def process(self, phase: 'Phase') -> Union['Phase', None]:
         if isinstance(self.target_phase, str):
-            return phase.stage.get_phase(phase_name(phase.stage, self.target_phase, 0))
+            return phase.stage.get_phase(self.target_phase)
         return self.target_phase
 
 
@@ -275,8 +331,7 @@ class RequestStage(Stage):
         DiscussionWithNextPhase(),
         ReviewPhase(),
         DiscussionPhase(),
-        accepted,
-        rejected,
+        [accepted, rejected]
     ]
 
 
@@ -285,8 +340,7 @@ class ConceptStage(Stage):
     phases = [
         DiscussionWithNextPhase(),
         ReviewPhase(),
-        DiscussionWithProgressionPhase(),
-        rejected,
+        [DiscussionWithProgressionPhase(), rejected]
     ]
 
 
@@ -298,8 +352,7 @@ class ProposalStage(Stage):
         DiscussionWithNextPhase(),
         ReviewPhase('AC Review', public_name='In AC review'),
         DiscussionPhase(public_name='In AC review'),
-        accepted,
-        rejected,
+        [accepted, rejected]
     ]