Newer
Older
from functools import partial
from itertools import groupby
from operator import attrgetter, methodcaller
from django.utils.text import mark_safe, slugify
from django.utils.translation import ugettext_lazy as _
from django_select2.forms import Select2Widget
from opentech.apply.categories.models import MetaCategory
from opentech.apply.users.models import User
from .models import AssignedReviewers, ApplicationSubmission, ReviewerRole
from .utils import render_icon
from .widgets import Select2MultiCheckboxesWidget, MetaCategorySelect2Widget
from .workflow import get_action_mapping
action = forms.ChoiceField(label='Take action')
self.user = kwargs.pop('user')
choices = list(self.instance.get_actions_for_user(self.user))
action_field = self.fields['action']
action_field.choices = choices
class BatchProgressSubmissionForm(forms.Form):
action = forms.ChoiceField(label='Take action')
submissions = forms.CharField(widget=forms.HiddenInput(attrs={'class': 'js-submissions-id'}))
def __init__(self, *args, round=None, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
workflow = round and round.workflow
self.action_mapping = get_action_mapping(workflow)
choices = [(action, detail['display']) for action, detail in self.action_mapping.items()]
self.fields['action'].choices = choices
def clean_submissions(self):
value = self.cleaned_data['submissions']
submission_ids = [int(submission) for submission in value.split(',')]
return ApplicationSubmission.objects.filter(id__in=submission_ids)
def clean_action(self):
value = self.cleaned_data['action']
action = self.action_mapping[value]['transitions']
Erin Mullaney
committed
class ScreeningSubmissionForm(forms.ModelForm):
class Meta:
model = ApplicationSubmission
Erin Mullaney
committed
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.should_show = False
Fredrik Jonsson
committed
if self.user.is_apply_staff:
self.should_show = True
Erin Mullaney
committed
class UpdateSubmissionLeadForm(forms.ModelForm):
class Meta:
model = ApplicationSubmission
fields = ('lead',)
def __init__(self, *args, **kwargs):
lead_field = self.fields['lead']
lead_field.label = f'Update lead from { self.instance.lead } to'
lead_field.queryset = lead_field.queryset.exclude(id=self.instance.lead.id)
class UpdateReviewersForm(forms.ModelForm):
reviewer_reviewers = forms.ModelMultipleChoiceField(
queryset=User.objects.reviewers().only('pk', 'full_name'),
widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Reviewers'}),
label='Reviewers',
required=False,
)
class Meta:
model = ApplicationSubmission
fields: list = []
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
assigned_roles = {
assigned.role: assigned.reviewer
for assigned in self.instance.assigned.filter(
role__isnull=False
)
}
self.role_fields = {}
field_data = make_role_reviewer_fields()
for data in field_data:
field_name = data['field_name']
self.fields[field_name] = data['field']
self.role_fields[field_name] = data['role']
self.fields[field_name].initial = assigned_roles.get(data['role'])
submitted_reviewers = User.objects.filter(
id__in=self.instance.assigned.reviewed().values('reviewer'),
)
if self.can_alter_external_reviewers(self.instance, self.user):
reviewers = self.instance.reviewers.all().only('pk')
self.prepare_field(
'reviewer_reviewers',
initial=reviewers,
# Move the non-role reviewers field to the end of the field list
self.fields.move_to_end('reviewer_reviewers')
else:
self.fields.pop('reviewer_reviewers')
def prepare_field(self, field_name, initial, excluded):
field = self.fields[field_name]
field.queryset = field.queryset.exclude(id__in=excluded)
field.initial = initial
def can_alter_external_reviewers(self, instance, user):
return instance.stage.has_external_review and (user == instance.lead or user.is_superuser)
role_reviewers = [
user
for field, user in self.cleaned_data.items()
if field in self.role_fields
# If any of the users match and are set to multiple roles, throw an error
if len(role_reviewers) != len(set(role_reviewers)) and any(role_reviewers):
self.add_error(None, _('Users cannot be assigned to multiple roles.'))
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
"""
1. Update role reviewers
2. Update non-role reviewers
2a. Remove those not on form
2b. Add in any new non-role reviewers selected
"""
# 1. Update role reviewers
assigned_roles = {
role: self.cleaned_data[field]
for field, role in self.role_fields.items()
for role, reviewer in assigned_roles.items():
if reviewer:
AssignedReviewers.objects.update_role(role, reviewer, instance)
# 2. Update non-role reviewers
# 2a. Remove those not on form
if self.can_alter_external_reviewers(self.instance, self.user):
reviewers = self.cleaned_data.get('reviewer_reviewers')
assigned_reviewers = instance.assigned.without_roles()
assigned_reviewers.never_tried_to_review().exclude(
reviewer__in=reviewers
remaining_reviewers = assigned_reviewers.values_list('reviewer_id', flat=True)
# 2b. Add in any new non-role reviewers selected
AssignedReviewers.objects.bulk_create_reviewers(
[reviewer for reviewer in reviewers if reviewer.id not in remaining_reviewers],
instance,
return instance
Erin Mullaney
committed
class BatchUpdateReviewersForm(forms.Form):
submissions = forms.CharField(widget=forms.HiddenInput(attrs={'class': 'js-submissions-id'}))
def __init__(self, *args, user=None, round=None, **kwargs):
super().__init__(*args, **kwargs)
self.role_fields = {}
field_data = make_role_reviewer_fields()
for data in field_data:
field_name = data['field_name']
self.fields[field_name] = data['field']
self.role_fields[field_name] = data['role']
def clean_submissions(self):
value = self.cleaned_data['submissions']
submission_ids = [int(submission) for submission in value.split(',')]
return ApplicationSubmission.objects.filter(id__in=submission_ids)
def clean(self):
cleaned_data = super().clean()
role_reviewers = [
user
for field, user in self.cleaned_data.items()
if field in self.role_fields
]
# If any of the users match and are set to multiple roles, throw an error
if len(role_reviewers) != len(set(role_reviewers)) and any(role_reviewers):
self.add_error(None, _('Users cannot be assigned to multiple roles.'))
return cleaned_data
def save(self):
submissions = self.cleaned_data['submissions']
assigned_roles = {
role: self.cleaned_data[field]
for field, role in self.role_fields.items()
}
for role, reviewer in assigned_roles.items():
if reviewer:
AssignedReviewers.objects.update_role(role, reviewer, *submissions)
def make_role_reviewer_fields():
role_fields = []
staff_reviewers = User.objects.staff().only('full_name', 'pk')
for role in ReviewerRole.objects.all().order_by('order'):
field_name = 'role_reviewer_' + slugify(str(role))
field = forms.ModelChoiceField(
queryset=staff_reviewers,
widget=Select2Widget(attrs={
'data-placeholder': 'Select a reviewer',
}),
required=False,
label=mark_safe(render_icon(role.icon) + f'{role.name} Reviewer'),
)
role_fields.append({
'role': role,
'field': field,
'field_name': field_name,
})
return role_fields
class UpdatePartnersForm(forms.ModelForm):
partner_reviewers = forms.ModelMultipleChoiceField(
queryset=User.objects.partners(),
widget=Select2MultiCheckboxesWidget(attrs={'data-placeholder': 'Partners'}),
label='Partners',
required=False,
)
class Meta:
model = ApplicationSubmission
fields: list = []
def __init__(self, *args, **kwargs):
kwargs.pop('user')
super().__init__(*args, **kwargs)
partners = self.instance.partners.all()
self.submitted_partners = User.objects.partners().filter(id__in=self.instance.reviews.values('author'))
partner_field = self.fields['partner_reviewers']
partner_field.queryset = partner_field.queryset.exclude(id__in=self.submitted_partners)
partner_field.initial = partners
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
instance.partners.set(
self.cleaned_data['partner_reviewers'] |
self.submitted_partners
)
return instance
class GroupedModelChoiceIterator(forms.models.ModelChoiceIterator):
def __init__(self, field, groupby):
self.groupby = groupby
super().__init__(field)
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
queryset = self.queryset
# Can't use iterator() when queryset uses prefetch_related()
if not queryset._prefetch_related_lookups:
queryset = queryset.iterator()
for group, objs in groupby(queryset, self.groupby):
yield (group, [self.choice(obj) for obj in objs])
class GroupedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, *args, choices_groupby, **kwargs):
if isinstance(choices_groupby, str):
choices_groupby = methodcaller(choices_groupby)
elif not callable(choices_groupby):
raise TypeError('choices_groupby must either be a str or a callable accepting a single argument')
self.iterator = partial(GroupedModelChoiceIterator, groupby=choices_groupby)
super().__init__(*args, **kwargs)
def label_from_instance(self, obj):
return {'label': super().label_from_instance(obj), 'disabled': not obj.is_leaf()}
class UpdateMetaCategoriesForm(forms.ModelForm):
meta_categories = GroupedModelMultipleChoiceField(
queryset=None, # updated in init method
widget=MetaCategorySelect2Widget(attrs={'data-placeholder': 'Meta categories'}),
label='Meta categories',
required=False,
help_text='Meta categories are hierarchical in nature, - highlights the depth in the tree.',
)
class Meta:
model = ApplicationSubmission
fields: list = []
def __init__(self, *args, **kwargs):
kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['meta_categories'].queryset = MetaCategory.get_root_descendants().exclude(depth=2)