Newer
Older
from functools import partial
from itertools import groupby
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe
from opentech.apply.categories.models import MetaTerm
from opentech.apply.users.models import User
from .models import AssignedReviewers, ApplicationSubmission, ReviewerRole
from .widgets import Select2MultiCheckboxesWidget, MetaTermSelect2Widget
from .workflow import get_action_mapping
class ApplicationSubmissionModelForm(forms.ModelForm):
"""
Application Submission model's save method performs several operations
which are not required in forms which update fields like status, partners etc.
It also has a side effect of creating a new file uploads every time with long filenames (#1572).
"""
def save(self, commit=True):
"""
Save this form's self.instance object if commit=True. Otherwise, add
a save_m2m() method to the form which can be called after the instance
is saved manually at a later time. Return the model instance.
https://github.com/django/django/blob/5d9cf79baf07fc4aed7ad1b06990532a65378155/django/forms/models.py#L444
"""
if self.errors:
raise ValueError(
"The %s could not be %s because the data didn't validate." % (
self.instance._meta.object_name,
'created' if self.instance._state.adding else 'changed',
)
)
if commit:
# If committing, save the instance and the m2m data immediately.
self.instance.save(skip_custom=True)
self._save_m2m()
else:
# If not committing, add a method to the form to allow deferred
# saving of m2m data.
self.save_m2m = self._save_m2m
return self.instance
class ProgressSubmissionForm(ApplicationSubmissionModelForm):
action = forms.ChoiceField(label='Take action')
self.user = kwargs.pop('user')
choices = list(self.instance.get_actions_for_user(self.user))
# Sort the transitions by the order they are listed.
sort_by = list(self.instance.phase.transitions.keys())
choices.sort(key=lambda k: sort_by.index(k[0]))
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']
class ScreeningSubmissionForm(ApplicationSubmissionModelForm):
Erin Mullaney
committed
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(ApplicationSubmissionModelForm):
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)
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class BatchUpdateSubmissionLeadForm(forms.Form):
lead = forms.ChoiceField(label='Lead')
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)
self.fields['lead'].choices = [(staff.id, staff) for staff in User.objects.staff()]
def clean_lead(self):
value = self.cleaned_data['lead']
return User.objects.get(id=value)
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 save(self):
new_lead = self.cleaned_data['lead']
submissions = self.cleaned_data['submissions']
for submission in submissions:
# Onle save if the lead has changed.
if submission.lead != new_lead:
submission.lead = new_lead
submission.save()
return None
class UpdateReviewersForm(ApplicationSubmissionModelForm):
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)
else:
AssignedReviewers.objects.filter(role=role, submission=instance).delete()
# 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)
else:
for submission in submissions:
AssignedReviewers.objects.filter(role=role, submission=submission).delete()
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,
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(ApplicationSubmissionModelForm):
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 UpdateMetaTermsForm(ApplicationSubmissionModelForm):
meta_terms = GroupedModelMultipleChoiceField(
queryset=None, # updated in init method
widget=MetaTermSelect2Widget(attrs={'data-placeholder': 'Meta terms'}),
label='Meta terms',
required=False,
help_text='Meta terms are hierarchical in nature.',
)
class Meta:
model = ApplicationSubmission
fields: list = []
def __init__(self, *args, **kwargs):
kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['meta_terms'].queryset = MetaTerm.get_root_descendants().exclude(depth=2)