Repository: Nitrate/Nitrate

Issue:
commit_unless_managed should be fixed
``commit_unless_managed`` is not available in Django 1.8 and later. It should be replaced with correct equivalent call in Django 1.8.


Discussion:
Part of #110 
After #159 is merged I can look into this since I already have some idea about this area in the code and already made progress adding some tests.
I have already started and part of them have been fixed. I can fix others except those in xmlrpc app you have done in #159.

Resolving patch:
diff --git a/tcms/testcases/models.py b/tcms/testcases/models.py
--- a/tcms/testcases/models.py
+++ b/tcms/testcases/models.py
@@ -418,14 +418,7 @@ def add_text(
         return latest_text
 
     def add_to_plan(self, plan):
-
-        try:
-            TestCasePlan.objects.get(case=self, plan=plan)
-        except TestCasePlan.DoesNotExist:
-            TestCasePlan.objects.get_or_create(
-                case=self,
-                plan=plan,
-            )
+        TestCasePlan.objects.get_or_create(case=self, plan=plan)
 
     def clear_components(self):
         try:
diff --git a/tcms/testplans/models.py b/tcms/testplans/models.py
--- a/tcms/testplans/models.py
+++ b/tcms/testplans/models.py
@@ -220,10 +220,7 @@ def clear_env_groups(self):
         return TCMSEnvPlanMap.objects.filter(plan=self).delete()
 
     def delete_case(self, case):
-        cursor = connection.writer_cursor
-        cursor.execute("DELETE from test_case_plans \
-            WHERE plan_id = %s \
-            AND case_id = %s", (self.plan_id, case.case_id))
+        TestCasePlan.objects.filter(case=case.pk, plan=self.pk).delete()
 
     @models.permalink
     def get_absolute_url(self):
diff --git a/tcms/xmlrpc/api/testcase.py b/tcms/xmlrpc/api/testcase.py
--- a/tcms/xmlrpc/api/testcase.py
+++ b/tcms/xmlrpc/api/testcase.py
@@ -4,12 +4,10 @@
 
 from django.conf import settings
 from django.core.exceptions import ObjectDoesNotExist
-from django.db import transaction
 from django.forms import EmailField, ValidationError
 
 from kobo.django.xmlrpc.decorators import user_passes_test
 
-from tcms.core.utils.tcms_router import connection
 from tcms.core.utils.timedelta2int import timedelta2int
 from tcms.management.models import TestTag
 from tcms.testcases.models import TestCase
@@ -879,20 +877,25 @@ def link_plan(request, case_ids, plan_ids):
             err_msg = 'TestPlan %s does not exist.' % ids_str
         raise ObjectDoesNotExist(err_msg)
 
+    # (plan_id, case_id) pair might probably exist in test_case_plans table, so
+    # skip the ones that do exist and create the rest.
+    # note: this query returns a list of tuples!
+    existing = TestCasePlan.objects.filter(
+        plan__in=plan_ids,
+        case__in=case_ids
+    ).values_list('plan', 'case')
+
     # Link the plans to cases
     def _generate_link_plan_value():
         for plan_id in plan_ids:
             for case_id in case_ids:
-                yield plan_id, case_id
+                if (plan_id, case_id) not in existing:
+                    yield plan_id, case_id
 
-    # (plan_id, case_id) pair might probably exist in test_case_plans table, so
-    # IGNORE tell MySQL to insert other non-exist ones without error report
-    # that will break whole INSERT INTO operation.
-    sql = 'INSERT IGNORE INTO test_case_plans (plan_id, case_id) VALUES\n%s'
-    sql = sql % ',\n'.join(str(value) for value in _generate_link_plan_value())
-    cursor = connection.writer_cursor
-    cursor.execute(sql)
-    transaction.commit_unless_managed()
+    TestCasePlan.objects.bulk_create([
+        TestCasePlan(plan_id=_plan_id, case_id=_case_id)
+        for _plan_id, _case_id in _generate_link_plan_value()
+    ])
 
 
 @log_call(namespace=__xmlrpc_namespace__)
@@ -1069,19 +1072,15 @@ def unlink_plan(requst, case_id, plan_id):
     Description: Unlink a test case from the given plan. If only one plan is linked, this will delete
                  the test case.
 
-    Params:      $case_ids - Integer/String: An integer or alias representing the ID in the database.
-                 $plan_id  - Integer: An integer representing the ID in the database.
+    Params:      $case_id - Integer/String: An integer or alias representing the ID in the database.
+                 $plan_id - Integer: An integer representing the ID in the database.
 
     Returns:     Array: Array of plans hash still linked if any, empty if not.
 
     Example:
     >>> TestCase.unlink_plan(12345, 137)
     """
-    sql = 'DELETE FROM test_case_plans WHERE plan_id = %s and case_id = %s'
-    cursor = connection.writer_cursor
-    cursor.execute(sql, [int(plan_id), int(case_id)])
-    transaction.commit_unless_managed()
-
+    TestCasePlan.objects.filter(case=case_id, plan=plan_id).delete()
     plan_pks = TestCasePlan.objects.filter(case=case_id).values_list('plan',
                                                                      flat=True)
     return TestPlan.to_xmlrpc(query={'pk__in': plan_pks})


Test patch:
diff --git a/tcms/testplans/tests.py b/tcms/testplans/tests.py
--- a/tcms/testplans/tests.py
+++ b/tcms/testplans/tests.py
@@ -9,10 +9,11 @@
 from django.test.client import Client
 
 from tcms.settings.common import TCMS_ROOT_PATH
-from tcms.testcases.models import TestCase
+from tcms.testcases.models import TestCase, TestCasePlan
 from tcms.testplans.models import TestPlan
 from tcms.tests.factories import ClassificationFactory
 from tcms.tests.factories import ProductFactory
+from tcms.tests.factories import TestCaseFactory
 from tcms.tests.factories import TestPlanFactory
 from tcms.tests.factories import TestPlanTypeFactory
 from tcms.tests.factories import UserFactory
@@ -148,3 +149,22 @@ def test_plan_history(self):
 
         response = self.c.get(location, {'plan_text_version': 1})
         self.assertEquals(response.status_code, httplib.OK)
+
+
+class TestPlanModel(test.TestCase):
+    """ Test some model operations directly without a view """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.plan_1 = TestPlanFactory()
+        cls.testcase_1 = TestCaseFactory()
+        cls.testcase_2 = TestCaseFactory()
+
+        cls.plan_1.add_case(cls.testcase_1)
+        cls.plan_1.add_case(cls.testcase_2)
+
+    def test_plan_delete(self):
+        self.plan_1.delete_case(self.testcase_1)
+        cases_left = TestCasePlan.objects.filter(plan=self.plan_1.pk)
+        self.assertEqual(1, cases_left.count())
+        self.assertEqual(self.testcase_2.pk, cases_left[0].case.pk)
diff --git a/tcms/xmlrpc/tests/test_testcase.py b/tcms/xmlrpc/tests/test_testcase.py
--- a/tcms/xmlrpc/tests/test_testcase.py
+++ b/tcms/xmlrpc/tests/test_testcase.py
@@ -2,20 +2,18 @@
 
 from django import test
 
+from tcms.testcases.models import TestCasePlan
 from tcms.xmlrpc.api import testcase as XmlrpcTestCase
 from tcms.xmlrpc.tests.utils import make_http_request
 from tcms.xmlrpc.tests.utils import user_should_have_perm
 
 from tcms.tests.factories import TestCaseFactory
+from tcms.tests.factories import TestPlanFactory
 from tcms.tests.factories import UserFactory
 
-__all__ = (
-    'TestNotificationRemoveCC',
-)
-
 
 class TestNotificationRemoveCC(test.TestCase):
-    """ Tests the XMLRPM testcase.notication_remove_cc method """
+    """ Tests the XML-RPC testcase.notication_remove_cc method """
 
     @classmethod
     def setUpClass(cls):
@@ -44,3 +42,82 @@ def test_remove_existing_cc(self):
 
         # now verify that the CC email has been removed
         self.assertEqual(0, self.testcase.emailing.cc_list.count())
+
+
+class TestUnlinkPlan(test.TestCase):
+    """ Test the XML-RPC method testcase.unlink_plan() """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.user = UserFactory()
+        cls.http_req = make_http_request(user=cls.user)
+        perm_name = 'testcases.delete_testcaseplan'
+        user_should_have_perm(cls.http_req.user, perm_name)
+
+        cls.testcase_1 = TestCaseFactory()
+        cls.testcase_2 = TestCaseFactory()
+        cls.plan_1 = TestPlanFactory()
+        cls.plan_2 = TestPlanFactory()
+
+        cls.testcase_1.add_to_plan(cls.plan_1)
+
+        cls.testcase_2.add_to_plan(cls.plan_1)
+        cls.testcase_2.add_to_plan(cls.plan_2)
+
+    def test_unlink_plan_from_case_with_single_plan(self):
+        result = XmlrpcTestCase.unlink_plan(self.http_req, self.testcase_1.pk, self.plan_1.pk)
+        self.assertEqual(0, self.testcase_1.plan.count())
+        self.assertEqual([], result)
+
+    def test_unlink_plan_from_case_with_two_plans(self):
+        result = XmlrpcTestCase.unlink_plan(self.http_req, self.testcase_2.pk, self.plan_1.pk)
+        self.assertEqual(1, self.testcase_2.plan.count())
+        self.assertEqual(1, len(result))
+        self.assertEqual(self.plan_2.pk, result[0]['plan_id'])
+
+
+class TestLinkPlan(test.TestCase):
+    """ Test the XML-RPC method testcase.link_plan() """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.user = UserFactory()
+        cls.http_req = make_http_request(user=cls.user)
+        perm_name = 'testcases.add_testcaseplan'
+        user_should_have_perm(cls.http_req.user, perm_name)
+
+        cls.testcase_1 = TestCaseFactory()
+        cls.testcase_2 = TestCaseFactory()
+        cls.testcase_3 = TestCaseFactory()
+
+        cls.plan_1 = TestPlanFactory()
+        cls.plan_2 = TestPlanFactory()
+        cls.plan_3 = TestPlanFactory()
+
+        # case 1 is already linked to plan 1
+        cls.testcase_1.add_to_plan(cls.plan_1)
+
+    def test_insert_ignores_existing_mappings(self):
+        plans = [self.plan_1.pk, self.plan_2.pk, self.plan_3.pk]
+        cases = [self.testcase_1.pk, self.testcase_2.pk, self.testcase_3.pk]
+        XmlrpcTestCase.link_plan(self.http_req, cases, plans)
+
+        # no duplicates for plan1/case1 were created
+        self.assertEqual(
+            1,
+            TestCasePlan.objects.filter(
+                plan=self.plan_1.pk,
+                case=self.testcase_1.pk
+            ).count()
+        )
+
+        # verify all case/plan combinations exist
+        for plan_id in plans:
+            for case_id in cases:
+                self.assertEqual(
+                    1,
+                    TestCasePlan.objects.filter(
+                        plan=plan_id,
+                        case=case_id
+                    ).count()
+                )