diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py index 644584191c714a3b295b866bab97b23cd72b31a5..01411c009d8da4e5e1c9b99a908a757277efbc3d 100644 --- a/opentech/apply/activity/messaging.py +++ b/opentech/apply/activity/messaging.py @@ -55,6 +55,7 @@ neat_related = { MESSAGES.SCREENING: 'old_status', MESSAGES.REVIEW_OPINION: 'opinion', MESSAGES.DELETE_REVIEW: 'review', + MESSAGES.UPDATE_PROJECT_LEAD: 'old_lead', } @@ -368,6 +369,7 @@ class SlackAdapter(AdapterBase): MESSAGES.DELETE_SUBMISSION: '{user} has deleted {submission.title}', MESSAGES.DELETE_REVIEW: '{user} has deleted {review.author} review for <{link}|{submission.title}>.', MESSAGES.CREATED_PROJECT: '{user} has created a Project: <{link}|{project.name}>.', + MESSAGES.UPDATE_PROJECT_LEAD: 'The lead of project <{link}|{project.name}> has been updated from {old_lead} to {project.lead} by {user}', } def __init__(self): diff --git a/opentech/apply/activity/migrations/0025_add_update_project_lead.py b/opentech/apply/activity/migrations/0025_add_update_project_lead.py new file mode 100644 index 0000000000000000000000000000000000000000..c8f5344d76e2aca9136145df833b8385e9a2967f --- /dev/null +++ b/opentech/apply/activity/migrations/0025_add_update_project_lead.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.13 on 2019-08-01 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activity', '0024_add_created_project_event'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='type', + field=models.CharField(choices=[('UPDATE_LEAD', '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')], max_length=50), + ), + ] diff --git a/opentech/apply/activity/options.py b/opentech/apply/activity/options.py index 60ff5e28d2d54b4d8d8947b9b77016d39e55f303..ecd066fc9bf2164d01eab30ccc0bb24206286e66 100644 --- a/opentech/apply/activity/options.py +++ b/opentech/apply/activity/options.py @@ -26,6 +26,7 @@ class MESSAGES(Enum): DELETE_SUBMISSION = 'Delete Submission' DELETE_REVIEW = 'Delete Review' CREATED_PROJECT = 'Created Project' + UPDATE_PROJECT_LEAD = 'Update Project Lead' @classmethod def choices(cls): diff --git a/opentech/apply/projects/forms.py b/opentech/apply/projects/forms.py index ae6e5ddd036049a3d07e4da376ffef922201529b..b4a4d0358245b65fcb0b7c0726fdcf161da1dbd5 100644 --- a/opentech/apply/projects/forms.py +++ b/opentech/apply/projects/forms.py @@ -1,6 +1,8 @@ from django import forms +from django.db.models import Q from opentech.apply.funds.models import ApplicationSubmission +from opentech.apply.users.groups import STAFF_GROUP_NAME from .models import Project @@ -20,3 +22,20 @@ class CreateProjectForm(forms.Form): def save(self, *args, **kwargs): submission = self.cleaned_data['submission'] return Project.create_from_submission(submission) + + +class UpdateProjectLeadForm(forms.ModelForm): + class Meta: + fields = ['lead'] + model = Project + + def __init__(self, user=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + lead_field = self.fields['lead'] + lead_field.label = f'Update lead from {self.instance.lead} to' + + qwargs = Q(groups__name=STAFF_GROUP_NAME) | Q(is_superuser=True) + lead_field.queryset = (lead_field.queryset.exclude(pk=self.instance.lead_id) + .filter(qwargs) + .distinct()) diff --git a/opentech/apply/projects/migrations/0003_add_project_lead.py b/opentech/apply/projects/migrations/0003_add_project_lead.py new file mode 100644 index 0000000000000000000000000000000000000000..2699ce4b680cf93c13f080948e29f14370cc4976 --- /dev/null +++ b/opentech/apply/projects/migrations/0003_add_project_lead.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0.13 on 2019-07-31 13:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('application_projects', '0002_add_submission_fields_to_project'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='lead', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py index ee5a32fab1d86e3123be5b7696af4a015a23e244..b7062767604b7d194d9486988ca0267c0482cd75 100644 --- a/opentech/apply/projects/models.py +++ b/opentech/apply/projects/models.py @@ -1,8 +1,10 @@ +from django.conf import settings from django.db import models from django.urls import reverse class Project(models.Model): + lead = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) submission = models.OneToOneField("funds.ApplicationSubmission", on_delete=models.CASCADE) name = models.TextField() diff --git a/opentech/apply/projects/templates/application_projects/project_admin_detail.html b/opentech/apply/projects/templates/application_projects/project_admin_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..433ec82fee8457936997337a08863182e505a1ee --- /dev/null +++ b/opentech/apply/projects/templates/application_projects/project_admin_detail.html @@ -0,0 +1,60 @@ +{% extends "application_projects/project_detail.html" %} + +{% load static %} + +{% block admin_sidebar %} +<div class="modal" id="assign-lead"> + <h4 class="modal__header-bar">Assign Lead</h4> + {% include 'funds/includes/delegated_form_base.html' with form=lead_form value='Update'%} +</div> + +{% if mobile %} + <a class="js-actions-toggle button button--white button--full-width button--actions">Actions to take</a> +{% endif %} + +<div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions {% if mobile %}sidebar__inner--mobile{% endif %}"> + + <h5>Actions to take</h5> + + <a data-fancybox + data-src="#send-for-approval" + class="button button--bottom-space button--primary button--full-width" + href="#"> + Send for approval + </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> + + <a data-fancybox + data-src="#assign-lead" + class="button button--bottom-space button--white button--full-width" + href="#"> + Program Manager + </a> + + <a data-fancybox + data-src="#update-meta-categories" + class="button button--bottom-space button--white button--full-width" + href="#"> + Meta Categories + </a> + +</div> +{% endblock %} + +{% block extra_css %} + <link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}"> +{% endblock %} + +{% block extra_js %} + {{ block.super }} + <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script> +{% endblock %} diff --git a/opentech/apply/projects/templates/application_projects/project_detail.html b/opentech/apply/projects/templates/application_projects/project_detail.html index f1e9654769e282008b0542341afc6deab9155f07..406b54ff9d23070322313c5474b050bdadee13b6 100644 --- a/opentech/apply/projects/templates/application_projects/project_detail.html +++ b/opentech/apply/projects/templates/application_projects/project_detail.html @@ -1,6 +1,7 @@ {% extends "base-apply.html" %} {% load static %} +{% load wagtailcore_tags %} {% block title %}{{ object.title }}{% endblock %} @@ -10,6 +11,29 @@ <div class="admin-bar"> <div class="admin-bar__inner"> <h1 class="beta heading heading--no-margin heading--bold">{{ object.name }}</h1> + <h5 class="heading heading--meta"> + + <span> + {% if public_page %} + <a class="link--transparent link--underlined" href="{% pageurl public_page %}" > + {{ object.submission.page }} + </a> + {% else %} + {{ object.submission.page }} + {% endif %} + </span> + + {% if request.user.is_apply_staff %} + <span> + <a class="link--transparent link--underlined" href="{% url 'apply:rounds:detail' pk=object.submission.round.pk %}">{{ object.submission.round }}</a> + </span> + {% else %} + <span>{{ object.submission.round }}</span> + {% endif %} + + <span>Lead: {{ object.lead }}</span> + + </h5> {# {% status_bar object.workflow object.phase request.user same_stage=True%} #} <div class="tabs js-tabs"> @@ -93,47 +117,10 @@ </article> <aside class="sidebar"> - {% if mobile %} - <a class="js-actions-toggle button button--white button--full-width button--actions">Actions to take</a> + {% if request.user.is_apply_staff %} + {% block admin_sidebar %}{% endblock %} {% endif %} - <div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions {% if mobile %}sidebar__inner--mobile{% endif %}"> - - <h5>Actions to take</h5> - - <a data-fancybox - data-src="#send-for-approval" - class="button button--bottom-space button--primary button--full-width" - href="#"> - Send for approval - </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> - - <a data-fancybox - data-src="#update-program-manager" - class="button button--bottom-space button--white button--full-width" - href="#"> - Program Manager - </a> - - <a data-fancybox - data-src="#update-meta-categories" - class="button button--bottom-space button--white button--full-width" - href="#"> - Meta Categories - </a> - - </div> - <div class="sidebar__inner"> <h5>Meta Categories</h5> diff --git a/opentech/apply/projects/tests/test_views.py b/opentech/apply/projects/tests/test_views.py index 37e7c26d6fda1c2bd2be202a93b79fc7e60619b0..2bb01a348ff80d3622941bce885647e4f235eab3 100644 --- a/opentech/apply/projects/tests/test_views.py +++ b/opentech/apply/projects/tests/test_views.py @@ -1,4 +1,3 @@ -from django.core.exceptions import PermissionDenied from django.test import RequestFactory, TestCase from opentech.apply.users.tests.factories import (ReviewerFactory, @@ -19,8 +18,8 @@ class TestProjectDetailView(TestCase): request = self.factory.get('/projects/1/') request.user = ReviewerFactory() - with self.assertRaises(PermissionDenied): - ProjectDetailView.as_view()(request, pk=self.project.pk) + response = ProjectDetailView.as_view()(request, pk=self.project.pk) + self.assertEqual(response.status_code, 403) def test_staff_user_has_access(self): request = self.factory.get('/projects/1/') @@ -40,5 +39,5 @@ class TestProjectDetailView(TestCase): request = self.factory.get('/projects/1/') request.user = UserFactory() - with self.assertRaises(PermissionDenied): - ProjectDetailView.as_view()(request, pk=self.project.pk) + response = ProjectDetailView.as_view()(request, pk=self.project.pk) + self.assertEqual(response.status_code, 200) diff --git a/opentech/apply/projects/views.py b/opentech/apply/projects/views.py index c8b086884c807a0d138587735c67a3a3664cad8a..d77ade136a5601f47b322491656a692fd81b43e0 100644 --- a/opentech/apply/projects/views.py +++ b/opentech/apply/projects/views.py @@ -1,11 +1,51 @@ +from copy import copy + from django.utils.decorators import method_decorator -from django.views.generic import DetailView +from django.views.generic import DetailView, UpdateView +from opentech.apply.activity.messaging import MESSAGES, messenger +from opentech.apply.utils.views import DelegateableView, DelegatedViewMixin from opentech.apply.users.decorators import staff_required +from opentech.apply.utils.views import ViewDispatcher +from .forms import UpdateProjectLeadForm from .models import Project @method_decorator(staff_required, name='dispatch') -class ProjectDetailView(DetailView): +class UpdateLeadView(DelegatedViewMixin, UpdateView): model = Project + form_class = UpdateProjectLeadForm + context_name = 'lead_form' + + def form_valid(self, form): + # Fetch the old lead from the database + old = copy(self.get_object()) + + response = super().form_valid(form) + + messenger( + MESSAGES.UPDATE_PROJECT_LEAD, + request=self.request, + user=self.request.user, + submission=form.instance.submission, + related=old.lead, + project=form.instance, + ) + + return response + + +class AdminProjectDetailView(DelegateableView, DetailView): + form_views = [UpdateLeadView] + model = Project + template_name_suffix = '_admin_detail' + + +class ApplicantProjectDetailView(DetailView): + model = Project + + +class ProjectDetailView(ViewDispatcher): + admin_view = AdminProjectDetailView + applicant_view = ApplicantProjectDetailView