Newer
Older
from datetime import date
Fredrik Jonsson
committed
from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import (
Case,
CharField,
Count,
F,
FloatField,
IntegerField,
OuterRef,
Q,
Subquery,
When,
)
from django.db.models.functions import Coalesce, Length, Left
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalManyToManyField
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
MultiFieldPanel,
ObjectList,
TabbedInterface,
)
from wagtail.core.models import Page, PageManager, PageQuerySet
from ..admin_forms import RoundBasePageAdminForm, WorkflowFormAdminForm
from ..edit_handlers import ReadOnlyPanel, ReadOnlyInlinePanel
from ..workflow import OPEN_CALL_PHASES
from .submissions import ApplicationSubmission
from .utils import admin_url, EmailForm, SubmittableStreamForm, WorkflowStreamForm, LIMIT_TO_REVIEWERS, LIMIT_TO_STAFF
class ApplicationBaseManager(PageQuerySet):
def order_by_end_date(self):
# OutRef path__startswith with find all descendants of the parent
# We only have children, so no issues at this time
rounds = RoundBase.objects.open().filter(path__startswith=OuterRef('path'))
qs = self.public().live().annotate(end_date=Subquery(rounds.values('end_date')[:1]))
return qs.order_by('end_date')
class ApplicationBase(EmailForm, WorkflowStreamForm): # type: ignore
is_createable = False
# 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='%(class)s_reviewers',
limit_choices_to=LIMIT_TO_REVIEWERS,
blank=True,
)
approval_form = models.ForeignKey(
'application_projects.ProjectApprovalForm',
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='funds',
)
Fredrik Jonsson
committed
guide_link = models.URLField(blank=True, max_length=255, help_text=_('Link to the apply guide.'))
slack_channel = models.CharField(blank=True, max_length=128, help_text=_('The slack #channel for notifications.'))
objects = PageManager.from_queryset(ApplicationBaseManager)()
parent_page_types = ['apply_home.ApplyHomePage']
def get_template(self, request, *args, **kwargs):
# We want to force children to use our base template
# template attribute is ignored by children
return 'funds/application_base.html'
def detail(self):
# The location to find out more information
return RoundBase.objects.child_of(self).open().first()
def next_deadline(self):
try:
return self.open_round.end_date
except AttributeError:
# There isn't an open round
return None
def serve(self, request):
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
request.show_round = True
return self.open_round.serve(request)
content_panels = WorkflowStreamForm.content_panels + [
Fredrik Jonsson
committed
FieldPanel('reviewers', widget=forms.SelectMultiple(attrs={'size': '16'})),
Fredrik Jonsson
committed
FieldPanel('guide_link'),
FieldPanel('slack_channel'),
]
edit_handler = TabbedInterface([
ObjectList(content_panels, heading='Content'),
EmailForm.email_tab,
ObjectList(WorkflowStreamForm.promote_panels, heading='Promote'),
])
class RoundBaseManager(PageQuerySet):
def open(self):
rounds = self.live().public().specific()
rounds = rounds.filter(
Q(start_date__lte=date.today()) &
Q(Q(end_date__isnull=True) | Q(end_date__gte=date.today()))
)
return rounds
def closed(self):
rounds = self.live().public().specific()
rounds = rounds.filter(end_date__lt=date.today())
return rounds
class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore
is_creatable = False
submission_class = ApplicationSubmission
objects = PageManager.from_queryset(RoundBaseManager)()
subpage_types = [] # type: ignore
base_form_class = RoundBasePageAdminForm
lead = models.ForeignKey(
settings.AUTH_USER_MODEL,
limit_choices_to=LIMIT_TO_STAFF,
related_name='%(class)s_lead',
on_delete=models.PROTECT,
)
reviewers = ParentalManyToManyField(
settings.AUTH_USER_MODEL,
related_name='%(class)s_reviewer',
limit_choices_to=LIMIT_TO_REVIEWERS,
blank=True,
)
start_date = models.DateField(null=True, blank=True, 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.'
)
sealed = models.BooleanField(default=False)
content_panels = SubmittableStreamForm.content_panels + [
FieldPanel('lead'),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('start_date'),
FieldPanel('end_date'),
]),
], heading="Dates"),
Fredrik Jonsson
committed
FieldPanel('reviewers', widget=forms.SelectMultiple(attrs={'size': '16'})),
Parbhat Puri
committed
ReadOnlyPanel('get_workflow_name_display', heading="Workflow", help_text="Copied from the fund."),
# Forms comes from parental key in models/forms.py
ReadOnlyInlinePanel('forms', help_text="Copied from the fund."),
ReadOnlyInlinePanel('review_forms', help_text="Copied from the fund."),
]
edit_handler = TabbedInterface([
ObjectList(content_panels, heading='Content'),
ObjectList(SubmittableStreamForm.promote_panels, heading='Promote'),
])
def get_template(self, request, *args, **kwargs):
# Make sure all children use the shared template
return 'funds/round.html'
def get_landing_page_template(self, request, *args, **kwargs):
# Make sure all children use the shared template
return 'funds/round_landing.html'
@cached_property
def fund(self):
return self.get_parent()
@property
def is_sealed(self):
return self.sealed and self.is_open
@property
def is_open(self):
return self.start_date <= date.today() <= self.end_date
def save(self, *args, **kwargs):
is_new = not self.id
if is_new and hasattr(self, 'parent_page'):
parent_page = self.parent_page[self.__class__][self.title]
self.workflow_name = parent_page.workflow_name
self.reviewers = parent_page.reviewers.all()
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__
self._copy_forms('forms')
self._copy_forms('review_forms')
def _copy_forms(self, field):
for form in getattr(self.get_parent().specific, field).all():
new_form = self._meta.get_field(field).related_model
self._copy_form(form, new_form)
def _copy_form(self, form, new_class):
# 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()
if hasattr(form, 'stage'):
new_class.objects.create(round=self, form=new_form, stage=form.stage)
else:
new_class.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,
**kwargs,
)
def clean(self):
super().clean()
conflict_query = ()
if self.start_date and self.end_date and self.start_date > self.end_date:
raise ValidationError({
'end_date': 'End date must come after the start date',
})
if self.start_date and 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)
)
conflict_query = (
Q(start_date__lte=self.start_date, end_date__isnull=True) |
Q(end_date__gte=self.start_date)
)
if not self.id and 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 = RoundBase.objects.child_of(self.parent_page[self.__class__][self.title])
else:
# don't need parent page, we are an actual object now.
base_query = RoundBase.objects.sibling_of(self)
if conflict_query:
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]
)
))
error = {
'start_date': error_message,
}
if self.end_date:
error['end_date'] = error_message
raise ValidationError(error)
def get_initial_data_open_call_submission(self, submission_id):
initial_values = {}
try:
submission_class = self.get_submission_class()
submission = submission_class.objects.get(id=submission_id)
if submission.status in OPEN_CALL_PHASES and self.get_parent() == submission.page:
Parbhat Puri
committed
title_block_id = submission.named_blocks.get('title')
if title_block_id:
field_data = submission.data(title_block_id)
initial_values[title_block_id] = field_data + ' (please edit)'
Parbhat Puri
committed
for field_id in submission.first_group_normal_text_blocks:
field_data = submission.data(field_id)
initial_values[field_id] = field_data
Parbhat Puri
committed
# Select first item in the Group toggle blocks
for toggle_block_id, toggle_field in submission.group_toggle_blocks:
try:
initial_values[toggle_block_id] = toggle_field.value['choices'][0]
except IndexError:
initial_values[toggle_block_id] = 'yes'
except KeyError:
Parbhat Puri
committed
pass
except (submission_class.DoesNotExist, ValueError):
pass
return initial_values
def get_form_parameters(self, submission_id=None):
form_parameters = {}
if submission_id:
initial_values = self.get_initial_data_open_call_submission(submission_id)
if initial_values:
form_parameters['initial'] = initial_values
return form_parameters
def get_form(self, *args, **kwargs):
form_class = self.get_form_class()
submission_id = kwargs.pop('submission_id', None)
if submission_id:
form_params = self.get_form_parameters(submission_id=submission_id)
else:
form_params = self.get_form_parameters()
form_params.update(kwargs)
return form_class(*args, **form_params)
def serve(self, request, *args, **kwargs):
if hasattr(request, 'is_preview') or hasattr(request, 'show_round'):
# Overriding serve method to pass submission id to get_form method
copy_open_submission = request.GET.get('open_call_submission')
if request.method == 'POST':
form = self.get_form(request.POST, request.FILES, page=self, user=request.user)
if form.is_valid():
form_submission = self.process_form_submission(form)
return self.render_landing_page(request, form_submission, *args, **kwargs)
else:
form = self.get_form(page=self, user=request.user, submission_id=copy_open_submission)
context = self.get_context(request)
context['form'] = form
Parbhat Puri
committed
context['show_all_group_fields'] = True if copy_open_submission else False
return render(
request,
self.get_template(request),
context
)
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# We hide the round as only the open round is used which is displayed through the
# fund page
raise Http404()
class LabBase(EmailForm, WorkflowStreamForm, SubmittableStreamForm): # type: ignore
is_createable = False
submission_class = ApplicationSubmission
# Adds validation around forms & workflows.
base_form_class = WorkflowFormAdminForm
lead = models.ForeignKey(
settings.AUTH_USER_MODEL,
limit_choices_to=LIMIT_TO_STAFF,
related_name='lab_lead',
on_delete=models.PROTECT,
)
reviewers = ParentalManyToManyField(
settings.AUTH_USER_MODEL,
related_name='labs_reviewer',
limit_choices_to=LIMIT_TO_REVIEWERS,
blank=True,
)
approval_form = models.ForeignKey(
'application_projects.ProjectApprovalForm',
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='labs',
)
Fredrik Jonsson
committed
guide_link = models.URLField(blank=True, max_length=255, help_text=_('Link to the apply guide.'))
slack_channel = models.CharField(blank=True, max_length=128, help_text=_('The slack #channel for notifications.'))
parent_page_types = ['apply_home.ApplyHomePage']
subpage_types = [] # type: ignore
content_panels = WorkflowStreamForm.content_panels + [
Fredrik Jonsson
committed
FieldPanel('reviewers', widget=forms.SelectMultiple(attrs={'size': '16'})),
Fredrik Jonsson
committed
FieldPanel('guide_link'),
FieldPanel('slack_channel'),
]
edit_handler = TabbedInterface([
ObjectList(content_panels, heading='Content'),
EmailForm.email_tab,
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 RoundsAndLabsQueryset(PageQuerySet):
def new(self):
return self.filter(start_date__gt=date.today())
def open(self):
return self.filter(Q(end_date__gte=date.today(), start_date__lte=date.today()) | Q(end_date__isnull=True))
def closed(self):
return self.filter(end_date__lt=date.today())
def by_lead(self, user):
return self.filter(lead_pk=user.pk)
class RoundsAndLabsProgressQueryset(RoundsAndLabsQueryset):
def active(self):
return self.filter(progress__lt=100)
def inactive(self):
return self.filter(progress=100)
class RoundsAndLabsManager(PageManager):
def get_queryset(self, base_queryset=RoundsAndLabsQueryset):
funds = ApplicationBase.objects.filter(path=OuterRef('parent_path'))
return base_queryset(self.model, using=self._db).type(SubmittableStreamForm).annotate(
lead=Coalesce(
F('roundbase__lead__full_name'),
F('labbase__lead__full_name'),
),
start_date=F('roundbase__start_date'),
end_date=F('roundbase__end_date'),
parent_path=Left(F('path'), Length('path') - ApplicationBase.steplen, output_field=CharField()),
fund=Subquery(funds.values('title')[:1]),
lead_pk=Coalesce(
F('roundbase__lead__pk'),
F('labbase__lead__pk'),
),
)
def with_progress(self):
submissions = ApplicationSubmission.objects.filter(Q(round=OuterRef('pk')) | Q(page=OuterRef('pk'))).current()
closed_submissions = submissions.inactive()
return self.get_queryset(RoundsAndLabsProgressQueryset).annotate(
total_submissions=Coalesce(
Subquery(
submissions.values('round').annotate(count=Count('pk')).values('count'),
output_field=IntegerField(),
),
0,
),
closed_submissions=Coalesce(
Subquery(
closed_submissions.values('round').annotate(count=Count('pk')).values('count'),
output_field=IntegerField(),
),
0,
),
).annotate(
progress=Case(
When(total_submissions=0, then=None),
default=(F('closed_submissions') * 100) / F('total_submissions'),
output_fields=FloatField(),
)
)
def open(self):
return self.get_queryset().open()
def closed(self):
return self.get_queryset().closed()
def new(self):
return self.get_queryset().new()
def by_lead(self, user):
return self.get_queryset().by_lead(user)
class RoundsAndLabs(Page):
"""
This behaves as a useful way to get all the rounds and labs that are defined
in the project regardless of how they are implemented (lab/round/sealed_round)
"""
class Meta:
proxy = True
def __eq__(self, other):
# This is one way equality RoundAndLab == Round/Lab
# Round/Lab == RoundAndLab returns False due to different
# Concrete class
if not isinstance(other, models.Model):
return False
if not isinstance(other, SubmittableStreamForm):
return False
my_pk = self.pk
if my_pk is None:
return self is other
return my_pk == other.pk
objects = RoundsAndLabsManager()
def save(self, *args, **kwargs):
raise NotImplementedError('Do not save through this model')