Filter the tasks dropdown by the chosen workflow

pull/6257/head
Matt Westcott 2020-04-23 19:53:40 +01:00 zatwierdzone przez Matt Westcott
rodzic c0d2dd7aea
commit 8b1a304533
3 zmienionych plików z 220 dodań i 1 usunięć

Wyświetl plik

@ -3,7 +3,8 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from django_filters.widgets import SuffixedMultiWidget
from wagtail.core.models import Page, TaskState, Workflow, WorkflowState
from wagtail.admin.staticfiles import versioned_static
from wagtail.core.models import Page, Task, TaskState, Workflow, WorkflowState
from .widgets import AdminDateInput
@ -67,6 +68,128 @@ class DateRangePickerWidget(SuffixedMultiWidget):
return [None, None]
class FilteredSelect(forms.Select):
"""
A select box variant that adds 'data-' attributes to the <select> and <option> elements
to allow the options to be dynamically filtered by another select box.
See wagtailadmin/js/filtered-select.js for an example of how these attributes are configured.
"""
def __init__(self, attrs=None, choices=(), filter_field=''):
super().__init__(attrs, choices)
self.filter_field = filter_field
def build_attrs(self, base_attrs, extra_attrs=None):
my_attrs = {
'data-widget': 'filtered-select',
'data-filter-field': self.filter_field,
}
if extra_attrs:
my_attrs.update(extra_attrs)
return super().build_attrs(base_attrs, my_attrs)
def optgroups(self, name, value, attrs=None):
# copy of Django's Select.optgroups, modified to accept filter_value as a
# third item in the tuple and expose that as a data-filter-value attribute
# on the final <option>
groups = []
has_selected = False
for index, choice in enumerate(self.choices):
try:
(option_value, option_label, filter_value) = choice
except ValueError:
# *ChoiceField will still output blank options as a 2-tuple,
# so need to handle that too
(option_value, option_label) = choice
filter_value = None
if option_value is None:
option_value = ''
subgroup = []
if isinstance(option_label, (list, tuple)):
group_name = option_value
subindex = 0
choices = option_label
else:
group_name = None
subindex = None
choices = [(option_value, option_label)]
groups.append((group_name, subgroup, index))
for subvalue, sublabel in choices:
selected = (
str(subvalue) in value
and (not has_selected or self.allow_multiple_selected)
)
has_selected |= selected
subgroup.append(self.create_option(
name, subvalue, sublabel, selected, index, subindex=subindex,
filter_value=filter_value
))
if subindex is not None:
subindex += 1
return groups
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None, filter_value=None):
option = super().create_option(
name, value, label, selected, index, subindex=subindex, attrs=attrs
)
if filter_value is not None:
option['attrs']['data-filter-value'] = filter_value
return option
@property
def media(self):
return forms.Media(js=[
versioned_static('wagtailadmin/js/filtered-select.js'),
])
class FilteredModelChoiceIterator(django_filters.fields.ModelChoiceIterator):
"""
A variant of Django's ModelChoiceIterator that, instead of yielding (value, label) tuples,
returns (value, label, filter_value) so that FilteredSelect can drop filter_value into
the data-filter-value attribute.
"""
def choice(self, obj):
return (
self.field.prepare_value(obj),
self.field.label_from_instance(obj),
self.field.get_filter_value(obj)
)
class FilteredModelChoiceField(django_filters.fields.ModelChoiceField):
widget = FilteredSelect
iterator = FilteredModelChoiceIterator
def __init__(self, *args, **kwargs):
self.filter_accessor = kwargs.pop('filter_accessor')
filter_field = kwargs.pop('filter_field')
super().__init__(*args, **kwargs)
self.widget.filter_field = filter_field
def get_filter_value(self, obj):
# filter_accessor identifies a property or method on the instances being listed here,
# which gives us a queryset of related objects. Turn this queryset into a list of IDs
# that will become the 'data-filter-value' used to filter this listing
queryset = getattr(obj, self.filter_accessor)
if callable(queryset):
queryset = queryset()
ids = queryset.values_list('pk', flat=True)
return ','.join([str(id) for id in ids])
class FilteredModelChoiceFilter(django_filters.ModelChoiceFilter):
field_class = FilteredModelChoiceField
class WagtailFilterSet(django_filters.FilterSet):
@classmethod
@ -112,6 +235,12 @@ class WorkflowTasksReportFilterSet(WagtailFilterSet):
field_name='workflow_state__workflow', queryset=Workflow.objects.all(), label=_("Workflow")
)
# When a workflow is chosen in the 'id_workflow' selector, filter this list of tasks
# to just the ones whose get_workflows() includes the selected workflow.
task = FilteredModelChoiceFilter(
queryset=Task.objects.all(), filter_field='id_workflow', filter_accessor='get_workflows'
)
class Meta:
model = TaskState
fields = ['workflow', 'task', 'status', 'created_at', 'finished_at']

Wyświetl plik

@ -0,0 +1,85 @@
/* A select box component that can be dynamically filtered by the option chosen in another select box.
<div>
<label for="id_continent">Continent</label>
<select id="id_continent">
<option value="">--------</option>
<option value="1">Europe</option>
<option value="2">Africa</option>
<option value="3">Asia</option>
</select>
</div>
<div>
<label for="id_country">Country</label>
<select id="id_country" data-widget="filtered-select" data-filter-field="id_continent">
<option value="">--------</option>
<option value="1" data-filter-value="3">China</option>
<option value="2" data-filter-value="2">Egypt</option>
<option value="3" data-filter-value="1">France</option>
<option value="4" data-filter-value="1">Germany</option>
<option value="5" data-filter-value="3">Japan</option>
<option value="6" data-filter-value="1,3">Russia</option>
<option value="7" data-filter-value="2">South Africa</option>
<option value="8" data-filter-value="1,3">Turkey</option>
</select>
</div>
*/
$(function() {
$('[data-widget="filtered-select"]').each(function() {
var sourceSelect = $('#' + this.dataset.filterField);
var self = $(this);
var optionData = [];
$('option', this).each(function() {
var filterValue;
if ('filterValue' in this.dataset) {
filterValue = this.dataset.filterValue.split(',')
} else {
filterValue = [];
}
optionData.push({
value: this.value,
label: this.label,
filterValue: filterValue
})
});
function updateFromSource() {
var currentValue = self.val();
self.empty();
var chosenFilter = sourceSelect.val();
var filteredValues;
if (chosenFilter == '') {
/* no filter selected - show all options */
filteredValues = optionData;
} else {
filteredValues = [];
for (var i = 0; i < optionData.length; i++) {
if (optionData[i].value == '' || optionData[i].filterValue.indexOf(chosenFilter) != -1) {
filteredValues.push(optionData[i]);
}
}
}
var foundValue = false;
for (var i = 0; i < filteredValues.length; i++) {
var option = $('<option>');
option.attr('value', filteredValues[i].value);
if (filteredValues[i].value === currentValue) foundValue = true;
option.text(filteredValues[i].label);
self.append(option);
}
if (foundValue) {
self.val(currentValue);
} else {
self.val('');
}
}
updateFromSource();
sourceSelect.change(updateFromSource);
})
});

Wyświetl plik

@ -2689,6 +2689,11 @@ class Task(models.Model):
"""Returns a ``QuerySet`` of the task states the current user can moderate"""
return TaskState.objects.none()
@property
def get_workflows(self):
"""Returns a ``QuerySet`` of the workflows that this task is part of """
return Workflow.objects.filter(workflow_tasks__task=self)
@transaction.atomic
def deactivate(self, user=None):
"""Set ``active`` to False and cancel all in progress task states linked to this task"""