Repository: chaoss/grimoirelab-sortinghat (language: python)

Issue:
Match individuals for the same source should only work for username
This feature - introduced by https://github.com/chaoss/grimoirelab-sortinghat/pull/844 - was only meant to be available when matching data by `username` and not by other criteria. It applies to all the criteria.




Discussion / hints:

Fix patch:
diff --git a/config/settings/devel.py b/config/settings/devel.py
index cc5682ea..fa9a2acb 100644
--- a/config/settings/devel.py
+++ b/config/settings/devel.py
@@ -179,3 +179,5 @@
         'DB': 0
     }
 }
+
+MULTI_TENANT = os.environ.get('SORTINGHAT_MULTI_TENANT', 'False').lower() in ('true', '1')
diff --git a/releases/unreleased/match-source-parameter-fixed-in-recommendations.yml b/releases/unreleased/match-source-parameter-fixed-in-recommendations.yml
new file mode 100644
index 00000000..ffe3a121
--- /dev/null
+++ b/releases/unreleased/match-source-parameter-fixed-in-recommendations.yml
@@ -0,0 +1,11 @@
+---
+title: Match source parameter fixed in recommendations
+category: fixed
+author: Jose Javier Merchante <jjmerchante@bitergia.com>
+issue: 875
+notes: >
+  Resolve an issue where the "match source" option in unify and recommendation
+  jobs was being applied to both emails and names. With this update, this option
+  will now only apply to usernames. Additionally, users can utilize name and email
+  simultaneously without the constraint of applying the "match source" option across
+  all parameters.
diff --git a/sortinghat/core/recommendations/matching.py b/sortinghat/core/recommendations/matching.py
index 939b6fde..fcf4f318 100644
--- a/sortinghat/core/recommendations/matching.py
+++ b/sortinghat/core/recommendations/matching.py
@@ -196,7 +196,7 @@ def _apply_recommender_exclusion_list(df):
     def _filter_criteria(df, c, strict=True, match_source=False):
         """Filter dataframe creating a basic subset including a given column"""
         cols = ['uuid', 'individual', c]
-        if match_source:
+        if match_source and c == 'username':
             cols += ['source']
             cdf = df[cols]
             cdf = cdf[cdf['source'].isin(MATCH_USERNAME_SOURCES)]
@@ -229,7 +229,7 @@ def _filter_criteria(df, c, strict=True, match_source=False):
     for c in criteria:
         cdf_x = _filter_criteria(df_x, c, strict, match_source)
         cdf_y = _filter_criteria(df_y, c, strict, match_source)
-        if match_source:
+        if match_source and c == 'username':
             cdf = pandas.merge(cdf_x, cdf_y, on=[c, 'source'], how='inner')
         else:
             cdf = pandas.merge(cdf_x, cdf_y, on=c, how='inner')
diff --git a/sortinghat/core/schema.py b/sortinghat/core/schema.py
index 9b35793a..97541a71 100644
--- a/sortinghat/core/schema.py
+++ b/sortinghat/core/schema.py
@@ -66,7 +66,7 @@
                   update_scheduled_task)
 from .context import SortingHatContext
 from .decorators import (check_auth, check_permissions)
-from .errors import InvalidFilterError, EqualIndividualError
+from .errors import InvalidFilterError, EqualIndividualError, InvalidValueError
 from .importer.backend import find_import_identities_backends
 from .jobs import (affiliate,
                    unify,
@@ -1141,6 +1141,9 @@ def mutate(self, info, criteria,
         tenant = get_db_tenant()
         ctx = SortingHatContext(user=user, tenant=tenant)
 
+        if match_source and 'username' not in criteria:
+            raise InvalidValueError(msg='Match source requires username criteria.')
+
         job = get_tenant_queue(tenant).enqueue(recommend_matches,
                                                ctx,
                                                source_uuids,
@@ -1232,6 +1235,9 @@ def mutate(self, info, criteria,
         tenant = get_db_tenant()
         ctx = SortingHatContext(user=user, tenant=tenant)
 
+        if match_source and 'username' not in criteria:
+            raise InvalidValueError(msg='Match source requires username criteria.')
+
         job = get_tenant_queue(tenant).enqueue(unify,
                                                ctx,
                                                criteria,
diff --git a/ui/src/components/JobModal.vue b/ui/src/components/JobModal.vue
index 39d1d1f1..3a137c5b 100644
--- a/ui/src/components/JobModal.vue
+++ b/ui/src/components/JobModal.vue
@@ -54,12 +54,6 @@
                 density="comfortable"
                 hide-details
               />
-              <v-checkbox
-                v-model="forms.unify.matchSource"
-                label="Only unify identities that share the same source"
-                density="comfortable"
-                hide-details
-              />
             </v-col>
           </v-row>
           <v-row class="ma-0">
@@ -89,6 +83,13 @@
                   density="comfortable"
                   hide-details
                 />
+                <v-checkbox
+                  class="ml-4"
+                  v-model="forms.unify.matchSource"
+                  label="Only unify identities that share the same source"
+                  density="comfortable"
+                  hide-details
+                />
               </fieldset>
             </v-col>
           </v-row>
@@ -108,12 +109,6 @@
                 density="comfortable"
                 hide-details
               />
-              <v-checkbox
-                v-model="forms.recommendMatches.matchSource"
-                label="Only recommend identities that share the same source"
-                density="comfortable"
-                hide-details
-              />
             </v-col>
           </v-row>
           <v-row class="ma-0">
@@ -143,6 +138,13 @@
                   density="comfortable"
                   hide-details
                 />
+                <v-checkbox
+                  class="ml-4"
+                  v-model="forms.recommendMatches.matchSource"
+                  label="Only recommend identities that share the same source"
+                  density="comfortable"
+                  hide-details
+                />
               </fieldset>
             </v-col>
           </v-row>
diff --git a/ui/src/views/SettingsGeneral.vue b/ui/src/views/SettingsGeneral.vue
index d0845ee3..8983da3a 100644
--- a/ui/src/views/SettingsGeneral.vue
+++ b/ui/src/views/SettingsGeneral.vue
@@ -146,12 +146,6 @@
                   density="comfortable"
                   hide-details
                 />
-                <v-checkbox
-                  v-model="tasks.unify.params.match_source"
-                  label="Only unify identities that share the same source"
-                  density="comfortable"
-                  hide-details
-                />
               </v-col>
             </v-row>
             <v-row class="ma-0 pl-2">
@@ -180,6 +174,13 @@
                   density="comfortable"
                   hide-details
                 />
+                <v-checkbox
+                  class="ml-4"
+                  v-model="tasks.unify.params.match_source"
+                  label="Only unify identities that share the same source"
+                  density="comfortable"
+                  hide-details
+                />
               </v-col>
             </v-row>
             <v-row class="ma-0 pl-2">


Test patch:
diff --git a/tests/rec/test_matches.py b/tests/rec/test_matches.py
index 5533e92a..a1e87ee4 100644
--- a/tests/rec/test_matches.py
+++ b/tests/rec/test_matches.py
@@ -440,7 +440,7 @@ def test_recommend_match_source(self):
         rec = recs[0]
         self.assertEqual(rec[0], self.john_smith.uuid)
         self.assertEqual(rec[1], self.john_smith.individual.mk)
-        self.assertEqual(rec[2], [])
+        self.assertEqual(rec[2], sorted([self.jsm3.individual.mk]))
 
         rec = recs[1]
         self.assertEqual(rec[0], self.jrae_no_name.uuid)
@@ -450,7 +450,7 @@ def test_recommend_match_source(self):
         rec = recs[2]
         self.assertEqual(rec[0], self.jr2.uuid)
         self.assertEqual(rec[1], self.jr2.individual.mk)
-        self.assertEqual(rec[2], sorted([jrae_github.individual.mk]))
+        self.assertEqual(rec[2], sorted([jrae_github.individual.mk, self.jrae.individual.mk]))
 
     def test_recommend_same_source_not_trusted(self):
         """Matches are not created for ids with same source but not github or gitlab"""
@@ -489,7 +489,7 @@ def test_recommend_same_source_not_trusted(self):
         rec = recs[0]
         self.assertEqual(rec[0], self.john_smith.uuid)
         self.assertEqual(rec[1], self.john_smith.individual.mk)
-        self.assertEqual(rec[2], [])
+        self.assertEqual(rec[2], sorted([self.jsm3.individual.mk]))
 
         rec = recs[1]
         self.assertEqual(rec[0], self.jrae_no_name.uuid)
@@ -499,4 +499,4 @@ def test_recommend_same_source_not_trusted(self):
         rec = recs[2]
         self.assertEqual(rec[0], self.jr2.uuid)
         self.assertEqual(rec[1], self.jr2.individual.mk)
-        self.assertEqual(rec[2], [])
+        self.assertEqual(rec[2], sorted([self.jrae.individual.mk]))
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 9567a90d..4411cc17 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -1020,14 +1020,17 @@ def test_recommend_matches_match_source(self):
         # Test
         expected = {
             'results': {
-                self.john_smith.uuid: [],
-                self.jrae_no_name.uuid: [],
-                self.jr2.uuid: sorted([jrae_github.individual.mk])
+                self.john_smith.uuid: sorted([self.jsm3.individual.mk]),
+                self.jrae_no_name.uuid: sorted([self.jrae2.individual.mk]),
+                self.jr2.uuid: sorted([jrae_github.individual.mk, self.jrae.individual.mk])
             }
         }
 
         recommendations_expected = [
-            sorted([self.jr2.individual.mk, jrae_github.individual.mk])
+            sorted([self.john_smith.individual.mk, self.jsm3.individual.mk]),
+            sorted([self.jrae_no_name.individual.mk, self.jrae2.individual.mk]),
+            sorted([self.jr2.individual.mk, jrae_github.individual.mk]),
+            sorted([self.jr2.individual.mk, self.jrae.individual.mk])
         ]
 
         source_uuids = [self.john_smith.uuid, self.jrae_no_name.uuid, self.jr2.uuid]
@@ -1055,7 +1058,7 @@ def test_recommend_matches_match_source(self):
 
         self.assertDictEqual(result, expected)
 
-        self.assertEqual(MergeRecommendation.objects.count(), 1)
+        self.assertEqual(MergeRecommendation.objects.count(), 4)
 
         for rec in recommendations_expected:
             self.assertTrue(
@@ -1419,7 +1422,7 @@ def test_unify_match_source(self):
 
         # Test
         expected = {
-            'results': [jrae_github.uuid],
+            'results': [jrae_github.uuid, self.jsmith.uuid],
             'errors': []
         }
 
@@ -1439,7 +1442,11 @@ def test_unify_match_source(self):
 
         individual = Individual.objects.get(mk=jrae_github.uuid)
         identities = individual.identities.all()
-        self.assertEqual(len(identities), 4)
+        self.assertEqual(len(identities), 7)
+
+        individual = Individual.objects.get(mk=self.jsmith.uuid)
+        identities = individual.identities.all()
+        self.assertEqual(len(identities), 6)
 
     def test_unify_last_modified(self):
         """Check if unify is applied only for individuals updated after a given date"""
diff --git a/tests/test_schema.py b/tests/test_schema.py
index e96b534b..b8e3aed5 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -10249,7 +10249,7 @@ def test_unify_match_source(self, mock_job_id_gen):
                             self.js_alt3.uuid, self.js_alt4.uuid,
                             self.jrae.uuid, self.jrae2.uuid, self.jrae3.uuid,
                             jrae_github.uuid],
-            'criteria': ['email', 'name', 'username'],
+            'criteria': ['username'],
             'strict': False,
             'matchSource': True
         }