diff --git a/opentech/apply/funds/admin.py b/opentech/apply/funds/admin.py index 9cde07a20e37691c651711b4e965e8de54cee986..d49ce33d61a047f030ce77dbf26e4fcbba52496c 100644 --- a/opentech/apply/funds/admin.py +++ b/opentech/apply/funds/admin.py @@ -9,13 +9,11 @@ from .admin_helpers import ( FormsFundRoundListFilter, RoundFundChooserView, ) -from .models import ApplicationForm, FundType, LabType, Round +from .models import ApplicationForm, FundType, LabType, RequestForPartners, Round, SealedRound from opentech.apply.categories.admin import CategoryAdmin -class RoundAdmin(ModelAdmin): - model = Round - menu_icon = 'repeat' +class BaseRoundAdmin(ModelAdmin): choose_parent_view_class = RoundFundChooserView choose_parent_template_name = 'funds/admin/parent_chooser.html' list_display = ('title', 'fund', 'start_date', 'end_date') @@ -25,12 +23,29 @@ class RoundAdmin(ModelAdmin): return obj.get_parent() +class RoundAdmin(BaseRoundAdmin): + model = Round + menu_icon = 'repeat' + + +class SealedRoundAdmin(BaseRoundAdmin): + model = SealedRound + menu_icon = 'locked' + menu_label = 'Sealed Rounds' + + class FundAdmin(ModelAdmin): model = FundType menu_icon = 'doc-empty' menu_label = 'Funds' +class RFPAdmin(ModelAdmin): + model = RequestForPartners + menu_icon = 'group' + menu_label = 'Request For Partners' + + class LabAdmin(ModelAdmin): model = LabType menu_icon = 'doc-empty' @@ -71,4 +86,4 @@ class ApplicationFormAdmin(ModelAdmin): class ApplyAdminGroup(ModelAdminGroup): menu_label = 'Apply' menu_icon = 'folder-open-inverse' - items = (RoundAdmin, FundAdmin, LabAdmin, ApplicationFormAdmin, ReviewFormAdmin, CategoryAdmin) + items = (RoundAdmin, SealedRoundAdmin, FundAdmin, LabAdmin, RFPAdmin, ApplicationFormAdmin, ReviewFormAdmin, CategoryAdmin) diff --git a/opentech/apply/funds/admin_helpers.py b/opentech/apply/funds/admin_helpers.py index 212a5f4be7bc462a1dc43b084d085346d7960050..f0a0002eaa17595bae42bf189b791b6d57dfc760 100644 --- a/opentech/apply/funds/admin_helpers.py +++ b/opentech/apply/funds/admin_helpers.py @@ -9,10 +9,15 @@ from wagtail.contrib.modeladmin.views import ChooseParentView from wagtail.core.models import Page +class VerboseLabelModelChoiceField(forms.ModelChoiceField): + def label_from_instance(str, obj): + return '[{}] {}'.format(obj._meta.verbose_name, obj.title) + + class FundChooserForm(ParentChooserForm): """Changes the default chooser to be fund orientated """ - parent_page = forms.ModelChoiceField( - label=_('Fund'), + parent_page = VerboseLabelModelChoiceField( + label=_('Fund or RFP'), required=True, empty_label=None, queryset=Page.objects.none(), @@ -22,7 +27,7 @@ class FundChooserForm(ParentChooserForm): class RoundFundChooserView(ChooseParentView): def get_form(self, request): - parents = self.permission_helper.get_valid_parent_pages(request.user) + parents = self.permission_helper.get_valid_parent_pages(request.user).specific() return FundChooserForm(parents, request.POST or None) diff --git a/opentech/apply/funds/migrations/0037_add_rfp_classes.py b/opentech/apply/funds/migrations/0037_add_rfp_classes.py new file mode 100644 index 0000000000000000000000000000000000000000..e2c201f30c7838db0fbb474df11b0dd36341b97c --- /dev/null +++ b/opentech/apply/funds/migrations/0037_add_rfp_classes.py @@ -0,0 +1,67 @@ +# Generated by Django 2.0.2 on 2018-08-01 14:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields +import opentech.apply.stream_forms.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0040_page_draft_title'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('review', '0006_remove_review_review'), + ('funds', '0036_fundreviewform_labreviewform'), + ] + + operations = [ + migrations.CreateModel( + name='RequestForPartners', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('to_address', models.CharField(blank=True, help_text='Optional - form submissions will be emailed to these addresses. Separate multiple addresses by comma.', max_length=255, verbose_name='to address')), + ('from_address', models.CharField(blank=True, max_length=255, verbose_name='from address')), + ('subject', models.CharField(blank=True, max_length=255, verbose_name='subject')), + ('workflow_name', models.CharField(choices=[('single', 'Request'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow')), + ('confirmation_text_extra', models.TextField(blank=True, help_text='Additional text for the application confirmation message.')), + ('reviewers', modelcluster.fields.ParentalManyToManyField(blank=True, limit_choices_to={'groups__name': 'Reviewer'}, related_name='requestforpartners_reviewers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'RFP', + }, + bases=(opentech.apply.stream_forms.models.BaseStreamForm, 'wagtailcore.page', models.Model), + ), + migrations.CreateModel( + name='RFPForm', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('form', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='funds.ApplicationForm')), + ('rfp', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='forms', to='funds.RequestForPartners')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RFPReviewForm', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('form', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='review.ReviewForm')), + ('rfp', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='review_forms', to='funds.RequestForPartners')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='fundtype', + name='reviewers', + field=modelcluster.fields.ParentalManyToManyField(blank=True, limit_choices_to={'groups__name': 'Reviewer'}, related_name='fundtype_reviewers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/opentech/apply/funds/migrations/0038_round_sealed.py b/opentech/apply/funds/migrations/0038_round_sealed.py new file mode 100644 index 0000000000000000000000000000000000000000..a4a8c6cb449c2eb436ab7bb109e3bb6021d5f265 --- /dev/null +++ b/opentech/apply/funds/migrations/0038_round_sealed.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-08-01 14:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0037_add_rfp_classes'), + ] + + operations = [ + migrations.AddField( + model_name='round', + name='sealed', + field=models.BooleanField(default=False), + ), + ] diff --git a/opentech/apply/funds/migrations/0039_create_sealed_round_page.py b/opentech/apply/funds/migrations/0039_create_sealed_round_page.py new file mode 100644 index 0000000000000000000000000000000000000000..108a44c471f8dfe7d152a972198a93ff00f469ed --- /dev/null +++ b/opentech/apply/funds/migrations/0039_create_sealed_round_page.py @@ -0,0 +1,41 @@ +# Generated by Django 2.0.2 on 2018-08-01 16:08 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields +import opentech.apply.stream_forms.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wagtailcore', '0040_page_draft_title'), + ('funds', '0038_round_sealed'), + ] + + operations = [ + migrations.CreateModel( + name='SealedRound', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('workflow_name', models.CharField(choices=[('single', 'Request'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow')), + ('start_date', models.DateField(default=datetime.date.today)), + ('end_date', models.DateField(blank=True, default=datetime.date.today, help_text='When no end date is provided the round will remain open indefinitely.', null=True)), + ('sealed', models.BooleanField(default=False)), + ('lead', models.ForeignKey(limit_choices_to={'groups__name': 'Staff'}, on_delete=django.db.models.deletion.PROTECT, related_name='sealedround_lead', to=settings.AUTH_USER_MODEL)), + ('reviewers', modelcluster.fields.ParentalManyToManyField(blank=True, limit_choices_to={'groups__name': 'Reviewer'}, related_name='sealedround_reviewer', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opentech.apply.stream_forms.models.BaseStreamForm, 'wagtailcore.page', models.Model), + ), + migrations.AlterField( + model_name='round', + name='reviewers', + field=modelcluster.fields.ParentalManyToManyField(blank=True, limit_choices_to={'groups__name': 'Reviewer'}, related_name='round_reviewer', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/opentech/apply/funds/migrations/0040_sealedroundforn.py b/opentech/apply/funds/migrations/0040_sealedroundforn.py new file mode 100644 index 0000000000000000000000000000000000000000..4884447ae3a07f905c77e3bb8a9f29e58a3d7f49 --- /dev/null +++ b/opentech/apply/funds/migrations/0040_sealedroundforn.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.2 on 2018-08-01 16:28 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0039_create_sealed_round_page'), + ] + + operations = [ + migrations.CreateModel( + name='SealedRoundForn', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('form', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='funds.ApplicationForm')), + ('round', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='forms', to='funds.SealedRound')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + ] diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py index cb4024d9457c5bd253db9f8f2b1bead53b592e8e..83abe9c9cce2b5b5485ba17aec671fb85838ad92 100644 --- a/opentech/apply/funds/models.py +++ b/opentech/apply/funds/models.py @@ -168,35 +168,39 @@ class EmailForm(AbstractEmailForm): email_tab = ObjectList(email_confirmation_panels, heading='Confirmation email') -class FundType(EmailForm, WorkflowStreamForm): # type: ignore +class RoundBasedParent(EmailForm, WorkflowStreamForm): # type: ignore class Meta: - verbose_name = _("Fund") + abstract = True # 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', + related_name='%(class)s_reviewers', limit_choices_to=LIMIT_TO_REVIEWERS, 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() - @property - def open_round(self): - rounds = Round.objects.child_of(self).live().public().specific() + def _open_for(self, round_type): + rounds = round_type.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() + @property + def open_round(self): + open_round = self._open_for(Round) + open_sealed_round = self._open_for(SealedRound) + return open_round or open_sealed_round + def next_deadline(self): try: return self.open_round.end_date @@ -223,6 +227,20 @@ class FundType(EmailForm, WorkflowStreamForm): # type: ignore ]) +class FundType(RoundBasedParent): + subpage_types = ['funds.Round'] + + class Meta: + verbose_name = _("Fund") + + +class RequestForPartners(RoundBasedParent): + subpage_types = ['funds.Round', 'funds.SealedRound'] + + class Meta: + verbose_name = _("RFP") + + class AbstractRelatedForm(Orderable): form = models.ForeignKey('ApplicationForm', on_delete=models.PROTECT) @@ -247,6 +265,22 @@ class AbstractRelatedForm(Orderable): return self.form.name +class FundForm(AbstractRelatedForm): + fund = ParentalKey('FundType', related_name='forms') + + +class RoundForm(AbstractRelatedForm): + round = ParentalKey('Round', related_name='forms') + + +class SealedRoundForn(AbstractRelatedForm): + round = ParentalKey('SealedRound', related_name='forms') + + +class RFPForm(AbstractRelatedForm): + rfp = ParentalKey('RequestForPartners', related_name='forms') + + class AbstractRelatedReviewForm(Orderable): form = models.ForeignKey('review.ReviewForm', on_delete=models.PROTECT) @@ -271,18 +305,14 @@ class AbstractRelatedReviewForm(Orderable): return self.form.name -class FundForm(AbstractRelatedForm): - fund = ParentalKey('FundType', related_name='forms') - - -class RoundForm(AbstractRelatedForm): - round = ParentalKey('Round', related_name='forms') - - class FundReviewForm(AbstractRelatedReviewForm): fund = ParentalKey('FundType', related_name='review_forms') +class RFPReviewForm(AbstractRelatedReviewForm): + rfp = ParentalKey('RequestForPartners', related_name='review_forms') + + class ApplicationForm(models.Model): name = models.CharField(max_length=255) form_fields = StreamField(ApplicationCustomFormFieldsBlock()) @@ -296,19 +326,22 @@ class ApplicationForm(models.Model): return self.name -class Round(WorkflowStreamForm, SubmittableStreamForm): # type: ignore - parent_page_types = ['funds.FundType'] +class AbstractRound(WorkflowStreamForm, SubmittableStreamForm): # type: ignore + class Meta: + abstract = True + + parent_page_types = ['funds.FundType', 'funds.RequestForPartners'] subpage_types = [] # type: ignore lead = models.ForeignKey( settings.AUTH_USER_MODEL, limit_choices_to=LIMIT_TO_STAFF, - related_name='round_lead', + related_name='%(class)s_lead', on_delete=models.PROTECT, ) reviewers = ParentalManyToManyField( settings.AUTH_USER_MODEL, - related_name='rounds_reviewer', + related_name='%(class)s_reviewer', limit_choices_to=LIMIT_TO_REVIEWERS, blank=True, ) @@ -319,6 +352,7 @@ class Round(WorkflowStreamForm, SubmittableStreamForm): # type: ignore 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'), @@ -426,6 +460,18 @@ class Round(WorkflowStreamForm, SubmittableStreamForm): # type: ignore raise Http404() +class Round(AbstractRound): + parent_page_types = ['funds.FundType', 'funds.RequestForPartners'] + + +class SealedRound(AbstractRound): + parent_page_types = ['funds.RequestForPartners'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.sealed = True + + class LabType(EmailForm, WorkflowStreamForm, SubmittableStreamForm): # type: ignore class Meta: verbose_name = _("Lab") diff --git a/opentech/apply/home/models.py b/opentech/apply/home/models.py index 41c079730c84e08dd0b2a5d96002bb43ee63658b..cc25885c0e7d5f44e0a912f33083602c665684d8 100644 --- a/opentech/apply/home/models.py +++ b/opentech/apply/home/models.py @@ -8,7 +8,7 @@ from django.db import models class ApplyHomePage(Page): # Only allow creating HomePages at the root level parent_page_types = ['wagtailcore.Page'] - subpage_types = ['funds.FundType', 'funds.LabType'] + subpage_types = ['funds.FundType', 'funds.LabType', 'funds.RequestForPartners'] strapline = models.CharField(blank=True, max_length=255)