Repository: Nitrate/Nitrate

Issue:
Remove duplicate cases search result table
This is aboue search cases and advanced search cases.

The search result table has same columns and same action buttons on top of the result table.


Discussion:

Resolving patch:
diff --git a/src/tcms/testcases/urls/cases_urls.py b/src/tcms/testcases/urls/cases_urls.py
--- a/src/tcms/testcases/urls/cases_urls.py
+++ b/src/tcms/testcases/urls/cases_urls.py
@@ -6,8 +6,7 @@
 urlpatterns = [
     path('new/', views.new, name='cases-new'),
     path('', views.all, name='cases-all'),
-    path('search/', views.search, name='cases-search'),
-    path('ajax/', views.ajax_search, name='cases-ajax-search'),
+    path('search/', views.search_cases, name='cases-search'),
 
     path('automated/', views.ChangeCaseAutomatedPropertyView.as_view(),
          name='cases-automated'),
diff --git a/src/tcms/testcases/views.py b/src/tcms/testcases/views.py
--- a/src/tcms/testcases/views.py
+++ b/src/tcms/testcases/views.py
@@ -507,125 +507,48 @@ def all(request, template_name="case/all.html"):
 
 
 @require_GET
-def search(request, template_name='case/all.html'):
-    """
-    generate the function of searching cases with search criteria
-    """
-    # This is ugly, but it's not a big deal at this moment because I'm going go
-    # rewrite the search functionality as well as for runs and plans.
-    search_criterias = len(list(request.GET.items())) > 0
-
-    search_form = SearchCaseForm(request.GET)
-    if request.GET.get('product'):
-        search_form.populate(product_id=request.GET['product'])
-    else:
-        search_form.populate()
-
-    context_data = {
-        'module': MODULE_NAME,
-        'search_form': search_form,
-        'search_criterias': search_criterias,
-    }
-    return render(request, template_name, context=context_data)
-
-
-@require_GET
-def ajax_search(request, template_name='case/common/json_cases.txt'):
+def search_cases(request):
     """Generate the case list in search case and case zone in plan
     """
-    SearchForm = SearchCaseForm
-
-    tp = plan_from_request_or_none(request)
-
-    action = request.GET.get('a')
-
-    # Initial the form and template
-    if action in ('search', 'sort'):
-        search_form = SearchForm(request.GET)
-    else:
-        # Hacking for case plan
-        confirmed_status_name = 'CONFIRMED'
-        # 'c' is meaning component
-        template_type = request.GET.get('template_type')
-        if template_type == 'case':
-            d_status = TestCaseStatus.objects.filter(name=confirmed_status_name)
-        elif template_type == 'review_case':
-            d_status = TestCaseStatus.objects.exclude(name=confirmed_status_name)
-        else:
-            d_status = TestCaseStatus.objects.all()
-
-        d_status_ids = d_status.values_list('pk', flat=True)
-
-        search_form = SearchForm(initial={'case_status': d_status_ids})
-
-    # Populate the form
-    if request.GET.get('product'):
-        search_form.populate(product_id=request.GET['product'])
-    elif tp and tp.product_id:
-        search_form.populate(product_id=tp.product_id)
-    else:
-        search_form.populate()
+    search_form = SearchCaseForm(request.GET)
 
-    # Query the database when search
-    if action in ('search', 'sort') and search_form.is_valid():
-        tcs = TestCase.list(search_form.cleaned_data)
-    elif action == 'initial':
-        tcs = TestCase.objects.filter(case_status__in=d_status)
+    if search_form.is_valid():
+        cases = (
+            TestCase
+            .list(search_form.cleaned_data)
+            .select_related('author', 'default_tester', 'case_status',
+                            'priority', 'category')
+            .only('pk', 'summary', 'author__username', 'default_tester__username',
+                  'is_automated', 'case_status__name', 'category__name',
+                  'priority__value', 'is_automated_proposed',  # 'default_tester__id',
+                  'create_date')
+            .order_by('-create_date')
+        )
     else:
-        tcs = TestCase.objects.none()
-
-    # Search the relationship
-    if tp:
-        tcs = tcs.filter(plan=tp)
-
-    tcs = tcs.select_related(
-        'author',
-        'default_tester',
-        'case_status',
-        'priority',
-        'category'
-    ).only(
-        'case_id',
-        'summary',
-        'create_date',
-        'is_automated',
-        'is_automated_proposed',
-        'case_status__name',
-        'category__name',
-        'priority__value',
-        'author__username',
-        'default_tester__id',
-        'default_tester__username'
-    )
-    tcs = tcs.extra(select={'num_issue': RawSQL.num_case_issues, })
+        cases = TestCase.objects.none()
 
     # columnIndexNameMap is required for correct sorting behavior, 5 should be
     # product, but we use run.build.product
     column_names = [
-        '',
-        '',
-        'case_id',
-        'summary',
-        'author__username',
-        'default_tester__username',
-        'is_automated',
-        'case_status__name',
-        'category__name',
-        'priority__value',
-        'create_date',
+        '', '', 'pk', 'summary', 'author__username',
+        'default_tester__username', 'is_automated', 'case_status__name',
+        'category__name', 'priority__value', 'create_date',
     ]
-    return ajax_response(request, tcs, column_names, template_name)
 
-
-def ajax_response(request, queryset, column_names, template_name):
-    """json template for the ajax request for searching"""
-    dt = DataTableResult(request.GET, queryset, column_names)
-
-    # prepare the JSON with the response, consider using :
-    # from django.template.defaultfilters import escapejs
-    t = get_template(template_name)
-    resp_data = t.render(dt.get_response_data(), request)
-    return JsonResponse(json.loads(resp_data))
+    if 'sEcho' in request.GET:
+        dt = DataTableResult(request.GET, cases, column_names,
+                             default_order_key='-pk')
+        resp_data = (get_template('case/common/json_cases.txt')
+                     .render(dt.get_response_data(), request))
+        return JsonResponse(json.loads(resp_data))
+    else:
+        context_data = {
+            'module': MODULE_NAME,
+            'object_list': cases[0:20],
+            'search_form': search_form,
+            'total_count': cases.count(),
+        }
+        return render(request, 'case/all.html', context=context_data)
 
 
 class SimpleTestCaseView(TemplateView, data.TestCaseViewDataMixin):


Test patch:
diff --git a/src/tests/testcases/test_views.py b/src/tests/testcases/test_views.py
--- a/src/tests/testcases/test_views.py
+++ b/src/tests/testcases/test_views.py
@@ -34,7 +34,7 @@
 from tcms.testcases.models import TestCasePlan
 from tcms.testcases.models import TestCaseTag
 from tcms.testcases.views import (
-    ajax_response, calculate_for_testcases, plan_from_request_or_none,
+    calculate_for_testcases, plan_from_request_or_none,
     update_case_email_settings)
 from tcms.testplans.models import TestPlan
 from tcms.testruns.models import TestCaseRun
@@ -818,38 +818,6 @@ def test_return_to_plan_review_cases_tab(self):
         self.assertRedirects(response, redirect_url, target_status_code=301)
 
 
-class TestAJAXSearchCases(BasePlanCase):
-    """Test ajax_search"""
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-
-        cls.search_data = {
-            'summary': '',
-            'author': '',
-            'product': '',
-            'plan': '',
-            'is_automated': '',
-            'category': '',
-            'component': '',
-            'issue_key': '',
-            'tag__name__in': '',
-            'a': 'search',
-        }
-
-        cls.search_url = reverse('cases-ajax-search')
-
-    def test_search_all_cases(self):
-        response = self.client.get(self.search_url, self.search_data)
-
-        data = json.loads(response.content)
-
-        cases_count = self.plan.case.count()
-        self.assertEqual(cases_count, data['iTotalRecords'])
-        self.assertEqual(cases_count, data['iTotalDisplayRecords'])
-
-
 class TestChangeCasesAutomated(BasePlanCase):
     """Test automated view method"""
 
@@ -1519,45 +1487,34 @@ class TestSearchCases(BasePlanCase):
 
     @classmethod
     def setUpTestData(cls):
-        super().setUpTestData()
-
-        cls.search_url = reverse('cases-search')
-
-    def test_search_without_selected_product(self):
-        response = self.client.get(self.search_url, {})
-        self.assertContains(
-            response,
-            '<option value="" selected="selected">---------</option>',
-            html=True)
-
-    def test_search_with_selected_product(self):
-        product = self.product
-        response = self.client.get(self.search_url, {'product': product.pk})
-        self.assertContains(
-            response,
-            f'<option value="{product.pk}" selected="selected">'
-            f'{product.name}'
-            f'</option>',
-            html=True
-        )
-
+        """Setup data for test
 
-class TestAjaxSearch(BasePlanCase):
-    """Test ajax_search"""
-
-    @classmethod
-    def setUpTestData(cls):
-        super(TestAjaxSearch, cls).setUpTestData()
+        Besides those initial test cases created by base class, this test needs
+        more cases in order to test switching other page.
+        """
+        super().setUpTestData()
 
         cls.functional_case = f.TestCaseCategoryFactory(
             name='Functional', product=cls.product
         )
 
         cls.case_2.category = cls.functional_case
+        cls.case_2.priority = Priority.objects.get(value='P2')
         cls.case_2.save()
         cls.case_3.category = cls.functional_case
+        cls.case_3.priority = Priority.objects.get(value='P5')
         cls.case_3.save()
 
+        # Create more cases. The total number must greater than 25 due to 20 is
+        # the default count per page.
+        for i in range(20):
+            f.TestCaseFactory(
+                author=cls.tester,
+                default_tester=None,
+                reviewer=cls.tester,
+                case_status=cls.case_status_confirmed,
+                plan=[cls.plan])
+
         cls.component_db = f.ComponentFactory(name='db', product=cls.product)
 
         f.TestCaseComponentFactory(case=cls.case_1, component=cls.component_db)
@@ -1574,73 +1531,48 @@ def setUpTestData(cls):
         cls.case_3.add_tag(cls.tag_python)
         cls.case_4.add_tag(cls.tag_fedora)
 
-    def test_search_in_general(self):
-        url = reverse('cases-ajax-search')
-
-        # List case 2 and case 3, and ordered by pk
-
-        resp = self.client.get(url, data={
-            'a': 'search',
-            'product': self.product.pk,
-            'summary': 'Test case',
-            'component': self.component_db.pk,
-            'tag__name__in': f'{self.tag_python.name},{self.tag_fedora.name}'
-        })
-
-        result = json.loads(resp.content)
-        cases_list = result['aaData']
-
-        self.assertEqual(2, result['iTotalRecords'])
-        self.assertEqual(2, len(cases_list))
-
-        self.assertIn(self.case_2.summary, cases_list[0][3])
-        self.assertIn(self.case_3.summary, cases_list[1][3])
-
-
-class TestAJAXResponse(BasePlanCase):
-    """Test ajax_response"""
+        cls.search_url = reverse('cases-search')
 
-    def setUp(self):
-        self.factory = RequestFactory()
+    def test_show_first_page_for_initial_query(self):
+        resp = self.client.get(self.search_url, data={})
+        self.assert200(resp)
 
-        self.column_names = [
-            'case_id',
-            'summary',
-            'author__username',
-            'default_tester__username',
-            'is_automated',
-            'case_status__name',
-            'category__name',
-            'priority__value',
-            'create_date',
+        bs = BeautifulSoup(resp.content.decode('utf-8'), 'html.parser')
+        case_ids = [
+            int(tr.find_all('td')[2].text)
+            for tr in bs.table.tbody.find_all('tr')
         ]
 
-        self.template = 'case/common/json_cases.txt'
+        expected_ids = list(TestCase.objects
+                            .values_list('pk', flat=True)
+                            .order_by('-create_date')[0:20])
 
-    def test_return_empty_cases(self):
-        url = reverse('cases-ajax-search')
-        request = self.factory.get(url, {
+        self.assertListEqual(expected_ids, case_ids)
 
+    def test_show_first_page_for_query(self):
+        resp = self.client.get(self.search_url, data={
+            'priority': Priority.objects.get(value='P2').pk
         })
-        request.user = self.tester
-
-        empty_cases = TestCase.objects.none()
-        response = ajax_response(request, empty_cases, self.column_names, self.template)
+        self.assert200(resp)
 
-        data = json.loads(response.content)
+        bs = BeautifulSoup(resp.content.decode('utf-8'), 'html.parser')
+        case_ids = [
+            int(tr.find_all('td')[2].text)
+            for tr in bs.table.tbody.find_all('tr')
+        ]
 
-        self.assertEqual(0, data['sEcho'])
-        self.assertEqual(0, data['iTotalRecords'])
-        self.assertEqual(0, data['iTotalDisplayRecords'])
-        self.assertEqual([], data['aaData'])
+        expected_ids = list(TestCase.objects
+                            .filter(priority__value='P2')
+                            .values_list('pk', flat=True)
+                            .order_by('-create_date'))
+        self.assertListEqual(expected_ids, case_ids)
 
-    def test_return_sorted_cases_by_name_desc(self):
-        url = reverse('cases-ajax-search')
-        request = self.factory.get(url, {
+    def test_switch_to_another_page_order_by_pk(self):
+        resp = self.client.get(self.search_url, data={
             'sEcho': 1,
-            'iDisplayStart': 0,
+            'iDisplayStart': 20,
             'iDisplayLength': 2,
-            'iSortCol_0': 0,
+            'iSortCol_0': 2,
             'sSortDir_0': 'desc',
             'iSortingCols': 1,
             'bSortable_0': 'true',
@@ -1648,27 +1580,17 @@ def test_return_sorted_cases_by_name_desc(self):
             'bSortable_2': 'true',
             'bSortable_3': 'true',
         })
-        request.user = self.tester
-
-        cases = self.plan.case.all()
-        response = ajax_response(request, cases, self.column_names, self.template)
 
-        data = json.loads(response.content)
-
-        total = self.plan.case.count()
-        self.assertEqual(1, data['sEcho'])
-        self.assertEqual(total, data['iTotalRecords'])
-        self.assertEqual(total, data['iTotalDisplayRecords'])
-        self.assertEqual(2, len(data['aaData']))
-
-        id_links = [row[2] for row in data['aaData']]
-        id_links.sort()
-        expected_id_links = [
-            f"<a href='{reverse('case-get', args=[case.pk])}'>{case.pk}</a>"
-            for case in self.plan.case.order_by('-pk')[0:2]
+        table_data = json.loads(resp.content)
+        case_ids = [
+            int(BeautifulSoup(item[2], 'html.parser').text)
+            for item in table_data['aaData']
         ]
-        expected_id_links.sort()
-        self.assertEqual(expected_id_links, id_links)
+
+        expected_ids = list(TestCase.objects
+                            .values_list('pk', flat=True)
+                            .order_by('-pk')[20:22])
+        self.assertListEqual(expected_ids, case_ids)
 
 
 class TestAddComponent(BasePlanCase):