From f90827d1f5d7c39d71bf9205367b222d9d8f06ce Mon Sep 17 00:00:00 2001
From: Dominik George <dominik.george@teckids.org>
Date: Fri, 7 Jan 2022 16:07:52 +0100
Subject: [PATCH] Allow combining matching fields for OR

---
 CHANGELOG.rst                       |  5 +++++
 README.rst                          |  2 +-
 aleksis/apps/ldap/apps.py           |  2 +-
 aleksis/apps/ldap/preferences.py    | 10 ++++++++++
 aleksis/apps/ldap/util/ldap_sync.py | 23 ++++++++++++++++++-----
 5 files changed, 35 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 246e3c0..6a90877 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,11 @@ and this project adheres to `Semantic Versioning`_.
 Unreleased
 ----------
 
+Added
+~~~~~
+
+* Person field matching can now be matched disjunctive
+
 Fixed
 ~~~~~
 
diff --git a/README.rst b/README.rst
index bc30e34..8cf24a8 100644
--- a/README.rst
+++ b/README.rst
@@ -19,7 +19,7 @@ Licence
 
 ::
 
-  Copyright © 2020, 2021 Dominik George <dominik.george@teckids.org>
+  Copyright © 2020, 2021, 2022 Dominik George <dominik.george@teckids.org>
   Copyright © 2020 Tom Teichler <tom.teichler@teckids.org>
 
   Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
diff --git a/aleksis/apps/ldap/apps.py b/aleksis/apps/ldap/apps.py
index 8b73e09..dd195b2 100644
--- a/aleksis/apps/ldap/apps.py
+++ b/aleksis/apps/ldap/apps.py
@@ -15,7 +15,7 @@ class LDAPConfig(AppConfig):
     }
     licence = "EUPL-1.2+"
     copyright_info = (
-        ([2020, 2021], "Dominik George", "dominik.george@teckids.org"),
+        ([2020, 2021, 2022], "Dominik George", "dominik.george@teckids.org"),
         ([2020], "Tom Teichler", "tom.teichler@teckids.org"),
     )
 
diff --git a/aleksis/apps/ldap/preferences.py b/aleksis/apps/ldap/preferences.py
index 53f852f..fa695e8 100644
--- a/aleksis/apps/ldap/preferences.py
+++ b/aleksis/apps/ldap/preferences.py
@@ -150,6 +150,16 @@ class LDAPGroupSyncOwnerAttrType(ChoicePreference):
     row = "ldap_group_sync_owner_attr"
 
 
+@site_preferences_registry.register
+class LDAPMatchingMode(ChoicePreference):
+    section = ldap
+    name = "matching_mode"
+    default = "AND"
+    required = True
+    verbose_name = _("LDAP sync matching mode")
+    choices = [("AND", _("All fields must match"), "OR", _("Any one field must match"))]
+
+
 @site_preferences_registry.register
 class EnableLDAPPasswordChange(BooleanPreference):
     section = ldap
diff --git a/aleksis/apps/ldap/util/ldap_sync.py b/aleksis/apps/ldap/util/ldap_sync.py
index 4547211..3f7b5d7 100644
--- a/aleksis/apps/ldap/util/ldap_sync.py
+++ b/aleksis/apps/ldap/util/ldap_sync.py
@@ -7,7 +7,7 @@ from django.apps import apps
 from django.conf import settings
 from django.core.files import File
 from django.db import DataError, IntegrityError, transaction
-from django.db.models import fields
+from django.db.models import Q, fields
 from django.db.models.fields.files import FileField
 from django.utils.text import slugify
 from django.utils.translation import gettext as _
@@ -270,11 +270,24 @@ def ldap_sync_from_user(user, dn, attrs):
             if missing_key not in matches:
                 defaults[missing_key] = getattr(user, missing_key)
 
-        if get_site_preferences()["ldap__create_missing_persons"]:
-            person, created = Person.objects.get_or_create(**matches, defaults=defaults)
-        else:
-            person = Person.objects.get(**matches)
+        q = Q()
+        matching_mode = get_site_preferences()["ldap__matching_mode"]
+        for field, value in matches.items():
+            add_q = Q(**{field: value})
+            if matching_mode == "AND":
+                q = q & add_q
+            elif matching_mode == "OR":
+                q = q | add_q
+            else:
+                raise ValueError(f"Invalid setting for matching mode: {matching_mode}")
+
+        try:
+            person = Person.objects.get(q)
             created = False
+        except Person.DoesNotExist:
+            if get_site_preferences()["ldap__create_missing_persons"]:
+                person = Person.objects.create(**matches, **defaults)
+                created = True
 
         user.save()
         person.user = user
-- 
GitLab