Skip to content
Snippets Groups Projects
Unverified Commit 198fb034 authored by Todd Dembrey's avatar Todd Dembrey Committed by GitHub
Browse files

Merge pull request #374 from OpenTechFund/feature/47-allow-applicants-to-submit-from-draft

Feature/47 allow applicants to submit from draft
parents 6f6770d9 6677daba
No related branches found
No related tags found
No related merge requests found
Showing
with 397 additions and 92 deletions
......@@ -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}'
)
......@@ -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 %}
......
......@@ -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)
......
......@@ -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}>'
......@@ -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
......
......@@ -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>
......
......@@ -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 %}
......
{% 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 %}
......@@ -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 %}
......
......@@ -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
......
......@@ -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,
......
......@@ -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):
......
......@@ -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
......
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)
......@@ -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"),
]
......@@ -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())
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
]
......
{% 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 %}
......
......@@ -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>
......
......@@ -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>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment