kopia lustrzana https://github.com/wagtail/wagtail
Filter the tasks dropdown by the chosen workflow
rodzic
c0d2dd7aea
commit
8b1a304533
|
@ -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']
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
});
|
|
@ -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"""
|
||||
|
|
Ładowanie…
Reference in New Issue