diff --git a/opentech/apply/funds/migrations/0059_add_community_review_workflow.py b/opentech/apply/funds/migrations/0059_add_community_review_workflow.py new file mode 100644 index 0000000000000000000000000000000000000000..81aaae5d5646e6decc021559061a9d4874d30914 --- /dev/null +++ b/opentech/apply/funds/migrations/0059_add_community_review_workflow.py @@ -0,0 +1,33 @@ +# Generated by Django 2.0.10 on 2019-03-25 13:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0058_add_group_toggle'), + ] + + operations = [ + migrations.AlterField( + model_name='applicationbase', + name='workflow_name', + field=models.CharField(choices=[('single', 'Request'), ('single_ext', 'Request with external review'), ('single_com', 'Request with community review'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow'), + ), + migrations.AlterField( + model_name='applicationsubmission', + name='workflow_name', + field=models.CharField(choices=[('single', 'Request'), ('single_ext', 'Request with external review'), ('single_com', 'Request with community review'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow'), + ), + migrations.AlterField( + model_name='labbase', + name='workflow_name', + field=models.CharField(choices=[('single', 'Request'), ('single_ext', 'Request with external review'), ('single_com', 'Request with community review'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow'), + ), + migrations.AlterField( + model_name='roundbase', + name='workflow_name', + field=models.CharField(choices=[('single', 'Request'), ('single_ext', 'Request with external review'), ('single_com', 'Request with community review'), ('double', 'Concept & Proposal')], default='single', max_length=100, verbose_name='Workflow'), + ), + ] diff --git a/opentech/apply/funds/models/utils.py b/opentech/apply/funds/models/utils.py index bae4ce5fbd2768cb7dae0eace3143b956773fb58..c6c8ecb6153718edac9468dd0848c472888d23d1 100644 --- a/opentech/apply/funds/models/utils.py +++ b/opentech/apply/funds/models/utils.py @@ -12,7 +12,7 @@ from wagtail.contrib.forms.models import AbstractEmailForm from opentech.apply.activity.messaging import messenger, MESSAGES from opentech.apply.stream_forms.models import AbstractStreamForm -from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, PARTNER_GROUP_NAME +from opentech.apply.users.groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, PARTNER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME from ..workflow import WORKFLOWS @@ -21,6 +21,7 @@ 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]} LIMIT_TO_PARTNERS = {'groups__name': PARTNER_GROUP_NAME} +LIMIT_TO_COMMUNITY_REVIEWERS = {'groups__name': COMMUNITY_REVIEWER_GROUP_NAME} def admin_url(page): diff --git a/opentech/apply/funds/workflow.py b/opentech/apply/funds/workflow.py index 6380e1b5437d74252fbaacc56fdbb71501fee910..c28f764fd933c8997c73ed14df204c1745873bcb 100644 --- a/opentech/apply/funds/workflow.py +++ b/opentech/apply/funds/workflow.py @@ -70,6 +70,7 @@ class Phase: public_name = phase name displayed to applicants in the system future_name = phase_name displayed to applicants if they haven't passed this stage """ + def __init__(self, name, display, stage, permissions, step, public=None, future=None, transitions=dict()): self.name = name self.display_name = display @@ -142,7 +143,9 @@ applicant_can = lambda user: user.is_applicant # NOQA reviewer_can = lambda user: user.is_reviewer # NOQA -partner_can = lambda user: user.is_partner # NOQA +partner_can = lambda user: user.is_partner # NOQA + +community_can = lambda user: user.is_community_reviewer # NOQA def make_permissions(edit=list(), review=list(), view=[staff_can, applicant_can, reviewer_can]): @@ -161,6 +164,8 @@ hidden_from_applicant_permissions = make_permissions(edit=[staff_can], review=[s reviewer_review_permissions = make_permissions(edit=[staff_can, partner_can], review=[staff_can, reviewer_can, partner_can]) +community_review_permissions = make_permissions(edit=[staff_can], review=[staff_can, reviewer_can, community_can]) + applicant_edit_permissions = make_permissions(edit=[applicant_can], review=[staff_can]) staff_applicant_edit_permissions = make_permissions(edit=[staff_can, applicant_can]) @@ -172,6 +177,8 @@ Request = Stage('Request', False) RequestExt = Stage('RequestExt', True) +RequestCom = Stage('RequestCom', True) + Concept = Stage('Concept', False) Proposal = Stage('Proposal', True) @@ -400,6 +407,154 @@ SingleStageExternalDefinition = [ ] +SingleStageCommunityDefinition = [ + { + INITIAL_STATE: { + 'transitions': { + 'com_internal_review': 'Open Review', + 'com_open_call': 'Open Call (public)', + 'com_community_review': 'Open Community Review', + 'com_rejected': 'Dismiss', + 'com_more_info': 'Request More Information', + 'com_determination': 'Ready For Determination', + }, + 'display': 'Screening', + 'public': 'Application Received', + 'stage': RequestCom, + 'permissions': default_permissions, + }, + 'com_more_info': { + 'transitions': { + INITIAL_STATE: { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestCom, + 'permissions': applicant_edit_permissions, + }, + 'com_open_call': { + 'transitions': { + 'com_rejected': 'Dismiss', + 'com_more_info': 'Request More Information', + }, + 'display': 'Open Call (public)', + 'stage': RequestCom, + 'permissions': staff_edit_permissions, + }, + }, + { + 'com_community_review': { + 'transitions': { + 'com_internal_review': 'Open Review', + 'com_rejected': 'Dismiss', + }, + 'display': 'Community Review', + 'public': 'OTF Review', + 'stage': RequestCom, + 'permissions': community_review_permissions, + }, + }, + { + 'com_internal_review': { + 'transitions': { + 'com_post_review_discussion': 'Close Review', + }, + 'display': 'Internal Review', + 'public': 'OTF Review', + 'stage': RequestCom, + 'permissions': default_permissions, + }, + }, + { + 'com_post_review_discussion': { + 'transitions': { + 'com_external_review': 'Open AC review', + 'com_rejected': 'Dismiss', + 'com_post_review_more_info': 'Request More Information', + 'com_determination': 'Ready For Determination', + }, + 'display': 'Ready For Discussion', + 'stage': RequestCom, + 'permissions': hidden_from_applicant_permissions, + }, + 'com_post_review_more_info': { + 'transitions': { + 'com_post_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestCom, + 'permissions': applicant_edit_permissions, + }, + }, + { + 'com_external_review': { + 'transitions': { + 'com_post_external_review_discussion': 'Close Review', + }, + 'display': 'Advisory Council Review', + 'stage': RequestCom, + 'permissions': reviewer_review_permissions, + }, + }, + { + 'com_post_external_review_discussion': { + 'transitions': { + 'com_accepted': 'Accept', + 'com_rejected': 'Dismiss', + 'com_post_external_review_more_info': 'Request More Information', + 'com_determination': 'Ready For Determination', + }, + 'display': 'Ready For Discussion', + 'stage': RequestCom, + 'permissions': hidden_from_applicant_permissions, + }, + 'com_post_external_review_more_info': { + 'transitions': { + 'com_post_external_review_discussion': { + 'display': 'Submit', + 'permissions': {UserPermissions.APPLICANT, UserPermissions.STAFF, UserPermissions.LEAD, UserPermissions.ADMIN}, + 'method': 'create_revision', + }, + }, + 'display': 'More information required', + 'stage': RequestCom, + 'permissions': applicant_edit_permissions, + }, + }, + { + 'com_determination': { + 'transitions': { + 'com_accepted': 'Accept', + 'com_rejected': 'Dismiss', + }, + 'display': 'Ready for Determination', + 'permissions': hidden_from_applicant_permissions, + 'stage': RequestCom, + }, + }, + { + 'com_accepted': { + 'display': 'Accepted', + 'future': 'Application Outcome', + 'stage': RequestCom, + 'permissions': staff_applicant_edit_permissions, + }, + 'com_rejected': { + 'display': 'Dismissed', + 'stage': RequestCom, + 'permissions': no_permissions, + }, + }, +] + + DoubleStageDefinition = [ { INITIAL_STATE: { @@ -660,12 +815,15 @@ Request = Workflow('Request', 'single', **phase_data(SingleStageDefinition)) RequestExternal = Workflow('Request with external review', 'single_ext', **phase_data(SingleStageExternalDefinition)) +RequestCommunity = Workflow('Request with community review', 'single_com', **phase_data(SingleStageCommunityDefinition)) + ConceptProposal = Workflow('Concept & Proposal', 'double', **phase_data(DoubleStageDefinition)) WORKFLOWS = { Request.admin_name: Request, RequestExternal.admin_name: RequestExternal, + RequestCommunity.admin_name: RequestCommunity, ConceptProposal.admin_name: ConceptProposal, } @@ -733,6 +891,7 @@ DETERMINATION_RESPONSE_PHASES = [ 'concept_review_discussion', 'post_external_review_discussion', 'ext_post_external_review_discussion', + 'com_post_external_review_discussion', ] @@ -817,3 +976,7 @@ PHASES_MAPPING = { 'statuses': phases_matching('rejected'), }, } + +OPEN_CALL_PHASES = [ + 'com_open_call', +] diff --git a/opentech/apply/users/groups.py b/opentech/apply/users/groups.py index a3e7bbe881d062c04a225103d1446362cc84cdc9..31887461c21ed6b096231790b4d80c21d0c7aad1 100644 --- a/opentech/apply/users/groups.py +++ b/opentech/apply/users/groups.py @@ -2,6 +2,7 @@ STAFF_GROUP_NAME = 'Staff' REVIEWER_GROUP_NAME = 'Reviewer' TEAMADMIN_GROUP_NAME = 'Team Admin' PARTNER_GROUP_NAME = 'Partner' +COMMUNITY_REVIEWER_GROUP_NAME = 'Community reviewer' GROUPS = [ { @@ -20,4 +21,8 @@ GROUPS = [ 'name': PARTNER_GROUP_NAME, 'permissions': [], }, + { + 'name': COMMUNITY_REVIEWER_GROUP_NAME, + 'permissions': [], + }, ] diff --git a/opentech/apply/users/migrations/0010_add_community_reviewer_group.py b/opentech/apply/users/migrations/0010_add_community_reviewer_group.py new file mode 100644 index 0000000000000000000000000000000000000000..a105e64e80b867eae4fec7f91b0bde6864e84466 --- /dev/null +++ b/opentech/apply/users/migrations/0010_add_community_reviewer_group.py @@ -0,0 +1,42 @@ + +# Generated by Django 2.0.9 on 2018-12-19 13:21 +from __future__ import unicode_literals + +from django.core.exceptions import ObjectDoesNotExist +from django.core.management.sql import emit_post_migrate_signal +from django.db import migrations + +from opentech.apply.users.groups import GROUPS, COMMUNITY_REVIEWER_GROUP_NAME + + +def add_groups(apps, schema_editor): + # Workaround for https://code.djangoproject.com/ticket/23422 + db_alias = schema_editor.connection.alias + emit_post_migrate_signal(2, False, db_alias) + + Group = apps.get_model('auth.Group') + Permission = apps.get_model('auth.Permission') + + for group_data in GROUPS: + group, created = Group.objects.get_or_create(name=group_data['name']) + for permission in group_data['permissions']: + try: + group.permissions.add(Permission.objects.get(codename=permission)) + except ObjectDoesNotExist: + print("Could not find the '%s' permission" % permission) + + +def remove_groups(apps, schema_editor): + Group = apps.get_model('auth.Group') + Group.objects.filter(name__in=[COMMUNITY_REVIEWER_GROUP_NAME]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_add_partner_group'), + ] + + operations = [ + migrations.RunPython(add_groups, remove_groups) + ] diff --git a/opentech/apply/users/models.py b/opentech/apply/users/models.py index 3d819bd2885b47b06d50d651d5742b222376f400..3752a5761040f3ce1fc3e66ce5cab329e592e653 100644 --- a/opentech/apply/users/models.py +++ b/opentech/apply/users/models.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import AbstractUser, BaseUserManager from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from .groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, PARTNER_GROUP_NAME +from .groups import REVIEWER_GROUP_NAME, STAFF_GROUP_NAME, PARTNER_GROUP_NAME, COMMUNITY_REVIEWER_GROUP_NAME from .utils import send_activation_email @@ -20,6 +20,9 @@ class UserQuerySet(models.QuerySet): def partners(self): return self.filter(groups__name=PARTNER_GROUP_NAME) + def community_reviewers(self): + return self.filter(groups__name=COMMUNITY_REVIEWER_GROUP_NAME) + class UserManager(BaseUserManager.from_queryset(UserQuerySet)): use_in_migrations = True @@ -104,6 +107,10 @@ class User(AbstractUser): def is_partner(self): return self.groups.filter(name=PARTNER_GROUP_NAME).exists() + @cached_property + def is_community_reviewer(self): + return self.groups.filter(name=COMMUNITY_REVIEWER_GROUP_NAME).exists() + @cached_property def is_applicant(self): return not self.is_apply_staff and not self.is_reviewer and not self.is_partner