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
49 changes: 48 additions & 1 deletion 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 @@ -16,6 +17,44 @@
from tcms.testcases.models import Property, TestCase, TestCasePlan


DURATION_FILTER_FIELDS = ("setup_duration", "testing_duration", "expected_duration")


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

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

if isinstance(value, str):
try:
return timedelta(seconds=float(value))
except ValueError:
duration = parse_duration(value)
if duration is not None:
return duration

return value


def _normalize_duration_filters(query):
normalized_query = {}

for key, value in query.items():
field_name, _separator, lookup = key.partition("__")
if field_name not in DURATION_FILTER_FIELDS or lookup == "isnull":
normalized_query[key] = value
continue

if lookup == "in":
normalized_query[key] = [_duration_filter_value(item) for item in value]
else:
normalized_query[key] = _duration_filter_value(value)

return normalized_query


@permissions_required("testcases.add_testcasecomponent")
@rpc_method(name="TestCase.add_component")
def add_component(case_id, component):
Expand Down Expand Up @@ -268,7 +307,15 @@ def filter(query=None): # pylint: disable=redefined-builtin
if query is None:
query = {}

test_case_ids = TestCase.objects.filter(**query).values("id")
query = _normalize_duration_filters(query)
test_case_ids = (
TestCase.objects.annotate(
expected_duration=Coalesce("setup_duration", timedelta(0))
+ Coalesce("testing_duration", timedelta(0))
)
.filter(**query)
.values("id")
)
qs = (
# note: queries from HistoricalTestCase
TestCase.history.annotate( # pylint: disable=no-member
Expand Down
35 changes: 35 additions & 0 deletions tcms/rpc/tests/test_testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,41 @@ 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_min", "setup_duration__gte", 120),
("setup_duration_max", "setup_duration__lte", "00:02:00"),
("testing_duration_min", "testing_duration__gte", 300),
("testing_duration_max", "testing_duration__lte", "00:05:00"),
("expected_duration_min", "expected_duration__gte", 420),
("expected_duration_max", "expected_duration__lte", "00:07:00"),
]
)
def test_filter_by_duration_fields(self, _name, lookup, value):
matching_case = TestCaseFactory(
setup_duration=timedelta(minutes=2),
testing_duration=timedelta(minutes=5),
)
matching_case.save()
if lookup.endswith("__gte"):
excluded_setup_duration = timedelta(seconds=1)
excluded_testing_duration = timedelta(seconds=1)
else:
excluded_setup_duration = timedelta(minutes=10)
excluded_testing_duration = timedelta(minutes=15)

excluded_case = TestCaseFactory(
setup_duration=excluded_setup_duration,
testing_duration=excluded_testing_duration,
)
excluded_case.save()

result = self.rpc_client.TestCase.filter({lookup: value})
result_ids = [testcase["id"] for testcase in result]

self.assertIn(matching_case.pk, result_ids)
self.assertNotIn(excluded_case.pk, result_ids)


class TestUpdate(APITestCase):
non_existing_username = "FakeUsername"
Expand Down
13 changes: 13 additions & 0 deletions tcms/testcases/static/testcases/js/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ function preProcessData (data, callbackF) {
})
}

function updateDurationFilter (selector, filterName, params) {
const value = $(selector).val()
if (value !== '') {
params[filterName] = Number(value)
}
}

export function pageTestcasesSearchReadyHandler () {
initializeDateTimePicker('#id_before')
initializeDateTimePicker('#id_after')
Expand Down Expand Up @@ -130,6 +137,12 @@ export function pageTestcasesSearchReadyHandler () {
};

updateParamsToSearchTags('#id_tag', params)
updateDurationFilter('#id_setup_duration__gte', 'setup_duration__gte', params)
updateDurationFilter('#id_setup_duration__lte', 'setup_duration__lte', params)
updateDurationFilter('#id_testing_duration__gte', 'testing_duration__gte', params)
updateDurationFilter('#id_testing_duration__lte', 'testing_duration__lte', params)
updateDurationFilter('#id_expected_duration__gte', 'expected_duration__gte', params)
updateDurationFilter('#id_expected_duration__lte', 'expected_duration__lte', params)

dataTableJsonRPC('TestCase.filter', params, callbackF, preProcessData)
},
Expand Down
38 changes: 38 additions & 0 deletions tcms/testcases/templates/testcases/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,44 @@
</div>
</div>

<div class="form-group">
<label class="col-md-1 col-lg-1" for="id_setup_duration__gte">{% 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__gte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_setup_duration__lte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
</div>
</div>

<label class="col-md-1 col-lg-1" for="id_testing_duration__gte">{% 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__gte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_testing_duration__lte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
</div>
</div>

<label class="col-md-1 col-lg-1" for="id_expected_duration__gte">{% 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__gte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
<span class="input-group-addon">{% trans "Max" %}</span>
<input id="id_expected_duration__lte" type="number" min="0" step="1" class="form-control"
placeholder="{% trans 'Seconds' %}">
</div>
</div>
</div>

<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
6 changes: 6 additions & 0 deletions tcms/testcases/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,12 @@ def setUpTestData(cls):
def test_page_renders(self):
response = self.client.get(self.search_url, {})
self.assertContains(response, '<option value="">----------</option>', html=True)
self.assertContains(response, 'id="id_setup_duration__gte"')
self.assertContains(response, 'id="id_setup_duration__lte"')
self.assertContains(response, 'id="id_testing_duration__gte"')
self.assertContains(response, 'id="id_testing_duration__lte"')
self.assertContains(response, 'id="id_expected_duration__gte"')
self.assertContains(response, 'id="id_expected_duration__lte"')

def test_get_parameter_should_be_accepted_for_a_product(self):
response = self.client.get(self.search_url, {"product": self.product.pk})
Expand Down