Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions tcms/rpc/api/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.utils import NotSupportedError
from django.forms import EmailField, ValidationError
from django.forms.models import model_to_dict
from django.utils.dateparse import parse_duration
from modernrpc.core import REQUEST_KEY, rpc_method

from tcms.core import helpers
Expand All @@ -15,6 +16,49 @@
from tcms.rpc.decorators import permissions_required
from tcms.testcases.models import Property, TestCase, TestCasePlan

_DURATION_FILTER_FIELDS = ("setup_duration", "testing_duration", "expected_duration")
_DURATION_FILTER_LOOKUPS = ("", "exact", "gt", "gte", "lt", "lte")


def _expected_duration_expression():
return Coalesce("setup_duration", timedelta(0)) + Coalesce(
"testing_duration", timedelta(0)
)


def _duration_filter_value(value):
if isinstance(value, timedelta):
return value

if isinstance(value, (int, float)):
return timedelta(seconds=value)

if isinstance(value, str):
value = value.strip()
if value == "":
return value

try:
return timedelta(seconds=float(value))
except ValueError:
parsed = parse_duration(value)
if parsed is not None:
return parsed
Comment on lines +41 to +46

return value
Comment on lines +36 to +48


def _normalize_duration_filter_query(query):
query = query.copy()

for field in _DURATION_FILTER_FIELDS:
for lookup in _DURATION_FILTER_LOOKUPS:
key = field if lookup == "" else f"{field}__{lookup}"
if key in query:
query[key] = _duration_filter_value(query[key])

return query


@permissions_required("testcases.add_testcasecomponent")
@rpc_method(name="TestCase.add_component")
Expand Down Expand Up @@ -267,13 +311,16 @@ def filter(query=None): # pylint: disable=redefined-builtin
"""
if query is None:
query = {}
query = _normalize_duration_filter_query(query)

test_case_ids = TestCase.objects.filter(**query).values("id")
test_cases = TestCase.objects.annotate(
expected_duration=_expected_duration_expression()
)
test_case_ids = test_cases.filter(**query).values("id")
Comment on lines +314 to +319
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_expression()
)
.filter(id__in=test_case_ids)
.values(
Expand Down
36 changes: 36 additions & 0 deletions tcms/rpc/tests/test_testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,42 @@ def test_duration_properties_in_result(
self.assertEqual(result[0]["testing_duration"], testing_duration)
self.assertEqual(result[0]["expected_duration"], expected_duration)

def test_filter_by_setup_duration_range(self):
matching = TestCaseFactory(setup_duration=timedelta(minutes=2))
TestCaseFactory(setup_duration=timedelta(seconds=30))

result = self.rpc_client.TestCase.filter(
{"setup_duration__gte": "60", "setup_duration__lte": "180"}
)

self.assertEqual([matching.pk], [testcase["id"] for testcase in result])

def test_filter_by_testing_duration_range(self):
matching = TestCaseFactory(testing_duration=timedelta(minutes=3))
TestCaseFactory(testing_duration=timedelta(minutes=5))

result = self.rpc_client.TestCase.filter(
{"testing_duration__gte": "120", "testing_duration__lte": "240"}
)

self.assertEqual([matching.pk], [testcase["id"] for testcase in result])

def test_filter_by_expected_duration_range(self):
matching = TestCaseFactory(
setup_duration=timedelta(minutes=1),
testing_duration=timedelta(minutes=2),
)
TestCaseFactory(
setup_duration=timedelta(minutes=3),
testing_duration=timedelta(minutes=3),
)

result = self.rpc_client.TestCase.filter(
{"expected_duration__gte": "180", "expected_duration__lte": "240"}
)

self.assertEqual([matching.pk], [testcase["id"] for testcase in result])


class TestUpdate(APITestCase):
non_existing_username = "FakeUsername"
Expand Down
29 changes: 29 additions & 0 deletions tcms/testcases/static/testcases/js/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export function pageTestcasesSearchReadyHandler () {
params.executions__run__in = [$('#id_run').val()]
};

updateDurationFilterParams(params)

const testPlanIds = selectedPlanIds()
if (testPlanIds.length) {
params.plan__in = testPlanIds
Expand Down Expand Up @@ -245,6 +247,33 @@ export function pageTestcasesSearchReadyHandler () {
}
}

function updateDurationFilterParams (params) {
const filters = {
setup_duration: {
min: $('#id_setup_duration_min').val(),
max: $('#id_setup_duration_max').val()
},
testing_duration: {
min: $('#id_testing_duration_min').val(),
max: $('#id_testing_duration_max').val()
},
expected_duration: {
min: $('#id_expected_duration_min').val(),
max: $('#id_expected_duration_max').val()
}
}

for (const field of Object.keys(filters)) {
if (filters[field].min) {
params[`${field}__gte`] = filters[field].min
}

if (filters[field].max) {
params[`${field}__lte`] = filters[field].max
}
}
}

function selectedPlanIds () {
const selectedIds = $('#id_test_plan').val()
const childIds = []
Expand Down
32 changes: 32 additions & 0 deletions tcms/testcases/templates/testcases/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,38 @@
</div>
</div>

<div class="form-group">
<label class="col-md-1 col-lg-1">{% trans "Setup duration" %}</label>
<div class="col-md-3 col-lg-3">
<div class="input-group">
<span class="input-group-addon">{% trans "Min" %}</span>
<input id="id_setup_duration_min" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_setup_duration_max" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
</div>
</div>

<label class="col-md-1 col-lg-1">{% trans "Testing duration" %}</label>
<div class="col-md-3 col-lg-3">
<div class="input-group">
<span class="input-group-addon">{% trans "Min" %}</span>
<input id="id_testing_duration_min" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_testing_duration_max" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
</div>
</div>

<label class="col-md-1 col-lg-1">{% trans "Expected duration" %}</label>
<div class="col-md-3 col-lg-3">
<div class="input-group">
<span class="input-group-addon">{% trans "Min" %}</span>
<input id="id_expected_duration_min" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_expected_duration_max" type="number" min="0" class="form-control" placeholder="{% trans 'Seconds' %}">
</div>
</div>
</div>
Comment on lines +152 to +182

<div class="form-group">
<div class="col-md-1 col-lg-1">
<button id="btn_search" type="submit" class="btn btn-default btn-lg">{% trans "Search" %}</button>
Expand Down