From 29723e9634c3123203ccf9946c3f78ac6b1acd9e Mon Sep 17 00:00:00 2001 From: Saurabh Kumar <theskumar@users.noreply.github.com> Date: Sat, 15 Jun 2024 12:32:24 +0530 Subject: [PATCH] Update Screening UI & UX (#3955) - Use htmx - Update/fix behaviour - code cleanup - js, api, python views/forms Fixes https://github.com/HyphaApp/hypha/issues/3873 https://github.com/HyphaApp/hypha/assets/236356/befc068a-864e-4ae2-a571-268f3fe288f7 --- .../apply/activity/adapters/activity_feed.py | 21 +- hypha/apply/api/v1/screening/__init__.py | 0 hypha/apply/api/v1/screening/filters.py | 9 - hypha/apply/api/v1/screening/permissions.py | 16 -- hypha/apply/api/v1/screening/serializers.py | 24 -- .../apply/api/v1/screening/tests/__init__.py | 0 .../api/v1/screening/tests/test_views.py | 265 ------------------ hypha/apply/api/v1/screening/views.py | 100 ------- hypha/apply/api/v1/urls.py | 11 +- hypha/apply/funds/forms.py | 31 -- .../commands/export_submissions_csv.py | 2 +- hypha/apply/funds/models/submissions.py | 50 +--- hypha/apply/funds/permissions.py | 10 + hypha/apply/funds/tables.py | 12 +- .../applicationsubmission_admin_detail.html | 7 +- .../funds/applicationsubmission_detail.html | 1 - .../funds/includes/admin_primary_actions.html | 4 - .../funds/includes/rendered_answers.html | 6 +- .../funds/includes/screening_form.html | 9 - .../screening_status_block-button.html | 114 ++++++++ .../includes/screening_status_block.html | 60 +--- .../funds/includes/submission-list-item.html | 15 +- .../templates/funds/submissions_result.html | 2 +- .../funds/templatetags/submission_tags.py | 7 - hypha/apply/funds/tests/test_views.py | 139 ++++----- hypha/apply/funds/urls.py | 6 + hypha/apply/funds/utils.py | 18 +- hypha/apply/funds/views.py | 96 ++++--- .../application_projects/project_detail.html | 2 +- .../application_projects/vendor_detail.html | 2 +- hypha/apply/users/templates/users/login.html | 2 +- .../static_src/javascript/screening-status.js | 111 -------- hypha/static_src/sass/components/_button.scss | 12 +- .../{_grid.scss => _hypha-grid.scss} | 4 +- hypha/static_src/sass/main.scss | 2 +- .../django/forms/widgets/multiple_input.html | 2 +- 36 files changed, 338 insertions(+), 834 deletions(-) delete mode 100644 hypha/apply/api/v1/screening/__init__.py delete mode 100644 hypha/apply/api/v1/screening/filters.py delete mode 100644 hypha/apply/api/v1/screening/permissions.py delete mode 100644 hypha/apply/api/v1/screening/serializers.py delete mode 100644 hypha/apply/api/v1/screening/tests/__init__.py delete mode 100644 hypha/apply/api/v1/screening/tests/test_views.py delete mode 100644 hypha/apply/api/v1/screening/views.py delete mode 100644 hypha/apply/funds/templates/funds/includes/screening_form.html create mode 100644 hypha/apply/funds/templates/funds/includes/screening_status_block-button.html delete mode 100644 hypha/static_src/javascript/screening-status.js rename hypha/static_src/sass/components/{_grid.scss => _hypha-grid.scss} (98%) diff --git a/hypha/apply/activity/adapters/activity_feed.py b/hypha/apply/activity/adapters/activity_feed.py index 97fd44220..3155a7772 100644 --- a/hypha/apply/activity/adapters/activity_feed.py +++ b/hypha/apply/activity/adapters/activity_feed.py @@ -330,7 +330,20 @@ class ActivityAdapter(AdapterBase): ) def handle_screening_statuses(self, source, old_status, **kwargs): - new_status = ", ".join([s.title for s in source.screening_statuses.all()]) - return _("Screening decision from {old_status} to {new_status}").format( - old_status=old_status, new_status=new_status - ) + new_status = source.get_current_screening_status() + + if str(new_status) == old_status: + return + + if new_status and old_status != "-": + return _( + 'Updated screening decision from "{old_status}" to "{new_status}".' + ).format(old_status=old_status, new_status=new_status) + elif new_status: + return _('Added screening decision to "{new_status}".').format( + new_status=new_status + ) + elif old_status != "-": + return _('Removed "{old_status}" screening decision.').format( + old_status=old_status + ) diff --git a/hypha/apply/api/v1/screening/__init__.py b/hypha/apply/api/v1/screening/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/hypha/apply/api/v1/screening/filters.py b/hypha/apply/api/v1/screening/filters.py deleted file mode 100644 index 610c71493..000000000 --- a/hypha/apply/api/v1/screening/filters.py +++ /dev/null @@ -1,9 +0,0 @@ -from django_filters import rest_framework as filters - -from hypha.apply.funds.models import ScreeningStatus - - -class ScreeningStatusFilter(filters.FilterSet): - class Meta: - model = ScreeningStatus - fields = ["yes", "default"] diff --git a/hypha/apply/api/v1/screening/permissions.py b/hypha/apply/api/v1/screening/permissions.py deleted file mode 100644 index 46e1729bc..000000000 --- a/hypha/apply/api/v1/screening/permissions.py +++ /dev/null @@ -1,16 +0,0 @@ -from rest_framework import permissions - - -class HasScreenPermission(permissions.BasePermission): - """ - Custom permission that user should have for screening the submission - """ - - def has_permission(self, request, view): - try: - submission = view.get_submission_object() - except KeyError: - return True - if submission.is_archive: - return False - return True diff --git a/hypha/apply/api/v1/screening/serializers.py b/hypha/apply/api/v1/screening/serializers.py deleted file mode 100644 index eeefea1b5..000000000 --- a/hypha/apply/api/v1/screening/serializers.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework import exceptions, serializers - -from hypha.apply.funds.models import ScreeningStatus - - -class ScreeningStatusListSerializer(serializers.ModelSerializer): - class Meta: - model = ScreeningStatus - fields = ("id", "title", "yes", "default") - - -class ScreeningStatusSerializer(serializers.Serializer): - id = serializers.IntegerField() - - def validate_id(self, value): - try: - ScreeningStatus.objects.get(id=value) - except ScreeningStatus.DoesNotExist as e: - raise exceptions.ValidationError({"detail": "Not found"}) from e - return value - - -class ScreeningStatusDefaultSerializer(serializers.Serializer): - yes = serializers.BooleanField() diff --git a/hypha/apply/api/v1/screening/tests/__init__.py b/hypha/apply/api/v1/screening/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/hypha/apply/api/v1/screening/tests/test_views.py b/hypha/apply/api/v1/screening/tests/test_views.py deleted file mode 100644 index 43e790e83..000000000 --- a/hypha/apply/api/v1/screening/tests/test_views.py +++ /dev/null @@ -1,265 +0,0 @@ -from django.test import override_settings -from django.urls import reverse_lazy -from model_bakery import baker -from rest_framework import status -from rest_framework.test import APITestCase - -from hypha.apply.funds.models import ScreeningStatus -from hypha.apply.funds.tests.factories.models import ApplicationSubmissionFactory -from hypha.apply.users.tests.factories import ReviewerFactory, StaffFactory, UserFactory - - -@override_settings(SECURE_SSL_REDIRECT=False) -class ScreeningStatusViewSetTests(APITestCase): - def setUp(self): - ScreeningStatus.objects.all().delete() - self.yes_screening_status = baker.make("funds.ScreeningStatus", yes=True) - - def get_screening_status_url(self, pk=None): - if pk: - return reverse_lazy("api:v1:screenings-detail", kwargs={"pk": pk}) - return reverse_lazy("api:v1:screenings-list") - - def test_staff_can_list_screening_statuses(self): - user = StaffFactory() - self.client.force_authenticate(user) - response = self.client.get(self.get_screening_status_url()) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), ScreeningStatus.objects.count()) - self.assertEqual(response.json()[0]["id"], self.yes_screening_status.id) - self.assertEqual(response.json()[0]["title"], self.yes_screening_status.title) - self.assertEqual(response.json()[0]["yes"], self.yes_screening_status.yes) - self.assertEqual( - response.json()[0]["default"], self.yes_screening_status.default - ) - - def test_staff_can_view_screening_statuses_detail(self): - user = StaffFactory() - self.client.force_authenticate(user) - response = self.client.get( - self.get_screening_status_url(pk=self.yes_screening_status.id) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_user_cant_list_screening_statuses(self): - user = UserFactory() - self.client.force_authenticate(user) - response = self.client.get(self.get_screening_status_url()) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_reviewer_cant_list_screening_statuses(self): - user = ReviewerFactory() - self.client.force_authenticate(user) - response = self.client.get(self.get_screening_status_url()) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@override_settings(SECURE_SSL_REDIRECT=False) -class SubmissionScreeningStatusViewSetTests(APITestCase): - def setUp(self): - ScreeningStatus.objects.all().delete() - self.yes_screening_status = baker.make("funds.ScreeningStatus", yes=True) - self.yes_default_screening_status = baker.make( - "funds.ScreeningStatus", yes=True, default=True - ) - self.no_screening_status = baker.make("funds.ScreeningStatus", yes=False) - self.no_default_screening_status = baker.make( - "funds.ScreeningStatus", yes=False, default=True - ) - self.submission = ApplicationSubmissionFactory() - - def get_submission_screening_status_url(self, submission_id=None): - return reverse_lazy( - "api:v1:submission-screening_statuses-list", - kwargs={"submission_pk": submission_id}, - ) - - def test_cant_add_screening_status_without_setting_default(self): - user = StaffFactory() - self.client.force_authenticate(user) - self.submission.screening_statuses.clear() - response = self.client.post( - self.get_submission_screening_status_url(submission_id=self.submission.id), - data={"id": self.yes_screening_status.id}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["detail"], - "Can't set screening decision without default being set", - ) - - def test_cant_add_two_types_of_screening_status(self): - user = StaffFactory() - self.client.force_authenticate(user) - self.submission.screening_statuses.clear() - self.submission.screening_statuses.add(self.yes_default_screening_status) - response = self.client.post( - self.get_submission_screening_status_url(submission_id=self.submission.id), - data={"id": self.no_screening_status.id}, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["detail"], - "Can't set screening decision for both yes and no", - ) - - def test_add_screening_status(self): - user = StaffFactory() - self.client.force_authenticate(user) - self.submission.screening_statuses.clear() - self.submission.screening_statuses.add(self.yes_default_screening_status) - response = self.client.post( - self.get_submission_screening_status_url(submission_id=self.submission.id), - data={"id": self.yes_screening_status.id}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(len(response.json()), 2) - - possible_ids = [ - self.yes_screening_status.id, - self.yes_default_screening_status.id, - ] - self.assertIn(response.json()[0]["id"], possible_ids) - self.assertIn(response.json()[1]["id"], possible_ids) - - def test_staff_can_list_submission_screening_statuses(self): - user = StaffFactory() - self.client.force_authenticate(user) - self.submission.screening_statuses.clear() - response = self.client.get( - self.get_submission_screening_status_url(submission_id=self.submission.id) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 0) - - def test_set_default_screening_status(self): - user = StaffFactory() - self.submission.screening_statuses.clear() - self.client.force_authenticate(user) - response = self.client.post( - reverse_lazy( - "api:v1:submission-screening_statuses-default", - kwargs={"submission_pk": self.submission.id}, - ), - data={"yes": True}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - default_set = self.submission.screening_statuses.get(default=True) - self.assertEqual(response.json()["id"], default_set.id) - self.assertEqual(response.json()["yes"], default_set.yes) - - def test_change_default_screening_status(self): - user = StaffFactory() - self.client.force_authenticate(user) - self.submission.screening_statuses.clear() - response = self.client.post( - reverse_lazy( - "api:v1:submission-screening_statuses-default", - kwargs={"submission_pk": self.submission.id}, - ), - data={"yes": True}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - default_set = self.submission.screening_statuses.get(default=True) - self.assertEqual(response.json()["id"], default_set.id) - self.assertEqual(response.json()["yes"], default_set.yes) - - response = self.client.post( - reverse_lazy( - "api:v1:submission-screening_statuses-default", - kwargs={"submission_pk": self.submission.id}, - ), - data={"yes": False}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - default_set = self.submission.screening_statuses.get(default=True) - self.assertEqual(response.json()["id"], default_set.id) - self.assertEqual(response.json()["yes"], default_set.yes) - - def test_cant_change_default_screening_status(self): - user = StaffFactory() - self.submission.screening_statuses.clear() - self.client.force_authenticate(user) - self.submission.screening_statuses.add( - self.yes_default_screening_status, self.yes_screening_status - ) - response = self.client.post( - reverse_lazy( - "api:v1:submission-screening_statuses-default", - kwargs={"submission_pk": self.submission.id}, - ), - data={"yes": False}, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - default_set = self.submission.screening_statuses.get(default=True) - self.assertEqual(response.json()["id"], default_set.id) - self.assertEqual(response.json()["yes"], default_set.yes) - - def test_remove_submission_screening_status(self): - user = StaffFactory() - self.submission.screening_statuses.clear() - self.client.force_authenticate(user) - self.submission.screening_statuses.add( - self.yes_default_screening_status, self.yes_screening_status - ) - response = self.client.delete( - reverse_lazy( - "api:v1:submission-screening_statuses-detail", - kwargs={ - "submission_pk": self.submission.id, - "pk": self.yes_screening_status.id, - }, - ) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - - def test_cant_remove_submission_default_screening_status(self): - user = StaffFactory() - self.submission.screening_statuses.clear() - self.submission.screening_statuses.add(self.yes_default_screening_status) - self.client.force_authenticate(user) - response = self.client.delete( - reverse_lazy( - "api:v1:submission-screening_statuses-detail", - kwargs={ - "submission_pk": self.submission.id, - "pk": self.yes_default_screening_status.id, - }, - ) - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["detail"], "Can't delete default screening decision." - ) - - def test_cant_remove_not_set_screening_status(self): - user = StaffFactory() - self.submission.screening_statuses.clear() - self.client.force_authenticate(user) - response = self.client.delete( - reverse_lazy( - "api:v1:submission-screening_statuses-detail", - kwargs={ - "submission_pk": self.submission.id, - "pk": self.yes_screening_status.id, - }, - ) - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_user_cant_list_screening_statuses(self): - user = UserFactory() - self.client.force_authenticate(user) - response = self.client.get( - self.get_submission_screening_status_url(submission_id=self.submission.id) - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_reviewer_cant_list_screening_statuses(self): - user = ReviewerFactory() - self.client.force_authenticate(user) - response = self.client.get( - self.get_submission_screening_status_url(submission_id=self.submission.id) - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/hypha/apply/api/v1/screening/views.py b/hypha/apply/api/v1/screening/views.py deleted file mode 100644 index 290dcd188..000000000 --- a/hypha/apply/api/v1/screening/views.py +++ /dev/null @@ -1,100 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django_filters import rest_framework as filters -from rest_framework import mixins, permissions, status, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response - -from hypha.apply.funds.models import ScreeningStatus - -from ..mixin import SubmissionNestedMixin -from ..permissions import IsApplyStaffUser -from .filters import ScreeningStatusFilter -from .permissions import HasScreenPermission -from .serializers import ( - ScreeningStatusDefaultSerializer, - ScreeningStatusListSerializer, - ScreeningStatusSerializer, -) - - -class ScreeningStatusViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = ScreeningStatusFilter - pagination_class = None - queryset = ScreeningStatus.objects.all() - serializer_class = ScreeningStatusListSerializer - - -class SubmissionScreeningStatusViewSet( - SubmissionNestedMixin, - mixins.ListModelMixin, - mixins.CreateModelMixin, - viewsets.GenericViewSet, -): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - HasScreenPermission, - ) - serializer_class = ScreeningStatusListSerializer - pagination_class = None - - def get_queryset(self): - submission = self.get_submission_object() - return submission.screening_statuses.all() - - def create(self, request, *args, **kwargs): - ser = ScreeningStatusSerializer(data=request.data) - ser.is_valid(raise_exception=True) - submission = self.get_submission_object() - screening_status = get_object_or_404( - ScreeningStatus, id=ser.validated_data["id"] - ) - if not submission.screening_statuses.filter(default=True).exists(): - raise ValidationError( - {"detail": "Can't set screening decision without default being set"} - ) - if ( - submission.screening_statuses.exists() - and submission.screening_statuses.first().yes != screening_status.yes - ): - raise ValidationError( - {"detail": "Can't set screening decision for both yes and no"} - ) - submission.screening_statuses.add(screening_status) - ser = self.get_serializer(submission.screening_statuses.all(), many=True) - return Response(ser.data, status=status.HTTP_201_CREATED) - - @action(detail=False, methods=["post"]) - def default(self, request, *args, **kwargs): - ser = ScreeningStatusDefaultSerializer(data=request.data) - ser.is_valid(raise_exception=True) - yes = ser.validated_data["yes"] - submission = self.get_submission_object() - if submission.screening_statuses.filter(default=False).exists(): - submission.screening_statuses.remove( - *submission.screening_statuses.filter(default=False) - ) - screening_status = ScreeningStatus.objects.get(default=True, yes=yes) - if submission.has_default_screening_status_set: - default_status = submission.screening_statuses.get() - submission.screening_statuses.remove(default_status) - submission.screening_statuses.add(screening_status) - ser = self.get_serializer(submission.screening_statuses.get(default=True)) - return Response(ser.data, status=status.HTTP_201_CREATED) - - def destroy(self, request, *args, **kwargs): - screening_status = self.get_object() - if screening_status.default: - raise ValidationError( - {"detail": "Can't delete default screening decision."} - ) - submission = self.get_submission_object() - submission.screening_statuses.remove(screening_status) - ser = self.get_serializer(submission.screening_statuses.all(), many=True) - return Response(ser.data) diff --git a/hypha/apply/api/v1/urls.py b/hypha/apply/api/v1/urls.py index c9b82e4cd..f17708e73 100644 --- a/hypha/apply/api/v1/urls.py +++ b/hypha/apply/api/v1/urls.py @@ -5,10 +5,6 @@ from hypha.apply.api.v1.determination.views import SubmissionDeterminationViewSe from hypha.apply.api.v1.projects.views import InvoiceDeliverableViewSet from hypha.apply.api.v1.reminder.views import SubmissionReminderViewSet from hypha.apply.api.v1.review.views import SubmissionReviewViewSet -from hypha.apply.api.v1.screening.views import ( - ScreeningStatusViewSet, - SubmissionScreeningStatusViewSet, -) from .views import ( CommentViewSet, @@ -28,7 +24,6 @@ router = routers.SimpleRouter() router.register(r"submissions", SubmissionViewSet, basename="submissions") router.register(r"comments", CommentViewSet, basename="comments") router.register(r"rounds", RoundViewSet, basename="rounds") -router.register(r"screening_statuses", ScreeningStatusViewSet, basename="screenings") router.register(r"meta_terms", MetaTermsViewSet, basename="meta-terms") submission_router = routers.NestedSimpleRouter( @@ -44,11 +39,7 @@ submission_router.register(r"reviews", SubmissionReviewViewSet, basename="review submission_router.register( r"determinations", SubmissionDeterminationViewSet, basename="determinations" ) -submission_router.register( - r"screening_statuses", - SubmissionScreeningStatusViewSet, - basename="submission-screening_statuses", -) + submission_router.register( r"reminders", SubmissionReminderViewSet, basename="submission-reminder" ) diff --git a/hypha/apply/funds/forms.py b/hypha/apply/funds/forms.py index 477b4605b..4860661fe 100644 --- a/hypha/apply/funds/forms.py +++ b/hypha/apply/funds/forms.py @@ -18,7 +18,6 @@ from .models import ( AssignedReviewers, Reminder, ReviewerRole, - ScreeningStatus, ) from .permissions import can_change_external_reviewers from .utils import model_form_initial, render_icon @@ -106,36 +105,6 @@ class BatchProgressSubmissionForm(forms.Form): return action -class ScreeningSubmissionForm(ApplicationSubmissionModelForm): - class Meta: - model = ApplicationSubmission - fields = ("screening_statuses",) - labels = {"screening_statuses": "Screening Decisions"} - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop("user") - super().__init__(*args, **kwargs) - instance = kwargs.get("instance") - if instance and instance.has_default_screening_status_set: - screening_status = instance.screening_statuses.get(default=True) - self.fields["screening_statuses"].queryset = ScreeningStatus.objects.filter( - yes=screening_status.yes - ) - self.should_show = False - if self.user.is_apply_staff: - self.should_show = True - - def clean(self): - cleaned_data = super().clean() - instance = self.instance - default_status = instance.screening_statuses.get(default=True) - if default_status not in cleaned_data["screening_statuses"]: - self.add_error( - "screening_statuses", "Can't remove default screening decision." - ) - return cleaned_data - - class UpdateSubmissionLeadForm(ApplicationSubmissionModelForm): class Meta: model = ApplicationSubmission diff --git a/hypha/apply/funds/management/commands/export_submissions_csv.py b/hypha/apply/funds/management/commands/export_submissions_csv.py index 4014ec81e..7d1748f6e 100644 --- a/hypha/apply/funds/management/commands/export_submissions_csv.py +++ b/hypha/apply/funds/management/commands/export_submissions_csv.py @@ -73,7 +73,7 @@ class Command(BaseCommand): submission_reapplied, submission.stage, submission.phase, - submission.joined_screening_statuses, + submission.get_current_screening_status(), submission.submit_time.strftime("%Y-%m-%d"), submission_region, submission_country, diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py index 62a8fa26b..5d5838cc9 100644 --- a/hypha/apply/funds/models/submissions.py +++ b/hypha/apply/funds/models/submissions.py @@ -1,4 +1,3 @@ -import json import operator from functools import partialmethod, reduce from typing import Optional, Self @@ -966,51 +965,14 @@ class ApplicationSubmission( def _get_REQUIRED_value(self, name): return self.data(name) - @property - def has_default_screening_status_set(self): - return self.screening_statuses.filter(default=True).exists() - - @property - def has_yes_default_screening_status_set(self): - return self.screening_statuses.filter(default=True, yes=True).exists() - - @property - def has_no_default_screening_status_set(self): - return self.screening_statuses.filter(default=True, yes=False).exists() + def get_current_screening_status(self): + return self.screening_statuses.first() - @property - def can_not_edit_default(self): - return self.screening_statuses.all().count() > 1 - - @property - def joined_screening_statuses(self): - return ", ".join([s.title for s in self.screening_statuses.all()]) + def get_yes_screening_status(self): + return self.screening_statuses.filter(yes=True).exists() - @property - def yes_screening_statuses(self): - ScreeningStatus = apps.get_model("funds", "ScreeningStatus") - return json.dumps( - { - status.title: status.id - for status in ScreeningStatus.objects.filter(yes=True) - } - ) - - @property - def no_screening_statuses(self): - ScreeningStatus = apps.get_model("funds", "ScreeningStatus") - return json.dumps( - { - status.title: status.id - for status in ScreeningStatus.objects.filter(yes=False) - } - ) - - @property - def supports_default_screening(self): - if self.screening_statuses.exists(): - return self.screening_statuses.filter(default=True).exists() - return True + def get_no_screening_status(self): + return self.screening_statuses.filter(yes=False).first() @receiver(post_transition, sender=ApplicationSubmission) diff --git a/hypha/apply/funds/permissions.py b/hypha/apply/funds/permissions.py index c10b6f17d..f6bbe0af4 100644 --- a/hypha/apply/funds/permissions.py +++ b/hypha/apply/funds/permissions.py @@ -152,7 +152,17 @@ def is_user_has_access_to_view_submission(user, submission): return False, "" +def can_view_submission_screening(user, submission): + submission_view, _ = is_user_has_access_to_view_submission(user, submission) + if not submission_view: + return False, "No access to view submission" + if user.is_applicant: + return False, "Applicant cannot view submission screening" + return True, "" + + permissions_map = { "submission_view": is_user_has_access_to_view_submission, "submission_edit": can_edit_submission, + "can_view_submission_screening": can_view_submission_screening, } diff --git a/hypha/apply/funds/tables.py b/hypha/apply/funds/tables.py index 4c34b3d1c..eb3323938 100644 --- a/hypha/apply/funds/tables.py +++ b/hypha/apply/funds/tables.py @@ -196,11 +196,15 @@ class BaseAdminSubmissionsTable(SubmissionsTable): def render_screening_status(self, value): try: - status = value.get(default=True).title + status = value.get() + classname = "status-yes" if status.yes else "status-no text-red-500" + return format_html( + f"<span class='font-medium text-xs {classname}'>{'ðŸ‘' if status.yes else '👎'} {status.title}</span>" + ) except ScreeningStatus.DoesNotExist: - return format_html("<span>{}</span>", "Awaiting") - else: - return format_html("<span>{}</span>", status) + return format_html( + "<span class='text-xs text-fg-muted'>{}</span>", "Awaiting" + ) class AdminSubmissionsTable(BaseAdminSubmissionsTable): diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html index 5a0454873..934d7fc75 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_admin_detail.html @@ -19,9 +19,6 @@ <div class="js-actions-sidebar sidebar__inner sidebar__inner--light-blue sidebar__inner--actions {% if mobile %}sidebar__inner--mobile{% endif %}"> {% include "funds/includes/admin_primary_actions.html" %} </div> - {% if default_screening_statuses.0 != None and default_screening_statuses.1 != None %} - {% include "funds/includes/screening_form.html" with submission=object default_yes=default_screening_statuses.0 default_no=default_screening_statuses.1 %} - {% endif %} {% include "funds/includes/progress_form.html" %} {% include "funds/includes/update_lead_form.html" %} {% include "funds/includes/update_reviewer_form.html" %} @@ -65,9 +62,7 @@ {% endblock %} {% block screening_status %} - {% if default_screening_statuses.0 != None and default_screening_statuses.1 != None %} - {% include 'funds/includes/screening_status_block.html' with default_yes=default_screening_statuses.0 default_no=default_screening_statuses.1 %} - {% endif %} + <div hx-trigger="revealed" hx-get="{% url "funds:submissions:partial-screening-card" object.id %}"></div> {% endblock %} {% block meta_terms %} diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index 96a1b6f58..b569642b5 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -224,5 +224,4 @@ <script src="{% static 'js/submission-text-cleanup.js' %}"></script> <script src="{% static 'js/edit-comment.js' %}"></script> <script src="{% static 'js/flag.js' %}"></script> - <script src="{% static 'js/screening-status.js' %}"></script> {% endblock %} diff --git a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html index 3e4da6a5f..e7478cbec 100644 --- a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html +++ b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html @@ -24,10 +24,6 @@ {% endif %} {% endif %} - {% if object.has_default_screening_status_set and not object.can_not_edit_default %} - <a data-fancybox data-src="#screen-application" class="button button--bottom-space button--primary button--full-width is-not-disabled" href="#">{% trans "Screen application" %}</a> - {% endif %} - {% if object.ready_for_determination %} {% trans "Complete draft determination" as determination_draft %} {% include 'determinations/includes/determination_button.html' with submission=object class="button--bottom-space" draft_text=determination_draft %} diff --git a/hypha/apply/funds/templates/funds/includes/rendered_answers.html b/hypha/apply/funds/templates/funds/includes/rendered_answers.html index 62ac8603d..0ad5c5bee 100644 --- a/hypha/apply/funds/templates/funds/includes/rendered_answers.html +++ b/hypha/apply/funds/templates/funds/includes/rendered_answers.html @@ -1,6 +1,6 @@ {% load i18n wagtailusers_tags workflow_tags %} <h3 class="text-xl border-b pb-2 font-bold">{% trans "Proposal Information" %}</h3> -<div class="grid grid--proposal-info"> +<div class="hypha-grid hypha-grid--proposal-info"> {% if object.get_value_display != "-" %} <div> <h5 class="text-base">{% trans "Requested Funding" %}</h5> @@ -25,13 +25,13 @@ {{ object.get_email_display }} </div> {% if object.get_address_display != "-" %} - <div> + <div class="hypha-grid__cell--span-two"> <h5 class="text-base">{% trans "Address" %}</h5> {{ object.get_address_display }} </div> {% endif %} {% if object.get_organization_name_display != "-" %} - <div> + <div class="hypha-grid__cell--span-two"> <h5 class="text-base">{% trans "Organization name" %}</h5> {{ object.get_organization_name_display }} </div> diff --git a/hypha/apply/funds/templates/funds/includes/screening_form.html b/hypha/apply/funds/templates/funds/includes/screening_form.html deleted file mode 100644 index 1d4248b52..000000000 --- a/hypha/apply/funds/templates/funds/includes/screening_form.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %} -{% if screening_form.should_show %} - <div class="modal" id="screen-application" data-yes-statuses="{{ submission.yes_screening_statuses }}" data-no-statuses="{{ submission.no_screening_statuses }}" data-default-yes="{{ default_yes }}" data-default-no="{{ default_no }}"> - <h4 class="modal__header-bar">{% trans "Screen application" %}</h4> - <p id="current-status">{% trans "Current decisions" %}: {{ submission.screening_statuses.all|join:', ' }}</p> - {% trans "Screen" as screen %} - {% include 'funds/includes/delegated_form_base.html' with form=screening_form value=screen %} - </div> -{% endif %} diff --git a/hypha/apply/funds/templates/funds/includes/screening_status_block-button.html b/hypha/apply/funds/templates/funds/includes/screening_status_block-button.html new file mode 100644 index 000000000..a769565c4 --- /dev/null +++ b/hypha/apply/funds/templates/funds/includes/screening_status_block-button.html @@ -0,0 +1,114 @@ +{% load i18n heroicons %} + +<div class="relative" + x-data="{ isOpen: false, openedWithKeyboard: false, leaveTimeout: null }" + @mouseleave.prevent="leaveTimeout = setTimeout(() => { isOpen = false }, 50)" + @mouseenter="leaveTimeout ? clearTimeout(leaveTimeout) : true" + @keydown.esc.prevent="isOpen = false, openedWithKeyboard = false" + @click.outside="isOpen = false, openedWithKeyboard = false" +> + <button + class="p-2 w-full h-full inline-flex flex-col items-center rounded-lg transition-colors text-sm border + hover:border-light-blue focus-visible:border-light-blue + {% if current_status %} + font-bold + {% if type == "yes" %}button--like-active bg-light-blue/10{% else %} button--dislike-active bg-tomato/10 border-red-100{% endif %} + {% endif %}" + + hx-post="{% url 'funds:submissions:partial-screening-card' submission.id %}" + hx-swap="outerHTML" + hx-trigger="click" + hx-target="#screening-status-{{ submission.id }}" + hx-vals='{"action": "{% if not current_status %}{{ default_status.id }}{% else %}clear{% endif %}"}' + + {% if can_screen and screening_options.count > 1 %} + @mouseover="isOpen = true" + @focus="openedWithKeyboard = true" + @keydown.space.prevent="openedWithKeyboard = true" + @keydown.enter.prevent="openedWithKeyboard = true" + @keydown.down.prevent="openedWithKeyboard = true" + :class="isOpen ? 'ring-1' : '{% if type == "yes" %}border-light-blue/10 {% else %} border-tomato/10{% endif %}'" + {% endif %} + + {% if current_status %} + hx-confirm='Are you sure you want to remove the "{{ current_status }}" screening decision?' + data-tippy-content="Remove" + {% else %} + data-tippy-content="Mark as {{ default_status }}" + {% endif %} + > + {% if type == "yes" %} + {% heroicon_solid "hand-thumb-up" size=30 class="mb-2 transition-colors fill-gray-800" aria_hidden=true %} + {% else %} + {% heroicon_solid "hand-thumb-down" size=30 class="mb-2 transition-colors" aria_hidden=true %} + {% endif %} + <span class="{% if current_status %}{% if type == "yes" %}text-dark-blue{% else %}text-tomato{% endif %}{% endif %} "> + {{ current_status|default_if_none:default_status }} + </span> + + </button> + + <!-- panel --> + {% if can_screen and screening_options.count > 1 %} + <div + class=" bg-white tras min-w-48 text-sm top absolute {% if type == 'yes' %}md:start-0 {% else %}end-0{% endif %} divide-y rounded border shadow-lg group-hover:absolute z-20" + x-cloak + role="menu" + x-show="isOpen || openedWithKeyboard" + {% comment %} x-transition.origin.top {% endcomment %} + x-transition:enter="origin-top transition ease-out duration-100" + x-transition:enter-start="opacity-0 scale-90 " + x-transition:enter-end="opacity-100 scale-100" + x-transition:leave="origin-top transition ease-in duration-100" + x-transition:leave-start="opacity-100 scale-100" + x-transition:leave-end="opacity-0 scale-90" + x-trap="openedWithKeyboard" + @click.outside="isOpen = false, openedWithKeyboard = false" + @keydown.down.prevent="$focus.wrap().next()" + @keydown.up.prevent="$focus.wrap().previous()" + > + <header class="subheading border-b px-3 py-2 flex justify-between items-center gap-2"> + <span class="SubMenuHeading font-medium text-inherit">Options</span> + <button type="button" @click='isOpen = false' class="appearance-none"> + {% heroicon_mini "x-mark" aria_hidden="true" width=16 height=16 class="stroke-1 hover:stroke-2" %} + </button> + </header> + <div class="divide-y font-semibold text-inherit"> + {% for status in screening_options %} + {% if current_status.id == status.id %} + <span class="text-gray-800 flex items-center gap-2 pe-2 py-2 ps-2 font-medium bg-gray-100"> + <span class="w-4"> + {% heroicon_mini "check" aria_hidden="true" size=18 class="stroke-2 me-1" %} + </span> + <span>{{ status }}</span> + </span> + {% else %} + <a + class="cursor-pointer text-gray-800 flex items-center gap-2 pe-2 py-2 hover:bg-gray-100 focus:bg-gray-100 {% if status.id == current_status.id %}ps-2 font-medium bg-gray-100{% else %} ps-8 font-normal {% endif %}" + role='button' + hx-post="{% url 'funds:submissions:partial-screening-card' object.id %}" + hx-swap="outerHTML" + hx-trigger="click" + hx-target="#screening-status-{{ object.id }}" + hx-vals='{"action": "{% if current_status.id != status.id %}{{ status.id }}{% endif %}"}' + > + <span>{{ status }}</span> + </a> + {% endif %} + {% endfor %} + {% if current_status %} + <a + class="flex ps-8 pe-3 cursor-pointer hover:bg-light-blue/10 py-3 font-normal text-red-600 whitespace-nowrap" + role='button' + hx-post="{% url 'funds:submissions:partial-screening-card' object.id %}" + hx-swap="outerHTML" + hx-trigger="click" + hx-target="#screening-status-{{ object.id }}" + hx-vals='{"action": "clear"}' + >Remove Decision</a> + {% endif %} + </div> + </div> + {% endif %} +</div> + diff --git a/hypha/apply/funds/templates/funds/includes/screening_status_block.html b/hypha/apply/funds/templates/funds/includes/screening_status_block.html index f7f0f7090..84767ddc4 100644 --- a/hypha/apply/funds/templates/funds/includes/screening_status_block.html +++ b/hypha/apply/funds/templates/funds/includes/screening_status_block.html @@ -1,54 +1,14 @@ {% load i18n submission_tags heroicons %} -{% can_screen object as screen_able %} - -<div class="sidebar__inner"> - - <h5>{% trans "Screening decision" %}</h5> - {% if object.supports_default_screening %} - <div class="flex justify-evenly"> - <div class="sidebar__screening-status-yes"> - <div class="thumb thumbs-up" data-id="{{ object.id }}" data-yes="true"> - <button class="js-like p-2 flex flex-col items-center {% if object.has_yes_default_screening_status_set %}button--js-like-active{% endif %}"> - {% heroicon_solid "hand-thumb-up" size=30 class="mb-2 transition-colors" aria_hidden=true %} - {{ default_yes }} - </button> - </div> - </div> - <div class="sidebar__screening-status-no"> - <div class="thumb thumbs-down" data-id="{{ object.id }}" data-yes="false" style="align-self: center;"> - <button class="js-dislike p-2 flex flex-col items-center {% if object.has_no_default_screening_status_set %}button--js-dislike-active{% endif %}"> - {% heroicon_solid "hand-thumb-down" size=30 class="mb-2 transition-colors" aria_hidden=true %} - {{ default_no }} - </button> - </div> - </div> - </div> - {% endif %} - - <div class="show-screening-options"> - {% if object.has_default_screening_status_set %} - <p id="screening-options-para"> - {% if object.screening_statuses.all.count > 1 %} - <div class="sidebar__screening-selected-options" id="Options"> - {% for status in object.screening_statuses.all%} - <span class="sidebar__screening-option">{{ status }}</span> - {% endfor%} - </div> - {% endif %} - {% if screen_able %} - <a id="screening-options" data-fancybox="" data-src="#screen-application" data-yes="{% if object.has_yes_default_screening_status_set %}true{% else %}false{% endif %}" class="link link--secondary-change" href="#">{% trans "Screening options" %}</a> - {% endif %} - </p> - {% endif %} - {% if not object.supports_default_screening %} - <p id="screening-options-para"> - <div class="sidebar__screening-selected-options"> - {% for status in object.screening_statuses.all%} - <span class="sidebar__screening-option">{{ status }}</span> - {% endfor%} - </div> - </p> - {% endif %} +<div class="sidebar__inner" id="screening-status-{{ object.id }}"> + <h2 class="text-lg font-medium"> + {% trans "Screening decision" %} + </h2> + <div class="grid grid-cols-2 gap-4"> + <!-- thumbs-up --> + {% include "funds/includes/screening_status_block-button.html" with default_status=default_yes current_status=current_yes screening_options=yes_screening_options type="yes" submission=object %} + + <!-- thumbs-down --> + {% include "funds/includes/screening_status_block-button.html" with default_status=default_no current_status=current_no screening_options=no_screening_options type="no" submission=object %} </div> </div> diff --git a/hypha/apply/funds/templates/funds/includes/submission-list-item.html b/hypha/apply/funds/templates/funds/includes/submission-list-item.html index f4c2a620d..f7e5b4f5d 100644 --- a/hypha/apply/funds/templates/funds/includes/submission-list-item.html +++ b/hypha/apply/funds/templates/funds/includes/submission-list-item.html @@ -21,17 +21,14 @@ {% trans "Archived Submission" as text_archived %} {% heroicon_outline "lock-closed" aria_hidden="true" size=21 class="inline stroke-red-800 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content=text_archived data_tippy_delay=200 %} <span class="sr-only">{% trans "Archived" %}</span> - {% else %} - {% if s.has_yes_default_screening_status_set %} - {% heroicon_mini "hand-thumb-up" aria_hidden="true" size=21 class="inline fill-green-400 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content="Screening Status: Yes" data_tippy_delay=200 %} + {% elif s.get_current_screening_status %} + {% if s.get_yes_screening_status %} + {% heroicon_mini "hand-thumb-up" aria_hidden="true" size=21 class="inline fill-green-400 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content=s.get_current_screening_status data_tippy_delay=200 %} {% else %} - {% if s.has_no_default_screening_status_set %} - {% heroicon_mini "hand-thumb-down" aria_hidden="true" size=21 class="inline fill-red-400 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content="Screening Status: No" data_tippy_delay=200 %} - {% else %} - {% heroicon_outline "question-mark-circle" aria_hidden="true" size=21 class="inline stroke-slate-300 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content="Awaiting Screening" data_tippy_delay=200 %} - <span class="sr-only">{% trans "Archived" %}</span> - {% endif %} + {% heroicon_mini "hand-thumb-down" aria_hidden="true" size=21 class="inline fill-red-400 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content=s.get_current_screening_status data_tippy_delay=200 %} {% endif %} + {% else %} + {% heroicon_outline "question-mark-circle" aria_hidden="true" size=21 class="inline stroke-slate-300 stroke-1.5 -mt-1 align-text-bottom" data_tippy_placement='right' data_tippy_content="Awaiting Screening" data_tippy_delay=200 %} {% endif %} </span> diff --git a/hypha/apply/funds/templates/funds/submissions_result.html b/hypha/apply/funds/templates/funds/submissions_result.html index e696b8240..77308c783 100644 --- a/hypha/apply/funds/templates/funds/submissions_result.html +++ b/hypha/apply/funds/templates/funds/submissions_result.html @@ -53,7 +53,7 @@ <h3>{% trans "Filter submissions to calculate values" %}</h3> {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action use_batch_actions=False filter_classes="filters-open" %} - <div class="grid"> + <div class="hypha-grid"> <div><strong>{% trans "Number of submission" %}:</strong> {{ count_values }}{% if not count_values == object_list.count %} ({{ object_list.count }}){% endif %}</div> <div><strong>{% trans "Average value" %}:</strong> {{ average_value|format_number_as_currency }}</div> <div><strong>{% trans "Total value" %}:</strong> {{ total_value|format_number_as_currency }}</div> diff --git a/hypha/apply/funds/templatetags/submission_tags.py b/hypha/apply/funds/templatetags/submission_tags.py index 0710a56ad..b6f14b52e 100644 --- a/hypha/apply/funds/templatetags/submission_tags.py +++ b/hypha/apply/funds/templatetags/submission_tags.py @@ -29,10 +29,3 @@ def submission_links(value): value = re.sub(rf"(?<!\w){sid}(?!\w)", link, value) return mark_safe(value) - - -@register.simple_tag -def can_screen(submission): - if submission.is_archive: - return False - return True diff --git a/hypha/apply/funds/tests/test_views.py b/hypha/apply/funds/tests/test_views.py index a97caa68c..e579df977 100644 --- a/hypha/apply/funds/tests/test_views.py +++ b/hypha/apply/funds/tests/test_views.py @@ -228,15 +228,19 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase): screening_outcome2.save() self.submission.screening_statuses.clear() self.submission.screening_statuses.add(screening_outcome2) - self.post_page( - self.submission, - { - "form-submitted-screening_form": "", - "screening_statuses": [screening_outcome1.id, screening_outcome2.id], - }, + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, + ) + self.client.post( + url, + {"action": screening_outcome1.id}, + secure=True, + follow=True, ) submission = self.refresh(self.submission) - self.assertEqual(submission.screening_statuses.count(), 2) + status = submission.get_current_screening_status() + assert status == screening_outcome1 def test_can_view_submission_screening_block(self): ScreeningStatus.objects.all().delete() @@ -250,17 +254,23 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase): screening_outcome2.default = True screening_outcome2.save() self.submission.screening_statuses.clear() - response = self.get_page(self.submission) + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, + ) + response = self.client.get( + url, + secure=True, + follow=True, + ) self.assertContains(response, "Screening decision") - def test_cant_view_submission_screening_block(self): - """ - If defaults are not set screening decision block is not visible - """ + # if there are no screening statuses, the block should not be visible ScreeningStatus.objects.all().delete() self.submission.screening_statuses.clear() - response = self.get_page(self.submission) - self.assertNotContains(response, "Screening decision") + response = self.client.get(url, secure=True, follow=True) + # it returns 204 status code + self.assertEqual(response.status_code, 204) def test_can_create_project(self): # check submission doesn't already have a Project @@ -343,29 +353,29 @@ class TestStaffSubmissionView(BaseSubmissionViewTestCase): def test_screen_application_primary_action_is_displayed(self): ScreeningStatus.objects.all().delete() # Submission not screened + screening_outcome_yes = ScreeningStatusFactory() + screening_outcome_yes.yes = False + screening_outcome_yes.default = True + screening_outcome_yes.save() screening_outcome = ScreeningStatusFactory() - screening_outcome.yes = False + screening_outcome.yes = True screening_outcome.default = True screening_outcome.save() self.submission.screening_statuses.clear() self.submission.screening_statuses.add(screening_outcome) - response = self.get_page(self.submission) - buttons = ( - BeautifulSoup(response.content, "html5lib") - .find(class_="sidebar") - .find_all("a", string="Screen application") + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, ) - self.assertEqual(len(buttons), 1) - self.submission.screening_statuses.clear() - - def test_screen_application_primary_action_is_not_displayed(self): - response = self.get_page(self.submission) - buttons = ( - BeautifulSoup(response.content, "html5lib") - .find(class_="sidebar") - .find_all("a", string="Screen application") + response = self.client.get( + url, + secure=True, + follow=True, ) - self.assertEqual(len(buttons), 0) + self.assertContains(response, "Screening decision") + buttons = BeautifulSoup(response.content, "html5lib").find_all("button") + self.assertEqual(len(buttons), 2) + self.submission.screening_statuses.clear() def test_can_see_create_review_primary_action(self): def assert_create_review_displayed(submission, button_text): @@ -1151,26 +1161,24 @@ class TestApplicantSubmissionView(BaseSubmissionViewTestCase): response = self.get_page(submission, "edit") self.assertEqual(response.status_code, 403) - def test_cant_screen_submission(self): + def test_cant_see_or_screen_submission(self): """ Test that an applicant cannot set the screening decision and that they don't see the screening decision form. """ - screening_outcome = ScreeningStatusFactory() - response = self.post_page( - self.submission, - { - "form-submitted-screening_form": "", - "screening_statuses": [screening_outcome.id], - }, + outcome = ScreeningStatusFactory() + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, ) - self.assertNotIn("screening_form", response.context_data) - submission = self.refresh(self.submission) - self.assertNotIn(screening_outcome, submission.screening_statuses.all()) + response = self.client.get(url, secure=True, follow=True) + self.assertEqual(response.status_code, 204) - def test_cant_see_screening_status_block(self): - response = self.get_page(self.submission) - self.assertNotContains(response, "Screening decision") + # trying to post to the screening decision form + response = self.client.post( + url, {"action": outcome.id}, secure=True, follow=True + ) + self.assertEqual(response.status_code, 204) def test_cant_see_add_determination_primary_action(self): def assert_add_determination_not_displayed(submission, button_text): @@ -1533,15 +1541,17 @@ class TestSuperUserSubmissionView(BaseSubmissionViewTestCase): screening_outcome2.save() self.submission.screening_statuses.clear() self.submission.screening_statuses.add(screening_outcome2) - self.post_page( - self.submission, - { - "form-submitted-screening_form": "", - "screening_statuses": [screening_outcome1.id, screening_outcome2.id], - }, + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, ) - submission = self.refresh(self.submission) - self.assertEqual(submission.screening_statuses.count(), 2) + self.client.post( + url, + data={"action": screening_outcome1.id}, + secure=True, + follow=True, + ) + assert self.submission.get_current_screening_status() == screening_outcome1 def test_can_screen_applications_in_final_status(self): """ @@ -1554,25 +1564,28 @@ class TestSuperUserSubmissionView(BaseSubmissionViewTestCase): screening_outcome1.yes = True screening_outcome1.save() screening_outcome2 = ScreeningStatusFactory() - screening_outcome2.yes = True + screening_outcome2.yes = False screening_outcome2.default = True screening_outcome2.save() submission.screening_statuses.add(screening_outcome2) - response = self.post_page( - submission, - { - "form-submitted-screening_form": "", - "screening_statuses": [screening_outcome1.id, screening_outcome2.id], - }, + url = reverse( + "funds:submissions:partial-screening-card", + kwargs={"pk": self.submission.pk}, ) - submission = self.refresh(submission) - self.assertEqual(response.context_data["screening_form"].should_show, True) - self.assertEqual(submission.screening_statuses.count(), 2) + response = self.client.post( + url, + data={"action": screening_outcome1.id}, + secure=True, + follow=True, + ) + assert response.status_code == 200 + assert self.submission.get_current_screening_status() == screening_outcome1 # Check that an activity was created that should only be viewable internally activity = Activity.objects.filter( - message__contains="Screening decision" + message__icontains="Screening decision" ).first() + assert activity self.assertEqual(activity.visibility, TEAM) diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index da8e5bd0a..58a0a7cb7 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -28,6 +28,7 @@ from .views import ( SubmissionSealedView, SubmissionStaffFlaggedView, SubmissionUserFlaggedView, + partial_screening_card, submission_success, ) from .views_beta import ( @@ -187,6 +188,11 @@ submission_urls = ( partial_submission_activities, name="partial-activities", ), + path( + "partial/screening-card/", + partial_screening_card, + name="partial-screening-card", + ), path( "partial/reviews-card/", partial_reviews_card, diff --git a/hypha/apply/funds/utils.py b/hypha/apply/funds/utils.py index 08775b257..a23658cac 100644 --- a/hypha/apply/funds/utils.py +++ b/hypha/apply/funds/utils.py @@ -12,31 +12,31 @@ def render_icon(image): return generate_image_tag(image, filter_spec, html_class="icon mr-2 align-middle") -def get_default_screening_statues(): +def get_or_create_default_screening_statuses( + yes_screen_status_qs, no_screening_status_qs +): """ Get the default screening decisions set. If the default for yes and no doesn't exit. First yes and first no screening decisions created should be set as default """ - yes_screening_statuses = ScreeningStatus.objects.filter(yes=True) - no_screening_statuses = ScreeningStatus.objects.filter(yes=False) default_yes = None default_no = None - if yes_screening_statuses.exists(): + if yes_screen_status_qs.exists(): try: - default_yes = yes_screening_statuses.get(default=True) + default_yes = yes_screen_status_qs.get(default=True) except ScreeningStatus.DoesNotExist: # Set first yes screening decision as default - default_yes = yes_screening_statuses.first() + default_yes = yes_screen_status_qs.first() default_yes.default = True default_yes.save() - if no_screening_statuses.exists(): + if no_screening_status_qs.exists(): try: - default_no = no_screening_statuses.get(default=True) + default_no = no_screening_status_qs.get(default=True) except ScreeningStatus.DoesNotExist: # Set first no screening decision as default - default_no = no_screening_statuses.first() + default_no = no_screening_status_qs.first() default_no.default = True default_no.save() return [default_yes, default_no] diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py index 429e7cfe7..204bc8261 100644 --- a/hypha/apply/funds/views.py +++ b/hypha/apply/funds/views.py @@ -57,6 +57,7 @@ from hypha.apply.determinations.views import ( BatchDeterminationCreateView, DeterminationCreateOrUpdateView, ) +from hypha.apply.funds.models.screening import ScreeningStatus from hypha.apply.projects.forms import ProjectCreateForm from hypha.apply.projects.models import Project from hypha.apply.review.models import Review @@ -86,7 +87,6 @@ from .forms import ( BatchUpdateSubmissionLeadForm, CreateReminderForm, ProgressSubmissionForm, - ScreeningSubmissionForm, UnarchiveSubmissionForm, UpdateMetaTermsForm, UpdatePartnersForm, @@ -128,7 +128,9 @@ from .tables import ( SummarySubmissionsTable, UserFlaggedSubmissionsTable, ) -from .utils import get_default_screening_statues +from .utils import ( + get_or_create_default_screening_statuses, +) from .workflow import ( DRAFT_STATE, PHASES_MAPPING, @@ -817,36 +819,6 @@ class CreateProjectView(DelegatedViewMixin, CreateView): return context -@method_decorator(staff_required, name="dispatch") -class ScreeningSubmissionView(DelegatedViewMixin, UpdateView): - model = ApplicationSubmission - form_class = ScreeningSubmissionForm - context_name = "screening_form" - - def dispatch(self, request, *args, **kwargs): - submission = self.get_object() - permission, reason = has_permission( - "submission_edit", request.user, object=submission, raise_exception=False - ) - if not permission: - messages.warning(self.request, reason) - return HttpResponseRedirect(submission.get_absolute_url()) - return super(ScreeningSubmissionView, self).dispatch(request, *args, **kwargs) - - def form_valid(self, form): - old = copy(self.get_object()) - response = super().form_valid(form) - # Record activity - messenger( - MESSAGES.SCREENING, - request=self.request, - user=self.request.user, - source=self.object, - related=", ".join([s.title for s in old.screening_statuses.all()]), - ) - return response - - @method_decorator(staff_required, name="dispatch") class UnarchiveSubmissionView(DelegatedViewMixin, UpdateView): model = ApplicationSubmission @@ -1089,7 +1061,6 @@ class AdminSubmissionDetailView(ActivityContextMixin, DelegateableView, DetailVi form_views = [ ArchiveSubmissionView, ProgressSubmissionView, - ScreeningSubmissionView, ReminderCreateView, CommentFormView, UpdateLeadView, @@ -1122,17 +1093,70 @@ class AdminSubmissionDetailView(ActivityContextMixin, DelegateableView, DetailVi if self.object.next: other_submissions = other_submissions.exclude(id=self.object.next.id) - default_screening_statuses = get_default_screening_statues() - return super().get_context_data( other_submissions=other_submissions, - default_screening_statuses=default_screening_statuses, archive_access_groups=get_archive_view_groups(), can_archive=can_alter_archived_submissions(self.request.user), **kwargs, ) +@login_required +def partial_screening_card(request, pk): + submission = get_object_or_404(ApplicationSubmission, pk=pk) + + view_permission, _ = has_permission( + "can_view_submission_screening", request.user, submission, raise_exception=False + ) + can_edit, _ = has_permission( + "submission_edit", request.user, submission, raise_exception=False + ) + + if not view_permission: + return HttpResponse(status=204) + + if can_edit and request.method == "POST": + action = request.POST.get("action") + old_status_str = str(submission.get_current_screening_status() or "-") + if action and action.isdigit(): + submission.screening_statuses.clear() + screening_status = ScreeningStatus.objects.get(id=action) + submission.screening_statuses.add(screening_status) + elif action == "clear": + submission.screening_statuses.clear() + + # Record activity + messenger( + MESSAGES.SCREENING, + request=request, + user=request.user, + source=submission, + related=old_status_str, + ) + + yes_screening_statuses = ScreeningStatus.objects.filter(yes=True) + no_screening_statuses = ScreeningStatus.objects.filter(yes=False) + + if not yes_screening_statuses or not no_screening_statuses: + return HttpResponse(status=204) + + default_yes, default_no = get_or_create_default_screening_statuses( + yes_screening_statuses, no_screening_statuses + ) + + ctx = { + "default_yes": default_yes, + "default_no": default_no, + "object": submission, + "can_screen": can_edit, + "current_yes": submission.screening_statuses.filter(yes=True).first(), + "current_no": submission.screening_statuses.filter(yes=False).first(), + "yes_screening_options": yes_screening_statuses, + "no_screening_options": no_screening_statuses, + } + return render(request, "funds/includes/screening_status_block.html", ctx) + + class ReviewerSubmissionDetailView(ActivityContextMixin, DelegateableView, DetailView): template_name_suffix = "_reviewer_detail" model = ApplicationSubmission diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index ade4c639e..3fbdf1d3a 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -127,7 +127,7 @@ <div class="wrapper wrapper--sidebar"> <article class="wrapper--sidebar--inner"> <h3>{% trans "Project Information" %}</h3> - <div class="grid grid--proposal-info"> + <div class="hypha-grid hypha-grid--proposal-info"> <div> <h5>{% trans "Contractor" %}</h5> <p>{{ object.vendor.name |default:"-" }}</p> diff --git a/hypha/apply/projects/templates/application_projects/vendor_detail.html b/hypha/apply/projects/templates/application_projects/vendor_detail.html index 9fb4d2f0b..a7aebf254 100644 --- a/hypha/apply/projects/templates/application_projects/vendor_detail.html +++ b/hypha/apply/projects/templates/application_projects/vendor_detail.html @@ -14,7 +14,7 @@ {% slot header %}{% trans "Contracting Information for" %} {{ project.title }}{% endslot %} {% endadminbar %} - <div class="grid"> + <div class="hypha-grid"> <div> <h5 class="vendor-info">{% trans "Last Updated" %}: {{ vendor.updated_at|date:'DATE_FORMAT' }}</h5> </div> diff --git a/hypha/apply/users/templates/users/login.html b/hypha/apply/users/templates/users/login.html index 6cfdcfe09..a0ebd0729 100644 --- a/hypha/apply/users/templates/users/login.html +++ b/hypha/apply/users/templates/users/login.html @@ -5,7 +5,7 @@ {% block body_class %}bg-white{% endblock %} {% block content %} - <div class="max-w-2xl grid bg-white mt-5 md:py-4"> + <div class="max-w-2xl hypha-grid bg-white mt-5 md:py-4"> <section> <div class="px-4 pt-4"> diff --git a/hypha/static_src/javascript/screening-status.js b/hypha/static_src/javascript/screening-status.js deleted file mode 100644 index 4f7d5c4f5..000000000 --- a/hypha/static_src/javascript/screening-status.js +++ /dev/null @@ -1,111 +0,0 @@ -(function ($) { - "use strict"; - - $(".thumb").on("click", function (e) { - e.preventDefault(); - - var $current = $(this); - var id = $current.data("id"); - var yes = $current.data("yes"); - - $.ajax({ - url: "/api/v1/submissions/" + id + "/screening_statuses/default/", - type: "POST", - data: { yes: yes }, - success: function (json) { - if (json) { - var screeningOptions = $( - '<p id="screening-options-para">' + - '<a id="screening-options-ajax" data-fancybox="" data-src="#screen-application" data-yes=' + - yes + - ' class="link link--secondary-change" href="#"> Screening Options</a></p>' - ); - if ( - $.trim( - $(".show-screening-options") - .find("#screening-options-para") - .html() - ) === "" - ) { - $(".show-screening-options").find("#Options").remove(); - $(".show-screening-options") - .find("#screening-options") - .remove(); - } - $(".show-screening-options") - .find("#screening-options-para") - .remove(); - $(".show-screening-options").append(screeningOptions); - if (yes === true) { - $(".js-dislike").removeClass( - "button--js-dislike-active" - ); - $current - .find("button") - .addClass("button--js-like-active"); - } else { - $(".js-like").removeClass("button--js-like-active"); - $current - .find("button") - .addClass("button--js-dislike-active"); - } - } - }, - }); - }); - - $(".show-screening-options").on( - "click", - "#screening-options-ajax", - function () { - var $screeningOptions = $(this); - var currentStatus = $screeningOptions.data("yes"); - var $screenApplication = $("#screen-application"); - var yesStatuses = $screenApplication.data("yes-statuses"); - var noStatuses = $screenApplication.data("no-statuses"); - var defaultYes = $screenApplication.data("default-yes"); - var defaultNo = $screenApplication.data("default-no"); - var $screeningStatuses = $screenApplication.find( - "#id_screening_statuses" - ); - $screeningStatuses = $screeningStatuses.empty(); - if (currentStatus === true) { - $("#current-status").text("Current decisions: " + defaultYes); - $.each(yesStatuses, function (key, value) { - if (key === defaultYes) { - $screeningStatuses.append( - $("<option></option>") - .attr("value", value) - .attr("selected", "selected") - .text(key) - ); - } else { - $screeningStatuses.append( - $("<option></option>") - .attr("value", value) - .text(key) - ); - } - }); - } else { - $("#current-status").text("Current decisions: " + defaultNo); - $.each(noStatuses, function (key, value) { - if (key === defaultNo) { - $screeningStatuses.append( - $("<option></option>") - .attr("value", value) - .attr("selected", "selected") - .text(key) - ); - } else { - $screeningStatuses.append( - $("<option></option>") - .attr("value", value) - .text(key) - ); - } - }); - } - } - ); -})(jQuery); diff --git a/hypha/static_src/sass/components/_button.scss b/hypha/static_src/sass/components/_button.scss index 33ce572b0..186e88a83 100644 --- a/hypha/static_src/sass/components/_button.scss +++ b/hypha/static_src/sass/components/_button.scss @@ -425,23 +425,15 @@ line-height: 1.15; } - &--js-like-active { - font-weight: 600; - cursor: not-allowed; - + &--like-active { svg { fill: $color--dark-blue; - cursor: not-allowed; } } - &--js-dislike-active { - font-weight: 600; - cursor: not-allowed; - + &--dislike-active { svg { fill: $color--tomato; - cursor: not-allowed; } } } diff --git a/hypha/static_src/sass/components/_grid.scss b/hypha/static_src/sass/components/_hypha-grid.scss similarity index 98% rename from hypha/static_src/sass/components/_grid.scss rename to hypha/static_src/sass/components/_hypha-grid.scss index 76ff94fbb..3a8e9e027 100644 --- a/hypha/static_src/sass/components/_grid.scss +++ b/hypha/static_src/sass/components/_hypha-grid.scss @@ -1,4 +1,4 @@ -.grid { +.hypha-grid { display: flex; flex-wrap: wrap; @@ -25,7 +25,7 @@ } @supports (display: grid) { - .grid { + .hypha-grid { display: grid; margin-block-end: 1rem; gap: 10px; diff --git a/hypha/static_src/sass/main.scss b/hypha/static_src/sass/main.scss index ab9dae30e..59fed1315 100644 --- a/hypha/static_src/sass/main.scss +++ b/hypha/static_src/sass/main.scss @@ -25,7 +25,7 @@ @import "components/error-bar"; @import "components/feed"; @import "components/filters"; -@import "components/grid"; +@import "components/hypha-grid"; @import "components/heading"; @import "components/icon"; @import "components/link"; diff --git a/hypha/templates/django/forms/widgets/multiple_input.html b/hypha/templates/django/forms/widgets/multiple_input.html index 765e63d6d..281e88a27 100644 --- a/hypha/templates/django/forms/widgets/multiple_input.html +++ b/hypha/templates/django/forms/widgets/multiple_input.html @@ -1,6 +1,6 @@ {% with id=widget.attrs.id %} <ul - class="!mb-0 grid grid--two {% if widget.attrs.class %} {{ widget.attrs.class }} {% endif %}" + class="!mb-0 hypha-grid hypha-grid--two {% if widget.attrs.class %} {{ widget.attrs.class }} {% endif %}" {% if id %} id="{{ id }}"{% endif %} > {% for group, options, index in widget.optgroups %} -- GitLab