Skip to content
Open
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
48 changes: 44 additions & 4 deletions tcms/rpc/api/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand Down Expand Up @@ -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(
Expand Down
23 changes: 23 additions & 0 deletions tcms/rpc/tests/test_testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions tcms/testcases/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions tcms/testcases/static/testcases/js/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tcms/testcases/templates/testcases/search.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down Expand Up @@ -149,6 +152,26 @@
</div>
</div>

<div class="form-group">
<label class="col-md-1 col-lg-1" for="id_setup_duration">{% trans "Setup duration" %}</label>
<div class="col-md-3 col-lg-3">
<input class="duration-picker" id="id_setup_duration" type="hidden"
value="{{ form.setup_duration.value|default:'0' }}">
</div>

<label class="col-md-1 col-lg-1" for="id_testing_duration">{% trans "Testing duration" %}</label>
<div class="col-md-3 col-lg-3">
<input class="duration-picker" id="id_testing_duration" type="hidden"
value="{{ form.testing_duration.value|default:'0' }}">
</div>

<label class="col-md-1 col-lg-1" for="id_expected_duration">{% trans "Expected duration" %}</label>
<div class="col-md-3 col-lg-3">
<input class="duration-picker" id="id_expected_duration" type="hidden"
value="{{ form.expected_duration.value|default:'0' }}">
</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
7 changes: 7 additions & 0 deletions tcms/testcases/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,10 @@ def test_get_parameter_should_be_accepted_for_a_product(self):
f'<option value="{self.product.pk}" selected>{self.product.name}</option>',
html=True,
)

def test_search_page_includes_duration_filters(self):
response = self.client.get(self.search_url, {})

self.assertContains(response, 'for="id_setup_duration"')
self.assertContains(response, 'for="id_testing_duration"')
self.assertContains(response, 'for="id_expected_duration"')