diff --git a/opentech/apply/funds/admin_forms.py b/opentech/apply/funds/admin_forms.py index 6958afbe9dd162ca49f1d6772850af1656093447..f668a26ecad40b3d39d544e04968dd61cfebf388 100644 --- a/opentech/apply/funds/admin_forms.py +++ b/opentech/apply/funds/admin_forms.py @@ -35,3 +35,14 @@ class WorkflowFormAdminForm(WagtailAdminPageForm): for form in valid_forms[number_of_stages:]: form.add_error('form', 'Exceeds required number of forms for stage, please remove.') + + +class RoundBasePageAdminForm(WagtailAdminPageForm): + def clean(self): + cleaned_data = super().clean() + + start_date = cleaned_data['start_date'] + if not start_date: + self.add_error('start_date', 'Please select start date.') + + return cleaned_data diff --git a/opentech/apply/funds/admin_views.py b/opentech/apply/funds/admin_views.py new file mode 100644 index 0000000000000000000000000000000000000000..d0a0cf1812ab5d1ac22cc674e2c489ffb4f6e1d9 --- /dev/null +++ b/opentech/apply/funds/admin_views.py @@ -0,0 +1,79 @@ +from django.core.exceptions import PermissionDenied +from django.shortcuts import redirect +from django.utils.translation import ugettext as _ + +from wagtail.admin import messages +from wagtail.admin.forms import CopyForm +from wagtail.admin.views.pages import get_valid_next_url_from_request +from wagtail.core import hooks +from wagtail.core.models import Page + + +def custom_admin_round_copy_view(request, page): + # Custom view to handle copied Round pages. + # https://github.com/wagtail/wagtail/blob/124827911463f0cb959edbb9d8d5685578540bd3/wagtail/admin/views/pages.py#L824 + + # Parent page defaults to parent of source page + parent_page = page.get_parent() + + # Check if the user has permission to publish subpages on the parent + can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage() + + form = CopyForm(request.POST or None, user=request.user, page=page, can_publish=can_publish) + + next_url = get_valid_next_url_from_request(request) + + # Prefill parent_page in case the form is invalid (as prepopulated value for the form field, + # because ModelChoiceField seems to not fall back to the user given value) + parent_page = Page.objects.get(id=request.POST['new_parent_page']) + + if form.is_valid(): + # Receive the parent page (this should never be empty) + if form.cleaned_data['new_parent_page']: + parent_page = form.cleaned_data['new_parent_page'] + + if not page.permissions_for_user(request.user).can_copy_to(parent_page, + form.cleaned_data.get('copy_subpages')): + raise PermissionDenied + + # Re-check if the user has permission to publish subpages on the new parent + can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage() + + # Copy the page + new_page = page.copy( + recursive=form.cleaned_data.get('copy_subpages'), + to=parent_page, + update_attrs={ + 'title': form.cleaned_data['new_title'], + 'slug': form.cleaned_data['new_slug'], + 'start_date': None, + 'end_date': None, + }, + keep_live=(can_publish and form.cleaned_data.get('publish_copies')), + user=request.user, + ) + + messages.info(request, _(( + "Please select the date in the copied page. " + "Newly copied pages have NONE value for the start and end date" + ))) + + # Give a success message back to the user + if form.cleaned_data.get('copy_subpages'): + messages.success( + request, _("Page '{0}' and {1} subpages copied.").format( + page.get_admin_display_title(), new_page.get_descendants().count() + ) + ) + else: + messages.success(request, _("Page '{0}' copied.").format(page.get_admin_display_title())) + + for fn in hooks.get_hooks('after_copy_page'): + result = fn(request, page, new_page) + if hasattr(result, 'status_code'): + return result + + # Redirect to explore of parent page + if next_url: + return redirect(next_url) + return redirect('wagtailadmin_explore', parent_page.id) diff --git a/opentech/apply/funds/migrations/0057_start_date_blank_null_roundbase.py b/opentech/apply/funds/migrations/0057_start_date_blank_null_roundbase.py new file mode 100644 index 0000000000000000000000000000000000000000..4de6a79112c79fe8375d36c1b28fd62fffcdc748 --- /dev/null +++ b/opentech/apply/funds/migrations/0057_start_date_blank_null_roundbase.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.9 on 2019-03-07 13:53 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0056_reviewers_rename'), + ] + + operations = [ + migrations.AlterField( + model_name='roundbase', + name='start_date', + field=models.DateField(blank=True, default=datetime.date.today, null=True), + ), + ] diff --git a/opentech/apply/funds/models/applications.py b/opentech/apply/funds/models/applications.py index f2a791dee8cbebd6dd92cb2ba855d519d1728147..4f69cb77ea91c53f8093c7dfbb38b4dab8a60280 100644 --- a/opentech/apply/funds/models/applications.py +++ b/opentech/apply/funds/models/applications.py @@ -34,7 +34,7 @@ from wagtail.admin.edit_handlers import ( ) from wagtail.core.models import Page, PageManager, PageQuerySet -from ..admin_forms import WorkflowFormAdminForm +from ..admin_forms import RoundBasePageAdminForm, WorkflowFormAdminForm from ..edit_handlers import ReadOnlyPanel, ReadOnlyInlinePanel from .submissions import ApplicationSubmission @@ -132,6 +132,9 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore subpage_types = [] # type: ignore + # Adds validation for making start_date required + base_form_class = RoundBasePageAdminForm + lead = models.ForeignKey( settings.AUTH_USER_MODEL, limit_choices_to=LIMIT_TO_STAFF, @@ -144,7 +147,7 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore limit_choices_to=LIMIT_TO_REVIEWERS, blank=True, ) - start_date = models.DateField(default=date.today) + start_date = models.DateField(null=True, blank=True, default=date.today) end_date = models.DateField( blank=True, null=True, @@ -230,18 +233,20 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore def clean(self): super().clean() - if self.end_date and self.start_date > self.end_date: + 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.end_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) ) - else: + elif self.start_date: conflict_query = ( Q(start_date__lte=self.start_date, end_date__isnull=True) | Q(end_date__gte=self.start_date) @@ -255,24 +260,25 @@ class RoundBase(WorkflowStreamForm, SubmittableStreamForm): # type: ignore # don't need parent page, we are an actual object now. base_query = RoundBase.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] - ) - )) - error = { - 'start_date': error_message, - } - if self.end_date: - error['end_date'] = error_message - - raise ValidationError(error) + 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 serve(self, request): if hasattr(request, 'is_preview') or hasattr(request, 'show_round'): diff --git a/opentech/apply/funds/wagtail_hooks.py b/opentech/apply/funds/wagtail_hooks.py index 30bf699dd0c286ea7c88906de4e992aa7ac7fa76..6f29a25ed6ae7e8b574191d81f85d44ca5d19dce 100644 --- a/opentech/apply/funds/wagtail_hooks.py +++ b/opentech/apply/funds/wagtail_hooks.py @@ -2,6 +2,7 @@ from wagtail.core import hooks from wagtail.contrib.modeladmin.options import modeladmin_register from .admin import ApplyAdminGroup +from .admin_views import custom_admin_round_copy_view from .models import RoundBase @@ -15,3 +16,10 @@ def before_create_page(request, parent_page, page_class): page_class.parent_page = {} page_class.parent_page.setdefault(page_class, {})[request.POST['title']] = parent_page return page_class + + +@hooks.register('before_copy_page') +def before_copy_round_page(request, page): + if isinstance(page.specific, RoundBase) and request.method == 'POST': + # Custom view to clear start_date and end_date from the copy being created. + return custom_admin_round_copy_view(request, page)