Skip to content
Snippets Groups Projects
models.py 13.7 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
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    from django.core.exceptions import ValidationError
    
    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
    
    from django.utils.translation import ugettext_lazy as _
    
    from modelcluster.fields import ParentalKey
    
    from wagtail.wagtailadmin.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.wagtailadmin.utils import send_mail
    
    from wagtail.wagtailcore.fields import StreamField
    
    from wagtail.wagtailcore.models import Orderable
    
    Dan Braghis's avatar
    Dan Braghis committed
    from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormSubmission
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    from opentech.apply.stream_forms.models import AbstractStreamForm
    
    from .blocks import CustomFormFieldsBlock, MustIncludeFieldBlock, REQUIRED_BLOCK_NAMES
    
    from .edit_handlers import FilteredFieldPanel, ReadOnlyPanel, ReadOnlyInlinePanel
    
    from .forms import WorkflowFormAdminForm
    
    from .workflow import SingleStage, DoubleStage
    
    
    Todd Dembrey's avatar
    Todd Dembrey committed
    
    
    WORKFLOW_CLASS = {
        SingleStage.name: SingleStage,
        DoubleStage.name: DoubleStage,
    }
    
    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):
            cleaned_data = form.cleaned_data
            for field in self.get_defined_fields():
                # Update the ids which are unique to use the unique name
                if isinstance(field.block, MustIncludeFieldBlock):
                    response = cleaned_data.pop(field.id)
                    cleaned_data[field.block.name] = response
    
    
            if form.user.is_authenticated():
                user = form.user
                cleaned_data['email'] = user.email
                cleaned_data['full_name'] = user.get_full_name()
            else:
    
                # Rely on the form having the following must include fields (see blocks.py)
    
                email = cleaned_data.get('email')
                full_name = cleaned_data.get('full_name')
    
                user, _ = User.objects.get_or_create_and_notify(
    
                    defaults={'full_name': full_name}
                )
    
            return self.get_submission_class().objects.create(
                form_data=cleaned_data,
    
                **self.get_submit_meta_data(user=user),
    
            )
    
        def get_submit_meta_data(self, **kwargs):
            return kwargs
    
    
    
    class WorkflowStreamForm(AbstractStreamForm):
        """
        Defines the common methods and fields for working with Workflows
        """
    
        class Meta:
            abstract = True
    
    
        WORKFLOWS = {
            'single': SingleStage.name,
            'double': DoubleStage.name,
        }
    
        workflow = models.CharField(choices=WORKFLOWS.items(), max_length=100, default='single')
    
        def get_defined_fields(self):
            # Only return the first form, will need updating for when working with 2 stage WF
            return self.forms.all()[0].fields
    
    
        @property
        def workflow_class(self):
            return WORKFLOW_CLASS[self.get_workflow_display()]
    
        content_panels = AbstractStreamForm.content_panels + [
            FieldPanel('workflow'),
            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(form)
            return submission
    
        def send_mail(self, form):
            data = form.cleaned_data
            email = data.get('email')
    
            context = {
    
                'name': data.get('full_name'),
    
                'email': email,
    
                'project_name': 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), (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):
    
        # Adds validation around forms & workflows. Isn't on Workflow class due to not displaying workflow field on Round
        base_form_class = WorkflowFormAdminForm
    
    
        parent_page_types = ['apply_home.ApplyHomePage']
        subpage_types = ['funds.Round']
    
    
            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):
            return self.open_round.end_date
    
            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)
    
    
        edit_handler = TabbedInterface([
            ObjectList(WorkflowStreamForm.content_panels, heading='Content'),
            EmailForm.email_tab,
            ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
        ])
    
    
    class AbstractRelatedForm(Orderable):
    
        form = models.ForeignKey('ApplicationForm')
    
    
        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):
    
        parent_page_types = ['funds.FundType']
        subpage_types = []  # type: ignore
    
    
        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'),
                ]),
    
            ReadOnlyPanel('get_workflow_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 save(self, *args, **kwargs):
            is_new = not self.id
            if is_new and hasattr(self, 'parent_page'):
    
                # We attached the parent page as part of the before_create_hook
                self.workflow = self.parent_page.workflow
    
            super().save(*args, **kwargs)
    
    
            if is_new and hasattr(self, 'parent_page'):
    
                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(form)
            return 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
    
        parent_page_types = ['apply_home.ApplyHomePage']
    
        subpage_types = []  # type: ignore
    
        edit_handler = TabbedInterface([
            ObjectList(SubmittableStreamForm.content_panels, heading='Content'),
            EmailForm.email_tab,
            ObjectList(SubmittableStreamForm.promote_panels, heading='Promote'),
        ])
    
        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):
            def build_json_order_by(field):
                if field.replace('-', '') not in REQUIRED_BLOCK_NAMES:
                    return field
    
                if field[0] == '-':
                    descending = True
                    field = field[1:]
                else:
                    descending = False
                return OrderBy(RawSQL("LOWER(form_data->>%s)", (field,)), descending=descending)
    
            field_ordering = [build_json_order_by(field) for field in field_names]
            return super().order_by(*field_ordering)
    
    
    
    class ApplicationSubmission(AbstractFormSubmission):
    
        form_data = JSONField(encoder=DjangoJSONEncoder)
    
        round = models.ForeignKey('wagtailcore.Page', on_delete=models.CASCADE, related_name='submissions', null=True)
    
        user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
    
        objects = JSONOrderable.as_manager()
    
    
        def get_data(self):
            # Updated for JSONField
            form_data = self.form_data
            form_data.update({
                'submit_time': self.submit_time,
            })
    
            return form_data
    
    
        def __getattr__(self, item):
            # fall back to values defined on the data
            if item in REQUIRED_BLOCK_NAMES:
                return self.get_data()[item]
            return super().__getattr__(item)
    
    
        def __str__(self):
            return str(super().__str__())