diff --git a/opentech/apply/activity/models.py b/opentech/apply/activity/models.py
index 643ca3caeebb36920655f70fe0e94d467d999e39..d17c46c116be88d9dc0f52bf9ff13d899c45debd 100644
--- a/opentech/apply/activity/models.py
+++ b/opentech/apply/activity/models.py
@@ -3,6 +3,8 @@ from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
 
+from django_fsm.signals import post_transition
+
 from opentech.apply.funds.models import ApplicationSubmission
 
 COMMENT = 'comment'
@@ -112,3 +114,18 @@ def log_submission_activity(sender, **kwargs):
             submission=submission,
             message=f'Submitted {submission.title} for {submission.page.title}'
         )
+
+
+@receiver(post_transition, sender=ApplicationSubmission)
+def log_status_update(sender, **kwargs):
+    instance = kwargs['instance']
+    old_phase = instance.workflow[kwargs['source']].display_name
+    new_phase = instance.workflow[kwargs['target']].display_name
+
+    by = kwargs['method_kwargs']['by']
+
+    Activity.actions.create(
+        user=by,
+        submission=instance,
+        message=f'Progressed from {old_phase} to {new_phase}'
+    )
diff --git a/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html
index d956bb83cb697f0fa361cc857079d96f0cb95bae..a9795e015ec078ce3a1b8cc46b3e16db33b58d95 100644
--- a/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html
+++ b/opentech/apply/dashboard/templates/dashboard/applicant_dashboard.html
@@ -23,12 +23,12 @@
     {% for submission in my_active_submissions %}
         <div class="wrapper wrapper--status-bar">
             <div>
-                <h5 class="heading heading--no-margin"><a class="link link--underlined" href="{% url 'funds:submission' submission.id %}">{{ submission.title }}</a></h5>
+                <h5 class="heading heading--no-margin"><a class="link link--underlined" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h5>
                 <h6 class="heading heading--no-margin heading--submission-meta"><span>Submitted:</span> {{ submission.submit_time.date }} by {{ submission.user.get_full_name }}</h6>
             </div>
             {% include "funds/includes/status_bar.html" with phases=submission.workflow current_phase=submission.phase class="status-bar--small" %}
             {% if request.user|has_edit_perm:submission %}
-                <a class="button button--primary" href="{% url 'funds:edit_submission' submission.id %}">Start your {{ submission.stage }} application</a>
+                <a class="button button--primary" href="{% url 'funds:submissions:edit' submission.id %}">Start your {{ submission.stage }} application</a>
             {% endif %}
         </div>
     {% empty %}
diff --git a/opentech/apply/funds/forms.py b/opentech/apply/funds/forms.py
index 3f150b64f88a8d5c7fc6b149058030e22dad8aab..821cdcfecf0892e02a9fb030b8893c908167bba5 100644
--- a/opentech/apply/funds/forms.py
+++ b/opentech/apply/funds/forms.py
@@ -15,9 +15,9 @@ class ProgressSubmissionForm(forms.ModelForm):
         fields: list = []
 
     def __init__(self, *args, **kwargs):
-        kwargs.pop('user')
+        self.user = kwargs.pop('user')
         super().__init__(*args, **kwargs)
-        choices = [(name, action) for name, action in self.instance.phase.transitions.items()]
+        choices = list(self.instance.get_actions_for_user(self.user))
         action_field = self.fields['action']
         action_field.choices = choices
         self.should_show = bool(choices)
@@ -32,7 +32,7 @@ class ProgressSubmissionForm(forms.ModelForm):
         return action_name
 
     def save(self, *args, **kwargs):
-        self.transition()
+        self.transition(by=self.user)
         return super().save(*args, **kwargs)
 
 
diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py
index 92775fc414668da260c35d42bcb2935efb80542c..8b43dbcf5c9169843c224794f10713b42d694ab4 100644
--- a/opentech/apply/funds/models.py
+++ b/opentech/apply/funds/models.py
@@ -40,8 +40,14 @@ from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME
 from .admin_forms import WorkflowFormAdminForm
 from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES
 from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
-from .workflow import active_statuses, get_review_statuses, review_statuses, INITIAL_STATE, WORKFLOWS
-
+from .workflow import (
+    active_statuses,
+    get_review_statuses,
+    INITIAL_STATE,
+    review_statuses,
+    UserPermissions,
+    WORKFLOWS,
+)
 
 LIMIT_TO_STAFF = {'groups__name': STAFF_GROUP_NAME}
 LIMIT_TO_REVIEWERS = {'groups__name': REVIEWER_GROUP_NAME}
@@ -493,31 +499,71 @@ class ApplicationSubmissionQueryset(JSONOrderable):
         return self.exclude(next__isnull=False)
 
 
+def make_permission_check(users):
+    def can_transition(instance, user):
+        if UserPermissions.STAFF in users and user.is_apply_staff:
+            return True
+        if UserPermissions.ADMIN in users and user.is_superuser:
+            return True
+        if UserPermissions.LEAD in users and instance.lead == user:
+            return True
+        if UserPermissions.APPLICANT in users and instance.user == user:
+            return True
+        return False
+
+    return can_transition
+
+
 class AddTransitions(models.base.ModelBase):
     def __new__(cls, name, bases, attrs, **kwargs):
         transition_prefix = 'transition'
         for workflow in WORKFLOWS.values():
             for phase, data in workflow.items():
-                for transition_name, action in data.all_transitions.items():
-                    method = data.transition_methods.get(transition_name)
+                for transition_name, action in data.transitions.items():
+                    method_name = '_'.join([transition_prefix, transition_name, str(data.step), data.stage.name])
+                    permission_name = method_name + '_permission'
+                    permission_func = make_permission_check(action['permissions'])
+
                     # Get the method defined on the parent or default to a NOOP
-                    transition_state = attrs.get(method, lambda self: None)
+                    transition_state = attrs.get(action.get('method'), lambda *args, **kwargs: None)
                     # Provide a neat name for graph viz display
-                    function_name = '_'.join([transition_prefix, slugify(action)])
-                    transition_state.__name__ = function_name
+                    transition_state.__name__ = slugify(action['display'])
+
+                    conditions = [attrs[condition] for condition in action.get('conditions', [])]
                     # Wrap with transition decorator
-                    transition_func = transition(attrs['status'], source=phase, target=transition_name)(transition_state)
+                    transition_func = transition(
+                        attrs['status'],
+                        source=phase,
+                        target=transition_name,
+                        permission=permission_func,
+                        conditions=conditions,
+                    )(transition_state)
 
                     # Attach to new class
-                    method_name = '_'.join([transition_prefix, transition_name, str(data.step)])
                     attrs[method_name] = transition_func
+                    attrs[permission_name] = permission_func
 
         def get_transition(self, transition):
-            return getattr(self, '_'.join([transition_prefix, transition, str(self.phase.step)]))
+            try:
+                return getattr(self, '_'.join([transition_prefix, transition, str(self.phase.step), self.stage.name]))
+            except TypeError:
+                # Defined on the class
+                return None
+            except AttributeError:
+                # For the other workflow
+                return None
 
         attrs['get_transition'] = get_transition
 
-        # attrs['restart'] = transition(attrs['status'], source='*', target=INITIAL_STATE)(lambda x: None)
+        def get_actions_for_user(self, user):
+            transitions = self.get_available_user_status_transitions(user)
+            actions = [
+                (transition.target, self.phase.transitions[transition.target]['display'])
+                for transition in transitions if self.get_transition(transition.target)
+            ]
+            yield from actions
+
+        attrs['get_actions_for_user'] = get_actions_for_user
 
         return super().__new__(cls, name, bases, attrs, **kwargs)
 
@@ -554,15 +600,27 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss
     objects = ApplicationSubmissionQueryset.as_manager()
 
     def not_progressed(self):
-        return not self.next and self.workflow != WORKFLOWS['single']
+        return not self.next
 
     @transition(
         status, source='*',
-        target=RETURN_VALUE(INITIAL_STATE, 'draft_proposal'),
-        conditions=[not_progressed]
+        target=RETURN_VALUE(INITIAL_STATE, 'draft_proposal', 'invited_to_proposal'),
+        permission=make_permission_check({UserPermissions.ADMIN}),
     )
-    def restart_stage(self):
-        return self.workflow.stages.index(self.stage)[INITIAL_STATE, 'draft_proposal']
+    def restart_stage(self, **kwargs):
+        """
+        If running form the console please include your user using the kwarg "by"
+
+        u = User.objects.get(email="<my@email.com>")
+        for a in ApplicationSubmission.objects.all():
+            a.restart_stage(by=u)
+            a.save()
+        """
+        if hasattr(self, 'previous'):
+            return 'draft_proposal'
+        elif self.next:
+            return 'invited_to_proposal'
+        return INITIAL_STATE
 
     @property
     def stage(self):
@@ -576,6 +634,12 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss
     def active(self):
         return self.status in active_statuses
 
+    @property
+    def last_edit(self):
+        # Best estimate of last edit
+        # TODO update when we have revisioning included
+        return self.activities.first()
+
     def ensure_user_has_account(self):
         if self.user and self.user.is_authenticated:
             self.form_data['email'] = self.user.email
@@ -632,7 +696,7 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss
             # We are a lab submission
             return getattr(self.page.specific, attribute)
 
-    def progress_application(self):
+    def progress_application(self, **kwargs):
         submission_in_db = ApplicationSubmission.objects.get(id=self.id)
 
         self.id = None
@@ -742,7 +806,7 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss
         return form_data
 
     def get_absolute_url(self):
-        return reverse('funds:submission', args=(self.id,))
+        return reverse('funds:submissions:detail', args=(self.id,))
 
     def __getattribute__(self, item):
         # __getattribute__ allows correct error handling from django compared to __getattr__
@@ -755,4 +819,4 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss
         return f'{self.title} from {self.full_name} for {self.page.title}'
 
     def __repr__(self):
-        return f'<{self.__class__.__name__}: {str(self.form_data)}>'
+        return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>'
diff --git a/opentech/apply/funds/tables.py b/opentech/apply/funds/tables.py
index c4749d3396fc419955d42d1b7c0a14d68915e87f..9ff36ac29160afc714dec5a3b1e855c61d949350 100644
--- a/opentech/apply/funds/tables.py
+++ b/opentech/apply/funds/tables.py
@@ -24,13 +24,13 @@ def make_row_class(record):
 
 class SubmissionsTable(tables.Table):
     """Base table for listing submissions, do not include admin data to this table"""
-    title = tables.LinkColumn('funds:submission', args=[A('pk')], orderable=True)
+    title = tables.LinkColumn('funds:submissions:detail', args=[A('pk')], orderable=True)
     submit_time = tables.DateColumn(verbose_name="Submitted")
     phase = tables.Column(verbose_name="Status", order_by=('status',))
     stage = tables.Column(verbose_name="Type", order_by=('status',))
     page = tables.Column(verbose_name="Fund")
     comments = tables.Column(accessor='activities.comments.all', verbose_name="Comments")
-    last_update = tables.DateColumn(accessor="activities.last.timestamp", verbose_name="Last updated")
+    last_update = tables.DateColumn(accessor="activities.first.timestamp", verbose_name="Last updated")
 
     class Meta:
         model = ApplicationSubmission
diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html
index 184e5add3c35a828f69da96d2228d3fdc36933d7..fc0040d5357688221a47691328793be9f0317555 100644
--- a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html
+++ b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html
@@ -21,7 +21,7 @@
         <div class="wrapper wrapper--button-container">
             {% include 'review/includes/review_button.html' with submission=object %}
             {% if request.user.is_apply_staff and object.reviews.exists %}
-                <a href="{% url 'apply:reviews:list' submission_pk=object.id %}" class="button button--white button--half-width">View all</a>
+                <a href="{% url 'apply:submissions:reviews:list' submission_pk=object.id %}" class="button button--white button--half-width">View all</a>
             {% endif %}
         </div>
     </div>
diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
index 88d872a6d598af9c8af2fc3c5b4cd2f1aa10202c..8c159b45df2f035ba710029b4155b989ac016e3f 100644
--- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
+++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html
@@ -42,14 +42,15 @@
                 <div>
                     <h4>Congratulations!</h4>
                     <h5>Your {{ object.previous.stage }} application has been accepted.</h5>
-                    <a class="button button--primary" href="{% url 'funds:edit_submission' object.id %}">Start your {{ object.stage }} application</a>
+                    <a class="button button--primary" href="{% url 'funds:submissions:edit' object.id %}">Start your {{ object.stage }} application</a>
                 </div>
             {% else %}
                 <div>
                     <h6 class="heading heading--submission-meta">
                         <span>Submitted: </span>{{ object.submit_time.date }} by {{ object.user.get_full_name }}
+                        <span>Edited: </span>{{ object.last_edit.timestamp.date }} by {{ object.last_edit.user.get_full_name }}
                         {% if request.user|has_edit_perm:object %}
-                            <a href="{% url 'funds:edit_submission' object.id %}">Edit</a>
+                            <a href="{% url 'funds:submissions:edit' object.id %}">Edit</a>
                         {% endif %}
                     </h6>
 
@@ -93,11 +94,11 @@
                 {% if other_submissions or object.previous %}
                     <div class="sidebar__inner">
                         {% if object.previous %}
-                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submission' object.previous.id %}">View linked {{ object.previous.stage }}</a></h6>
+                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submissions:detail' object.previous.id %}">View linked {{ object.previous.stage }}</a></h6>
                         {% endif %}
 
                         {% if object.next %}
-                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submission' object.next.id %}">View linked {{ object.next.stage }}</a></h6>
+                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submissions:detail' object.next.id %}">View linked {{ object.next.stage }}</a></h6>
                         {% endif %}
 
                         {% for submission in other_submissions %}
@@ -105,7 +106,7 @@
                                 <h6 class="heading heading--light-grey heading--small heading--uppercase">Past Submissions</h6>
                             {% endif %}
 
-                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submission' submission.id %}">{{ submission.title }}</a></h6>
+                            <h6><a class="link link--underlined link--bold" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h6>
                         {% endfor %}
                     </div>
                 {% endif %}
diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_form.html b/opentech/apply/funds/templates/funds/applicationsubmission_form.html
index 14042db62af53b00a44055c8924c7baf6594c8c8..b4f4ad174115a32dd930672f9da196ba80751bfc 100644
--- a/opentech/apply/funds/templates/funds/applicationsubmission_form.html
+++ b/opentech/apply/funds/templates/funds/applicationsubmission_form.html
@@ -1,8 +1,28 @@
 {% extends "base-apply.html" %}
+{% block title %}Editing: {{object.title }}{% endblock %}
 {% block content %}
+<div class="wrapper wrapper--breakout wrapper--admin">
+    <div class="wrapper wrapper--large">
+        <h2 class="heading heading--no-margin">Editing: {{ object.title }}</h2>
+    </div>
+</div>
+
+{% if form.errors or form.non_field_errors %}
+<div class="wrapper wrapper--medium wrapper--error">
+    <svg class="icon icon--error"><use xlink:href="#error"></use></svg>
+    <h5 class="heading heading--no-margin heading--regular">There were some errors with your form. Please amend the fields highlighted below</h5>
+    {% if form.non_field_errors %}
+        <ul>
+            {% for error in form.non_field_errors %}
+                <li class="error">{{ error }}</li>
+            {% endfor %}
+        </ul>
+    {% endif %}
+</div>
+{% endif %}
+
 <div class="wrapper wrapper--medium wrapper--light-grey-bg wrapper--form">
     <form class="form" action="" method="post" enctype="multipart/form-data">
-        {{ form.media }}
         {% csrf_token %}
 
         {% for field in form %}
@@ -12,7 +32,13 @@
             {{ field }}
             {% endif %}
         {% endfor %}
-        <input class="button button--primary" type="submit" value="Submit" />
+        {% for button_name, button_value in buttons %}
+            <input class="button button--primary" type="submit" name="{{ button_name }}" value="{{ button_value }}" />
+        {% endfor %}
     </form>
 </div>
 {% endblock %}
+
+{% block extra_js %}
+    {{ form.media }}
+{% endblock %}
diff --git a/opentech/apply/funds/templates/funds/includes/review_table_row.html b/opentech/apply/funds/templates/funds/includes/review_table_row.html
index d857f48ddf6710919df352ed6a6f7b8db5dd3647..ffe2bde74f89db48d32b345e5bfb819aeb832d50 100644
--- a/opentech/apply/funds/templates/funds/includes/review_table_row.html
+++ b/opentech/apply/funds/templates/funds/includes/review_table_row.html
@@ -7,7 +7,7 @@
         {% else %}
             <td class="reviews-sidebar__author" colspan="2">
                 {% if request.user.is_apply_staff %}
-                    <a href="{% url 'apply:reviews:review' submission_pk=review.submission.id pk=review.id %}">
+                    <a href="{% url 'apply:submissions:reviews:review' submission_pk=review.submission.id pk=review.id %}">
                         <span>{{ review.author }}</span>
                     </a>
                 {% else %}
diff --git a/opentech/apply/funds/templates/funds/includes/status_bar.html b/opentech/apply/funds/templates/funds/includes/status_bar.html
index e15b2a985b8bdbd4c677164bccb3e2ada595e2a1..bc4fce3f3e4b0bcae5b618911e39ea7010cf7b24 100644
--- a/opentech/apply/funds/templates/funds/includes/status_bar.html
+++ b/opentech/apply/funds/templates/funds/includes/status_bar.html
@@ -3,7 +3,7 @@
         {% if not same_stage or current_phase.stage == phase.stage %}
             {% ifchanged phase.step %}
                 <div class="status-bar__item
-                            {% if phase_name == current_phase.name %}
+                            {% if phase.step == current_phase.step %}
                                 status-bar__item--is-current
                             {% elif current_phase.step > phase.step %}
                                 status-bar__item--is-complete
diff --git a/opentech/apply/funds/tests/factories/blocks.py b/opentech/apply/funds/tests/factories/blocks.py
index 7c526cd3b49db33ed1b4f90393beb194e0b9753b..4df08e498feb8b4e9159baa252637c3599359093 100644
--- a/opentech/apply/funds/tests/factories/blocks.py
+++ b/opentech/apply/funds/tests/factories/blocks.py
@@ -103,6 +103,11 @@ class RichTextFieldBlockFactory(FormFieldBlockFactory):
         model = blocks.RichTextFieldBlock
 
 
+class ValueFieldBlockFactory(FormFieldBlockFactory):
+    class Meta:
+        model = blocks.ValueBlock
+
+
 class StreamFieldUUIDFactory(wagtail_factories.StreamFieldFactory):
     def generate(self, *args, **kwargs):
         blocks = super().generate(*args, **kwargs)
@@ -117,6 +122,7 @@ class StreamFieldUUIDFactory(wagtail_factories.StreamFieldFactory):
 
 CustomFormFieldsFactory = StreamFieldUUIDFactory({
     'title': TitleBlockFactory,
+    'value': ValueFieldBlockFactory,
     'email': EmailBlockFactory,
     'full_name': FullNameBlockFactory,
     'char': CharFieldBlockFactory,
diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py
index a83464ee39a47a89c05324fdfabf4fe10ceb3186..abef318fc8457979e8e80c6ad01910dc4df83571 100644
--- a/opentech/apply/funds/tests/factories/models.py
+++ b/opentech/apply/funds/tests/factories/models.py
@@ -15,8 +15,7 @@ from opentech.apply.funds.models import (
     Round,
     RoundForm,
 )
-from opentech.apply.users.tests.factories import UserFactory
-from opentech.apply.users.groups import STAFF_GROUP_NAME
+from opentech.apply.users.tests.factories import StaffFactory, UserFactory
 
 from . import blocks
 
@@ -102,7 +101,7 @@ class RoundFactory(wagtail_factories.PageFactory):
     title = factory.Sequence('Round {}'.format)
     start_date = factory.LazyFunction(datetime.date.today)
     end_date = factory.LazyFunction(lambda: datetime.date.today() + datetime.timedelta(days=7))
-    lead = factory.SubFactory(UserFactory, groups__name=STAFF_GROUP_NAME)
+    lead = factory.SubFactory(StaffFactory)
 
     @factory.post_generation
     def forms(self, create, extracted, **kwargs):
@@ -132,7 +131,7 @@ class LabFactory(wagtail_factories.PageFactory):
 
     # Will need to update how the stages are identified as Fund Page changes
     workflow_name = factory.LazyAttribute(lambda o: list(FundType.WORKFLOW_CHOICES.keys())[o.workflow_stages - 1])
-    lead = factory.SubFactory(UserFactory, groups__name=STAFF_GROUP_NAME)
+    lead = factory.SubFactory(StaffFactory)
 
     @factory.post_generation
     def forms(self, create, extracted, **kwargs):
@@ -192,8 +191,9 @@ class ApplicationSubmissionFactory(factory.DjangoModelFactory):
     form_data = factory.SubFactory(FormDataFactory, form_fields=factory.SelfAttribute('..form_fields'))
     page = factory.SubFactory(FundTypeFactory)
     workflow_name = factory.LazyAttribute(lambda o: list(FundType.WORKFLOW_CHOICES.keys())[o.workflow_stages - 1])
-    round = factory.SubFactory(RoundFactory, workflow_name=factory.SelfAttribute('..workflow_name'))
+    round = factory.SubFactory(RoundFactory, workflow_name=factory.SelfAttribute('..workflow_name'), lead=factory.SelfAttribute('..lead'))
     user = factory.SubFactory(UserFactory)
+    lead = factory.SubFactory(StaffFactory)
 
     @classmethod
     def _generate(cls, strat, params):
diff --git a/opentech/apply/funds/tests/test_models.py b/opentech/apply/funds/tests/test_models.py
index e290e9527d84f53e3daad75b90316efbc0c0275f..5626409f60cf4a85be2b72d00097cbf5fbbca844 100644
--- a/opentech/apply/funds/tests/test_models.py
+++ b/opentech/apply/funds/tests/test_models.py
@@ -201,7 +201,7 @@ class TestFormSubmission(TestCase):
 
         page = page or self.round_page
         fields = page.get_form_fields()
-        data = {k: v for k, v in zip(fields, ['project', email, name])}
+        data = {k: v for k, v in zip(fields, ['project', 0, email, name])}
 
         request = self.request_factory.post('', data)
         request.user = user
diff --git a/opentech/apply/funds/tests/test_views.py b/opentech/apply/funds/tests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..80c65891239d48181be97215a1b823619e04dbff
--- /dev/null
+++ b/opentech/apply/funds/tests/test_views.py
@@ -0,0 +1,88 @@
+from django.test import TestCase, RequestFactory
+from django.urls import reverse
+
+from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory
+from opentech.apply.users.tests.factories import UserFactory, StaffFactory
+
+
+class SubmissionTestCase(TestCase):
+    user_factory = None
+
+    def setUp(self):
+        self.factory = RequestFactory()
+        self.user = self.user_factory()
+        self.client.force_login(self.user)
+
+    def submission_url(self, submission, view_name='detail'):
+        view_name = f'funds:submissions:{ view_name }'
+        url = reverse(view_name, kwargs={'pk': submission.id})
+        request = self.factory.get(url, secure=True)
+        return request.build_absolute_uri()
+
+    def get_submission_page(self, submission, view_name='detail'):
+        return self.client.get(self.submission_url(submission, view_name), secure=True, follow=True)
+
+    def post_submission_page(self, submission, data, view_name='detail'):
+        return self.client.post(self.submission_url(submission, view_name), data, secure=True, follow=True)
+
+    def refresh(self, instance):
+        return instance.__class__.objects.get(id=instance.id)
+
+
+class TestStaffSubmissionView(SubmissionTestCase):
+    user_factory = StaffFactory
+
+    def test_can_view_a_submission(self):
+        submission = ApplicationSubmissionFactory()
+        response = self.get_submission_page(submission)
+        self.assertContains(response, submission.title)
+
+    def test_can_progress_stage(self):
+        submission = ApplicationSubmissionFactory(status='concept_review_discussion', workflow_stages=2, lead=self.user)
+        response = self.post_submission_page(submission, {'form-submitted-progress_form': '', 'action': 'invited_to_proposal'})
+
+        # Cant use refresh from DB with FSM
+        submission_original = self.refresh(submission)
+        submission_next = submission_original.next
+
+        self.assertRedirects(response, self.submission_url(submission_next))
+        self.assertEqual(submission_original.status, 'invited_to_proposal')
+        self.assertEqual(submission_next.status, 'draft_proposal')
+
+    def test_cant_progress_stage_if_not_lead(self):
+        submission = ApplicationSubmissionFactory(status='concept_review_discussion', workflow_stages=2)
+        self.post_submission_page(submission, {'form-submitted-progress_form': '', 'action': 'invited_to_proposal'})
+
+        submission = self.refresh(submission)
+
+        self.assertEqual(submission.status, 'concept_review_discussion')
+        self.assertIsNone(submission.next)
+
+
+class TestApplicantSubmissionView(SubmissionTestCase):
+    user_factory = UserFactory
+
+    def test_can_view_own_submission(self):
+        submission = ApplicationSubmissionFactory(user=self.user)
+        response = self.get_submission_page(submission)
+        self.assertContains(response, submission.title)
+
+    def test_cant_view_others_submission(self):
+        submission = ApplicationSubmissionFactory()
+        response = self.get_submission_page(submission)
+        self.assertEqual(response.status_code, 403)
+
+    def test_can_edit_own_submission(self):
+        submission = ApplicationSubmissionFactory(user=self.user, status='draft_proposal', workflow_stages=2)
+        response = self.get_submission_page(submission, 'edit')
+        self.assertContains(response, submission.title)
+
+    def test_cant_edit_submission_incorrect_state(self):
+        submission = ApplicationSubmissionFactory(user=self.user, workflow_stages=2)
+        response = self.get_submission_page(submission, 'edit')
+        self.assertEqual(response.status_code, 403)
+
+    def test_cant_edit_other_submission(self):
+        submission = ApplicationSubmissionFactory(status='draft_proposal', workflow_stages=2)
+        response = self.get_submission_page(submission, 'edit')
+        self.assertEqual(response.status_code, 403)
diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py
index c95ae992adc7f19f01fa225a989cfbdec53e4534..7bb844b4e12c184720c6ae9bdb3f6db850fae2fb 100644
--- a/opentech/apply/funds/urls.py
+++ b/opentech/apply/funds/urls.py
@@ -5,10 +5,14 @@ from .views import SubmissionSearchView, SubmissionDetailView, SubmissionEditVie
 
 app_name = 'funds'
 
+submission_urls = ([
+    path('', SubmissionListView.as_view(), name="list"),
+    path('<int:pk>/', SubmissionDetailView.as_view(), name="detail"),
+    path('<int:pk>/edit/', SubmissionEditView.as_view(), name="edit"),
+    path('<int:submission_pk>/', include('opentech.apply.review.urls', namespace="reviews")),
+], 'submissions')
+
 urlpatterns = [
-    path('submissions/', SubmissionListView.as_view(), name="submissions"),
-    path('submissions/<int:pk>/', SubmissionDetailView.as_view(), name="submission"),
-    path('submissions/<int:pk>/edit', SubmissionEditView.as_view(), name="edit_submission"),
-    path('submissions/<int:submission_pk>/', include('opentech.apply.review.urls', namespace="reviews")),
+    path('submissions/', include(submission_urls)),
     path('search', SubmissionSearchView.as_view(), name="search"),
 ]
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index f1f16005e4295e75e45109e85312c0866e744168..22c75a1a9956fe6195f5b4c375cd9ae913f6636b 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -70,24 +70,14 @@ class ProgressSubmissionView(DelegatedViewMixin, UpdateView):
     context_name = 'progress_form'
 
     def form_valid(self, form):
-        old_phase = form.instance.phase.display_name
         response = super().form_valid(form)
-        new_phase = form.instance.phase.display_name
-        Activity.actions.create(
-            user=self.request.user,
-            submission=self.kwargs['submission'],
-            message=f'Progressed from {old_phase} to {new_phase}'
-        )
         return self.progress_stage(form.instance) or response
 
     def progress_stage(self, instance):
-        try:
-            proposal_transition = instance.get_transition('draft_proposal')
-        except AttributeError:
-            pass
-        else:
+        proposal_transition = instance.get_transition('draft_proposal')
+        if proposal_transition:
             if can_proceed(proposal_transition):
-                proposal_transition()
+                proposal_transition(by=self.request.user)
                 instance.save()
             return HttpResponseRedirect(instance.get_absolute_url())
 
@@ -197,6 +187,18 @@ class SubmissionEditView(UpdateView):
             raise PermissionDenied
         return super().dispatch(request, *args, **kwargs)
 
+    @property
+    def transitions(self):
+        transitions = self.object.get_available_user_status_transitions(self.request.user)
+        return {
+            transition.name: transition
+            for transition in transitions
+        }
+
+    def buttons(self):
+        yield ('save', 'Save')
+        yield from ((transition, transition.title) for transition in self.transitions)
+
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
         instance = kwargs.pop('instance')
@@ -215,10 +217,24 @@ class SubmissionEditView(UpdateView):
         kwargs['initial'] = form_data
         return kwargs
 
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(buttons=self.buttons(), **kwargs)
+
     def get_form_class(self):
         return self.object.get_form_class()
 
     def form_valid(self, form):
         self.object.form_data = form.cleaned_data
         self.object.save()
+
+        if 'save' in self.request.POST:
+            return self.form_invalid(form)
+
+        transition = set(self.request.POST.keys()) & set(self.transitions.keys())
+
+        if transition:
+            transition_object = self.transitions[transition.pop()]
+            self.object.get_transition(transition_object.target)(by=self.request.user)
+            self.object.save()
+
         return HttpResponseRedirect(self.get_success_url())
diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py
index 532ead63ab20ea36d3b88e8bcd1d6ab7f4289f8b..36f21603e9d09c66808090bb9630458aef3a84f1 100644
--- a/opentech/apply/funds/workflow.py
+++ b/opentech/apply/funds/workflow.py
@@ -1,4 +1,5 @@
 from collections import defaultdict
+from enum import Enum
 import itertools
 
 
@@ -13,6 +14,13 @@ be fixed when streamfield, may require intermediate fix prior to launch]
 """
 
 
+class UserPermissions(Enum):
+    STAFF = 1
+    ADMIN = 2
+    LEAD = 3
+    APPLICANT = 4
+
+
 class Workflow(dict):
     def __init__(self, name, admin_name, **data):
         self.name = name
@@ -40,24 +48,23 @@ class Phase:
         self.step = step
 
         # For building transition methods on the parent
-        self.all_transitions = {}
-        self.transition_methods = {}
-
-        # For building form actions
         self.transitions = {}
-        for transition, action in transitions.items():
+
+        default_permissions = {UserPermissions.STAFF, UserPermissions.ADMIN, UserPermissions.LEAD}
+
+        for transition_target, action in transitions.items():
+            transition = dict()
             try:
-                self.all_transitions[transition] = action['display']
-                method_name = action.get('action')
-                if method_name:
-                    self.transition_methods[transition] = method_name
-                show_in_form = action.get('form', True)
-            except TypeError:
-                show_in_form = True
-                self.all_transitions[transition] = action
-
-            if show_in_form:
-                self.transitions[transition] = self.all_transitions[transition]
+                transition['display'] = action.get('display')
+            except AttributeError:
+                transition['display'] = action
+                transition['permissions'] = default_permissions
+            else:
+                transition['method'] = action.get('method')
+                conditions = action.get('conditions', '')
+                transition['conditions'] = conditions.split(',') if conditions else []
+                transition['permissions'] = action.get('permissions', default_permissions)
+            self.transitions[transition_target] = transition
 
     def __str__(self):
         return self.display_name
@@ -114,13 +121,23 @@ SingleStageDefinition = {
     INITIAL_STATE: {
         'transitions': {
             'internal_review': 'Open Review',
-            'rejected': 'Reject',
+            'rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Request,
         'permissions': Permission(),
         'step': 0,
     },
+    'more_info': {
+        'transitions': {
+            INITIAL_STATE: {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Request,
+        'permissions': CanEditPermission(),
+        'step': 0,
+    },
     'internal_review': {
         'transitions': {
             'post_review_discussion': 'Close Review',
@@ -132,14 +149,25 @@ SingleStageDefinition = {
     },
     'post_review_discussion': {
         'transitions': {
-            'accepted': 'Accept',
-            'rejected': 'Reject',
+            'accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'post_review_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Request,
         'permissions': Permission(),
         'step': 2,
     },
+    'post_review_more_info': {
+        'transitions': {
+            'post_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Request,
+        'permissions': CanEditPermission(),
+        'step': 2,
+    },
+
     'accepted': {
         'display': 'Accepted',
         'stage': Request,
@@ -159,13 +187,23 @@ DoubleStageDefinition = {
     INITIAL_STATE: {
         'transitions': {
             'concept_internal_review': 'Open Review',
-            'concept_rejected': 'Reject',
+            'concept_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'concept_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Concept,
         'permissions': Permission(),
         'step': 0,
     },
+    'concept_more_info': {
+        'transitions': {
+            INITIAL_STATE: {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Concept,
+        'permissions': CanEditPermission(),
+        'step': 0,
+    },
     'concept_internal_review': {
         'transitions': {
             'concept_review_discussion': 'Close Review',
@@ -177,18 +215,33 @@ DoubleStageDefinition = {
     },
     'concept_review_discussion': {
         'transitions': {
-            'invited_to_proposal': 'Invite to Proposal',
-            'concept_rejected': 'Reject',
+            'invited_to_proposal': {'display': 'Invite to Proposal', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'concept_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'concept_review_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Concept,
         'permissions': Permission(),
         'step': 2,
     },
+    'concept_review_more_info': {
+        'transitions': {
+            'concept_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Concept,
+        'permissions': CanEditPermission(),
+        'step': 2,
+    },
     'invited_to_proposal': {
-        'display': 'Invited for Proposal',
+        'display': 'Concept Accepted',
         'transitions': {
-            'draft_proposal': {'display': 'Progress', 'action': 'progress_application', 'form': False},
+            'draft_proposal': {
+                'display': 'Progress',
+                'method': 'progress_application',
+                'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD},
+                'conditions': 'not_progressed',
+            },
         },
         'stage': Concept,
         'permissions': Permission(),
@@ -202,7 +255,7 @@ DoubleStageDefinition = {
     },
     'draft_proposal': {
         'transitions': {
-            'proposal_discussion': 'Submit',
+            'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
         },
         'display': 'Invited for Proposal',
         'stage': Proposal,
@@ -212,13 +265,23 @@ DoubleStageDefinition = {
     'proposal_discussion': {
         'transitions': {
             'proposal_internal_review': 'Open Review',
-            'proposal_rejected': 'Reject',
+            'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'proposal_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Proposal,
         'permissions': Permission(),
         'step': 5,
     },
+    'proposal_more_info': {
+        'transitions': {
+            'proposal_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Proposal,
+        'permissions': CanEditPermission(),
+        'step': 5,
+    },
     'proposal_internal_review': {
         'transitions': {
             'post_proposal_review_discussion': 'Close Review',
@@ -231,13 +294,23 @@ DoubleStageDefinition = {
     'post_proposal_review_discussion': {
         'transitions': {
             'external_review': 'Open AC review',
-            'proposal_rejected': 'Reject',
+            'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'post_proposal_review_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Proposal,
         'permissions': ReviewerReviewPermission(),
         'step': 7,
     },
+    'post_proposal_review_more_info': {
+        'transitions': {
+            'post_proposal_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Proposal,
+        'permissions': CanEditPermission(),
+        'step': 7,
+    },
     'external_review': {
         'transitions': {
             'post_external_review_discussion': 'Close Review',
@@ -249,14 +322,24 @@ DoubleStageDefinition = {
     },
     'post_external_review_discussion': {
         'transitions': {
-            'proposal_accepted': 'Accept',
-            'proposal_rejected': 'Reject',
+            'proposal_accepted': {'display': 'Accept', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'proposal_rejected': {'display': 'Reject', 'permissions': {UserPermissions.ADMIN, UserPermissions.LEAD}},
+            'post_external_review_more_info': 'Request More Information',
         },
         'display': 'Under Discussion',
         'stage': Proposal,
         'permissions': Permission(),
         'step': 9,
     },
+    'post_external_review_more_info': {
+        'transitions': {
+            'post_external_review_discussion': {'display': 'Submit', 'permissions': {UserPermissions.APPLICANT}},
+        },
+        'display': 'More information required',
+        'stage': Proposal,
+        'permissions': CanEditPermission(),
+        'step': 9,
+    },
     'proposal_accepted': {
         'display': 'Accepted',
         'stage': Proposal,
@@ -300,7 +383,7 @@ for key, value in PHASES:
     STATUSES[value.display_name].add(key)
 
 active_statuses = [
-    status for status in PHASES
+    status for status, _ in PHASES
     if 'accepted' not in status or 'rejected' not in status or 'invited' not in status
 ]
 
diff --git a/opentech/apply/review/templates/review/includes/review_button.html b/opentech/apply/review/templates/review/includes/review_button.html
index a94e03ed6d46a7874ca9f85a497f5b7548bf4ccf..e21ac23c42b5f07324a2e8d720a66373b17cc9eb 100644
--- a/opentech/apply/review/templates/review/includes/review_button.html
+++ b/opentech/apply/review/templates/review/includes/review_button.html
@@ -1,7 +1,7 @@
 {% load review_tags workflow_tags %}
 {% if request.user|has_review_perm:submission %}
     {% if request.user|has_draft:submission or request.user|can_review:submission %}
-        <a href="{% url 'apply:reviews:form' submission_pk=submission.id %}" class="button button--primary button--half-width">
+        <a href="{% url 'apply:submissions:reviews:form' submission_pk=submission.id %}" class="button button--primary button--half-width">
             {% if request.user|has_draft:submission %}
                 Update draft
             {% elif request.user|can_review:submission %}
diff --git a/opentech/apply/review/templates/review/review_detail.html b/opentech/apply/review/templates/review/review_detail.html
index 3b71fddfc4db8ba5d8fcc797213984d242b8eafd..49b6eeee09234a45cb7e171e9381c740a2a51a42 100644
--- a/opentech/apply/review/templates/review/review_detail.html
+++ b/opentech/apply/review/templates/review/review_detail.html
@@ -5,7 +5,7 @@
 <div class="wrapper wrapper--breakout wrapper--admin">
     <div class="wrapper wrapper--large">
         <h2 class="heading heading--no-margin">Review</h2>
-        <h5>For <a href="{% url "funds:submission" review.submission.id %}">{{ review.submission.title }}</a></h5>
+        <h5>For <a href="{% url "funds:submissions:detail" review.submission.id %}">{{ review.submission.title }}</a></h5>
     </div>
 </div>
 
diff --git a/opentech/apply/review/templates/review/review_form.html b/opentech/apply/review/templates/review/review_form.html
index b7c88f163ad4b6c0309512459ecb92444fdf9e15..ec8fe019ae1d2dc8b866d3c544391bd9cf58d968 100644
--- a/opentech/apply/review/templates/review/review_form.html
+++ b/opentech/apply/review/templates/review/review_form.html
@@ -4,7 +4,7 @@
 <div class="wrapper wrapper--breakout wrapper--admin">
     <div class="wrapper wrapper--medium">
         <h2 class="heading heading--no-margin">{{ title|default:"Create Review" }}</h2>
-        <h5>For <a href="{% url "funds:submission" submission.id %}">{{ submission.title }}</a></h5>
+        <h5>For <a href="{% url "funds:submissions:detail" submission.id %}">{{ submission.title }}</a></h5>
     </div>
 </div>
 
diff --git a/opentech/apply/review/templates/review/review_list.html b/opentech/apply/review/templates/review/review_list.html
index d1a239c69d50ffbe5077b817399bffc403a1cbfe..cbf3c28328289de491b292fabf259e410f4bb753 100644
--- a/opentech/apply/review/templates/review/review_list.html
+++ b/opentech/apply/review/templates/review/review_list.html
@@ -8,7 +8,7 @@
     <div class="wrapper wrapper--medium">
         <div>
             <h2 class="heading heading--no-margin">Reviews</h2>
-            <h5>For <a href="{% url "funds:submission" submission.id %}">{{ submission.title }}</a></h5>
+            <h5>For <a href="{% url "funds:submissions:detail" submission.id %}">{{ submission.title }}</a></h5>
         </div>
         {% if request.user|has_review_perm:submission %}
             {% if request.user|has_draft:submission or request.user|can_review:submission %}
diff --git a/opentech/apply/review/tests/factories.py b/opentech/apply/review/tests/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..520e2f4667d0c0ad2aca1883b76614b97c62ae40
--- /dev/null
+++ b/opentech/apply/review/tests/factories.py
@@ -0,0 +1,30 @@
+import factory
+
+from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory
+from opentech.apply.users.tests.factories import StaffFactory
+
+from ..models import Review
+from ..views import get_form_for_stage
+
+
+class ReviewDataFactory(factory.DictFactory):
+    @classmethod
+    def _build(cls, model_class, *args, **kwargs):
+        submission = kwargs.pop('submission')
+        form = get_form_for_stage(submission)(request=None, submission=None)
+        form_fields = {}
+        for field_name, field in form.fields.items():
+            form_fields[field_name] = 0
+
+        form_fields.update(**kwargs)
+        return super()._build(model_class, *args, **form_fields)
+
+
+class ReviewFactory(factory.DjangoModelFactory):
+    class Meta:
+        model = Review
+
+    submission = factory.SubFactory(ApplicationSubmissionFactory)
+    author = factory.SubFactory(StaffFactory)
+    review = factory.Dict({'submission': factory.SelfAttribute('..submission')}, dict_factory=ReviewDataFactory)
+    is_draft = False
diff --git a/opentech/apply/review/tests/test_views.py b/opentech/apply/review/tests/test_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f4d45a23049e18f65ed7e5e445938ad2024f105
--- /dev/null
+++ b/opentech/apply/review/tests/test_views.py
@@ -0,0 +1,116 @@
+from django.test import TestCase, RequestFactory
+from django.urls import reverse
+
+from opentech.apply.users.tests.factories import StaffFactory, UserFactory
+from .factories import ReviewFactory
+from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory
+
+
+class BaseTestCase(TestCase):
+    url_name = ''
+    user_factory = None
+
+    def setUp(self):
+        self.factory = RequestFactory()
+        self.user = self.user_factory()
+        self.client.force_login(self.user)
+
+    def url(self, instance, view_name='review'):
+        full_url_name = self.url_name.format(view_name)
+        url = reverse(full_url_name, kwargs=self.get_kwargs(instance))
+        request = self.factory.get(url, secure=True)
+        return request.build_absolute_uri()
+
+    def get_page(self, instance, view_name='review'):
+        return self.client.get(self.url(instance, view_name), secure=True, follow=True)
+
+    def post_page(self, instance, data, view_name='review'):
+        return self.client.post(self.url(instance, view_name), data, secure=True, follow=True)
+
+    def refresh(self, instance):
+        return instance.__class__.objects.get(id=instance.id)
+
+
+class StaffReviewsTestCase(BaseTestCase):
+    user_factory = StaffFactory
+    url_name = 'funds:submissions:reviews:{}'
+
+    def get_kwargs(self, instance):
+        return {'pk': instance.id, 'submission_pk': instance.submission.id}
+
+    def test_can_access_review(self):
+        submission = ApplicationSubmissionFactory()
+        review = ReviewFactory(submission=submission, author=self.user)
+        response = self.get_page(review)
+        self.assertContains(response, review.submission.title)
+        self.assertContains(response, self.user.full_name)
+        self.assertContains(response, reverse('funds:submissions:detail', kwargs={'pk': submission.id}))
+
+    def test_cant_access_other_review(self):
+        submission = ApplicationSubmissionFactory()
+        review = ReviewFactory(submission=submission)
+        response = self.get_page(review)
+        self.assertEqual(response.status_code, 403)
+
+
+class StaffReviewListingTestCase(BaseTestCase):
+    user_factory = StaffFactory
+    url_name = 'funds:submissions:reviews:{}'
+
+    def get_kwargs(self, instance):
+        return {'submission_pk': instance.id}
+
+    def test_can_access_review_listing(self):
+        submission = ApplicationSubmissionFactory()
+        reviews = ReviewFactory.create_batch(3, submission=submission)
+        response = self.get_page(submission, 'list')
+        self.assertContains(response, submission.title)
+        self.assertContains(response, reverse('funds:submissions:detail', kwargs={'pk': submission.id}))
+        for review in reviews:
+            self.assertContains(response, review.author.full_name)
+
+
+class StaffReviewFormTestCase(BaseTestCase):
+    user_factory = StaffFactory
+    url_name = 'funds:submissions:reviews:{}'
+
+    def get_kwargs(self, instance):
+        return {'submission_pk': instance.id}
+
+    def test_can_access_form(self):
+        submission = ApplicationSubmissionFactory(status='internal_review')
+        response = self.get_page(submission, 'form')
+        self.assertContains(response, submission.title)
+        self.assertContains(response, reverse('funds:submissions:detail', kwargs={'pk': submission.id}))
+
+    def test_cant_access_wrong_status(self):
+        submission = ApplicationSubmissionFactory()
+        response = self.get_page(submission, 'form')
+        self.assertEqual(response.status_code, 403)
+
+    def test_cant_resubmit_review(self):
+        submission = ApplicationSubmissionFactory(status='internal_review')
+        ReviewFactory(submission=submission, author=self.user)
+        response = self.post_page(submission, {'data': 'value'}, 'form')
+        self.assertEqual(response.context['has_submitted_review'], True)
+        self.assertEqual(response.context['title'], 'Update Review draft')
+
+    def test_can_edit_draft_review(self):
+        submission = ApplicationSubmissionFactory(status='internal_review')
+        ReviewFactory(submission=submission, author=self.user, is_draft=True)
+        response = self.post_page(submission, {'data': 'value'}, 'form')
+        self.assertEqual(response.context['has_submitted_review'], False)
+        self.assertEqual(response.context['title'], 'Update Review draft')
+
+
+class UserReviewFormTestCase(BaseTestCase):
+    user_factory = UserFactory
+    url_name = 'funds:submissions:reviews:{}'
+
+    def get_kwargs(self, instance):
+        return {'submission_pk': instance.id}
+
+    def test_cant_access_form(self):
+        submission = ApplicationSubmissionFactory(status='internal_review')
+        response = self.get_page(submission, 'form')
+        self.assertEqual(response.status_code, 403)
diff --git a/opentech/apply/users/tests/factories.py b/opentech/apply/users/tests/factories.py
index 9e1ddfb5152e6e8bb4f9a37dc92e81324a9ef439..ca15228392d95f0a20c1b0ac51905c6763ba2418 100644
--- a/opentech/apply/users/tests/factories.py
+++ b/opentech/apply/users/tests/factories.py
@@ -3,6 +3,8 @@ from django.contrib.auth.models import Group
 
 import factory
 
+from ..groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME
+
 
 class GroupFactory(factory.DjangoModelFactory):
     class Meta:
@@ -18,7 +20,7 @@ class UserFactory(factory.DjangoModelFactory):
 
     email = factory.Sequence('email{}@email.com'.format)
 
-    @factory.PostGeneration
+    @factory.post_generation
     def groups(self, create, extracted, **kwargs):
         if create:
             if not extracted:
@@ -27,3 +29,21 @@ class UserFactory(factory.DjangoModelFactory):
                 groups = extracted
 
             self.groups.add(groups)
+
+
+class AdminFactory(UserFactory):
+    is_admin = True
+
+
+class StaffFactory(UserFactory):
+    @factory.post_generation
+    def groups(self, create, extracted, **kwargs):
+        if create:
+            self.groups.add(GroupFactory(name=STAFF_GROUP_NAME))
+
+
+class ReviewerFactory(UserFactory):
+    @factory.post_generation
+    def groups(self, create, extracted, **kwargs):
+        if create:
+            self.groups.add(GroupFactory(name=REVIEWER_GROUP_NAME))
diff --git a/opentech/public/navigation/templates/navigation/primarynav-apply.html b/opentech/public/navigation/templates/navigation/primarynav-apply.html
index ff243a51dc05f37bfd6da69c422234d0731a8680..a9c9bda2226cb6e045a7409b2e7b0dde80d65577 100644
--- a/opentech/public/navigation/templates/navigation/primarynav-apply.html
+++ b/opentech/public/navigation/templates/navigation/primarynav-apply.html
@@ -2,7 +2,7 @@
     <ul class="nav nav--primary" role="menubar">
         {% if request.user.is_apply_staff %}
             {% include "navigation/primarynav-apply-item.html" with name="Dashboard" url="dashboard:dashboard" %}
-            {% include "navigation/primarynav-apply-item.html" with name="Submissions" url="funds:submissions" %}
+            {% include "navigation/primarynav-apply-item.html" with name="Submissions" url="funds:submissions:list" %}
         {% else %}
             {% include "navigation/primarynav-apply-item.html" with name="Dashboard" url="dashboard:dashboard" %}
         {% endif %}
diff --git a/requirements.txt b/requirements.txt
index 3d21931b73686ab65c18f3cd114a033539cd125b..ffdcd69b1c839754762df8767919b6755ec30713 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,7 +14,7 @@ uwsgidecorators==1.1.0
 
 factory_boy==2.9.2
 # wagtail_factories - waiting on merge and release form master branch
-git+git://github.com/todd-dembrey/wagtail-factories.git#egg=wagtail_factories
+git+git://github.com/mvantellingen/wagtail-factories.git#egg=wagtail_factories
 
 flake8