diff --git a/opentech/apply/funds/migrations/0060_prepare_assigned_reviewers_for_data_migration.py b/opentech/apply/funds/migrations/0060_prepare_assigned_reviewers_for_data_migration.py new file mode 100644 index 0000000000000000000000000000000000000000..bfa578d937545983c378b2352dfcb116be045edf --- /dev/null +++ b/opentech/apply/funds/migrations/0060_prepare_assigned_reviewers_for_data_migration.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.9 on 2019-02-24 20:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0009_alter_user_last_name_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('funds', '0059_add_community_review_workflow'), + ] + + operations = [ + migrations.AddField( + model_name='assignedreviewers', + name='type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='auth.Group'), + ), + migrations.AlterField( + model_name='assignedreviewers', + name='reviewer', + field=models.ForeignKey(limit_choices_to={'groups__name__in': ['Staff', 'Reviewer', 'Community reviewer', 'Partner']}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='assignedreviewers', + unique_together={('submission', 'role'), ('submission', 'reviewer')}, + ), + ] diff --git a/opentech/apply/funds/migrations/0061_data_migrate_type_for_assigned_reviewers.py b/opentech/apply/funds/migrations/0061_data_migrate_type_for_assigned_reviewers.py new file mode 100644 index 0000000000000000000000000000000000000000..c61f59cf408df5d9f601bf50adc39fb52c373cc7 --- /dev/null +++ b/opentech/apply/funds/migrations/0061_data_migrate_type_for_assigned_reviewers.py @@ -0,0 +1,52 @@ +# Generated by Django 2.0.9 on 2019-02-24 20:10 + +from django.db import migrations + +# Copied from opentech.apply.users.groups at time of migration to avoid +# importing and creating a future dependency. Changes to Group names should +# be handled in another migration + +STAFF_GROUP_NAME = 'Staff' +REVIEWER_GROUP_NAME = 'Reviewer' +PARTNER_GROUP_NAME = 'Partner' +COMMUNITY_REVIEWER_GROUP_NAME = 'Community reviewer' + +REVIEWER_GROUPS = set([ + STAFF_GROUP_NAME, + REVIEWER_GROUP_NAME, + COMMUNITY_REVIEWER_GROUP_NAME, + PARTNER_GROUP_NAME, +]) + + +def add_reviewer_type(apps, schema_editor): + AssignedReviewer = apps.get_model('funds', 'AssignedReviewers') + Group = apps.get_model('auth', 'Group') + for assigned in AssignedReviewer.objects.prefetch_related('reviewer__groups'): + groups = set(assigned.reviewer.groups.values_list('name', flat=True)) & REVIEWER_GROUPS + if len(groups) > 1: + if PARTNER_GROUP_NAME in groups and assigned.reviewer in assigned.submission.partners.all(): + groups = {PARTNER_GROUP_NAME} + elif COMMUNITY_REVIEWER_GROUP_NAME in groups: + groups = {COMMUNITY_REVIEWER_GROUP_NAME} + elif assigned.reviewer.is_staff: + groups = {STAFF_GROUP_NAME} + else: + groups = {REVIEWER_GROUP_NAME} + elif not groups: + groups = {REVIEWER_GROUP_NAME} + + group = Group.objects.get(name=groups.pop()) + assigned.type = group + assigned.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0060_prepare_assigned_reviewers_for_data_migration'), + ] + + operations = [ + migrations.RunPython(add_reviewer_type, migrations.RunPython.noop), + ] diff --git a/opentech/apply/funds/migrations/0062_make_reviewer_type_required.py b/opentech/apply/funds/migrations/0062_make_reviewer_type_required.py new file mode 100644 index 0000000000000000000000000000000000000000..a487faf759388d0c9bb21e598b1d05085e9d6079 --- /dev/null +++ b/opentech/apply/funds/migrations/0062_make_reviewer_type_required.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.9 on 2019-02-24 20:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0061_data_migrate_type_for_assigned_reviewers'), + ] + + operations = [ + migrations.AlterField( + model_name='assignedreviewers', + name='type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='auth.Group'), + ), + ] diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 5f2a9027306d1e7b2e89eb612be34e6d64d8dd50..6cbad915efb432ed5da2d92eed75df2c41f4e029 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -28,7 +28,7 @@ from opentech.apply.stream_forms.files import StreamFieldDataEncoder from opentech.apply.stream_forms.models import BaseStreamForm from .mixins import AccessFormData -from .utils import LIMIT_TO_STAFF, LIMIT_TO_STAFF_AND_REVIEWERS, LIMIT_TO_PARTNERS, WorkflowHelpers +from .utils import LIMIT_TO_STAFF, LIMIT_TO_REVIEWER_GROUPS, LIMIT_TO_PARTNERS, WorkflowHelpers from ..blocks import ApplicationCustomFormFieldsBlock, NAMED_BLOCKS from ..workflow import ( active_statuses, @@ -744,7 +744,11 @@ class AssignedReviewers(models.Model): reviewer = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - limit_choices_to=LIMIT_TO_STAFF_AND_REVIEWERS, + limit_choices_to=LIMIT_TO_REVIEWER_GROUPS, + ) + type = models.ForeignKey( + 'auth.Group', + on_delete=models.PROTECT, ) submission = models.ForeignKey( ApplicationSubmission, @@ -761,7 +765,7 @@ class AssignedReviewers(models.Model): objects = AssignedReviewersQuerySet.as_manager() class Meta: - unique_together = ('submission', 'role') + unique_together = (('submission', 'role'), ('submission', 'reviewer')) def __str__(self): return f'{self.reviewer} as {self.role}' diff --git a/opentech/apply/funds/models/utils.py b/opentech/apply/funds/models/utils.py index c6c8ecb6153718edac9468dd0848c472888d23d1..6ff742f0168dbd106fe18007ba48bfbb537f5b83 100644 --- a/opentech/apply/funds/models/utils.py +++ b/opentech/apply/funds/models/utils.py @@ -19,9 +19,15 @@ from ..workflow import WORKFLOWS 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} +LIMIT_TO_REVIEWER_GROUPS = {'groups__name__in': [ + STAFF_GROUP_NAME, + REVIEWER_GROUP_NAME, + COMMUNITY_REVIEWER_GROUP_NAME, + PARTNER_GROUP_NAME, +]} + def admin_url(page): diff --git a/opentech/apply/review/migrations/0017_add_temp_author_field.py b/opentech/apply/review/migrations/0017_add_temp_author_field.py new file mode 100644 index 0000000000000000000000000000000000000000..7a5e44a4a21586842ed31bf1b084817298aca6d5 --- /dev/null +++ b/opentech/apply/review/migrations/0017_add_temp_author_field.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.9 on 2019-02-24 03:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0016_review_visibility'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='author_temp', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='review', to='funds.AssignedReviewers', null=True), + ), + migrations.AddField( + model_name='reviewopinion', + name='author_temp', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opinions', to='funds.AssignedReviewers', null=True), + ), + + ] diff --git a/opentech/apply/review/migrations/0018_migrate_author_data.py b/opentech/apply/review/migrations/0018_migrate_author_data.py new file mode 100644 index 0000000000000000000000000000000000000000..d92a25e3c5ed6ed87fced9690045a6461801b9c7 --- /dev/null +++ b/opentech/apply/review/migrations/0018_migrate_author_data.py @@ -0,0 +1,84 @@ +# Generated by Django 2.0.9 on 2019-02-24 03:16 + +from django.db import migrations +from django.core.exceptions import ObjectDoesNotExist + +# Copied from opentech.apply.users.groups at time of migration to avoid +# importing and creating a future dependency. Changes to Group names should +# be handled in another migration + +STAFF_GROUP_NAME = 'Staff' +REVIEWER_GROUP_NAME = 'Reviewer' +PARTNER_GROUP_NAME = 'Partner' +COMMUNITY_REVIEWER_GROUP_NAME = 'Community reviewer' + +REVIEWER_GROUPS = set([ + STAFF_GROUP_NAME, + REVIEWER_GROUP_NAME, + COMMUNITY_REVIEWER_GROUP_NAME, + PARTNER_GROUP_NAME, +]) + + +def add_to_assigned_reviewers(apps, schema_editor): + Review = apps.get_model('review', 'Review') + AssignedReviewer = apps.get_model('funds', 'AssignedReviewers') + Group = apps.get_model('auth', 'Group') + for review in Review.objects.select_related('author'): + groups = set(review.author.groups.values_list('name', flat=True)) & REVIEWER_GROUPS + if len(groups) > 1: + if PARTNER_GROUP_NAME in groups and review.author in review.submission.partners.all(): + groups = {PARTNER_GROUP_NAME} + elif COMMUNITY_REVIEWER_GROUP_NAME in groups: + groups = {COMMUNITY_REVIEWER_GROUP_NAME} + elif review.author.is_staff: + groups = {STAFF_GROUP_NAME} + else: + groups = {REVIEWER_GROUP_NAME} + elif not groups: + groups = {REVIEWER_GROUP_NAME} + + group = Group.objects.get(name=groups.pop()) + + assignment, _ = AssignedReviewer.objects.update_or_create( + submission=review.submission, + reviewer=review.author, + type=group, + ) + review.author_temp = assignment + review.save() + for opinion in review.opinions.select_related('author'): + opinion_assignment, _ = AssignedReviewer.objects.update_or_create( + submission=review.submission, + reviewer=opinion.author, + type=Group.objects.get(name=STAFF_GROUP_NAME), + ) + opinion.author_temp = opinion_assignment + opinion.save() + + +def add_to_review_and_opinion(apps, schema_editor): + AssignedReviewer = apps.get_model('funds', 'AssignedReviewers') + for assigned in AssignedReviewer.objects.all(): + try: + assigned.review.author = assigned.reviewer + assigned.review.save() + except ObjectDoesNotExist: + pass + if assigned.opinions.exists(): + for opinion in assigned.opinions.all(): + opinion.author = assigned.reviewer + opinion.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0017_add_temp_author_field'), + ('funds', '0062_make_reviewer_type_required'), + ('users', '0010_add_community_reviewer_group'), + ] + + operations = [ + migrations.RunPython(add_to_assigned_reviewers, add_to_review_and_opinion), + ] diff --git a/opentech/apply/review/migrations/0019_replace_existing_author_field.py b/opentech/apply/review/migrations/0019_replace_existing_author_field.py new file mode 100644 index 0000000000000000000000000000000000000000..a5de51876c95f40b2c9854838c8603c315166307 --- /dev/null +++ b/opentech/apply/review/migrations/0019_replace_existing_author_field.py @@ -0,0 +1,42 @@ +# Generated by Django 2.0.9 on 2019-02-24 03:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('review', '0018_migrate_author_data'), + ] + + operations = [ + migrations.RemoveField( + model_name='review', + name='author', + ), + migrations.RenameField( + model_name='review', + old_name='author_temp', + new_name='author', + ), + migrations.AlterField( + model_name='review', + name='author', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='review', to='funds.AssignedReviewers'), + ), + migrations.RemoveField( + model_name='reviewopinion', + name='author', + ), + migrations.RenameField( + model_name='reviewopinion', + old_name='author_temp', + new_name='author', + ), + migrations.AlterField( + model_name='reviewopinion', + name='author', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opinions', to='funds.AssignedReviewers'), + ), + ] diff --git a/opentech/apply/review/models.py b/opentech/apply/review/models.py index b604831a529c97d20a1fed04ddaa25e6f26b2b8d..c7de2c3599a36904b150895466eae8002d8248bf 100644 --- a/opentech/apply/review/models.py +++ b/opentech/apply/review/models.py @@ -121,9 +121,10 @@ class ReviewQuerySet(models.QuerySet): class Review(ReviewFormFieldsMixin, BaseStreamForm, AccessFormData, models.Model): submission = models.ForeignKey('funds.ApplicationSubmission', on_delete=models.CASCADE, related_name='reviews') revision = models.ForeignKey('funds.ApplicationRevision', on_delete=models.SET_NULL, related_name='reviews', null=True) - author = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.PROTECT, + author = models.OneToOneField( + 'funds.AssignedReviewers', + related_name='review', + on_delete=models.CASCADE, ) form_data = JSONField(default=dict, encoder=DjangoJSONEncoder) @@ -190,8 +191,9 @@ def update_submission_reviewers_list(sender, **kwargs): class ReviewOpinion(models.Model): review = models.ForeignKey(Review, on_delete=models.CASCADE, related_name='opinions') author = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.PROTECT, + 'funds.AssignedReviewers', + related_name='opinions', + on_delete=models.CASCADE, ) opinion = models.IntegerField(choices=OPINION_CHOICES)