diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index 7c4efae3bc92a578770b0a9c8a6463ffdc25d180..ce03a45c3cf985d9a55927111e8f2809ec667b20 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -217,10 +217,10 @@ class ActivityAdapter(AdapterBase): MESSAGES.SCREENING: 'Screening status from {old_status} to {source.screening_status}', MESSAGES.REVIEW_OPINION: '{user} {opinion.opinion_display}s with {opinion.review.author}''s review of {source}', MESSAGES.CREATED_PROJECT: '{user} has created Project', - MESSAGES.UPDATE_PROJECT_LEAD: 'Lead changed from from {old_lead} to {source.lead} by {user}', - MESSAGES.SEND_FOR_APPROVAL: '{user} has requested approval on Project', - MESSAGES.APPROVE_PROJECT: '{user} has approved Project "{source.title}"', - MESSAGES.REJECT_PROJECT: '{user} has rejected Project "{source.title}" with the comment: {comment}', + MESSAGES.UPDATE_PROJECT_LEAD: 'Lead changed from {old_lead} to {source.lead} by {user}', + MESSAGES.SEND_FOR_APPROVAL: '{user} has requested approval', + MESSAGES.APPROVE_PROJECT: '{user} has approved', + MESSAGES.REQUEST_PROJECT_CHANGE: '{user} has requested changes to for acceptance: "{comment}"', } def recipients(self, message_type, **kwargs): @@ -235,6 +235,9 @@ class ActivityAdapter(AdapterBase): MESSAGES.REVIEW_OPINION, MESSAGES.BATCH_REVIEWERS_UPDATED, MESSAGES.PARTNERS_UPDATED, + MESSAGES.APPROVE_PROJECT, + MESSAGES.REQUEST_PROJECT_CHANGE, + MESSAGES.SEND_FOR_APPROVAL, ]: return {'visibility': INTERNAL} @@ -384,6 +387,7 @@ class SlackAdapter(AdapterBase): MESSAGES.EDIT_REVIEW: '{user} has edited {review.author} review for <{link}|{source.title}>.', MESSAGES.SEND_FOR_APPROVAL: '{user} has requested approval on project <{link}|{source.title}>.', MESSAGES.APPROVE_PROJECT: '{user} has approved project <{link}|{source.title}>.', + MESSAGES.REQUEST_PROJECT_CHANGE: '{user} has requested changes for project acceptance on <{link}|{source.title}>.', } def __init__(self): diff --git a/opentech/apply/activity/migrations/0036_add_reject_project.py b/opentech/apply/activity/migrations/0036_add_reject_project.py index 74be39307108406c0cbc0eb740a749cd8a09e488..640d50dd02f33fe548e24d29d49056766fb341fe 100644 --- a/opentech/apply/activity/migrations/0036_add_reject_project.py +++ b/opentech/apply/activity/migrations/0036_add_reject_project.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='event', name='type', - field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('BATCH_UPDATE_LEAD', 'Batch Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('BATCH_TRANSITION', 'Batch Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('BATCH_DETERMINATION_OUTCOME', 'Batch Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('PARTNERS_UPDATED', 'Partners Updated'), ('PARTNERS_UPDATED_PARTNER', 'Partners Updated Partner'), ('READY_FOR_REVIEW', 'Ready For Review'), ('BATCH_READY_FOR_REVIEW', 'Batch Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission'), ('REVIEW_OPINION', 'Review Opinion'), ('DELETE_SUBMISSION', 'Delete Submission'), ('DELETE_REVIEW', 'Delete Review'), ('CREATED_PROJECT', 'Created Project'), ('UPDATE_PROJECT_LEAD', 'Update Project Lead'), ('EDIT_REVIEW', 'Edit Review'), ('SEND_FOR_APPROVAL', 'Send for Approval'), ('APPROVE_PROJECT', 'Project was Approved'), ('REJECT_PROJECT', 'Project was Rejected')], max_length=50), + field=models.CharField(choices=[('UPDATE_LEAD', 'Update Lead'), ('BATCH_UPDATE_LEAD', 'Batch Update Lead'), ('EDIT', 'Edit'), ('APPLICANT_EDIT', 'Applicant Edit'), ('NEW_SUBMISSION', 'New Submission'), ('SCREENING', 'Screening'), ('TRANSITION', 'Transition'), ('BATCH_TRANSITION', 'Batch Transition'), ('DETERMINATION_OUTCOME', 'Determination Outcome'), ('BATCH_DETERMINATION_OUTCOME', 'Batch Determination Outcome'), ('INVITED_TO_PROPOSAL', 'Invited To Proposal'), ('REVIEWERS_UPDATED', 'Reviewers Updated'), ('BATCH_REVIEWERS_UPDATED', 'Batch Reviewers Updated'), ('PARTNERS_UPDATED', 'Partners Updated'), ('PARTNERS_UPDATED_PARTNER', 'Partners Updated Partner'), ('READY_FOR_REVIEW', 'Ready For Review'), ('BATCH_READY_FOR_REVIEW', 'Batch Ready For Review'), ('NEW_REVIEW', 'New Review'), ('COMMENT', 'Comment'), ('PROPOSAL_SUBMITTED', 'Proposal Submitted'), ('OPENED_SEALED', 'Opened Sealed Submission'), ('REVIEW_OPINION', 'Review Opinion'), ('DELETE_SUBMISSION', 'Delete Submission'), ('DELETE_REVIEW', 'Delete Review'), ('CREATED_PROJECT', 'Created Project'), ('UPDATE_PROJECT_LEAD', 'Update Project Lead'), ('EDIT_REVIEW', 'Edit Review'), ('SEND_FOR_APPROVAL', 'Send for Approval'), ('APPROVE_PROJECT', 'Project was Approved'), ('REQUEST_PROJECT_CHANGE', 'Project change requested')], max_length=50), ), ] diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py index 8c7a8fac5c3ea89b4f0edc278ded1bded8abc643..6300607c6dc5d672db4a5be39ba70988028935a1 100644 --- a/opentech/apply/activity/options.py +++ b/opentech/apply/activity/options.py @@ -31,7 +31,7 @@ class MESSAGES(Enum): EDIT_REVIEW = 'Edit Review' SEND_FOR_APPROVAL = 'Send for Approval' APPROVE_PROJECT = 'Project was Approved' - REJECT_PROJECT = 'Project was Rejected' + REQUEST_PROJECT_CHANGE = 'Project change requested' @classmethod def choices(cls): diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py index eb7a98fd7e6ddfea016f3c386c16da0f6b17d34c..0599a29d7b0873f6666f9e0fcaa13472fb93efd6 100644 --- a/opentech/apply/projects/forms.py +++ b/opentech/apply/projects/forms.py @@ -28,11 +28,13 @@ class CreateProjectForm(forms.Form): class CreateApprovalForm(forms.ModelForm): class Meta: model = Approval - fields = ['id'] - widgets = {'id': forms.HiddenInput()} + fields = ['by'] + widgets = {'by': forms.HiddenInput()} def __init__(self, user=None, *args, **kwargs): - super().__init__(*args, **kwargs) + initial = kwargs.pop('initial', {}) + initial.update(by=user) + super().__init__(*args, initial=initial, **kwargs) class ProjectEditForm(forms.ModelForm): diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py index ec9374b6eb3228418dc5b9db7577697af114289f..e83ca544085f5512aac8e6d77c8c2c42c88c9744 100644 --- a/opentech/apply/projects/models.py +++ b/opentech/apply/projects/models.py @@ -106,6 +106,10 @@ class Project(models.Model): if self.proposed_start > self.proposed_end: raise ValidationError(_('Proposed End Date must be after Proposed Start Date')) + def editable(self): + # Someone must lead the project to make changes + return self.lead and not self.is_locked + def get_absolute_url(self): return reverse('apply:projects:detail', args=[self.id]) @@ -122,8 +126,8 @@ class Project(models.Model): we infer it from the current status being "Comitted" and the Project being locked. """ - return self.status == COMMITTED and not self.is_locked - return self.status == COMMITTED and self.is_locked + correct_state = self.status == COMMITTED and not self.is_locked + return correct_state and self.user_has_updated_details class DocumentCategory(models.Model): diff --git a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html index 4fb310d0785409468273bdc59fef32226ce0c43a..80ed895b7f05020eb5c5735af9b70ad8a52bf6c9 100644 --- a/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html +++ b/opentech/apply/projects/templates/application_projects/includes/supporting_documents.html @@ -23,13 +23,20 @@ <p class="docs-block__title">Approval Form</p> </div> <div class="docs-block__row-inner"> - <a class="docs-block__link" href="{% url 'apply:projects:edit' pk=object.pk %}"> - {% if object.user_has_updated_details %} - Edit - {% else %} - Create - {% endif %} - </a> + {% if object.editable %} + <a class="docs-block__link" href="{% url 'apply:projects:edit' pk=object.pk %}"> + {% if object.user_has_updated_details %} + Edit + {% else %} + Create + {% endif %} + </a> + {% endif %} + {% if object.user_has_updated_details %} + <a class="docs-block__link" href="#"> + View + </a> + {% endif %} </div> </li> @@ -38,9 +45,11 @@ <svg class="icon docs-block__icon"><use xlink:href="#tick"></use></svg> <p class="docs-block__title">Supporting documents</p> </div> - <div class="docs-block__row-inner"> - <a data-fancybox data-src="#upload-supporting-doc" class="docs-block__link" href="#">Upload new</a> - </div> + {% if object.editable %} + <div class="docs-block__row-inner"> + <a data-fancybox data-src="#upload-supporting-doc" class="docs-block__link" href="#">Upload new</a> + </div> + {% endif %} {% if remaining_document_categories %} <div class="docs-block__info-text"> <p> @@ -80,8 +89,15 @@ </li> </ul> <div class="docs-block__buttons"> - <button class="button button--primary" href="#">Submit for approval</button> - <button class="button button--primary" href="#">Ready for contracting</button> + {% if object.can_send_for_approval %} + <a data-fancybox + data-src="#send-for-approval" + class="button button--primary" + href="#"> + Submit for Approval + </a> + {% endif %} + <!-- <button class="button button--primary" href="#">Ready for contracting</button> --> </div> </div> diff --git a/opentech/apply/projects/templates/application_projects/project_admin_detail.html b/opentech/apply/projects/templates/application_projects/project_admin_detail.html index b6ee3c94f3dd8031b3216217df08930ea5dac43a..ecb7076a50a46c5f4ce8a407af8bf466b56cb340 100644 --- a/opentech/apply/projects/templates/application_projects/project_admin_detail.html +++ b/opentech/apply/projects/templates/application_projects/project_admin_detail.html @@ -14,14 +14,14 @@ {% include 'funds/includes/delegated_form_base.html' with form=lead_form value='Update'%} </div> -<div class="modal" id="add-approval"> +<div class="modal" id="approve"> <h4 class="modal__header-bar">Add Approval</h4> {% include 'funds/includes/delegated_form_base.html' with form=add_approval_form value='Approve'%} </div> -<div class="modal" id="reject-project"> - <h4 class="modal__header-bar">Reject Project</h4> - {% include 'funds/includes/delegated_form_base.html' with form=rejection_form value='Reject'%} +<div class="modal" id="request-project-changes"> + <h4 class="modal__header-bar">Request Changes</h4> + {% include 'funds/includes/delegated_form_base.html' with form=rejection_form value='Request Changes'%} </div> {% if mobile %} @@ -37,33 +37,33 @@ data-src="#send-for-approval" class="button button--bottom-space button--primary button--full-width" href="#"> - Send for approval + Submit for Approval </a> {% endif %} - {% user_has_approved object request.user as has_approved %} - {% if object.can_make_approval and not has_approved %} - <a data-fancybox - data-src="#add-approval" - class="button button--bottom-space button--primary button--full-width" - href="#"> - Add approval - </a> - - <a data-fancybox - data-src="#reject-project" - class="button button--bottom-space button--primary button--full-width" - href="#"> - Reject - </a> + {% if object.can_make_approval %} + {% user_can_approve_project object request.user as user_can_approve %} + <a data-fancybox + data-src="#approve" + class="button button--bottom-space button--primary button--full-width {% if user_can_approve %}is-not-disabled{% else %}is-disabled{% endif %}" + href="#"> + Approve + </a> + + <a data-fancybox + data-src="#request-project-changes" + class="button button--bottom-space button--primary button--full-width {% if user_can_approve %}is-not-disabled{% else %}is-disabled{% endif %}" + href="#"> + Request changes + </a> {% endif %} - <a data-fancybox - data-src="#ready-for-contracting" - class="button button--primary button--full-width" - href="#"> - Ready for contracting - </a> + <!-- <a data-fancybox --> + <!-- data-src="#ready-for-contracting" --> + <!-- class="button button--primary button--full-width" --> + <!-- href="#"> --> + <!-- Ready for contracting --> + <!-- </a> --> <p class="sidebar__separator">Assign</p> @@ -75,12 +75,12 @@ Lead </a> - <a data-fancybox - data-src="#update-meta-categories" - class="button button--bottom-space button--white button--full-width" - href="#"> - Meta Categories - </a> + <!-- <a data-fancybox --> + <!-- data-src="#update-meta-categories" --> + <!-- class="button button--bottom-space button--white button--full-width" --> + <!-- href="#"> --> + <!-- Meta Categories --> + <!-- </a> --> </div> @@ -89,7 +89,7 @@ <h5>Approved By:</h5> {% for approval in approvals %} - <p>{{ approval.by }}</p> + <p>{{ approval.by }} - {{ approval.created_at|date:"Y-m-d" }}</p> {% endfor %} </div> {% endif %} diff --git a/opentech/apply/projects/templates/application_projects/project_detail.html b/opentech/apply/projects/templates/application_projects/project_detail.html index b6c8bad4221a0c8a08628687d157ffbf184bd35d..01d4870ba4fa3450b860e19bf245e46d88c9c8a8 100644 --- a/opentech/apply/projects/templates/application_projects/project_detail.html +++ b/opentech/apply/projects/templates/application_projects/project_detail.html @@ -128,7 +128,9 @@ {# {% include "funds/includes/invoice_block.html" %} #} {# </div> #} - {% include "application_projects/includes/supporting_documents.html" %} + {% if request.user.is_apply_staff %} + {% include "application_projects/includes/supporting_documents.html" %} + {% endif %} </article> <aside class="sidebar"> diff --git a/opentech/apply/projects/templatetags/approval_tools.py b/opentech/apply/projects/templatetags/approval_tools.py index 87eb3bd4efd24c39bc3951eb8a3b0c053142e36d..1af889f3ef9dd0fca8d85717ca38942b5c784c9f 100644 --- a/opentech/apply/projects/templatetags/approval_tools.py +++ b/opentech/apply/projects/templatetags/approval_tools.py @@ -3,7 +3,11 @@ from django import template register = template.Library() -@register.simple_tag def user_has_approved(project, user): """Has the given User already approved the given Project""" return project.approvals.filter(by=user).exists() + + +@register.simple_tag +def user_can_approve_project(project, user): + return user.is_approver and not user_has_approved(project, user) diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py index ada41a99edce3847333bc4decb6150d345ee4545..c234c9141fb0eb03bd3320a9666d601f07692493 100644 --- a/opentech/apply/projects/tests/test_views.py +++ b/opentech/apply/projects/tests/test_views.py @@ -23,7 +23,7 @@ class TestCreateApprovalView(BaseViewTestCase): project = ProjectFactory() self.assertEqual(project.approvals.count(), 0) - response = self.post_page(project, {'form-submitted-add_approval_form': ''}) + response = self.post_page(project, {'form-submitted-add_approval_form': '', 'by': self.user.id}) self.assertEqual(response.status_code, 200) project.refresh_from_db() diff --git a/opentech/apply/projects/views.py b/opentech/apply/projects/views.py index b787b26be8b49b4d75ffe9cab9520661d6b40f7a..04acf80b631a59a2340c4bd1a5c5b50d5db9f3c2 100644 --- a/opentech/apply/projects/views.py +++ b/opentech/apply/projects/views.py @@ -1,10 +1,9 @@ from copy import copy from django.db import transaction -from django.http import Http404 from django.shortcuts import redirect from django.utils.decorators import method_decorator -from django.views.generic import CreateView, DetailView, FormView, UpdateView +from django.views.generic import CreateView, DetailView, UpdateView from opentech.apply.activity.messaging import MESSAGES, messenger from opentech.apply.activity.views import ActivityContextMixin, CommentFormView @@ -25,19 +24,9 @@ class CreateApprovalView(DelegatedViewMixin, CreateView): @transaction.atomic() def form_valid(self, form): - try: - project = Project.objects.get(pk=self.kwargs['pk']) - except Project.DoesNotExist: - raise Http404("No Project found with ID={self.kwargs['pk']}") - - Approval.objects.create( - by=self.request.user, - project=project, - ) - - project.is_locked = False - project.status = CONTRACTING - project.save(update_fields=['is_locked', 'status']) + project = self.kwargs['object'] + form.instance.project = project + response = super().form_valid(form) messenger( MESSAGES.APPROVE_PROJECT, @@ -46,29 +35,32 @@ class CreateApprovalView(DelegatedViewMixin, CreateView): source=project, ) - return redirect(project) + project.is_locked = False + project.status = CONTRACTING + project.save(update_fields=['is_locked', 'status']) + + return response @method_decorator(staff_required, name='dispatch') -class RejectionView(DelegatedViewMixin, FormView): +class RejectionView(DelegatedViewMixin, UpdateView): context_name = 'rejection_form' form_class = RejectionForm model = Project def form_valid(self, form): - try: - project = Project.objects.get(pk=self.kwargs['pk']) - except Project.DoesNotExist: - raise Http404("No Project found with ID={self.kwargs['pk']}") - messenger( - MESSAGES.REJECT_PROJECT, + MESSAGES.REQUEST_PROJECT_CHANGE, request=self.request, user=self.request.user, - source=project, + source=self.object, comment=form.cleaned_data['comment'], ) - return redirect(project) + + self.object.is_locked = False + self.object.save(update_fields=['is_locked']) + + return redirect(self.object) @method_decorator(staff_required, name='dispatch') @@ -85,8 +77,7 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): MESSAGES.SEND_FOR_APPROVAL, request=self.request, user=self.request.user, - source=form.instance.submission, - project=form.instance, + source=self.object, ) return response