diff --git a/tcms/rpc/api/testcase.py b/tcms/rpc/api/testcase.py index f1c7dcb7f9..26a198f412 100644 --- a/tcms/rpc/api/testcase.py +++ b/tcms/rpc/api/testcase.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.db.models.functions import Coalesce from django.db.utils import NotSupportedError -from django.forms import EmailField, ValidationError +from django.forms import DurationField, EmailField, ValidationError from django.forms.models import model_to_dict from modernrpc.core import REQUEST_KEY, rpc_method @@ -15,6 +15,45 @@ from tcms.rpc.decorators import permissions_required from tcms.testcases.models import Property, TestCase, TestCasePlan +_DURATION_FIELD = DurationField(required=False) +_DURATION_FILTER_FIELDS = ("setup_duration", "testing_duration", "expected_duration") + + +def _expected_duration(): + return Coalesce("setup_duration", timedelta(0)) + Coalesce( + "testing_duration", timedelta(0) + ) + + +def _clean_duration(value): + if isinstance(value, (list, tuple)): + return [_clean_duration(item) for item in value] + + if isinstance(value, (int, float)) and not isinstance(value, bool): + return timedelta(seconds=value) + + if isinstance(value, str): + return _DURATION_FIELD.clean(value) + + return value + + +def _clean_duration_filters(query): + cleaned_query = {} + + for key, value in query.items(): + field_name = key.split("__", 1)[0] + + if field_name in _DURATION_FILTER_FIELDS: + value = _clean_duration(value) + + if field_name == "expected_duration": + key = key.replace("expected_duration", "_expected_duration", 1) + + cleaned_query[key] = value + + return cleaned_query + @permissions_required("testcases.add_testcasecomponent") @rpc_method(name="TestCase.add_component") @@ -268,12 +307,13 @@ def filter(query=None): # pylint: disable=redefined-builtin if query is None: query = {} - test_case_ids = TestCase.objects.filter(**query).values("id") + query = _clean_duration_filters(query) + test_case_ids = TestCase.objects.annotate(_expected_duration=_expected_duration()) + test_case_ids = test_case_ids.filter(**query).values("id") qs = ( # note: queries from HistoricalTestCase TestCase.history.annotate( # pylint: disable=no-member - expected_duration=Coalesce("setup_duration", timedelta(0)) - + Coalesce("testing_duration", timedelta(0)) + expected_duration=_expected_duration() ) .filter(id__in=test_case_ids) .values( diff --git a/tcms/rpc/tests/test_testcase.py b/tcms/rpc/tests/test_testcase.py index ef22180890..fabf06e7f5 100644 --- a/tcms/rpc/tests/test_testcase.py +++ b/tcms/rpc/tests/test_testcase.py @@ -247,6 +247,29 @@ def test_duration_properties_in_result( self.assertEqual(result[0]["testing_duration"], testing_duration) self.assertEqual(result[0]["expected_duration"], expected_duration) + @parameterized.expand( + [ + ("setup_duration", {"setup_duration": "45"}), + ("testing_duration", {"testing_duration": "300"}), + ("expected_duration", {"expected_duration": "345"}), + ] + ) + def test_filter_by_duration_fields_returns_matching_test_case(self, _name, query): + testcase = TestCaseFactory( + setup_duration=timedelta(seconds=45), + testing_duration=timedelta(minutes=5), + ) + testcase.save() + other_testcase = TestCaseFactory( + setup_duration=timedelta(minutes=1), + testing_duration=timedelta(minutes=2), + ) + other_testcase.save() + + result = self.rpc_client.TestCase.filter(query) + + self.assertEqual([testcase.pk], [case["id"] for case in result]) + class TestUpdate(APITestCase): non_existing_username = "FakeUsername" diff --git a/tcms/testcases/forms.py b/tcms/testcases/forms.py index 482bc276ff..11a50dd02f 100644 --- a/tcms/testcases/forms.py +++ b/tcms/testcases/forms.py @@ -87,6 +87,19 @@ class Meta: model = TestCase fields = "__all__" + setup_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + testing_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + expected_duration = forms.DurationField( + widget=DurationWidget(), + required=False, + ) + # overriden initial values product = forms.ModelChoiceField(queryset=Product.objects.all(), required=False) category = forms.ModelChoiceField(queryset=Category.objects.none(), required=False) diff --git a/tcms/testcases/static/testcases/js/search.js b/tcms/testcases/static/testcases/js/search.js index c5378b1ed4..b24184b260 100644 --- a/tcms/testcases/static/testcases/js/search.js +++ b/tcms/testcases/static/testcases/js/search.js @@ -62,9 +62,37 @@ function preProcessData (data, callbackF) { }) } +function updateDurationParam (inputSelector, paramName, params) { + const value = $(inputSelector).val() + + if (value && value !== '0') { + params[paramName] = value + } +} + export function pageTestcasesSearchReadyHandler () { initializeDateTimePicker('#id_before') initializeDateTimePicker('#id_after') + $('.duration-picker').durationPicker({ + translations: { + day: $('html').data('trans-day'), + days: $('html').data('trans-days'), + + hour: $('html').data('trans-hour'), + hours: $('html').data('trans-hours'), + + minute: $('html').data('trans-minute'), + minutes: $('html').data('trans-minutes'), + + second: $('html').data('trans-second'), + seconds: $('html').data('trans-seconds') + }, + + showDays: true, + showHours: true, + showMinutes: true, + showSeconds: true + }) const table = $('#resultsTable').DataTable({ pageLength: $('#navbar').data('defaultpagesize'), @@ -111,6 +139,10 @@ export function pageTestcasesSearchReadyHandler () { params.executions__run__in = [$('#id_run').val()] }; + updateDurationParam('#id_setup_duration', 'setup_duration', params) + updateDurationParam('#id_testing_duration', 'testing_duration', params) + updateDurationParam('#id_expected_duration', 'expected_duration', params) + const testPlanIds = selectedPlanIds() if (testPlanIds.length) { params.plan__in = testPlanIds diff --git a/tcms/testcases/templates/testcases/search.html b/tcms/testcases/templates/testcases/search.html index 085e8605c7..1dbdf46ce6 100644 --- a/tcms/testcases/templates/testcases/search.html +++ b/tcms/testcases/templates/testcases/search.html @@ -2,6 +2,9 @@ {% load i18n %} {% load static %} +{% block head %} + {{ form.media }} +{% endblock %} {% block title %}{% trans "Search Test Cases" %}{% endblock %} {% block page_id %}page-testcases-search{% endblock %} @@ -149,6 +152,26 @@ +