Skip to content
Snippets Groups Projects
models.py 30 KiB
Newer Older
  • Learn to ignore specific revisions
  • from datetime import date
    
    from django.conf import settings
    
    from django.contrib.auth import get_user_model
    
    from django.contrib.postgres.fields import JSONField
    
    from django.core.exceptions import ValidationError, ObjectDoesNotExist, PermissionDenied
    
    from django.core.files.storage import default_storage
    
    from django.core.serializers.json import DjangoJSONEncoder
    
    from django.db import models
    
    from django.db.models import Q
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    from django.db.models.expressions import RawSQL, OrderBy
    
    from django.http import Http404
    
    from django.template.loader import render_to_string
    
    from django.urls import reverse
    
    from django.utils.text import mark_safe, slugify
    
    from django.utils.translation import ugettext_lazy as _
    
    from django_fsm import can_proceed, FSMField, transition, RETURN_VALUE
    
    from modelcluster.fields import ParentalKey, ParentalManyToManyField
    
    from wagtail.admin.edit_handlers import (
    
        FieldPanel,
    
    Dan Braghis's avatar
    Dan Braghis committed
        InlinePanel,
    
        MultiFieldPanel,
    
    Dan Braghis's avatar
    Dan Braghis committed
        ObjectList,
    
        StreamFieldPanel,
    
    Dan Braghis's avatar
    Dan Braghis committed
        TabbedInterface,
    
    from wagtail.admin.utils import send_mail
    from wagtail.core.fields import StreamField
    from wagtail.core.models import Orderable
    from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormSubmission
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    
    
    from opentech.apply.stream_forms.blocks import UploadableMediaBlock
    
    from opentech.apply.stream_forms.models import AbstractStreamForm, BaseStreamForm
    
    from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME
    
    from .admin_forms import WorkflowFormAdminForm
    
    from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES
    
    from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
    
    from .workflow import (
        active_statuses,
        get_review_statuses,
        INITIAL_STATE,
        review_statuses,
        UserPermissions,
        WORKFLOWS,
    
    Dan Braghis's avatar
    Dan Braghis committed
        DETERMINATION_PHASES,
    )
    
    LIMIT_TO_STAFF = {'groups__name': STAFF_GROUP_NAME}
    LIMIT_TO_REVIEWERS = {'groups__name': REVIEWER_GROUP_NAME}
    LIMIT_TO_STAFF_AND_REVIEWERS = {'groups__name__in': [STAFF_GROUP_NAME, REVIEWER_GROUP_NAME]}
    
    
    
    def admin_url(page):
        return reverse('wagtailadmin_pages:edit', args=(page.id,))
    
    
    
    class SubmittableStreamForm(AbstractStreamForm):
    
        """
        Controls how stream forms are submitted. Any Page allowing submissions should inherit from here.
        """
    
        class Meta:
            abstract = True
    
        def get_submission_class(self):
            return ApplicationSubmission
    
        def process_form_submission(self, form):
    
            if not form.user.is_authenticated:
    
                form.user = None
    
            return self.get_submission_class().objects.create(
    
                form_data=form.cleaned_data,
    
                form_fields=self.get_defined_fields(),
    
                **self.get_submit_meta_data(user=form.user),
    
            )
    
        def get_submit_meta_data(self, **kwargs):
            return kwargs
    
    
    
    class WorkflowHelpers(models.Model):
    
        Defines the common methods and fields for working with Workflows within Django models
    
        class Meta:
            abstract = True
    
        WORKFLOW_CHOICES = {
            name: workflow.name
            for name, workflow in WORKFLOWS.items()
    
        workflow_name = models.CharField(choices=WORKFLOW_CHOICES.items(), max_length=100, default='single', verbose_name="Workflow")
    
            return WORKFLOWS[self.workflow_name]
    
    class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm):  # type: ignore
    
        """
        Defines the common methods and fields for working with Workflows within Wagtail pages
        """
        class Meta:
            abstract = True
    
    
        def get_defined_fields(self, stage=None):
            if not stage:
                form_index = 0
            else:
    
                form_index = self.workflow.stages.index(stage)
    
            return self.forms.all()[form_index].fields
    
        content_panels = AbstractStreamForm.content_panels + [
    
            InlinePanel('forms', label="Forms"),
        ]
    
    
    class EmailForm(AbstractEmailForm):
        """
        Defines the behaviour for pages that hold information about emailing applicants
    
        Email Confirmation Panel should be included to allow admins to make changes.
        """
        class Meta:
            abstract = True
    
        confirmation_text_extra = models.TextField(blank=True, help_text="Additional text for the application confirmation message.")
    
    
        def process_form_submission(self, form):
            submission = super().process_form_submission(form)
    
            self.send_mail(submission)
    
        def send_mail(self, submission):
            user = submission.user
    
            context = {
    
                'name': user.get_full_name(),
                'email': user.email,
                'project_name': submission.form_data.get('title'),
    
                'extra_text': self.confirmation_text_extra,
                'fund_type': self.title,
            }
    
    
            subject = self.subject if self.subject else 'Thank you for your submission to Open Technology Fund'
    
            send_mail(subject, render_to_string('funds/email/confirmation.txt', context), (user.email,), self.from_address, )
    
    Dan Braghis's avatar
    Dan Braghis committed
        email_confirmation_panels = [
    
    Dan Braghis's avatar
    Dan Braghis committed
            MultiFieldPanel(
                [
                    FieldRowPanel([
                        FieldPanel('from_address', classname="col6"),
                        FieldPanel('to_address', classname="col6"),
                    ]),
                    FieldPanel('subject'),
                    FieldPanel('confirmation_text_extra'),
                ],
                heading="Confirmation email",
    
        email_tab = ObjectList(email_confirmation_panels, heading='Confirmation email')
    
    class FundType(EmailForm, WorkflowStreamForm):  # type: ignore
    
        # Adds validation around forms & workflows. Isn't on Workflow class due to not displaying workflow field on Round
        base_form_class = WorkflowFormAdminForm
    
    
        reviewers = ParentalManyToManyField(
    
            settings.AUTH_USER_MODEL,
            related_name='fund_reviewers',
            limit_choices_to=LIMIT_TO_REVIEWERS,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            blank=True,
    
        parent_page_types = ['apply_home.ApplyHomePage']
        subpage_types = ['funds.Round']
    
    
        def detail(self):
            # The location to find out more information
            return self.fund_public.first()
    
    
            rounds = Round.objects.child_of(self).live().public().specific()
    
            return rounds.filter(
                Q(start_date__lte=date.today()) &
                Q(Q(end_date__isnull=True) | Q(end_date__gte=date.today()))
            ).first()
    
        def next_deadline(self):
    
            try:
                return self.open_round.end_date
            except AttributeError:
                # There isn't an open round
                return None
    
            if hasattr(request, 'is_preview') or not self.open_round:
    
                return super().serve(request)
    
            # delegate to the open_round to use the latest form instances
    
            return self.open_round.serve(request)
    
    
        content_panels = WorkflowStreamForm.content_panels + [
            FieldPanel('reviewers'),
        ]
    
    
            ObjectList(content_panels, heading='Content'),
    
            EmailForm.email_tab,
            ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
        ])
    
    
    class AbstractRelatedForm(Orderable):
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        form = models.ForeignKey('ApplicationForm', on_delete=models.PROTECT)
    
        panels = [
            FilteredFieldPanel('form', filter_query={'roundform__isnull': True})
        ]
    
    
        @property
        def fields(self):
            return self.form.form_fields
    
    
        class Meta(Orderable.Meta):
            abstract = True
    
        def __eq__(self, other):
            try:
                return self.fields == other.fields
            except AttributeError:
                return False
    
    
        def __str__(self):
            return self.form.name
    
    
    
    class FundForm(AbstractRelatedForm):
        fund = ParentalKey('FundType', related_name='forms')
    
    
    class RoundForm(AbstractRelatedForm):
        round = ParentalKey('Round', related_name='forms')
    
    
    
    class ApplicationForm(models.Model):
        name = models.CharField(max_length=255)
        form_fields = StreamField(CustomFormFieldsBlock())
    
        panels = [
            FieldPanel('name'),
            StreamFieldPanel('form_fields'),
        ]
    
        def __str__(self):
            return self.name
    
    class Round(WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
    
        parent_page_types = ['funds.FundType']
        subpage_types = []  # type: ignore
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        lead = models.ForeignKey(
            settings.AUTH_USER_MODEL,
    
            limit_choices_to=LIMIT_TO_STAFF,
            related_name='round_lead',
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            on_delete=models.PROTECT,
        )
    
        reviewers = ParentalManyToManyField(
    
            settings.AUTH_USER_MODEL,
            related_name='rounds_reviewer',
            limit_choices_to=LIMIT_TO_REVIEWERS,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            blank=True,
    
        start_date = models.DateField(default=date.today)
        end_date = models.DateField(
            blank=True,
            null=True,
            default=date.today,
    
            help_text='When no end date is provided the round will remain open indefinitely.'
    
        content_panels = SubmittableStreamForm.content_panels + [
    
            MultiFieldPanel([
                FieldRowPanel([
                    FieldPanel('start_date'),
                    FieldPanel('end_date'),
                ]),
    
            FieldPanel('reviewers'),
    
            ReadOnlyPanel('get_workflow_name_display', heading="Workflow"),
    
            ReadOnlyInlinePanel('forms', help_text="Are copied from the parent fund."),
    
        edit_handler = TabbedInterface([
            ObjectList(content_panels, heading='Content'),
            ObjectList(SubmittableStreamForm.promote_panels, heading='Promote'),
        ])
    
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            # We attached the parent page as part of the before_create_hook
            if hasattr(self, 'parent_page'):
    
                self.workflow_name = self.parent_page.workflow_name
    
                self.reviewers = self.parent_page.reviewers.all()
    
        def save(self, *args, **kwargs):
            is_new = not self.id
            if is_new and hasattr(self, 'parent_page'):
    
                # Ensure that the workflow hasn't changed
    
                self.workflow_name = self.parent_page.workflow_name
    
            super().save(*args, **kwargs)
    
    
            if is_new and hasattr(self, 'parent_page'):
    
                # Would be nice to do this using model clusters as part of the __init__
    
                for form in self.parent_page.forms.all():
                    # Create a copy of the existing form object
                    new_form = form.form
                    new_form.id = None
    
                    new_form.name = '{} for {} ({})'.format(new_form.name, self.title, self.get_parent().title)
    
                    new_form.save()
                    RoundForm.objects.create(round=self, form=new_form)
    
    
        def get_submit_meta_data(self, **kwargs):
            return super().get_submit_meta_data(
    
                page=self.get_parent(),
                round=self,
    
        def process_form_submission(self, form):
            submission = super().process_form_submission(form)
    
            self.get_parent().specific.send_mail(submission)
    
        def clean(self):
            super().clean()
    
    
            if self.end_date and self.start_date > self.end_date:
    
                raise ValidationError({
                    'end_date': 'End date must come after the start date',
                })
    
            if self.end_date:
                conflict_query = (
                    Q(start_date__range=[self.start_date, self.end_date]) |
                    Q(end_date__range=[self.start_date, self.end_date]) |
                    Q(start_date__lte=self.start_date, end_date__gte=self.end_date)
                )
            else:
                conflict_query = (
                    Q(start_date__lte=self.start_date, end_date__isnull=True) |
                    Q(end_date__gte=self.start_date)
                )
    
    
            if hasattr(self, 'parent_page'):
                # Check if the create hook has added the parent page, we aren't an object yet.
                # Ensures we can access related objects during the clean phase instead of save.
                base_query = Round.objects.child_of(self.parent_page)
            else:
                # don't need parent page, we are an actual object now.
                base_query = Round.objects.sibling_of(self)
    
            conflicting_rounds = base_query.filter(
    
                conflict_query
    
            ).exclude(id=self.id)
    
            if conflicting_rounds.exists():
                error_message = mark_safe('Overlaps with the following rounds:<br> {}'.format(
    
                    '<br>'.join([
                        f'<a href="{admin_url(round)}">{round.title}</a>: {round.start_date} - {round.end_date}'
                        for round in conflicting_rounds]
                    )
    
                    'start_date': error_message,
    
                }
                if self.end_date:
                    error['end_date'] = error_message
    
                raise ValidationError(error)
    
            if hasattr(request, 'is_preview') or hasattr(request, 'show_round'):
    
                return super().serve(request)
    
            # We hide the round as only the open round is used which is displayed through the
            # fund page
            raise Http404()
    
    class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm):  # type: ignore
    
        lead = models.ForeignKey(
            settings.AUTH_USER_MODEL,
    
            limit_choices_to=LIMIT_TO_STAFF,
    
            related_name='lab_lead',
            on_delete=models.PROTECT,
        )
    
        reviewers = ParentalManyToManyField(
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            settings.AUTH_USER_MODEL,
            related_name='labs_reviewer',
            limit_choices_to=LIMIT_TO_REVIEWERS,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            blank=True,
    
        parent_page_types = ['apply_home.ApplyHomePage']
    
        subpage_types = []  # type: ignore
    
        content_panels = WorkflowStreamForm.content_panels + [
    
            FieldPanel('lead'),
            FieldPanel('reviewers'),
    
            ObjectList(content_panels, heading='Content'),
    
            ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
    
        def detail(self):
            # The location to find out more information
            return self.lab_public.first()
    
    
        def get_submit_meta_data(self, **kwargs):
            return super().get_submit_meta_data(
                page=self,
                round=None,
                **kwargs,
            )
    
        def open_round(self):
            return self.live
    
    
    
    class LabForm(AbstractRelatedForm):
    
        lab = ParentalKey('LabType', related_name='forms')
    
    
    
    class JSONOrderable(models.QuerySet):
    
        def order_by(self, *field_names):
    
            if not self.json_field:
                raise ValueError(
                    'json_field cannot be blank, please provide a field on which to perform the ordering'
                )
    
    
            def build_json_order_by(field):
    
    Dan Braghis's avatar
    Dan Braghis committed
                try:
                    if field.replace('-', '') not in REQUIRED_BLOCK_NAMES:
                        return field
                except AttributeError:
    
                    return field
    
                if field[0] == '-':
                    descending = True
                    field = field[1:]
                else:
                    descending = False
    
                return OrderBy(RawSQL(f'LOWER({self.json_field}->>%s)', (field,)), descending=descending)
    
    
            field_ordering = [build_json_order_by(field) for field in field_names]
            return super().order_by(*field_ordering)
    
    
    
    class ApplicationSubmissionQueryset(JSONOrderable):
        json_field = 'form_data'
    
        def active(self):
            return self.filter(status__in=active_statuses)
    
    
        def inactive(self):
            return self.exclude(status__in=active_statuses)
    
    
        def in_review(self):
            return self.filter(status__in=review_statuses)
    
    
        def in_review_for(self, user):
            user_review_statuses = get_review_statuses(user)
            return self.filter(status__in=user_review_statuses).filter(reviewers=user).exclude(reviews__author=user)
    
    
        def current(self):
            # Applications which have the current stage active (have not been progressed)
            return self.exclude(next__isnull=False)
    
    
    def make_permission_check(users):
        def can_transition(instance, user):
            if UserPermissions.STAFF in users and user.is_apply_staff:
                return True
            if UserPermissions.ADMIN in users and user.is_superuser:
                return True
            if UserPermissions.LEAD in users and instance.lead == user:
                return True
            if UserPermissions.APPLICANT in users and instance.user == user:
                return True
            return False
    
        return can_transition
    
    
    
    class AddTransitions(models.base.ModelBase):
        def __new__(cls, name, bases, attrs, **kwargs):
    
            transition_prefix = 'transition'
    
            for workflow in WORKFLOWS.values():
    
                for phase, data in workflow.items():
    
                    for transition_name, action in data.transitions.items():
                        method_name = '_'.join([transition_prefix, transition_name, str(data.step), data.stage.name])
                        permission_name = method_name + '_permission'
                        permission_func = make_permission_check(action['permissions'])
    
    
                        # Get the method defined on the parent or default to a NOOP
    
                        transition_state = attrs.get(action.get('method'), lambda *args, **kwargs: None)
    
                        # Provide a neat name for graph viz display
    
                        transition_state.__name__ = slugify(action['display'])
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
                        conditions = [attrs[condition] for condition in action.get('conditions', [])]
    
                        # Wrap with transition decorator
    
                        transition_func = transition(
                            attrs['status'],
                            source=phase,
                            target=transition_name,
                            permission=permission_func,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
                            conditions=conditions,
    
                        attrs[method_name] = transition_func
    
                        attrs[permission_name] = permission_func
    
            def get_transition(self, transition):
    
                try:
                    return getattr(self, '_'.join([transition_prefix, transition, str(self.phase.step), self.stage.name]))
                except TypeError:
                    # Defined on the class
                    return None
                except AttributeError:
                    # For the other workflow
                    return None
    
    
            attrs['get_transition'] = get_transition
    
    
            def get_actions_for_user(self, user):
                transitions = self.get_available_user_status_transitions(user)
                actions = [
                    (transition.target, self.phase.transitions[transition.target]['display'])
                    for transition in transitions if self.get_transition(transition.target)
                ]
                yield from actions
    
            attrs['get_actions_for_user'] = get_actions_for_user
    
    
            def perform_transition(self, action, user):
                transition = self.get_transition(action)
                if not can_proceed(transition):
                    action = self.phase.transitions[action]
                    raise PermissionDenied(f'You do not have permission to "{ action }"')
    
                transition(by=user)
                self.save()
    
            attrs['perform_transition'] = perform_transition
    
    
            return super().__new__(cls, name, bases, attrs, **kwargs)
    
    class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmission, metaclass=AddTransitions):
    
        field_template = 'funds/includes/submission_field.html'
    
    
        form_data = JSONField(encoder=DjangoJSONEncoder)
    
        form_fields = StreamField(CustomFormFieldsBlock())
    
        page = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT)
        round = models.ForeignKey('wagtailcore.Page', on_delete=models.PROTECT, related_name='submissions', null=True)
    
        lead = models.ForeignKey(
            settings.AUTH_USER_MODEL,
    
            limit_choices_to=LIMIT_TO_STAFF,
    
            related_name='submission_lead',
            on_delete=models.PROTECT,
        )
    
        next = models.OneToOneField('self', on_delete=models.CASCADE, related_name='previous', null=True)
    
        reviewers = models.ManyToManyField(
            settings.AUTH_USER_MODEL,
            related_name='submissions_reviewer',
            limit_choices_to=LIMIT_TO_STAFF_AND_REVIEWERS,
    
    Todd Dembrey's avatar
    Todd Dembrey committed
            blank=True,
    
        user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    
        search_data = models.TextField()
    
        # Workflow inherited from WorkflowHelpers
    
        status = FSMField(default=INITIAL_STATE, protected=True)
    
        is_draft = False
    
        live_revision = models.OneToOneField(
            'ApplicationRevision',
            on_delete=models.PROTECT,
            related_name='live',
            null=True,
            editable=False,
        )
        draft_revision = models.OneToOneField(
            'ApplicationRevision',
            on_delete=models.PROTECT,
            related_name='draft',
            null=True,
            editable=False,
        )
    
    
        # Meta: used for migration purposes only
        drupal_id = models.IntegerField(null=True, blank=True, editable=False)
    
    
        objects = ApplicationSubmissionQueryset.as_manager()
    
    Todd Dembrey's avatar
    Todd Dembrey committed
        def not_progressed(self):
            return not self.next
    
    
        @transition(
            status, source='*',
    
            target=RETURN_VALUE(INITIAL_STATE, 'draft_proposal', 'invited_to_proposal'),
    
            permission=make_permission_check({UserPermissions.ADMIN}),
    
        def restart_stage(self, **kwargs):
            """
            If running form the console please include your user using the kwarg "by"
    
            u = User.objects.get(email="<my@email.com>")
            for a in ApplicationSubmission.objects.all():
                a.restart_stage(by=u)
                a.save()
            """
            if hasattr(self, 'previous'):
                return 'draft_proposal'
            elif self.next:
                return 'invited_to_proposal'
            return INITIAL_STATE
    
            return self.phase.stage
    
            return self.workflow.get(self.status)
    
        @property
        def active(self):
    
            return self.status in active_statuses
    
        @property
        def last_edit(self):
            # Best estimate of last edit
            # TODO update when we have revisioning included
            return self.activities.first()
    
    
        def ensure_user_has_account(self):
    
            if self.user and self.user.is_authenticated:
    
                self.form_data['email'] = self.user.email
                self.form_data['full_name'] = self.user.get_full_name()
            else:
                # Rely on the form having the following must include fields (see blocks.py)
                email = self.form_data.get('email')
                full_name = self.form_data.get('full_name')
    
                User = get_user_model()
    
                if 'skip_account_creation_notification' in self.form_data:
                    self.form_data.pop('skip_account_creation_notification', None)
                    self.user, _ = User.objects.get_or_create(
                        email=email,
                        defaults={'full_name': full_name}
                    )
                else:
                    self.user, _ = User.objects.get_or_create_and_notify(
                        email=email,
                        site=self.page.get_site(),
                        defaults={'full_name': full_name}
                    )
    
        def save_path(self, file_name):
            file_path = os.path.join('submissions', 'user', str(self.user.id), file_name)
            return default_storage.generate_filename(file_path)
    
    
        def handle_file(self, file):
            # File is potentially optional
            if file:
    
                try:
                    filename = self.save_path(file.name)
                except AttributeError:
                    # file is not changed, it is still the dictionary
                    return file
    
    
                saved_name = default_storage.save(filename, file)
                return {
                    'name': file.name,
                    'path': saved_name,
                    'url': default_storage.url(saved_name)
                }
    
        def handle_files(self, files):
            if isinstance(files, list):
                return [self.handle_file(file) for file in files]
    
            return self.handle_file(files)
    
    
        def get_from_parent(self, attribute):
            try:
                return getattr(self.round.specific, attribute)
            except AttributeError:
                # We are a lab submission
                return getattr(self.page.specific, attribute)
    
    
        def progress_application(self, **kwargs):
    
            submission_in_db = ApplicationSubmission.objects.get(id=self.id)
    
            self.id = None
            self.form_fields = self.get_from_parent('get_defined_fields')(self.stage)
    
            self.save()
    
            submission_in_db.next = self
            submission_in_db.save()
    
    
        def from_draft(self):
            self.is_draft = True
            self.form_data = self.draft_revision.form_data
            return self
    
        def create_revision(self, draft=False):
            self.clean_submission()
            current_data = ApplicationSubmission.objects.get(id=self.id).form_data
            if current_data != self.form_data:
    
                if self.live_revision == self.draft_revision:
                    revision = ApplicationRevision.objects.create(submission=self, form_data=self.form_data)
                else:
                    revision = self.draft_revision
                    revision.form_data = self.form_data
    
    
                if draft:
                    self.form_data = self.live_revision.form_data
                else:
                    self.live_revision = revision
    
                self.draft_revision = revision
                self.save()
    
        def clean_submission(self):
            self.process_form_data()
            self.ensure_user_has_account()
            self.process_file_data()
    
        def process_form_data(self):
    
            for field in self.form_fields:
                # Update the ids which are unique to use the unique name
                if isinstance(field.block, MustIncludeFieldBlock):
                    response = self.form_data.pop(field.id, None)
                    if response:
                        self.form_data[field.block.name] = response
    
        def process_file_data(self):
    
            for field in self.form_fields:
    
                if isinstance(field.block, UploadableMediaBlock):
    
                    file = self.form_data.get(field.id, {})
    
                    self.form_data[field.id] = self.handle_files(file)
    
        def save(self, *args, **kwargs):
            if self.is_draft:
                raise ValueError('Cannot save with draft data')
    
            self.clean_submission()
    
    
            creating = not self.id
            if creating:
    
                # We are creating the object default to first stage
    
                self.workflow_name = self.get_from_parent('workflow_name')
    
                # Copy extra relevant information to the child
    
                self.lead = self.get_from_parent('lead')
    
            # add a denormed version of the answer for searching
            self.search_data = ' '.join(self.prepare_search_values())
    
    
            if creating:
                self.reviewers.set(self.get_from_parent('reviewers').all())
    
                first_revision = ApplicationRevision.objects.create(submission=self, form_data=self.form_data)
                self.live_revision = first_revision
                self.draft_revision = first_revision
                self.save()
    
        def missing_reviewers(self):
    
            return self.reviewers.exclude(id__in=self.reviews.submitted().values('author'))
    
        def staff_not_reviewed(self):
    
            return self.missing_reviewers.staff()
    
        def reviewers_not_reviewed(self):
    
            return self.missing_reviewers.reviewers().exclude(id__in=self.staff_not_reviewed)
    
        def reviewed_by(self, user):
    
            return self.reviews.submitted().filter(author=user).exists()
    
        def has_permission_to_review(self, user):
    
            if user in self.reviewers_not_reviewed:
    
        def can_review(self, user):
            if self.reviewed_by(user):
                return False
    
    
            return self.has_permission_to_review(user)
    
    Dan Braghis's avatar
    Dan Braghis committed
        def has_permission_to_add_determination(self, user):
            return user.is_superuser or self.lead == user
    
        @property
        def in_determination_phase(self):
            return self.status in DETERMINATION_PHASES
    
        @property
        def can_have_determination(self):
            if not self.in_determination_phase:
                return False
    
            try:
                return not self.determination.submitted
            except ObjectDoesNotExist:
                return True
    
    
        def data_and_fields(self):
    
            for stream_value in self.form_fields:
    
                    data = self.form_data[stream_value.id]
    
                except KeyError:
                    pass  # It was a named field or a paragraph
                else:
    
            fields = [
                field.render(context={'data': data})
                for data, field in self.data_and_fields()
            ]
    
            return mark_safe(''.join(fields))
    
        def prepare_search_values(self):
    
            for data, stream in self.data_and_fields():
    
                value = stream.block.get_searchable_content(stream.value, data)
                if value:
    
                    if isinstance(value, list):
                        yield ', '.join(value)
                    else:
                        yield value
    
            # Add named fields into the search index
            for field in ['email', 'title']:
                yield getattr(self, field)
    
    
        def get_data(self):
            # Updated for JSONField
    
            form_data = self.form_data.copy()
    
            form_data.update({
                'submit_time': self.submit_time,
            })
    
            return form_data
    
    
        def get_absolute_url(self):
    
            return reverse('funds:submissions:detail', args=(self.id,))
    
        def __getattribute__(self, item):
            # __getattribute__ allows correct error handling from django compared to __getattr__
    
            # fall back to values defined on the data
            if item in REQUIRED_BLOCK_NAMES:
    
            return super().__getattribute__(item)
    
            return f'{self.title} from {self.full_name} for {self.page.title}'
    
        def __repr__(self):
    
            return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>'
    
    
    
    class ApplicationRevision(models.Model):
        submission = models.ForeignKey(ApplicationSubmission, related_name='revisions', on_delete=models.CASCADE)
        form_data = JSONField(encoder=DjangoJSONEncoder)