From a9bc0883944e43a9cedc2bdc70c2a11c8ca2216f Mon Sep 17 00:00:00 2001 From: Wes Appler Date: Tue, 17 Dec 2024 16:21:35 -0500 Subject: [PATCH 1/9] Removed unused v1 APIs + code, removed all code related to Intacct & deliverables --- hypha/apply/api/v1/determination/__init__.py | 0 .../apply/api/v1/determination/permissions.py | 29 - .../apply/api/v1/determination/serializers.py | 39 -- hypha/apply/api/v1/determination/utils.py | 34 -- hypha/apply/api/v1/determination/views.py | 277 ---------- hypha/apply/api/v1/filters.py | 133 ----- hypha/apply/api/v1/mixin.py | 19 - hypha/apply/api/v1/permissions.py | 19 - hypha/apply/api/v1/projects/__init__.py | 0 hypha/apply/api/v1/projects/serializers.py | 29 - hypha/apply/api/v1/projects/views.py | 85 --- hypha/apply/api/v1/reminder/__init__.py | 0 hypha/apply/api/v1/reminder/serializers.py | 28 - hypha/apply/api/v1/reminder/views.py | 73 --- hypha/apply/api/v1/review/__init__.py | 0 hypha/apply/api/v1/review/fields.py | 35 -- hypha/apply/api/v1/review/permissions.py | 118 ----- hypha/apply/api/v1/review/serializers.py | 115 ---- hypha/apply/api/v1/review/tests/__init__.py | 0 hypha/apply/api/v1/review/utils.py | 59 --- hypha/apply/api/v1/review/views.py | 298 ----------- hypha/apply/api/v1/serializers.py | 344 +----------- hypha/apply/api/v1/stream_serializers.py | 157 ------ hypha/apply/api/v1/urls.py | 50 +- hypha/apply/api/v1/utils.py | 112 ---- hypha/apply/api/v1/views.py | 288 +--------- ...invoicedeliverable_deliverable_and_more.py | 26 + hypha/apply/projects/models/__init__.py | 5 +- hypha/apply/projects/models/payment.py | 43 +- hypha/apply/projects/models/project.py | 46 -- hypha/apply/projects/services/__init__.py | 0 .../projects/services/sageintacct/__init__.py | 27 - .../services/sageintacct/exceptions.py | 61 --- .../services/sageintacct/sageintacctsdk.py | 71 --- .../projects/services/sageintacct/utils.py | 165 ------ .../services/sageintacct/wrapper/__init__.py | 11 - .../services/sageintacct/wrapper/api_base.py | 495 ------------------ .../services/sageintacct/wrapper/constants.py | 17 - .../services/sageintacct/wrapper/invoice.py | 9 - .../services/sageintacct/wrapper/project.py | 12 - .../sageintacct/wrapper/purchasing.py | 12 - .../includes/deliverables_block.html | 45 -- .../invoice_admin_detail.html | 13 - .../application_projects/invoice_detail.html | 2 - .../projects/templatetags/invoice_tools.py | 5 - hypha/apply/projects/tests/factories.py | 19 +- hypha/apply/projects/tests/test_models.py | 64 --- hypha/apply/projects/utils.py | 75 +-- hypha/apply/projects/views/payment.py | 11 +- hypha/static_src/javascript/deliverables.js | 135 ----- 50 files changed, 59 insertions(+), 3651 deletions(-) delete mode 100644 hypha/apply/api/v1/determination/__init__.py delete mode 100644 hypha/apply/api/v1/determination/permissions.py delete mode 100644 hypha/apply/api/v1/determination/serializers.py delete mode 100644 hypha/apply/api/v1/determination/utils.py delete mode 100644 hypha/apply/api/v1/determination/views.py delete mode 100644 hypha/apply/api/v1/filters.py delete mode 100644 hypha/apply/api/v1/mixin.py delete mode 100644 hypha/apply/api/v1/projects/__init__.py delete mode 100644 hypha/apply/api/v1/projects/serializers.py delete mode 100644 hypha/apply/api/v1/projects/views.py delete mode 100644 hypha/apply/api/v1/reminder/__init__.py delete mode 100644 hypha/apply/api/v1/reminder/serializers.py delete mode 100644 hypha/apply/api/v1/reminder/views.py delete mode 100644 hypha/apply/api/v1/review/__init__.py delete mode 100644 hypha/apply/api/v1/review/fields.py delete mode 100644 hypha/apply/api/v1/review/permissions.py delete mode 100644 hypha/apply/api/v1/review/serializers.py delete mode 100644 hypha/apply/api/v1/review/tests/__init__.py delete mode 100644 hypha/apply/api/v1/review/utils.py delete mode 100644 hypha/apply/api/v1/review/views.py delete mode 100644 hypha/apply/api/v1/stream_serializers.py delete mode 100644 hypha/apply/api/v1/utils.py create mode 100644 hypha/apply/projects/migrations/0094_remove_invoicedeliverable_deliverable_and_more.py delete mode 100644 hypha/apply/projects/services/__init__.py delete mode 100644 hypha/apply/projects/services/sageintacct/__init__.py delete mode 100644 hypha/apply/projects/services/sageintacct/exceptions.py delete mode 100644 hypha/apply/projects/services/sageintacct/sageintacctsdk.py delete mode 100644 hypha/apply/projects/services/sageintacct/utils.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/__init__.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/api_base.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/constants.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/invoice.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/project.py delete mode 100644 hypha/apply/projects/services/sageintacct/wrapper/purchasing.py delete mode 100644 hypha/apply/projects/templates/application_projects/includes/deliverables_block.html delete mode 100644 hypha/apply/projects/templates/application_projects/invoice_admin_detail.html delete mode 100644 hypha/static_src/javascript/deliverables.js diff --git a/hypha/apply/api/v1/determination/__init__.py b/hypha/apply/api/v1/determination/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/api/v1/determination/permissions.py b/hypha/apply/api/v1/determination/permissions.py deleted file mode 100644 index 78d4b00dd3..0000000000 --- a/hypha/apply/api/v1/determination/permissions.py +++ /dev/null @@ -1,29 +0,0 @@ -from rest_framework import permissions - -from hypha.apply.determinations.permissions import ( - can_create_determination, - can_edit_determination, -) - - -class HasDeterminationCreatePermission(permissions.BasePermission): - """ - Custom permission that user should have for creating determination. - """ - - def has_permission(self, request, view): - try: - submission = view.get_submission_object() - except KeyError: - return True - return can_create_determination(request.user, submission) - - -class HasDeterminationDraftPermission(permissions.BasePermission): - """ - Custom permission that user should have for editing determination. - """ - - def has_object_permission(self, request, view, obj): - submission = view.get_submission_object() - return can_edit_determination(request.user, obj, submission) diff --git a/hypha/apply/api/v1/determination/serializers.py b/hypha/apply/api/v1/determination/serializers.py deleted file mode 100644 index 13f74c6d4f..0000000000 --- a/hypha/apply/api/v1/determination/serializers.py +++ /dev/null @@ -1,39 +0,0 @@ -from rest_framework import serializers - -from hypha.apply.determinations.models import Determination - - -class SubmissionDeterminationSerializer(serializers.ModelSerializer): - class Meta: - model = Determination - fields = [ - "id", - "is_draft", - ] - extra_kwargs = { - "is_draft": {"required": False}, - } - - def validate(self, data): - validated_data = super().validate(data) - validated_data["form_data"] = dict(validated_data.items()) - return validated_data - - def update(self, instance, validated_data): - instance = super().update(instance, validated_data) - instance.send_notice = ( - self.validated_data[instance.send_notice_field.id] - if instance.send_notice_field - else True - ) - message = self.validated_data[instance.message_field.id] - instance.message = "" if message is None else message - try: - instance.outcome = int(self.validated_data[instance.determination_field.id]) - # Need to catch KeyError as outcome field would not exist in case of edit. - except KeyError: - pass - instance.is_draft = self.validated_data.get("is_draft", False) - instance.form_data = self.validated_data["form_data"] - instance.save() - return instance diff --git a/hypha/apply/api/v1/determination/utils.py b/hypha/apply/api/v1/determination/utils.py deleted file mode 100644 index fdc0d6a89f..0000000000 --- a/hypha/apply/api/v1/determination/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.utils.translation import gettext as _ - -from hypha.apply.determinations.options import ( - DETERMINATION_CHOICES, - TRANSITION_DETERMINATION, -) -from hypha.apply.determinations.utils import determination_actions - - -def get_fields_for_stage(submission): - forms = submission.get_from_parent("determination_forms").all() - index = submission.workflow.stages.index(submission.stage) - try: - return forms[index].form.form_fields - except IndexError: - return forms[0].form.form_fields - - -def outcome_choices_for_phase(submission, user): - """ - Outcome choices correspond to Phase transitions. - We need to filter out non-matching choices. - i.e. a transition to In Review is not a determination, while Needs more info or Rejected are. - """ - available_choices = [("", _("-- No determination selected -- "))] - choices = dict(DETERMINATION_CHOICES) - for transition_name in determination_actions(user, submission): - try: - determination_type = TRANSITION_DETERMINATION[transition_name] - except KeyError: - pass - else: - available_choices.append((determination_type, choices[determination_type])) - return available_choices diff --git a/hypha/apply/api/v1/determination/views.py b/hypha/apply/api/v1/determination/views.py deleted file mode 100644 index d90653c812..0000000000 --- a/hypha/apply/api/v1/determination/views.py +++ /dev/null @@ -1,277 +0,0 @@ -from django import forms -from django.conf import settings -from django.db import transaction -from django.shortcuts import get_object_or_404 -from django.utils import timezone -from rest_framework import permissions, status, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response -from wagtail.blocks.field_block import RichTextBlock - -from hypha.apply.activity.messaging import MESSAGES, messenger -from hypha.apply.activity.models import Activity -from hypha.apply.determinations.blocks import DeterminationBlock -from hypha.apply.determinations.models import Determination -from hypha.apply.determinations.options import NEEDS_MORE_INFO -from hypha.apply.determinations.utils import ( - has_final_determination, - transition_from_outcome, -) -from hypha.apply.projects.models import Project -from hypha.apply.stream_forms.models import BaseStreamForm - -from ..mixin import SubmissionNestedMixin -from ..permissions import IsApplyStaffUser -from ..review.serializers import FieldSerializer -from ..stream_serializers import WagtailSerializer -from .permissions import ( - HasDeterminationCreatePermission, - HasDeterminationDraftPermission, -) -from .serializers import SubmissionDeterminationSerializer -from .utils import get_fields_for_stage, outcome_choices_for_phase - - -class SubmissionDeterminationViewSet( - BaseStreamForm, WagtailSerializer, SubmissionNestedMixin, viewsets.GenericViewSet -): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - permission_classes_by_action = { - "create": [ - permissions.IsAuthenticated, - HasDeterminationCreatePermission, - IsApplyStaffUser, - ], - "draft": [ - permissions.IsAuthenticated, - HasDeterminationDraftPermission, - IsApplyStaffUser, - ], - } - serializer_class = SubmissionDeterminationSerializer - - def get_permissions(self): - try: - # return permission_classes depending on `action` - return [ - permission() - for permission in self.permission_classes_by_action[self.action] - ] - except KeyError: - # action is not set return default permission_classes - return [permission() for permission in self.permission_classes] - - def get_defined_fields(self): - """ - Get form fields created for determining this submission. - - These form fields will be used to get respective serializer fields. - """ - if self.action in ["retrieve", "update"]: - # For detail and edit api form fields used while submitting - # determination should be used. - determination = self.get_object() - return determination.form_fields - submission = self.get_submission_object() - return get_fields_for_stage(submission) - - def get_serializer_class(self): - """ - Override get_serializer_class to send draft parameter - if the request is to save as draft or the determination submitted - is saved as draft. - """ - if self.action == "retrieve": - determination = self.get_object() - draft = determination.is_draft - elif self.action == "draft": - draft = True - else: - draft = self.request.data.get("is_draft", False) - return super().get_serializer_class(draft) - - def get_queryset(self): - submission = self.get_submission_object() - return Determination.objects.filter(submission=submission, is_draft=False) - - def get_object(self): - """ - Get the determination object by id. If not found raise 404. - """ - queryset = self.get_queryset() - obj = get_object_or_404(queryset, id=self.kwargs["pk"]) - self.check_object_permissions(self.request, obj) - return obj - - def get_determination_data(self, determination): - """ - Get determination data which will be used for determination detail api. - """ - determination_data = determination.form_data - field_blocks = determination.form_fields - for field_block in field_blocks: - if isinstance(field_block.block, DeterminationBlock): - determination_data[field_block.id] = determination.outcome - if isinstance(field_block.block, RichTextBlock): - determination_data[field_block.id] = field_block.value.source - determination_data["id"] = determination.id - determination_data["is_draft"] = determination.is_draft - return determination_data - - def retrieve(self, request, *args, **kwargs): - """ - Get details of a determination on a submission - """ - determination = self.get_object() - ser = self.get_serializer(self.get_determination_data(determination)) - return Response(ser.data) - - def get_form_fields(self): - form_fields = super(SubmissionDeterminationViewSet, self).get_form_fields() - submission = self.get_submission_object() - field_blocks = self.get_defined_fields() - for field_block in field_blocks: - if isinstance(field_block.block, DeterminationBlock): - outcome_choices = outcome_choices_for_phase( - submission, self.request.user - ) - if self.action == "update": - # Outcome can not be edited after being set once, so we do not - # need to render this field. - # form_fields.pop(field_block.id) - form_fields[field_block.id].widget = forms.TextInput( - attrs={"readonly": "readonly"} - ) - else: - # Outcome field choices need to be set according to the phase. - form_fields[field_block.id].choices = outcome_choices - return form_fields - - @action(detail=False, methods=["get"]) - def fields(self, request, *args, **kwargs): - """ - List details of all the form fields that were created by admin for adding determinations. - - These field details will be used in frontend to render the determination form. - """ - form_fields = self.get_form_fields() - fields = FieldSerializer(form_fields.items(), many=True) - return Response(fields.data) - - def get_draft_determination(self): - submission = self.get_submission_object() - try: - determination = Determination.objects.get( - submission=submission, is_draft=True - ) - except Determination.DoesNotExist: - return - else: - return determination - - @action(detail=False, methods=["get"]) - def draft(self, request, *args, **kwargs): - """ - Returns the draft determination submitted on a submission by current user. - """ - determination = self.get_draft_determination() - if not determination: - return Response({}) - ser = self.get_serializer(self.get_determination_data(determination)) - return Response(ser.data) - - def create(self, request, *args, **kwargs): - """ - Create a determination on a submission. - - Accept a post data in form of `{field_id: value}`. - `field_id` is same id which you get from the `/fields` api. - `value` should be submitted with html tags, so that response can - be displayed with correct formatting, e.g. in case of rich text field, - we need to show the data with same formatting user has submitted. - - Accepts optional parameter `is_draft` when a determination is to be saved as draft. - - Raise ValidationError if a determination is already submitted by the user. - """ - submission = self.get_submission_object() - ser = self.get_serializer(data=request.data) - ser.is_valid(raise_exception=True) - if has_final_determination(submission): - return ValidationError( - {"detail": "A final determination has already been submitted."} - ) - determination = self.get_draft_determination() - if determination is None: - determination = Determination.objects.create( - submission=submission, author=request.user - ) - determination.form_fields = self.get_defined_fields() - determination.save() - ser.update(determination, ser.validated_data) - if determination.is_draft: - ser = self.get_serializer(self.get_determination_data(determination)) - return Response(ser.data, status=status.HTTP_201_CREATED) - with transaction.atomic(): - messenger( - MESSAGES.DETERMINATION_OUTCOME, - request=self.request, - user=determination.author, - submission=submission, - related=determination, - ) - proposal_form = ser.validated_data.get("proposal_form") - transition = transition_from_outcome(int(determination.outcome), submission) - - if determination.outcome == NEEDS_MORE_INFO: - # We keep a record of the message sent to the user in the comment - Activity.comments.create( - message=determination.stripped_message, - timestamp=timezone.now(), - user=self.request.user, - source=submission, - related_object=determination, - ) - submission.perform_transition( - transition, - self.request.user, - request=self.request, - notify=False, - proposal_form=proposal_form, - ) - - if submission.accepted_for_funding and settings.PROJECTS_AUTO_CREATE: - Project.create_from_submission(submission) - - messenger( - MESSAGES.DETERMINATION_OUTCOME, - request=self.request, - user=determination.author, - source=submission, - related=determination, - ) - ser = self.get_serializer(self.get_determination_data(determination)) - return Response(ser.data, status=status.HTTP_201_CREATED) - - def update(self, request, *args, **kwargs): - """ - Update a determination submitted on a submission. - """ - determination = self.get_object() - ser = self.get_serializer(data=request.data) - ser.is_valid(raise_exception=True) - ser.update(determination, ser.validated_data) - - messenger( - MESSAGES.DETERMINATION_OUTCOME, - request=self.request, - user=determination.author, - source=determination.submission, - related=determination, - ) - ser = self.get_serializer(self.get_determination_data(determination)) - return Response(ser.data) diff --git a/hypha/apply/api/v1/filters.py b/hypha/apply/api/v1/filters.py deleted file mode 100644 index 8487df78c4..0000000000 --- a/hypha/apply/api/v1/filters.py +++ /dev/null @@ -1,133 +0,0 @@ -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ -from django_filters import rest_framework as filters -from wagtail.models import Page - -from hypha.apply.categories.blocks import CategoryQuestionBlock -from hypha.apply.categories.models import Option -from hypha.apply.funds.models import ApplicationSubmission, FundType, LabType -from hypha.apply.funds.reviewers.services import get_all_reviewers -from hypha.apply.funds.workflow import PHASES - -from .utils import ( - get_round_leads, - get_screening_statuses, - get_used_rounds, -) - - -class SubmissionsFilter(filters.FilterSet): - round = filters.ModelMultipleChoiceFilter( - field_name="round", queryset=get_used_rounds() - ) - status = filters.MultipleChoiceFilter(choices=PHASES) - active = filters.BooleanFilter(method="filter_active", label=_("Active")) - submit_date = filters.DateFromToRangeFilter( - field_name="submit_time", label=_("Submit date") - ) - fund = filters.ModelMultipleChoiceFilter( - field_name="page", - label=_("fund"), - ) - screening_statuses = filters.ModelMultipleChoiceFilter( - field_name="screening_statuses", - queryset=get_screening_statuses(), - null_label=_("No Screening"), - ) - reviewers = filters.ModelMultipleChoiceFilter( - field_name="reviewers", - queryset=get_all_reviewers(), - ) - lead = filters.ModelMultipleChoiceFilter( - field_name="lead", - queryset=get_round_leads(), - ) - category_options = filters.MultipleChoiceFilter( - choices=[], label=_("Category"), method="filter_category_options" - ) - id = filters.ModelMultipleChoiceFilter( - field_name="id", - queryset=ApplicationSubmission.objects.exclude_draft() - .current() - .with_latest_update(), - method="filter_id", - ) - - class Meta: - model = ApplicationSubmission - fields = ( - "id", - "status", - "round", - "active", - "submit_date", - "fund", - "screening_statuses", - "reviewers", - "lead", - ) - - def __init__(self, *args, exclude=None, limit_statuses=None, **kwargs): - if exclude is None: - exclude = [] - super().__init__(*args, **kwargs) - self.filters["category_options"].extra["choices"] = [ - (option.id, option.value) - for option in Option.objects.filter(category__filter_on_dashboard=True) - ] - - self.fund.queryset = (Page.objects.type(FundType) | Page.objects.type(LabType),) - - def filter_active(self, qs, name, value): - if value is None: - return qs - - if value: - return qs.active() - else: - return qs.inactive() - - def filter_category_options(self, queryset, name, value): - """ - Filter submissions based on the category options selected. - - In order to do that we need to first get all the category fields used in the submission. - - And then use those category fields to filter submissions with their form_data. - """ - query = Q() - submission_data = queryset.values("form_fields", "form_data").distinct() - for submission in submission_data: - for field in submission["form_fields"]: - if isinstance(field.block, CategoryQuestionBlock): - try: - category_options = category_ids = submission["form_data"][ - field.id - ] - except KeyError: - include_in_filter = False - else: - if isinstance(category_options, str): - category_options = [category_options] - include_in_filter = set(category_options) & set(value) - # Check if filter options has any value in category options - # If yes then those submissions should be filtered in the list - if include_in_filter: - kwargs = { - "{0}__{1}".format("form_data", field.id): category_ids - } - query |= Q(**kwargs) - return queryset.filter(query) - - def filter_id(self, qs, name, value): - if not value: - return qs - return qs.filter(id__in=[x.id for x in value]) - - -class NewerThanFilter(filters.ModelChoiceFilter): - def filter(self, qs, value): - if not value: - return qs - - return qs.newer(value) diff --git a/hypha/apply/api/v1/mixin.py b/hypha/apply/api/v1/mixin.py deleted file mode 100644 index f2c3b85ccb..0000000000 --- a/hypha/apply/api/v1/mixin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.shortcuts import get_object_or_404 - -from hypha.apply.funds.models import ApplicationSubmission -from hypha.apply.projects.models import Invoice, Project - - -class SubmissionNestedMixin: - def get_submission_object(self): - return get_object_or_404(ApplicationSubmission, id=self.kwargs["submission_pk"]) - - -class InvoiceNestedMixin: - def get_invoice_object(self): - return get_object_or_404(Invoice, id=self.kwargs["invoice_pk"]) - - -class ProjectNestedMixin: - def get_project_object(self): - return get_object_or_404(Project, id=self.kwargs["project_pk"]) diff --git a/hypha/apply/api/v1/permissions.py b/hypha/apply/api/v1/permissions.py index 14df601e69..73eeb265d4 100644 --- a/hypha/apply/api/v1/permissions.py +++ b/hypha/apply/api/v1/permissions.py @@ -1,11 +1,6 @@ from rest_framework import permissions -class IsAuthor(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - return obj.user == request.user - - class IsApplyStaffUser(permissions.BasePermission): """ Custom permission to only allow organisation Staff or higher @@ -16,17 +11,3 @@ def has_permission(self, request, view): def has_object_permission(self, request, view, obj): return request.user.is_apply_staff - - -class IsFinance1User(permissions.BasePermission): - def has_permission(self, request, view): - return request.user.is_finance - - def has_object_permission(self, request, view, obj): - return request.user.is_finance - - -class HasDeliverableEditPermission(permissions.BasePermission): - def has_permission(self, request, view): - invoice = view.get_invoice_object() - return invoice.can_user_edit_deliverables(request.user) diff --git a/hypha/apply/api/v1/projects/__init__.py b/hypha/apply/api/v1/projects/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/api/v1/projects/serializers.py b/hypha/apply/api/v1/projects/serializers.py deleted file mode 100644 index 06edda05fc..0000000000 --- a/hypha/apply/api/v1/projects/serializers.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.utils.translation import gettext_lazy as _ -from rest_framework import exceptions, serializers - -from hypha.apply.projects.models import Deliverable, InvoiceDeliverable - - -class InvoiceDeliverableListSerializer(serializers.ModelSerializer): - invoice_id = serializers.SerializerMethodField() - project_id = serializers.IntegerField(source="deliverable.project.id") - - class Meta: - model = InvoiceDeliverable - fields = ("id", "deliverable", "quantity", "invoice_id", "project_id") - depth = 1 - - def get_invoice_id(self, obj): - return self.context["invoice"].id - - -class DeliverableSerializer(serializers.Serializer): - id = serializers.IntegerField() - quantity = serializers.IntegerField(min_value=1, default=1) - - def validate_id(self, value): - try: - Deliverable.objects.get(id=value) - except Deliverable.DoesNotExist as e: - raise exceptions.ValidationError({"detail": _("Not found")}) from e - return value diff --git a/hypha/apply/api/v1/projects/views.py b/hypha/apply/api/v1/projects/views.py deleted file mode 100644 index f5df4bc54d..0000000000 --- a/hypha/apply/api/v1/projects/views.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext_lazy as _ -from rest_framework import mixins, permissions, status, viewsets -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response - -from hypha.apply.projects.models.payment import InvoiceDeliverable -from hypha.apply.projects.models.project import Deliverable - -from ..mixin import InvoiceNestedMixin, ProjectNestedMixin -from ..permissions import ( - HasDeliverableEditPermission, - IsApplyStaffUser, - IsFinance1User, -) -from .serializers import ( - DeliverableSerializer, - InvoiceDeliverableListSerializer, -) - - -class InvoiceDeliverableViewSet( - InvoiceNestedMixin, - ProjectNestedMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet, -): - permission_classes = ( - permissions.IsAuthenticated, - HasDeliverableEditPermission, - IsApplyStaffUser | IsFinance1User, - ) - serializer_class = InvoiceDeliverableListSerializer - pagination_class = None - - def get_queryset(self): - invoice = self.get_invoice_object() - return invoice.deliverables.all() - - def create(self, request, *args, **kwargs): - ser = DeliverableSerializer(data=request.data) - ser.is_valid(raise_exception=True) - project = self.get_project_object() - deliverable_id = ser.validated_data["id"] - if not project.deliverables.filter(id=deliverable_id).exists(): - raise ValidationError({"detail": _("Not Found")}) - invoice = self.get_invoice_object() - deliverable = get_object_or_404(Deliverable, id=deliverable_id) - if invoice.deliverables.filter(deliverable=deliverable).exists(): - raise ValidationError({"detail": _("Invoice Already has this deliverable")}) - quantity = ser.validated_data["quantity"] - if deliverable.available_to_invoice < quantity: - raise ValidationError( - {"detail": _("Required quantity is more than available")} - ) - invoice_deliverable = InvoiceDeliverable.objects.create( - deliverable=deliverable, quantity=ser.validated_data["quantity"] - ) - invoice.deliverables.add(invoice_deliverable) - ser = self.get_serializer(invoice.deliverables.all(), many=True) - return Response( - { - "deliverables": ser.data, - "total": invoice.deliverables_total_amount["total"], - }, - status=status.HTTP_201_CREATED, - ) - - def get_serializer_context(self): - context = super().get_serializer_context() - context["invoice"] = self.get_invoice_object() - return context - - def destroy(self, request, *args, **kwargs): - deliverable = self.get_object() - invoice = self.get_invoice_object() - invoice.deliverables.remove(deliverable) - ser = self.get_serializer(invoice.deliverables.all(), many=True) - return Response( - { - "deliverables": ser.data, - "total": invoice.deliverables_total_amount["total"], - }, - ) diff --git a/hypha/apply/api/v1/reminder/__init__.py b/hypha/apply/api/v1/reminder/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/api/v1/reminder/serializers.py b/hypha/apply/api/v1/reminder/serializers.py deleted file mode 100644 index 147fe1bc3d..0000000000 --- a/hypha/apply/api/v1/reminder/serializers.py +++ /dev/null @@ -1,28 +0,0 @@ -from rest_framework import serializers - -from hypha.apply.funds.models import Reminder - - -class SubmissionReminderSerializer(serializers.ModelSerializer): - def validate(self, data): - """ - Check title is empty. - """ - required_fields = ["title"] - for field in required_fields: - if not data.get(field, None): - raise serializers.ValidationError({field: "shouldn't be empty"}) - return data - - class Meta: - model = Reminder - fields = ( - "time", - "action_type", - "is_expired", - "id", - "action", - "title", - "description", - ) - read_only_fields = ("action_type", "is_expired") diff --git a/hypha/apply/api/v1/reminder/views.py b/hypha/apply/api/v1/reminder/views.py deleted file mode 100644 index f3e05a0428..0000000000 --- a/hypha/apply/api/v1/reminder/views.py +++ /dev/null @@ -1,73 +0,0 @@ -from rest_framework import mixins, permissions, viewsets -from rest_framework.decorators import action -from rest_framework.response import Response - -from hypha.apply.funds.models import Reminder - -from ..mixin import SubmissionNestedMixin -from ..permissions import IsApplyStaffUser -from .serializers import SubmissionReminderSerializer - - -class SubmissionReminderViewSet( - SubmissionNestedMixin, - mixins.ListModelMixin, - mixins.CreateModelMixin, - viewsets.GenericViewSet, -): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - serializer_class = SubmissionReminderSerializer - pagination_class = None - - def get_queryset(self): - submission = self.get_submission_object() - return Reminder.objects.filter(submission=submission).order_by("-time") - - def perform_create(self, serializer): - serializer.save(user=self.request.user, submission=self.get_submission_object()) - - def destroy(self, request, *args, **kwargs): - reminder = self.get_object() - reminder.delete() - ser = self.get_serializer(self.get_queryset(), many=True) - return Response(ser.data) - - @action(detail=False, methods=["get"]) - def fields(self, request, *args, **kwargs): - """ - List details of all the form fields that were created by admin for adding reminders. - - These field details will be used in frontend to render the reminder form. - """ - fields = [ - { - "id": "title", - "kwargs": {"required": True, "label": "Title", "max_length": 60}, - "type": "TextInput", - }, - { - "id": "description", - "type": "textArea", - "kwargs": {"label": "Description"}, - "widget": {"attrs": {"cols": 40, "rows": 5}, "type": "Textarea"}, - }, - { - "id": "time", - "kwargs": {"label": "Time", "required": True}, - "type": "DateTime", - }, - { - "id": "action", - "kwargs": { - "label": "Action", - "required": True, - "choices": Reminder.ACTIONS.items(), - "initial": Reminder.REVIEW, - }, - "type": "Select", - }, - ] - return Response(fields) diff --git a/hypha/apply/api/v1/review/__init__.py b/hypha/apply/api/v1/review/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/api/v1/review/fields.py b/hypha/apply/api/v1/review/fields.py deleted file mode 100644 index 0bc9ba7aa8..0000000000 --- a/hypha/apply/api/v1/review/fields.py +++ /dev/null @@ -1,35 +0,0 @@ -from collections import OrderedDict - -from rest_framework import serializers -from rest_framework.exceptions import ValidationError - -from hypha.apply.review.options import RATE_CHOICES - - -class ScoredAnswerListField(serializers.ListField): - childs = [serializers.CharField(), serializers.ChoiceField(choices=RATE_CHOICES)] - - def __init__(self, *args, **kwargs): - draft = kwargs.pop("draft", False) - super().__init__(*args, **kwargs) - if draft: - self.childs = [ - serializers.CharField( - required=False, allow_null=True, allow_blank=True - ), - serializers.ChoiceField(choices=RATE_CHOICES), - ] - - def run_child_validation(self, data): - result = [] - errors = OrderedDict() - - for idx, item in enumerate(data): - try: - result.append(self.childs[idx].run_validation(item)) - except ValidationError as e: - errors[idx] = e.detail - - if not errors: - return result - raise ValidationError(errors) diff --git a/hypha/apply/api/v1/review/permissions.py b/hypha/apply/api/v1/review/permissions.py deleted file mode 100644 index d55e6a7f09..0000000000 --- a/hypha/apply/api/v1/review/permissions.py +++ /dev/null @@ -1,118 +0,0 @@ -from rest_framework import permissions - - -class HasReviewCreatePermission(permissions.BasePermission): - """ - Custom permission that user should have for creating review. - """ - - def has_permission(self, request, view): - try: - submission = view.get_submission_object() - except KeyError: - return True - return submission.phase.permissions.can_review( - request.user - ) and submission.has_permission_to_review(request.user) - - -class HasReviewEditPermission(permissions.BasePermission): - """ - Custom permission that user should have for editing review. - """ - - def has_object_permission(self, request, view, obj): - return ( - request.user.has_perm("review.change_review") - or request.user == obj.author.reviewer - ) - - -class HasReviewDetailPermission(permissions.BasePermission): - """ - Custom permission that user should have for viewing review. - """ - - def has_object_permission(self, request, view, obj): - user = request.user - author = obj.author.reviewer - submission = obj.submission - - if user.is_apply_staff: - return True - - if user == author: - return True - - if user.is_reviewer and obj.reviewer_visibility: - return True - - if ( - user.is_community_reviewer - and submission.community_review - and obj.reviewer_visibility - and submission.user != user - ): - return True - - return False - - -class HasReviewDeletePermission(permissions.BasePermission): - """ - Custom permission that user should have for deleting review. - """ - - def has_object_permission(self, request, view, obj): - return ( - request.user.has_perm("review.delete_review") or request.user == obj.author - ) - - -class HasReviewOpinionPermission(permissions.BasePermission): - """ - Custom permission that user should have for posting opinion on a review. - """ - - def has_object_permission(self, request, view, obj): - review = obj - user = request.user - author = review.author.reviewer - submission = review.submission - - if user.is_apply_staff: - return True - - if user == author: - return False - - if user.is_reviewer and review.reviewer_visibility: - return True - - if ( - user.is_community_reviewer - and submission.community_review - and review.reviewer_visibility - and submission.user != user - ): - return True - - return False - - -class HasReviewDraftPermission(permissions.BasePermission): - """ - Custom permission that user should have to access draft review. - """ - - def has_object_permission(self, request, view, obj): - try: - submission = view.get_submission_object() - except KeyError: - return True - return ( - submission.can_review(request.user) - and submission.assigned.draft_reviewed() - .filter(reviewer=request.user) - .exists() - ) diff --git a/hypha/apply/api/v1/review/serializers.py b/hypha/apply/api/v1/review/serializers.py deleted file mode 100644 index 299b6e573e..0000000000 --- a/hypha/apply/api/v1/review/serializers.py +++ /dev/null @@ -1,115 +0,0 @@ -from rest_framework import serializers - -from hypha.apply.review.models import Review, ReviewOpinion -from hypha.apply.review.options import NA, NO, PRIVATE -from hypha.apply.stream_forms.forms import BlockFieldWrapper - -from ..utils import get_field_kwargs, get_field_widget - - -class ReviewOpinionReadSerializer(serializers.ModelSerializer): - author_id = serializers.ReadOnlyField(source="author.id") - opinion = serializers.ReadOnlyField(source="get_opinion_display") - - class Meta: - model = ReviewOpinion - fields = ("author_id", "opinion") - - -class ReviewOpinionWriteSerializer(serializers.ModelSerializer): - class Meta: - model = ReviewOpinion - fields = ("opinion",) - extra_kwargs = {"opinion": {"write_only": True}} - - -class SubmissionReviewSerializer(serializers.ModelSerializer): - opinions = ReviewOpinionReadSerializer(read_only=True, many=True) - - class Meta: - model = Review - fields = [ - "id", - "score", - "is_draft", - "opinions", - ] - extra_kwargs = {"score": {"read_only": True}, "is_draft": {"required": False}} - - def get_recommendation(self, obj): - return { - "value": obj.recommendation, - "display": obj.get_recommendation_display(), - } - - def validate(self, data): - validated_data = super().validate(data) - validated_data["form_data"] = dict(validated_data.items()) - return validated_data - - def update(self, instance, validated_data): - instance = super().update(instance, validated_data) - instance.score = self.calculate_score(instance, self.validated_data) - recommendation = self.validated_data.get(instance.recommendation_field.id, NO) - instance.recommendation = int( - recommendation if recommendation is not None else NO - ) - instance.is_draft = self.validated_data.get("is_draft", False) - - # Old review forms do not have the required visibility field. - # This will set visibility to PRIVATE by default. - try: - instance.visibility = self.validated_data[instance.visibility_field.id] - except AttributeError: - instance.visibility = PRIVATE - - if not instance.is_draft: - # Capture the revision against which the user was reviewing - instance.revision = instance.submission.live_revision - - instance.save() - return instance - - def calculate_score(self, instance, data): - scores = [] - for field in instance.score_fields: - score = data.get(field.id, ["", NA]) - # Include NA answers as 0. - score = score[1] if score is not None else NA - if score == NA: - score = 0 - scores.append(score) - # Check if there are score_fields_without_text and also - # append scores from them. - for field in instance.score_fields_without_text: - score = data.get(field.id, "") - # Include '' answers as 0. - if score is None or score == "": - score = 0 - scores.append(int(score)) - - try: - return sum(scores) / len(scores) - except ZeroDivisionError: - return NA - - -class FieldSerializer(serializers.Serializer): - id = serializers.SerializerMethodField() - type = serializers.SerializerMethodField() - kwargs = serializers.SerializerMethodField() - widget = serializers.SerializerMethodField() - - def get_id(self, obj): - return obj[0] - - def get_type(self, obj): - if isinstance(obj[1], BlockFieldWrapper): - return "LoadHTML" - return obj[1].__class__.__name__ - - def get_kwargs(self, obj): - return get_field_kwargs(obj[1]) - - def get_widget(self, obj): - return get_field_widget(obj[1]) diff --git a/hypha/apply/api/v1/review/tests/__init__.py b/hypha/apply/api/v1/review/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/api/v1/review/utils.py b/hypha/apply/api/v1/review/utils.py deleted file mode 100644 index c172bf7812..0000000000 --- a/hypha/apply/api/v1/review/utils.py +++ /dev/null @@ -1,59 +0,0 @@ -from django.core.exceptions import PermissionDenied - -from hypha.apply.funds.workflow import INITIAL_STATE - - -def review_workflow_actions(request, submission): - submission_stepped_phases = submission.workflow.stepped_phases - action = None - if submission.status == INITIAL_STATE: - # Automatically transition the application to "Internal review". - action = submission_stepped_phases[2][0].name - elif submission.status == "proposal_discussion": - # Automatically transition the proposal to "Internal review". - action = "proposal_internal_review" - elif ( - submission.status == submission_stepped_phases[2][0].name - and submission.reviews.count() > 1 - ): - # Automatically transition the application to "Ready for discussion". - action = submission_stepped_phases[3][0].name - elif ( - submission.status == "ext_external_review" - and submission.reviews.by_reviewers().count() > 1 - ): - # Automatically transition the application to "Ready for discussion". - action = "ext_post_external_review_discussion" - elif ( - submission.status == "com_external_review" - and submission.reviews.by_reviewers().count() > 1 - ): - # Automatically transition the application to "Ready for discussion". - action = "com_post_external_review_discussion" - elif ( - submission.status == "external_review" - and submission.reviews.by_reviewers().count() > 1 - ): - # Automatically transition the proposal to "Ready for discussion". - action = "post_external_review_discussion" - - # If action is set run perform_transition(). - if action: - try: - submission.perform_transition( - action, - request.user, - request=request, - notify=False, - ) - except (PermissionDenied, KeyError): - pass - - -def get_review_form_fields_for_stage(submission): - forms = submission.get_from_parent("review_forms").all() - index = submission.workflow.stages.index(submission.stage) - try: - return forms[index].form.form_fields - except IndexError: - return forms[0].form.form_fields diff --git a/hypha/apply/api/v1/review/views.py b/hypha/apply/api/v1/review/views.py deleted file mode 100644 index 09033429c2..0000000000 --- a/hypha/apply/api/v1/review/views.py +++ /dev/null @@ -1,298 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework import permissions, status, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.response import Response -from wagtail.blocks.field_block import RichTextBlock - -from hypha.apply.activity.messaging import MESSAGES, messenger -from hypha.apply.funds.models import AssignedReviewers -from hypha.apply.review.models import Review, ReviewOpinion -from hypha.apply.stream_forms.models import BaseStreamForm - -from ..mixin import SubmissionNestedMixin -from ..permissions import IsApplyStaffUser -from ..stream_serializers import WagtailSerializer -from .permissions import ( - HasReviewCreatePermission, - HasReviewDeletePermission, - HasReviewDetailPermission, - HasReviewDraftPermission, - HasReviewEditPermission, - HasReviewOpinionPermission, -) -from .serializers import ( - FieldSerializer, - ReviewOpinionWriteSerializer, - SubmissionReviewSerializer, -) -from .utils import get_review_form_fields_for_stage, review_workflow_actions - - -class SubmissionReviewViewSet( - BaseStreamForm, WagtailSerializer, SubmissionNestedMixin, viewsets.GenericViewSet -): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - permission_classes_by_action = { - "create": [ - permissions.IsAuthenticated, - HasReviewCreatePermission, - IsApplyStaffUser, - ], - "retrieve": [ - permissions.IsAuthenticated, - HasReviewDetailPermission, - IsApplyStaffUser, - ], - "update": [ - permissions.IsAuthenticated, - HasReviewEditPermission, - IsApplyStaffUser, - ], - "delete": [ - permissions.IsAuthenticated, - HasReviewDeletePermission, - IsApplyStaffUser, - ], - "opinions": [ - permissions.IsAuthenticated, - HasReviewOpinionPermission, - IsApplyStaffUser, - ], - "fields": [ - permissions.IsAuthenticated, - HasReviewCreatePermission, - IsApplyStaffUser, - ], - "draft": [ - permissions.IsAuthenticated, - HasReviewDraftPermission, - IsApplyStaffUser, - ], - } - serializer_class = SubmissionReviewSerializer - - def get_permissions(self): - try: - # return permission_classes depending on `action` - return [ - permission() - for permission in self.permission_classes_by_action[self.action] - ] - except KeyError: - # action is not set return default permission_classes - return [permission() for permission in self.permission_classes] - - def get_defined_fields(self): - """ - Get form fields created for reviewing this submission. - - These form fields will be used to get respective serializer fields. - """ - if self.action in ["retrieve", "update", "opinions"]: - # For detail and edit api form fields used while submitting - # review should be used. - review = self.get_object() - return review.form_fields - if self.action == "draft": - review = self.get_review_by_reviewer() - return review.form_fields - submission = self.get_submission_object() - return get_review_form_fields_for_stage(submission) - - def get_serializer_class(self): - """ - Override get_serializer_class to send draft parameter - if the request is to save as draft or the review submitted - is saved as draft. - """ - if self.action == "retrieve": - review = self.get_object() - draft = review.is_draft - elif self.action == "draft": - draft = True - else: - draft = self.request.data.get("is_draft", False) - return super().get_serializer_class(draft) - - def get_queryset(self): - submission = self.get_submission_object() - return Review.objects.filter(submission=submission, is_draft=False) - - def get_object(self): - """ - Get the review object by id. If not found raise 404. - """ - queryset = self.get_queryset() - obj = get_object_or_404(queryset, id=self.kwargs["pk"]) - self.check_object_permissions(self.request, obj) - return obj - - def get_reviewer(self): - """ - Get the AssignedReviewers for the current user on a submission. - """ - submission = self.get_submission_object() - ar, _ = AssignedReviewers.objects.get_or_create_for_user( - submission=submission, - reviewer=self.request.user, - ) - return ar - - def create(self, request, *args, **kwargs): - """ - Create a review on a submission. - - Accept a post data in form of `{field_id: value}`. - `field_id` is same id which you get from the `/fields` api. - `value` should be submitted with html tags, so that response can - be displayed with correct formatting, e.g. in case of rich text field, - we need to show the data with same formatting user has submitted. - - Accepts optional parameter `is_draft` when a review is to be saved as draft. - - Raise ValidationError if a review is already submitted by the user. - """ - submission = self.get_submission_object() - ser = self.get_serializer(data=request.data) - ser.is_valid(raise_exception=True) - instance, create = ser.Meta.model.objects.get_or_create( - submission=submission, author=self.get_reviewer() - ) - if not create and not instance.is_draft: - raise ValidationError( - {"detail": "You have already posted a review for this submission"} - ) - instance.form_fields = self.get_defined_fields() - instance.save() - ser.update(instance, ser.validated_data) - if not instance.is_draft: - messenger( - MESSAGES.NEW_REVIEW, - request=self.request, - user=self.request.user, - source=submission, - related=instance, - ) - # Automatic workflow actions. - review_workflow_actions(self.request, submission) - ser = self.get_serializer(self.get_review_data(instance)) - return Response(ser.data, status=status.HTTP_201_CREATED) - - def get_review_data(self, review): - """ - Get review data which will be used for review detail api. - """ - review_data = review.form_data - review_data["id"] = review.id - review_data["score"] = review.score - review_data["opinions"] = review.opinions - review_data["is_draft"] = review.is_draft - for field_block in review.form_fields: - if isinstance(field_block.block, RichTextBlock): - review_data[field_block.id] = field_block.value.source - return review_data - - def retrieve(self, request, *args, **kwargs): - """ - Get details of a review on a submission - """ - review = self.get_object() - ser = self.get_serializer(self.get_review_data(review)) - return Response(ser.data) - - def update(self, request, *args, **kwargs): - """ - Update a review submitted on a submission. - """ - review = self.get_object() - ser = self.get_serializer(data=request.data) - ser.is_valid(raise_exception=True) - ser.update(review, ser.validated_data) - - messenger( - MESSAGES.EDIT_REVIEW, - user=self.request.user, - request=self.request, - source=review.submission, - related=review, - ) - # Automatic workflow actions. - review_workflow_actions(self.request, review.submission) - ser = self.get_serializer(self.get_review_data(review)) - return Response(ser.data) - - def destroy(self, request, *args, **kwargs): - """Delete a review on a submission""" - review = self.get_object() - messenger( - MESSAGES.DELETE_REVIEW, - user=request.user, - request=request, - source=review.submission, - related=review, - ) - review.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - def get_review_by_reviewer(self): - submission = self.get_submission_object() - review = Review.objects.get( - submission=submission, author__reviewer=self.request.user - ) - return review - - @action(detail=False, methods=["get"]) - def draft(self, request, *args, **kwargs): - """ - Returns the draft review submitted on a submission by current user. - """ - try: - review = self.get_review_by_reviewer() - except Review.DoesNotExist: - return Response({}) - if not review.is_draft: - return Response({}) - ser = self.get_serializer(self.get_review_data(review)) - return Response(ser.data) - - @action(detail=False, methods=["get"]) - def fields(self, request, *args, **kwargs): - """ - List details of all the form fields that were created by admin for adding reviews. - - These field details will be used in frontend to render the review form. - """ - fields = self.get_form_fields() - fields = FieldSerializer(fields.items(), many=True) - return Response(fields.data) - - @action(detail=True, methods=["post"]) - def opinions(self, request, *args, **kwargs): - """ - Used to add opinions on a review. - - Options are 0 and 1. DISAGREE = 0 AGREE = 1 - - Response is similar to detail api of the review. - """ - review = self.get_object() - ser = ReviewOpinionWriteSerializer(data=request.data) - ser.is_valid(raise_exception=True) - opinion = ser.validated_data["opinion"] - try: - review_opinion = ReviewOpinion.objects.get( - review=review, author=self.get_reviewer() - ) - except ReviewOpinion.DoesNotExist: - ReviewOpinion.objects.create( - review=review, author=self.get_reviewer(), opinion=opinion - ) - else: - review_opinion.opinion = opinion - review_opinion.save() - ser = self.get_serializer(self.get_review_data(review)) - return Response(ser.data, status=status.HTTP_201_CREATED) diff --git a/hypha/apply/api/v1/serializers.py b/hypha/apply/api/v1/serializers.py index 850a8ff3c8..dc54552cdf 100644 --- a/hypha/apply/api/v1/serializers.py +++ b/hypha/apply/api/v1/serializers.py @@ -1,334 +1,11 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from hypha.apply.categories.models import MetaTerm -from hypha.apply.determinations.models import Determination -from hypha.apply.determinations.templatetags.determination_tags import ( - show_determination_button, -) -from hypha.apply.determinations.views import DeterminationCreateOrUpdateView -from hypha.apply.funds.models import ( - ApplicationSubmission, - RoundsAndLabs, - ScreeningStatus, -) -from hypha.apply.review.models import Review, ReviewOpinion -from hypha.apply.review.options import RECOMMENDATION_CHOICES -from hypha.apply.users.roles import PARTNER_GROUP_NAME, STAFF_GROUP_NAME +from hypha.apply.funds.models import RoundsAndLabs User = get_user_model() -class ActionSerializer(serializers.Field): - def to_representation(self, instance): - actions = instance.get_actions_for_user(self.context["request"].user) - representation = [] - for transition, action in actions: - action_dict = {"value": transition, "display": action} - - # Sometimes the status does not exist in the - # determination matrix. - try: - redirect = DeterminationCreateOrUpdateView.should_redirect( - None, - instance, - transition, - ) - except KeyError: - redirect = None - if redirect: - action_dict["type"] = "redirect" - action_dict["target"] = redirect.url - else: - action_dict["type"] = "submit" - - representation.append(action_dict) - return representation - - -class OpinionSerializer(serializers.ModelSerializer): - author_id = serializers.ReadOnlyField(source="author.id") - opinion = serializers.ReadOnlyField(source="get_opinion_display") - - class Meta: - model = ReviewOpinion - fields = ("author_id", "opinion") - - -class ReviewSerializer(serializers.ModelSerializer): - user_id = serializers.SerializerMethodField() - author_id = serializers.ReadOnlyField(source="author.id") - url = serializers.ReadOnlyField(source="get_absolute_url") - opinions = OpinionSerializer(read_only=True, many=True) - recommendation = serializers.SerializerMethodField() - score = serializers.ReadOnlyField(source="get_score_display") - - class Meta: - model = Review - fields = ( - "id", - "score", - "user_id", - "author_id", - "url", - "opinions", - "recommendation", - ) - - def get_recommendation(self, obj): - return { - "value": obj.recommendation, - "display": obj.get_recommendation_display(), - } - - def get_user_id(self, obj): - return obj.author.reviewer.id - - -class ReviewSummarySerializer(serializers.Serializer): - reviews = ReviewSerializer(many=True, read_only=True) - count = serializers.ReadOnlyField(source="reviews.count") - score = serializers.ReadOnlyField(source="reviews.score") - recommendation = serializers.SerializerMethodField() - assigned = serializers.SerializerMethodField() - - def get_recommendation(self, obj): - recommendation = obj.reviews.recommendation() - return { - "value": recommendation, - "display": dict(RECOMMENDATION_CHOICES).get(recommendation), - } - - def get_assigned(self, obj): - assigned_reviewers = obj.assigned.select_related("reviewer", "role", "type") - response = [ - { - "id": assigned.id, - "name": str(assigned.reviewer), - "role": { - "icon": assigned.role and assigned.role.icon_url("fill-12x12"), - "name": assigned.role and assigned.role.name, - "order": assigned.role and assigned.role.order, - }, - "is_staff": assigned.type.name == STAFF_GROUP_NAME, - "is_partner": assigned.type.name == PARTNER_GROUP_NAME, - } - for assigned in assigned_reviewers - ] - return response - - -class TimestampField(serializers.Field): - def to_representation(self, value): - return value.timestamp() * 1000 - - -class DeterminationSerializer(serializers.ModelSerializer): - outcome = serializers.ReadOnlyField(source="get_outcome_display") - author = serializers.CharField(read_only=True) - url = serializers.ReadOnlyField(source="get_absolute_url") - updated_at = serializers.DateTimeField(read_only=True) - is_draft = serializers.BooleanField(read_only=True) - - class Meta: - model = Determination - fields = ("id", "outcome", "author", "url", "updated_at", "is_draft") - - -class DeterminationSummarySerializer(serializers.Serializer): - determinations = DeterminationSerializer(many=True, read_only=True) - count = serializers.ReadOnlyField(source="determinations.count") - - -class SubmissionListSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name="api:v1:submissions-detail") - round = serializers.SerializerMethodField() - last_update = TimestampField() - - class Meta: - model = ApplicationSubmission - fields = ("id", "title", "status", "url", "round", "last_update", "summary") - - def get_round(self, obj): - """ - This gets round or lab ID. - """ - return obj.round_id or obj.page_id - - -class MetaTermsDetailSerializer(serializers.ModelSerializer): - parent = serializers.SerializerMethodField() - - class Meta: - model = MetaTerm - fields = ("id", "name", "parent") - - def get_parent(self, obj): - parent = obj.get_parent() - if parent: - parent_data = {"id": parent.id, "name": parent.name} - return parent_data - - -class SubmissionSummarySerializer(serializers.Serializer): - summary = serializers.CharField(write_only=True) - - -class SubmissionMetaTermsSerializer(serializers.Serializer): - meta_terms = serializers.PrimaryKeyRelatedField( - many=True, queryset=MetaTerm.objects.all() - ) - - -class SubmissionDetailSerializer(serializers.ModelSerializer): - questions = serializers.SerializerMethodField() - meta_questions = serializers.SerializerMethodField() - meta_terms = MetaTermsDetailSerializer(many=True) - stage = serializers.CharField(source="stage.name") - actions = ActionSerializer(source="*") - review = ReviewSummarySerializer(source="*") - determination = DeterminationSummarySerializer(source="*") - phase = serializers.CharField() - screening = serializers.SerializerMethodField() - action_buttons = serializers.SerializerMethodField() - is_determination_form_attached = serializers.BooleanField(read_only=True) - is_user_staff = serializers.SerializerMethodField() - flags = serializers.SerializerMethodField() - reminders = serializers.SerializerMethodField() - - class Meta: - model = ApplicationSubmission - fields = ( - "id", - "summary", - "title", - "stage", - "status", - "phase", - "meta_questions", - "meta_terms", - "questions", - "actions", - "review", - "screening", - "action_buttons", - "determination", - "is_determination_form_attached", - "is_user_staff", - "screening", - "flags", - "reminders", - ) - - def serialize_questions(self, obj, fields): - for field_id in fields: - yield obj.serialize(field_id) - - def get_is_user_staff(self, obj): - request = self.context["request"] - return request.user.is_apply_staff - - def get_meta_questions(self, obj): - meta_questions = { - "title": "Project Name", - "full_name": "Legal Name", - "email": "Email", - "value": "Requested Funding", - "duration": "Project Duration", - "address": "Address", - } - data = self.serialize_questions(obj, obj.named_blocks.values()) - data = [ - { - **response, - "question": meta_questions.get(response["type"], response["question"]), - } - for response in data - ] - return data - - def get_screening(self, obj): - selected_default = {} - selected_reasons = [] - all_screening = [] - - for screening in obj.screening_statuses.values(): - if screening["default"]: - selected_default = screening - else: - selected_reasons.append(screening) - - for screening in ScreeningStatus.objects.values(): - all_screening.append(screening) - - screening = { - "selected_reasons": selected_reasons, - "selected_default": selected_default, - "all_screening": all_screening, - } - return screening - - def get_flags(self, obj): - flags = [ - {"type": "user", "selected": obj.flagged_by(self.context["request"].user)}, - {"type": "staff", "selected": obj.flagged_staff}, - ] - - return flags - - def get_questions(self, obj): - return self.serialize_questions(obj, obj.normal_blocks) - - def get_reminders(self, obj): - reminders = [] - for reminder in obj.reminders.all(): - reminders.append( - { - "id": reminder.id, - "submission_id": reminder.submission_id, - "title": reminder.title, - "action_type": reminder.action_type, - "is_expired": reminder.is_expired, - } - ) - return reminders - - def get_action_buttons(self, obj): - request = self.context["request"] - add_review = ( - obj.phase.permissions.can_review(request.user) - and obj.can_review(request.user) - and not obj.assigned.draft_reviewed().filter(reviewer=request.user).exists() - ) - show_determination = show_determination_button(request.user, obj) - return { - "add_review": add_review, - "show_determination_button": show_determination, - } - - -class SubmissionActionSerializer(serializers.ModelSerializer): - actions = ActionSerializer(source="*", read_only=True) - - class Meta: - model = ApplicationSubmission - fields = ("id", "actions") - - -class RoundLabDetailSerializer(serializers.ModelSerializer): - workflow = serializers.SerializerMethodField() - - class Meta: - model = RoundsAndLabs - fields = ("id", "title", "workflow") - - def get_workflow(self, obj): - return [ - {"value": phase.name, "display": phase.display_name} - for phase in obj.workflow.values() - ] - - class RoundLabSerializer(serializers.ModelSerializer): class Meta: model = RoundsAndLabs @@ -389,22 +66,3 @@ def get_landing_url(self, obj): elif hasattr(obj, "labbase"): return obj.labbase.get_full_url() return None - - -class UserSerializer(serializers.Serializer): - id = serializers.CharField(read_only=True) - email = serializers.CharField(read_only=True) - - -class MetaTermsSerializer(serializers.ModelSerializer): - children = serializers.SerializerMethodField( - read_only=True, method_name="get_children_nodes" - ) - - class Meta: - model = MetaTerm - fields = ("name", "id", "children") - - def get_children_nodes(self, obj): - child_queryset = obj.get_children() - return MetaTermsSerializer(child_queryset, many=True).data diff --git a/hypha/apply/api/v1/stream_serializers.py b/hypha/apply/api/v1/stream_serializers.py deleted file mode 100644 index bbc529aac7..0000000000 --- a/hypha/apply/api/v1/stream_serializers.py +++ /dev/null @@ -1,157 +0,0 @@ -import inspect -from collections import OrderedDict - -from django.forms import TypedChoiceField -from rest_framework import serializers - -from hypha.apply.review.fields import ScoredAnswerField -from hypha.apply.stream_forms.forms import BlockFieldWrapper - -from .review.fields import ScoredAnswerListField - -IGNORE_ARGS = ["self", "cls"] - - -class WagtailSerializer: - def get_serializer_fields(self, draft=False): - """ - Get the respective serializer fields for all the form fields. - """ - serializer_fields = OrderedDict() - form_fields = self.get_form_fields() - for field_id, field in form_fields.items(): - serializer_fields[field_id] = self._get_field( - field, self.get_serializer_field_class(field), draft - ) - return serializer_fields - - def _get_field(self, form_field, serializer_field_class, draft=False): - """ - Get the serializer field from the form field with all - the kwargs defined. - """ - kwargs = self._get_field_kwargs(form_field, serializer_field_class) - field_kwargs = kwargs.copy() - for kwarg, value in kwargs.items(): - # set corresponding DRF attributes which don't have alternative - # in Django form fields - if kwarg == "required": - field_kwargs["allow_blank"] = not value - field_kwargs["allow_null"] = not value - - if draft: - # Set required false for fields if it's a draft. - field_kwargs["required"] = False - field_kwargs["allow_null"] = True - field_kwargs["allow_blank"] = True - try: - field = serializer_field_class(**field_kwargs) - except TypeError: - # ScoredAnswerField doesn't have allow_blank attribute - field_kwargs.pop("allow_blank") - if serializer_field_class.__name__ == "ScoredAnswerListField": - field_kwargs["draft"] = True - return serializer_field_class(**field_kwargs) - return serializer_field_class(**field_kwargs) - - return field - - def find_function_args(self, func): - """ - Get the list of parameter names which function accepts. - """ - try: - spec = ( - inspect.getfullargspec(func) - if hasattr(inspect, "getfullargspec") - else inspect.getargspec(func) - ) - return [i for i in spec[0] if i not in IGNORE_ARGS] - except TypeError: - return [] - - def find_class_args(self, klass): - """ - Find all class arguments (parameters) which can be passed in ``__init__``. - """ - args = set() - for i in klass.mro(): - if i is object or not hasattr(i, "__init__"): - continue - args |= set(self.find_function_args(i.__init__)) - - return list(args) - - def find_matching_class_kwargs(self, reference_object, klass): - return { - i: getattr(reference_object, i) - for i in self.find_class_args(klass) - if hasattr(reference_object, i) - } - - def _get_field_kwargs(self, form_field, serializer_field_class): - """ - For a given Form field, determine what validation attributes - have been set. Includes things like max_length, required, etc. - These will be used to create an instance of ``rest_framework.fields.Field``. - :param form_field: a ``django.forms.field.Field`` instance - :return: dictionary of attributes to set - """ - attrs = self.find_matching_class_kwargs(form_field, serializer_field_class) - - if "choices" in attrs: - choices = OrderedDict(attrs["choices"]).keys() - attrs["choices"] = OrderedDict(zip(choices, choices, strict=True)) - - if getattr(form_field, "initial", None): - attrs["default"] = form_field.initial - - # avoid "May not set both `required` and `default`" - if attrs.get("required") and "default" in attrs: - del attrs["required"] - - if isinstance(form_field, BlockFieldWrapper): - attrs["read_only"] = True - return attrs - - # avoid "May not set both `read_only` and `required`" - if form_field.widget.attrs.get("readonly", False) == "readonly": - attrs["read_only"] = True - del attrs["required"] - return attrs - - def get_serializer_field_class(self, field): - """ - Assumes that a serializer field exist with the same name as form field. - - TODO: - In case there are form fields not existing in serializer fields, we would - have to create mapping b/w form fields and serializer fields to get the - respective classes. But for now this works. - """ - if isinstance(field, BlockFieldWrapper): - return serializers.CharField - if isinstance(field, ScoredAnswerField): - return ScoredAnswerListField - if isinstance(field, TypedChoiceField): - return serializers.ChoiceField - class_name = field.__class__.__name__ - return getattr(serializers, class_name) - - def get_serializer_class(self, draft=False): - # Model serializers needs to have each field declared in the field options - # of Meta. This code adds the dynamically generated serializer fields - # to the serializer class meta fields. - model_fields = [ - field - for field in self.serializer_class.Meta.fields - if hasattr(self.serializer_class.Meta.model, field) - ] - self.serializer_class.Meta.fields = model_fields + [ - *self.get_serializer_fields(draft).keys() - ] - return type( - "WagtailStreamSerializer", - (self.serializer_class,), - self.get_serializer_fields(draft), - ) diff --git a/hypha/apply/api/v1/urls.py b/hypha/apply/api/v1/urls.py index c415ccec25..c44eab0792 100644 --- a/hypha/apply/api/v1/urls.py +++ b/hypha/apply/api/v1/urls.py @@ -1,54 +1,10 @@ -from django.urls import path from rest_framework_nested import routers -from hypha.apply.api.v1.determination.views import SubmissionDeterminationViewSet -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 .views import ( - CurrentUser, - RoundViewSet, - SubmissionActionViewSet, - SubmissionFilters, - SubmissionViewSet, -) +from .views import RoundViewSet app_name = "v1" - router = routers.SimpleRouter() -router.register(r"submissions", SubmissionViewSet, basename="submissions") -router.register(r"rounds", RoundViewSet, basename="rounds") - -submission_router = routers.NestedSimpleRouter( - router, r"submissions", lookup="submission" -) -submission_router.register( - r"actions", SubmissionActionViewSet, basename="submission-actions" -) -submission_router.register(r"reviews", SubmissionReviewViewSet, basename="reviews") -submission_router.register( - r"determinations", SubmissionDeterminationViewSet, basename="determinations" -) - -submission_router.register( - r"reminders", SubmissionReminderViewSet, basename="submission-reminder" -) - -urlpatterns = [ - path("user/", CurrentUser.as_view(), name="user"), - path("submissions_filter/", SubmissionFilters.as_view(), name="submissions-filter"), - path( - "projects//invoices//deliverables/", - InvoiceDeliverableViewSet.as_view({"post": "create"}), - name="set-deliverables", - ), - path( - "projects//invoices//deliverables//", - InvoiceDeliverableViewSet.as_view({"delete": "destroy"}), - name="remove-deliverables", - ), -] +router.register("rounds", RoundViewSet, basename="rounds") -urlpatterns = router.urls + submission_router.urls + urlpatterns +urlpatterns = router.urls diff --git a/hypha/apply/api/v1/utils.py b/hypha/apply/api/v1/utils.py deleted file mode 100644 index f6af1638d1..0000000000 --- a/hypha/apply/api/v1/utils.py +++ /dev/null @@ -1,112 +0,0 @@ -from collections import OrderedDict - -from django import forms -from django.contrib.auth import get_user_model -from tinymce.widgets import TinyMCE -from wagtail.models import Page - -from hypha.apply.categories.models import Option -from hypha.apply.funds.models import ApplicationSubmission, Round, ScreeningStatus -from hypha.apply.review.fields import ScoredAnswerField, ScoredAnswerWidget -from hypha.apply.stream_forms.forms import BlockFieldWrapper - -User = get_user_model() - - -def get_field_kwargs(form_field): - if isinstance(form_field, BlockFieldWrapper): - return {"text": form_field.block.value.source} - kwargs = OrderedDict() - kwargs = { - "initial": form_field.initial, - "required": form_field.required, - "label": form_field.label, - "label_suffix": form_field.label_suffix, - "help_text": form_field.help_text, - "help_link": form_field.help_link, - } - if isinstance(form_field, forms.CharField): - if hasattr(form_field, "word_limit"): - kwargs["word_limit"] = form_field.word_limit - kwargs["max_length"] = form_field.max_length - kwargs["min_length"] = form_field.min_length - kwargs["empty_value"] = form_field.empty_value - if isinstance(form_field, forms.ChoiceField): - kwargs["choices"] = form_field.choices - if isinstance(form_field, forms.TypedChoiceField): - kwargs["empty_value"] = form_field.empty_value - if isinstance(form_field, forms.IntegerField): - kwargs["max_value"] = form_field.max_value - kwargs["min_value"] = form_field.min_value - if isinstance(form_field, ScoredAnswerField): - fields = [ - { - "type": form_field.fields[0].__class__.__name__, - "max_length": form_field.fields[0].max_length, - "min_length": form_field.fields[0].min_length, - "empty_value": form_field.fields[0].empty_value, - }, - { - "type": form_field.fields[1].__class__.__name__, - "choices": form_field.fields[1].choices, - }, - ] - kwargs["fields"] = fields - return kwargs - - -def get_field_widget(form_field): - if isinstance(form_field, BlockFieldWrapper): - return {"type": "LoadHTML", "attrs": {}} - widget = { - "type": form_field.widget.__class__.__name__, - "attrs": form_field.widget.attrs, - } - if isinstance(form_field.widget, TinyMCE): - mce_attrs = form_field.widget.mce_attrs - plugins = mce_attrs.get("plugins") - if not isinstance(plugins, list): - mce_attrs["plugins"] = [plugins] - if "toolbar1" in mce_attrs: - mce_attrs["toolbar"] = mce_attrs.pop("toolbar1") - widget["mce_attrs"] = mce_attrs - if isinstance(form_field.widget, ScoredAnswerWidget): - field_widgets = form_field.widget.widgets - widgets = [ - { - "type": field_widgets[0].__class__.__name__, - "attrs": field_widgets[0].attrs, - "mce_attrs": field_widgets[0].mce_attrs, - }, - { - "type": field_widgets[1].__class__.__name__, - "attrs": field_widgets[1].attrs, - }, - ] - widget["widgets"] = widgets - return widget - - -def get_round_leads(): - return User.objects.filter(submission_lead__isnull=False).distinct() - - -def get_screening_statuses(): - return ScreeningStatus.objects.filter( - id__in=ApplicationSubmission.objects.all() - .values("screening_statuses__id") - .distinct("screening_statuses__id") - ) - - -def get_used_rounds(): - return Round.objects.filter(submissions__isnull=False).distinct() - - -def get_used_funds(): - # Use page to pick up on both Labs and Funds - return Page.objects.filter(applicationsubmission__isnull=False).distinct() - - -def get_category_options(): - return Option.objects.filter(category__filter_on_dashboard=True) diff --git a/hypha/apply/api/v1/views.py b/hypha/apply/api/v1/views.py index b6ae04268d..910d604f84 100644 --- a/hypha/apply/api/v1/views.py +++ b/hypha/apply/api/v1/views.py @@ -1,261 +1,34 @@ -from django.core.exceptions import PermissionDenied as DjangoPermissionDenied -from django.db.models import Prefetch -from django_filters import rest_framework as filters -from rest_framework import mixins, permissions, viewsets +from django.http import Http404 +from rest_framework import mixins, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError from rest_framework.response import Response -from rest_framework.views import APIView from rest_framework_api_key.permissions import HasAPIKey -from hypha.apply.determinations.views import DeterminationCreateOrUpdateView -from hypha.apply.funds.models import ApplicationSubmission, RoundsAndLabs -from hypha.apply.funds.reviewers.services import get_all_reviewers -from hypha.apply.funds.workflow import STATUSES -from hypha.apply.review.models import Review +from hypha.apply.funds.models import RoundsAndLabs -from .filters import SubmissionsFilter -from .mixin import SubmissionNestedMixin from .pagination import StandardResultsSetPagination -from .permissions import IsApplyStaffUser from .serializers import ( OpenRoundLabSerializer, - RoundLabDetailSerializer, RoundLabSerializer, - SubmissionActionSerializer, - SubmissionDetailSerializer, - SubmissionListSerializer, - SubmissionMetaTermsSerializer, - SubmissionSummarySerializer, - UserSerializer, ) -from .utils import ( - get_category_options, - get_round_leads, - get_screening_statuses, - get_used_funds, - get_used_rounds, -) - - -class SubmissionViewSet(viewsets.ReadOnlyModelViewSet, viewsets.GenericViewSet): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - filter_backends = (filters.DjangoFilterBackend,) - filter_class = SubmissionsFilter - pagination_class = StandardResultsSetPagination - - def get_serializer_class(self): - if self.action == "list": - return SubmissionListSerializer - return SubmissionDetailSerializer - - def get_queryset(self): - if self.action == "list": - return ( - ApplicationSubmission.objects.exclude_draft() - .current() - .with_latest_update() - ) - return ApplicationSubmission.objects.exclude_draft().prefetch_related( - Prefetch("reviews", Review.objects.submitted()), - ) - - @action(detail=True, methods=["put"]) - def set_summary(self, request, pk=None): - submission = self.get_object() - serializer = SubmissionSummarySerializer(data=request.data) - serializer.is_valid(raise_exception=True) - summary = serializer.validated_data["summary"] - submission.summary = summary - submission.save(update_fields=["summary"]) - serializer = self.get_serializer(submission) - return Response(serializer.data) - - @action(detail=True, methods=["post"]) - def meta_terms(self, request, pk=None): - submission = self.get_object() - serializer = SubmissionMetaTermsSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - meta_terms_ids = serializer.validated_data["meta_terms"] - submission.meta_terms.set(meta_terms_ids) - serializer = self.get_serializer(submission) - return Response(serializer.data) - - -class SubmissionFilters(APIView): - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - - def filter_unique_options(self, options): - unique_items = [ - dict(item) for item in {tuple(option.items()) for option in options} - ] - return list(filter(lambda x: len(x.get("label")), unique_items)) - - def format(self, filterKey, label, options): - if label == "Screenings": - options.insert(0, {"key": None, "label": "No Screening"}) - return {"filterKey": filterKey, "label": label, "options": options} - - def get(self, request, format=None): - filter_options = [ - self.format( - "fund", - "Funds", - [ - {"key": fund.get("id"), "label": fund.get("title")} - for fund in get_used_funds().values() - ], - ), - self.format( - "round", - "Rounds", - [ - {"key": round.get("id"), "label": round.get("title")} - for round in get_used_rounds().values() - ], - ), - self.format( - "status", - "Statuses", - [ - {"key": list(STATUSES.get(label)), "label": label} - for label in dict(STATUSES) - ], - ), - self.format( - "screening_statuses", - "Screenings", - self.filter_unique_options( - [ - {"key": screening.get("id"), "label": screening.get("title")} - for screening in get_screening_statuses().values() - ] - ), - ), - self.format( - "lead", - "Leads", - [ - { - "key": lead.get("id"), - "label": lead.get("full_name") or lead.get("email"), - } - for lead in get_round_leads().values() - ], - ), - self.format( - "reviewers", - "Reviewers", - self.filter_unique_options( - [ - { - "key": reviewer.get("id"), - "label": reviewer.get("full_name") or reviewer.get("email"), - } - for reviewer in get_all_reviewers().values() - ] - ), - ), - self.format( - "category_options", - "Category", - self.filter_unique_options( - [ - {"key": option.get("id"), "label": option.get("value")} - for option in get_category_options().values() - ] - ), - ), - ] - return Response(filter_options) - - -class SubmissionActionViewSet(SubmissionNestedMixin, viewsets.GenericViewSet): - serializer_class = SubmissionActionSerializer - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - - def get_object(self): - return self.get_submission_object() - - def list(self, request, *args, **kwargs): - """ - List all the actions that can be taken on a submission. - - E.g. All the states this submission can be transistion to. - """ - obj = self.get_object() - ser = self.get_serializer(obj) - return Response(ser.data) - - def create(self, request, *args, **kwargs): - """ - Transistion a submission from one state to other. - - E.g. To transition a submission from `Screening` to `Internal Review` - following post data can be used: - - ``` - {"action": "internal_review"} - ``` - """ - action = request.data.get("action") - if not action: - raise ValidationError("Action must be provided.") - obj = self.get_object() - - redirect = DeterminationCreateOrUpdateView.should_redirect(request, obj, action) - if redirect: - raise NotFound( - { - "detail": "The action should be performed at the determination view", - "target": redirect.url, - } - ) - try: - obj.perform_transition(action, self.request.user, request=self.request) - except DjangoPermissionDenied as e: - raise PermissionDenied(str(e)) from e - # refresh_from_db() raises errors for particular actions. - obj = self.get_object() - serializer = SubmissionDetailSerializer( - obj, - context={ - "request": request, - }, - ) - return Response( - { - "id": serializer.data["id"], - "status": serializer.data["status"], - "actions": serializer.data["actions"], - "phase": serializer.data["phase"], - } - ) class RoundViewSet( mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet ): + """ViewSet that can be utilized to see the open rounds & labs within Hypha + + This is a "public" REST API that still requires an API key to function. This API key + can be created in the django-admin as per the `djangorestframework-api-key` docs: + https://florimondmanca.github.io/djangorestframework-api-key/guide/#creating-and-managing-api-keys + + Requires a `Authorization: Api-Key ` HTTP Header (assuming + `API_KEY_CUSTOM_HEADER` hasn't been set) and will only work with the proper action + appended to the end of the URL. ie: `/api/v1/rounds/open` + """ + serializer_class = RoundLabSerializer - permission_classes = ( - permissions.IsAuthenticated, - IsApplyStaffUser, - ) - permission_classes_by_action = { - "open": [ - HasAPIKey | permissions.IsAuthenticated, - HasAPIKey | IsApplyStaffUser, - ], - } + pagination_class = StandardResultsSetPagination @property @@ -263,26 +36,13 @@ def queryset(): return RoundsAndLabs.objects.all() def get_serializer_class(self): - if self.action == "list": - return RoundLabSerializer - elif self.action == "open": - return OpenRoundLabSerializer - return RoundLabDetailSerializer - - def get_object(self): - obj = super(RoundViewSet, self).get_object() - return obj.specific + return OpenRoundLabSerializer def get_permissions(self): - try: - # return permission_classes depending on `action` - return [ - permission() - for permission in self.permission_classes_by_action[self.action] - ] - except KeyError: - # action is not set return default permission_classes - return [permission() for permission in self.permission_classes] + if self.action != "open": + raise Http404 + + return [HasAPIKey()] @action(methods=["get"], detail=False) def open(self, request): @@ -293,11 +53,3 @@ def open(self, request): return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - - -class CurrentUser(APIView): - permission_classes = [permissions.IsAuthenticated] - - def get(self, request, format=None): - ser = UserSerializer(request.user) - return Response(ser.data) diff --git a/hypha/apply/projects/migrations/0094_remove_invoicedeliverable_deliverable_and_more.py b/hypha/apply/projects/migrations/0094_remove_invoicedeliverable_deliverable_and_more.py new file mode 100644 index 0000000000..a34533420c --- /dev/null +++ b/hypha/apply/projects/migrations/0094_remove_invoicedeliverable_deliverable_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.17 on 2024-12-16 22:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("application_projects", "0093_remove_reportversion_form_fields"), + ] + + operations = [ + migrations.RemoveField( + model_name="invoicedeliverable", + name="deliverable", + ), + migrations.RemoveField( + model_name="invoice", + name="deliverables", + ), + migrations.DeleteModel( + name="Deliverable", + ), + migrations.DeleteModel( + name="InvoiceDeliverable", + ), + ] diff --git a/hypha/apply/projects/models/__init__.py b/hypha/apply/projects/models/__init__.py index 0fbeec492e..5a4604b235 100644 --- a/hypha/apply/projects/models/__init__.py +++ b/hypha/apply/projects/models/__init__.py @@ -1,9 +1,8 @@ -from .payment import Invoice, InvoiceDeliverable, SupportingDocument +from .payment import Invoice, SupportingDocument from .project import ( Contract, ContractDocumentCategory, ContractPacketFile, - Deliverable, DocumentCategory, PacketFile, PAFApprovals, @@ -35,6 +34,4 @@ "ReportConfig", "Invoice", "SupportingDocument", - "Deliverable", - "InvoiceDeliverable", ] diff --git a/hypha/apply/projects/models/payment.py b/hypha/apply/projects/models/payment.py index 481f5fd9ef..7be431192d 100644 --- a/hypha/apply/projects/models/payment.py +++ b/hypha/apply/projects/models/payment.py @@ -5,9 +5,7 @@ from django.conf import settings from django.core.validators import MinValueValidator from django.db import models -from django.db.models import F, Q, Sum, Value -from django.db.models.fields import FloatField -from django.db.models.fields.related import ManyToManyField +from django.db.models import Q, Sum, Value from django.db.models.functions import Coalesce from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -101,25 +99,6 @@ def unpaid_value(self): return self.filter(~Q(status=PAID)).total_value("paid_value") -class InvoiceDeliverable(models.Model): - deliverable = models.ForeignKey( - "Deliverable", on_delete=models.CASCADE, related_name="deliverables" - ) - quantity = models.IntegerField( - help_text=_("Quantity Selected on an Invoice"), default=0 - ) - - wagtail_reference_index_ignore = True - - def __str__(self): - return self.deliverable.name - - def get_absolute_api_url(self): - return reverse( - "api:v1:remove-deliverables", kwargs={"pk": self.pk, "invoice_pk": self.pk} - ) - - class Invoice(models.Model): project = models.ForeignKey( "Project", on_delete=models.CASCADE, related_name="invoices" @@ -150,7 +129,6 @@ class Invoice(models.Model): invoice_date = models.DateField(null=True, verbose_name=_("Invoice date")) paid_date = models.DateField(null=True, verbose_name=_("Paid date")) status = FSMField(default=SUBMITTED, choices=INVOICE_STATUS_CHOICES) - deliverables = ManyToManyField("InvoiceDeliverable", related_name="invoices") objects = InvoiceQueryset.as_manager() wagtail_reference_index_ignore = True @@ -242,17 +220,6 @@ def can_user_change_status(self, user): return False - def can_user_edit_deliverables(self, user): - if not (user.is_apply_staff or user.is_finance): - return False - if user.is_apply_staff: - if self.status in {SUBMITTED, RESUBMITTED, CHANGES_REQUESTED_BY_FINANCE}: - return True - if user.is_finance: - if self.status in {APPROVED_BY_STAFF}: - return True - return False - @property def value(self): return self.paid_value @@ -263,14 +230,6 @@ def get_absolute_url(self): kwargs={"pk": self.project.pk, "invoice_pk": self.pk}, ) - @property - def deliverables_total_amount(self): - return self.deliverables.all().aggregate( - total=Sum( - F("deliverable__unit_price") * F("quantity"), output_field=FloatField() - ) - ) - @property def filename(self): return os.path.basename(self.document.name) diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 2bb189bfd2..7af8779499 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -1,4 +1,3 @@ -import decimal import logging from django import forms @@ -475,22 +474,6 @@ def can_send_for_approval(self): def is_in_progress(self): return self.status == INVOICING_AND_REPORTING - @property - def has_deliverables(self): - return self.deliverables.exists() - - @property - def program_project_id(self): - """ - Program project id is used to fetch deliverables from IntAcct. - - Stored in external_project_information as the first item of referenceno(PONUMBER). - """ - reference_number = self.external_project_information.get("PONUMBER", None) - if reference_number: - return reference_number.split("-")[0] - return "" - class ProjectSOW(BaseStreamForm, AccessFormData, models.Model): project = models.OneToOneField( @@ -817,32 +800,3 @@ class Meta: ] base_form_class = ContractDocumentCategoryAdminForm - - -class Deliverable(models.Model): - external_id = models.CharField( - max_length=30, - blank=True, - help_text="ID of this deliverable at integrated payment service.", - ) - name = models.TextField() - available_to_invoice = models.IntegerField(default=1) - unit_price = models.DecimalField( - max_digits=10, - decimal_places=2, - validators=[MinValueValidator(decimal.Decimal("0.01"))], - ) - extra_information = models.JSONField( - default=dict, - help_text="More details of the deliverable at integrated payment service.", - ) - project = models.ForeignKey( - Project, - null=True, - blank=True, - on_delete=models.CASCADE, - related_name="deliverables", - ) - - def __str__(self): - return self.name diff --git a/hypha/apply/projects/services/__init__.py b/hypha/apply/projects/services/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hypha/apply/projects/services/sageintacct/__init__.py b/hypha/apply/projects/services/sageintacct/__init__.py deleted file mode 100644 index cd576e36d6..0000000000 --- a/hypha/apply/projects/services/sageintacct/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Sage Intacct init -""" - -from .exceptions import ( - ExpiredTokenError, - InternalServerError, - InvalidTokenError, - NoPrivilegeError, - NotFoundItemError, - SageIntacctSDKError, - WrongParamsError, -) -from .sageintacctsdk import SageIntacctSDK - -__all__ = [ - "SageIntacctSDK", - "SageIntacctSDKError", - "ExpiredTokenError", - "InvalidTokenError", - "NoPrivilegeError", - "WrongParamsError", - "NotFoundItemError", - "InternalServerError", -] - -name = "sageintacctsdk" diff --git a/hypha/apply/projects/services/sageintacct/exceptions.py b/hypha/apply/projects/services/sageintacct/exceptions.py deleted file mode 100644 index 7d64be8a59..0000000000 --- a/hypha/apply/projects/services/sageintacct/exceptions.py +++ /dev/null @@ -1,61 +0,0 @@ -class SageIntacctSDKError(Exception): - """The base exception class for SageIntacctSDK. - - Parameters: - msg (str): Short description of the error. - response: Error response from the API call. - """ - - def __init__(self, msg, response=None): - super(SageIntacctSDKError, self).__init__(msg) - self.message = msg - self.response = response - - def __str__(self): - return repr(self.message) - - -class SageIntacctSDKWarning(Warning): - """The base Warning class for SageIntacctSDK. - - Parameters: - msg (str): Short description of the alert. - response: Error response from the API call. - """ - - def __init__(self, msg, response=None): - super(SageIntacctSDKWarning, self).__init__(msg) - self.message = msg - self.response = response - - def __str__(self): - return repr(self.message) - - -class ExpiredTokenError(SageIntacctSDKError): - """Expired (old) access token, 498 error.""" - - -class InvalidTokenError(SageIntacctSDKError): - """Wrong/non-existing access token, 401 error.""" - - -class NoPrivilegeError(SageIntacctSDKError): - """The user has insufficient privilege, 403 error.""" - - -class WrongParamsError(SageIntacctSDKError): - """Some of the parameters (HTTP params or request body) are wrong, 400 error.""" - - -class NotFoundItemError(SageIntacctSDKError): - """Not found the item from URL, 404 error.""" - - -class InternalServerError(SageIntacctSDKError): - """The rest SageIntacctSDK errors, 500 error.""" - - -# WARNING SECTION -class DataIntegrityWarning(SageIntacctSDKWarning): - """Warns the user that a query did not return all records meeting specified criteria""" diff --git a/hypha/apply/projects/services/sageintacct/sageintacctsdk.py b/hypha/apply/projects/services/sageintacct/sageintacctsdk.py deleted file mode 100644 index bf6a8cf118..0000000000 --- a/hypha/apply/projects/services/sageintacct/sageintacctsdk.py +++ /dev/null @@ -1,71 +0,0 @@ -from .wrapper import ApiBase, Invoice, Project, Purchasing - - -class SageIntacctSDK: - """ - Sage Intacct SDK - """ - - def __init__( - self, - sender_id: str, - sender_password: str, - user_id: str, - company_id: str, - user_password: str, - entity_id: str = None, - ): - """ - Initialize connection to Sage Intacct - :param sender_id: Sage Intacct sender id - :param sender_password: Sage Intacct sener password - :param user_id: Sage Intacct user id - :param company_id: Sage Intacct company id - :param user_password: Sage Intacct user password - :param (optional) entity_id: Sage Intacct entity ID - """ - # Initializing variables - self.__sender_id = sender_id - self.__sender_password = sender_password - self.__user_id = user_id - self.__company_id = company_id - self.__user_password = user_password - self.__entity_id = entity_id - - self.api_base = ApiBase() - self.purchasing = Purchasing() - self.project = Project() - self.invoice = Invoice() - self.update_sender_id() - self.update_sender_password() - self.update_session_id() - - def update_sender_id(self): - """ - Update the sender id in all API objects. - """ - self.api_base.set_sender_id(self.__sender_id) - self.purchasing.set_sender_id(self.__sender_id) - self.project.set_sender_id(self.__sender_id) - self.invoice.set_sender_id(self.__sender_id) - - def update_sender_password(self): - """ - Update the sender password in all API objects. - """ - self.api_base.set_sender_password(self.__sender_password) - self.purchasing.set_sender_password(self.__sender_password) - self.project.set_sender_password(self.__sender_password) - self.invoice.set_sender_password(self.__sender_password) - - def update_session_id(self): - """ - Update the session id and change it in all API objects. - """ - self.__session_id = self.api_base.get_session_id( - self.__user_id, self.__company_id, self.__user_password, self.__entity_id - ) - self.api_base.set_session_id(self.__session_id) - self.purchasing.set_session_id(self.__session_id) - self.project.set_session_id(self.__session_id) - self.invoice.set_session_id(self.__session_id) diff --git a/hypha/apply/projects/services/sageintacct/utils.py b/hypha/apply/projects/services/sageintacct/utils.py deleted file mode 100644 index 15a2d85c65..0000000000 --- a/hypha/apply/projects/services/sageintacct/utils.py +++ /dev/null @@ -1,165 +0,0 @@ -import logging -from datetime import timedelta - -from django.conf import settings - -from .sageintacctsdk import SageIntacctSDK - - -def fetch_deliverables(program_project_id=""): - """ - Fetch deliverables from IntAcct using the program project id(DEPARTMENTID). - - Returns a list of deliverables or an empty list. - - Also logs any error that occurred during the API call. - """ - if not program_project_id: - return [] - formatted_filter = { - "and": { - "equalto": [ - {"field": "DOCPARID", "value": "Project Contract"}, - {"field": "DEPARTMENTID", "value": program_project_id}, - ], - "greaterthan": {"field": "QTY_REMAINING", "value": 0.0}, - } - } - - try: - connection = SageIntacctSDK( - sender_id=settings.INTACCT_SENDER_ID, - sender_password=settings.INTACCT_SENDER_PASSWORD, - user_id=settings.INTACCT_USER_ID, - company_id=settings.INTACCT_COMPANY_ID, - user_password=settings.INTACCT_USER_PASSWORD, - ) - except Exception as e: - logging.error(e) - return [] - - deliverables = connection.purchasing.get_by_query(filter_payload=formatted_filter) - return deliverables - - -def get_deliverables_json(invoice): - """ - Get a json format of deliverables attached to the invoice. - - Used when creating invoice in IntAcct. - """ - deliverables = invoice.deliverables.all() - deliverables_list = [] - for deliverable in deliverables: - project_deliverable = deliverable.deliverable - extra_info = project_deliverable.extra_information - deliverables_list.append( - { - "itemid": project_deliverable.external_id, - "quantity": deliverable.quantity, - "unit": extra_info["UNIT"], - "price": project_deliverable.unit_price, - "locationid": extra_info["LOCATIONID"], - "departmentid": extra_info["DEPARTMENTID"], - "projectid": extra_info["PROJECTID"], - "customerid": extra_info["CUSTOMERID"], - "classid": extra_info["CLASSID"], - "billable": extra_info["BILLABLE"], - } - ) - return deliverables_list - - -def create_intacct_invoice(invoice): - """ - Creates a Contract Invoice Release at IntAcct. - - Note that the order of field send in the query is important. - - API call may also fail if the order of the field is not correct. - """ - project = invoice.project - external_project_information = project.external_project_information - external_projectid = project.external_projectid - transactiontype = "Contract Invoice Release" - date_created = invoice.requested_at - createdfrom = external_project_information["DOCPARID"] + "-" + external_projectid - vendorid = external_project_information["CUSTVENDID"] - referenceno = external_project_information["PONUMBER"] - project.created_at + timedelta(days=20) - datedue = date_created + timedelta(days=20) - contract_start_date = project.proposed_start - contract_end_date = project.proposed_end - deliverables = get_deliverables_json(invoice) - vendordocno = invoice.vendor_document_number - data = { - "transactiontype": transactiontype, - "datecreated": { - "year": date_created.year, - "month": date_created.month, - "day": date_created.day, - }, - "createdfrom": createdfrom, - "vendorid": vendorid, - "referenceno": referenceno, - "vendordocno": vendordocno, - "datedue": { - "year": datedue.year, - "month": datedue.month, - "day": datedue.day, - }, - "returnto": {"contactname": ""}, - "payto": {"contactname": ""}, - "customfields": { - "customfield": [ - { - "customfieldname": "CONTRACT_START_DATE", - "customfieldvalue": f"{contract_start_date.month}/{contract_start_date.day}/{contract_start_date.year}", - }, - { - "customfieldname": "CONTRACT_END_DATE", - "customfieldvalue": f"{contract_end_date.month}/{contract_end_date.day}/{contract_end_date.year}", - }, - ] - }, - "potransitems": {"potransitem": deliverables}, - } - try: - connection = SageIntacctSDK( - sender_id=settings.INTACCT_SENDER_ID, - sender_password=settings.INTACCT_SENDER_PASSWORD, - user_id=settings.INTACCT_USER_ID, - company_id=settings.INTACCT_COMPANY_ID, - user_password=settings.INTACCT_USER_PASSWORD, - ) - except Exception as e: - logging.error(e) - return - invoice = connection.invoice.post(data) - return invoice - - -def fetch_project_details(external_projectid): - """ - Fetch detail of a project contract from IntAcct. - - These details will be further used to fetch deliverables and create invoices. - """ - formatted_filter = {"equalto": {"field": "DOCNO", "value": external_projectid}} - - try: - connection = SageIntacctSDK( - sender_id=settings.INTACCT_SENDER_ID, - sender_password=settings.INTACCT_SENDER_PASSWORD, - user_id=settings.INTACCT_USER_ID, - company_id=settings.INTACCT_COMPANY_ID, - user_password=settings.INTACCT_USER_PASSWORD, - ) - except Exception as e: - logging.error(e) - return {} - - data = connection.project.get_by_query(filter_payload=formatted_filter) - if data: - return data[0] - return {} diff --git a/hypha/apply/projects/services/sageintacct/wrapper/__init__.py b/hypha/apply/projects/services/sageintacct/wrapper/__init__.py deleted file mode 100644 index 4af59e24f8..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .api_base import ApiBase -from .invoice import Invoice -from .project import Project -from .purchasing import Purchasing - -__all__ = [ - "ApiBase", - "Purchasing", - "Invoice", - "Project", -] diff --git a/hypha/apply/projects/services/sageintacct/wrapper/api_base.py b/hypha/apply/projects/services/sageintacct/wrapper/api_base.py deleted file mode 100644 index d0e8867a70..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/api_base.py +++ /dev/null @@ -1,495 +0,0 @@ -import datetime -import json -import re -import uuid -from typing import Dict, List, Tuple -from urllib.parse import unquote -from warnings import warn - -import requests -import xmltodict - -from ..exceptions import ( - DataIntegrityWarning, - ExpiredTokenError, - InternalServerError, - InvalidTokenError, - NoPrivilegeError, - NotFoundItemError, - SageIntacctSDKError, - WrongParamsError, -) -from .constants import dimensions_fields_mapping - - -class ApiBase: - """The base class for all API classes.""" - - def __init__( - self, - dimension: str = None, - pagesize: int = 2000, - post_legacy_method: str = None, - ): - self.__sender_id = None - self.__sender_password = None - self.__session_id = None - self.__api_url = "https://api.intacct.com/ia/xml/xmlgw.phtml" - self.__dimension = dimension - self.__pagesize = pagesize - self.__post_legacy_method = post_legacy_method - - def set_sender_id(self, sender_id: str): - """ - Set the sender id for APIs - :param sender_id: sender id - :return: None - """ - self.__sender_id = sender_id - - def set_sender_password(self, sender_password: str): - """ - Set the sender password for APIs - :param sender_password: sender id - :return: None - """ - self.__sender_password = sender_password - - def get_session_id( - self, user_id: str, company_id: str, user_password: str, entity_id: str = None - ): - """ - Sets the session id for APIs - :param access_token: acceess token (JWT) - :return: session id - """ - - timestamp = datetime.datetime.now() - dict_body = { - "request": { - "control": { - "senderid": self.__sender_id, - "password": self.__sender_password, - "controlid": timestamp, - "uniqueid": False, - "dtdversion": 3.0, - "includewhitespace": False, - }, - "operation": { - "authentication": { - "login": { - "userid": user_id, - "companyid": company_id, - "password": user_password, - "locationid": entity_id, - } - }, - "content": { - "function": { - "@controlid": str(uuid.uuid4()), - "getAPISession": None, - } - }, - }, - } - } - - response = self.__post_request(dict_body, self.__api_url) - - if response["authentication"]["status"] == "success": - session_details = response["result"]["data"]["api"] - self.__api_url = session_details["endpoint"] - self.__session_id = session_details["sessionid"] - - return self.__session_id - - else: - raise SageIntacctSDKError("Error: {0}".format(response["errormessage"])) - - def set_session_id(self, session_id: str): - """ - Set the session id for APIs - :param session_id: session id - :return: None - """ - self.__session_id = session_id - - def __support_id_msg(self, errormessages): - """Finds whether the error messages is list / dict and assign type and error assignment. - - Parameters: - errormessages (dict / list): error message received from Sage Intacct. - - Returns: - Error message assignment and type. - """ - error = {} - if isinstance(errormessages["error"], list): - error["error"] = errormessages["error"][0] - error["type"] = "list" - elif isinstance(errormessages["error"], dict): - error["error"] = errormessages["error"] - error["type"] = "dict" - - return error - - def __decode_support_id(self, errormessages): - """Decodes Support ID. - - Parameters: - errormessages (dict / list): error message received from Sage Intacct. - - Returns: - Same error message with decoded Support ID. - """ - support_id_msg = self.__support_id_msg(errormessages) - data_type = support_id_msg["type"] - error = support_id_msg["error"] - if error and error["description2"]: - message = error["description2"] - support_id = re.search("Support ID: (.*)]", message) - if support_id.group(1): - decoded_support_id = unquote(support_id.group(1)) - message = message.replace(support_id.group(1), decoded_support_id) - - # Converting dict to list even for single error response - if data_type == "dict": - errormessages["error"] = [errormessages["error"]] - - errormessages["error"][0]["description2"] = message if message else None - - return errormessages - - def __post_request(self, dict_body: dict, api_url: str): - """Create a HTTP post request. - - Parameters: - data (dict): HTTP POST body data for the wanted API. - api_url (str): Url for the wanted API. - - Returns: - A response from the request (dict). - """ - - api_headers = {"content-type": "application/xml"} - body = xmltodict.unparse(dict_body) - - response = requests.post(api_url, headers=api_headers, data=body) - - parsed_xml = xmltodict.parse(response.text, force_list={self.__dimension}) - parsed_response = json.loads(json.dumps(parsed_xml)) - - if response.status_code == 200: - if parsed_response["response"]["control"]["status"] == "success": - api_response = parsed_response["response"]["operation"] - - if parsed_response["response"]["control"]["status"] == "failure": - exception_msg = self.__decode_support_id( - parsed_response["response"]["errormessage"] - ) - raise WrongParamsError( - "Some of the parameters are wrong", exception_msg - ) - - if api_response["authentication"]["status"] == "failure": - raise InvalidTokenError( - "Invalid token / Incorrect credentials", - api_response["errormessage"], - ) - - if api_response["result"]["status"] == "success": - return api_response - - if api_response["result"]["status"] == "failure": - exception_msg = self.__decode_support_id( - api_response["result"]["errormessage"] - ) - - for error in exception_msg["error"]: - if ( - error["description2"] - and "You do not have permission for API" - in error["description2"] - ): - raise InvalidTokenError( - "The user has insufficient privilege", exception_msg - ) - - raise WrongParamsError( - "Error during {0}".format(api_response["result"]["function"]), - exception_msg, - ) - - if response.status_code == 400: - raise WrongParamsError("Some of the parameters are wrong", parsed_response) - - if response.status_code == 401: - raise InvalidTokenError( - "Invalid token / Incorrect credentials", parsed_response - ) - - if response.status_code == 403: - raise NoPrivilegeError( - "Forbidden, the user has insufficient privilege", parsed_response - ) - - if response.status_code == 404: - raise NotFoundItemError("Not found item with ID", parsed_response) - - if response.status_code == 498: - raise ExpiredTokenError("Expired token, try to refresh it", parsed_response) - - if response.status_code == 500: - raise InternalServerError("Internal server error", parsed_response) - - raise SageIntacctSDKError("Error: {0}".format(parsed_response)) - - def format_and_send_request(self, data: Dict): - """Format data accordingly to convert them to xml. - - Parameters: - data (dict): HTTP POST body data for the wanted API. - - Returns: - A response from the __post_request (dict). - """ - - key = next(iter(data)) - timestamp = datetime.datetime.now() - - dict_body = { - "request": { - "control": { - "senderid": self.__sender_id, - "password": self.__sender_password, - "controlid": timestamp, - "uniqueid": False, - "dtdversion": 3.0, - "includewhitespace": False, - }, - "operation": { - "authentication": {"sessionid": self.__session_id}, - "content": { - "function": {"@controlid": str(uuid.uuid4()), key: data[key]} - }, - }, - } - } - - response = self.__post_request(dict_body, self.__api_url) - return response["result"] - - def post(self, data: Dict): - if self.__dimension in ("CCTRANSACTION", "EPPAYMENT"): - return self.__construct_post_legacy_payload(data) - - return self.__construct_post_payload(data) - - def __construct_post_payload(self, data: Dict): - payload = {"create": {self.__dimension: data}} - - return self.format_and_send_request(payload) - - def __construct_post_legacy_payload(self, data: Dict): - payload = {self.__post_legacy_method: data} - - return self.format_and_send_request(payload) - - def count(self): - get_count = { - "query": { - "object": self.__dimension, - "select": {"field": "RECORDNO"}, - "pagesize": "1", - } - } - - response = self.format_and_send_request(get_count) - return int(response["data"]["@totalcount"]) - - def read_by_query(self, fields: list = None): - """Read by Query from Sage Intacct - - Parameters: - fields (list): Get selective fields to be returned. (optional). - - Returns: - Dict. - """ - payload = { - "readByQuery": { - "object": self.__dimension, - "fields": ",".join(fields) if fields else "*", - "query": None, - "pagesize": "1000", - } - } - - return self.format_and_send_request(payload) - - def get(self, field: str, value: str, fields: list = None): - """Get data from Sage Intacct based on filter. - - Parameters: - field (str): A parameter to filter by the field. (required). - value (str): A parameter to filter by the field - value. (required). - - Returns: - Dict. - """ - data = { - "readByQuery": { - "object": self.__dimension, - "fields": ",".join(fields) if fields else "*", - "query": "{0} = '{1}'".format(field, value), - "pagesize": "1000", - } - } - - return self.format_and_send_request(data)["data"] - - def get_all(self, field: str = None, value: str = None, fields: list = None): - """Get all data from Sage Intacct - - Returns: - List of Dict. - """ - complete_data = [] - count = self.count() - pagesize = self.__pagesize - for offset in range(0, count, pagesize): - data = { - "query": { - "object": self.__dimension, - "select": { - "field": fields - if fields - else dimensions_fields_mapping[self.__dimension] - }, - "pagesize": pagesize, - "offset": offset, - } - } - - if field and value: - data["query"]["filter"] = {"equalto": {"field": field, "value": value}} - - paginated_data = self.format_and_send_request(data)["data"][ - self.__dimension - ] - complete_data.extend(paginated_data) - - return complete_data - - __query_filter = List[Tuple[str, str, str]] - - def get_by_query( - self, - fields: List[str] = None, - and_filter: __query_filter = None, - or_filter: __query_filter = None, - filter_payload: dict = None, - ): - """Get data from Sage Intacct using query method based on filter. - - See sage intacct documentation here for query structures: - https://developer.intacct.com/web-services/queries/ - - Parameters: - fields (str): A parameter to filter by the field. (required). - and_filter (list(tuple)): List of tuple containing (operator (str),field (str), value (str)) - or_filter (list(tuple)): List of tuple containing (operator (str),field (str), value (str)) - filter_payload (dict): Formatted query payload in dictionary format. - if 'between' operators is used on and_filter or or_filter field must be submitted as - [str,str] - if 'in' operator is used field may be submitted as [str,str,str,...] - - Returns: - Dict. - """ - - complete_data = [] - count = self.count() - pagesize = self.__pagesize - offset = 0 - formatted_filter = filter_payload - data = { - "query": { - "object": self.__dimension, - "select": { - "field": fields - if fields - else dimensions_fields_mapping[self.__dimension] - }, - "pagesize": pagesize, - "offset": offset, - } - } - if and_filter and or_filter: - formatted_filter = {"and": {}} - for operator, field, value in and_filter: - formatted_filter["and"].setdefault(operator, {}).update( - {"field": field, "value": value} - ) - formatted_filter["and"]["or"] = {} - for operator, field, value in or_filter: - formatted_filter["and"]["or"].setdefault(operator, {}).update( - {"field": field, "value": value} - ) - - elif and_filter: - if len(and_filter) > 1: - formatted_filter = {"and": {}} - for operator, field, value in and_filter: - formatted_filter["and"].setdefault(operator, {}).update( - {"field": field, "value": value} - ) - else: - formatted_filter = {} - for operator, field, value in and_filter: - formatted_filter.setdefault(operator, {}).update( - {"field": field, "value": value} - ) - elif or_filter: - if len(or_filter) > 1: - formatted_filter = {"or": {}} - for operator, field, value in or_filter: - formatted_filter["or"].setdefault(operator, {}).update( - {"field": field, "value": value} - ) - else: - formatted_filter = {} - for operator, field, value in or_filter: - formatted_filter.setdefault(operator, {}).update( - {"field": field, "value": value} - ) - - if formatted_filter: - data["query"]["filter"] = formatted_filter - - for offset in range(0, count, pagesize): - data["query"]["offset"] = offset - paginated_data = self.format_and_send_request(data)["data"] - complete_data.extend(paginated_data[self.__dimension]) - filtered_total = int(paginated_data["@totalcount"]) - if paginated_data["@numremaining"] == "0": - break - if filtered_total != len(complete_data): - warn( - message="Your data may not be complete. Records returned do not equal total query record count", - category=DataIntegrityWarning, - stacklevel=2, - ) - return complete_data - - def get_lookup(self): - """Returns all fields with attributes from the object called on. - - Parameters: - self - Returns: - Dict. - """ - - data = {"lookup": {"object": self.__dimension}} - return self.format_and_send_request(data)["data"] diff --git a/hypha/apply/projects/services/sageintacct/wrapper/constants.py b/hypha/apply/projects/services/sageintacct/wrapper/constants.py deleted file mode 100644 index 8df5562610..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/constants.py +++ /dev/null @@ -1,17 +0,0 @@ -dimensions_fields_mapping = { - "PODOCUMENTENTRY": [ - "ITEMID", - "ITEMNAME", - "ITEMDESC", - "QTY_REMAINING", - "UNIT", - "PRICE", - "PROJECTID", - "LOCATIONID", - "CLASSID", - "BILLABLE", - "DEPARTMENTID", - "CUSTOMERID", - ], - "PODOCUMENT": ["DOCNO", "DOCPARID", "PONUMBER", "CUSTVENDID"], -} diff --git a/hypha/apply/projects/services/sageintacct/wrapper/invoice.py b/hypha/apply/projects/services/sageintacct/wrapper/invoice.py deleted file mode 100644 index 7fba7db6bd..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/invoice.py +++ /dev/null @@ -1,9 +0,0 @@ -from .api_base import ApiBase - - -class Invoice(ApiBase): - """Class to create Contract Invoice Release at Sage IntAcct.""" - - def post(self, data: dict): - data = {"create_potransaction": data} - return self.format_and_send_request(data) diff --git a/hypha/apply/projects/services/sageintacct/wrapper/project.py b/hypha/apply/projects/services/sageintacct/wrapper/project.py deleted file mode 100644 index 99ed9f7eb1..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/project.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Sage Intacct contract -""" - -from .api_base import ApiBase - - -class Project(ApiBase): - """Class for contract APIs.""" - - def __init__(self): - ApiBase.__init__(self, dimension="PODOCUMENT") diff --git a/hypha/apply/projects/services/sageintacct/wrapper/purchasing.py b/hypha/apply/projects/services/sageintacct/wrapper/purchasing.py deleted file mode 100644 index 8181a8ee9f..0000000000 --- a/hypha/apply/projects/services/sageintacct/wrapper/purchasing.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Sage Intacct purchasing -""" - -from .api_base import ApiBase - - -class Purchasing(ApiBase): - """Class for Purchasing APIs.""" - - def __init__(self): - ApiBase.__init__(self, dimension="PODOCUMENTENTRY") diff --git a/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html b/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html deleted file mode 100644 index 9fd5599342..0000000000 --- a/hypha/apply/projects/templates/application_projects/includes/deliverables_block.html +++ /dev/null @@ -1,45 +0,0 @@ -{% load i18n invoice_tools apply_tags %} - diff --git a/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html b/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html deleted file mode 100644 index 521956e33b..0000000000 --- a/hypha/apply/projects/templates/application_projects/invoice_admin_detail.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "application_projects/invoice_detail.html" %} -{% load i18n static invoice_tools %} - -{% block deliverables %} - {% if object.project.has_deliverables %} - {% include 'application_projects/includes/deliverables_block.html' with deliverables=deliverables invoice=object project=object.project %} - {% endif %} -{% endblock %} - -{% block extra_js %} - {{ block.super }} - -{% endblock %} diff --git a/hypha/apply/projects/templates/application_projects/invoice_detail.html b/hypha/apply/projects/templates/application_projects/invoice_detail.html index 420bdb8c1a..97f0f3dd3b 100644 --- a/hypha/apply/projects/templates/application_projects/invoice_detail.html +++ b/hypha/apply/projects/templates/application_projects/invoice_detail.html @@ -53,8 +53,6 @@
{% trans "Supporting Documents" %}