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