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.utils.translation import gettext_lazy as _
|
||||||
from django_filters.widgets import SuffixedMultiWidget
|
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
|
from .widgets import AdminDateInput
|
||||||
|
|
||||||
|
@ -67,6 +68,128 @@ class DateRangePickerWidget(SuffixedMultiWidget):
|
||||||
return [None, None]
|
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):
|
class WagtailFilterSet(django_filters.FilterSet):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -112,6 +235,12 @@ class WorkflowTasksReportFilterSet(WagtailFilterSet):
|
||||||
field_name='workflow_state__workflow', queryset=Workflow.objects.all(), label=_("Workflow")
|
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:
|
class Meta:
|
||||||
model = TaskState
|
model = TaskState
|
||||||
fields = ['workflow', 'task', 'status', 'created_at', 'finished_at']
|
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"""
|
"""Returns a ``QuerySet`` of the task states the current user can moderate"""
|
||||||
return TaskState.objects.none()
|
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
|
@transaction.atomic
|
||||||
def deactivate(self, user=None):
|
def deactivate(self, user=None):
|
||||||
"""Set ``active`` to False and cancel all in progress task states linked to this task"""
|
"""Set ``active`` to False and cancel all in progress task states linked to this task"""
|
||||||
|
|
Ładowanie…
Reference in New Issue