Repository: svthalia/concrexit

Issue:
Fix "complex_logic" issue in website/members/models/membership.py
Consider simplifying this complex logical expression.

https://codeclimate.com/github/svthalia/concrexit/website/members/models/membership.py#issue_5eceacb0e96d310001000384


Discussion:

Resolving patch:
diff --git a/website/activemembers/models.py b/website/activemembers/models.py
--- a/website/activemembers/models.py
+++ b/website/activemembers/models.py
@@ -1,8 +1,6 @@
 """The models defined by the activemembers package"""
 import datetime
 import logging
-import math
-from typing import Union, Iterable
 
 from django.conf import settings
 from django.contrib.auth.models import Permission
@@ -18,6 +16,7 @@
 from django.utils.translation import gettext_lazy as _
 from tinymce import HTMLField
 
+from utils.snippets import overlaps
 from utils.translation import ModelTranslateMeta, MultilingualField, localize_attr_name
 
 logger = logging.getLogger(__name__)
@@ -363,50 +362,3 @@ def __str__(self):
 
     class Meta:
         unique_together = ("member", "year")
-
-
-HasTimespan = Union[MemberGroupMembership, MemberGroup]
-
-
-def overlaps(check: HasTimespan, others: Iterable[HasTimespan], can_equal=False):
-    """Check for overlapping date ranges
-
-    This works by checking the maximum of the two `since` times, and the minimum of
-    the two `until` times. Because there are no infinite dates, the value date_max
-    is created for when the `until` value is None; this signifies a timespan that
-    has not ended yet and is the maximum possible date in Python's datetime.
-
-    The ranges overlap when the maximum start time is smaller than the minimum
-    end time, as can be seen in this example of two integer ranges:
-
-    check: . . . .[4]. . . . 9
-    other: . . 2 . .[5]. . . .
-
-    check: . . . .[4]. . . . 9
-    other: . . 2 . . . . . . . [date_max]
-
-    And when non overlapping:
-    check: . . . . . .[6] . . 9
-    other: . . 2 . .[5]. . . .
-
-    4 < 5 == True so these intervals overlap, while 6 < 5 == False so these intervals
-    don't overlap
-
-    The can_equal argument is used for boards, where the end date can't be the same
-    as the start date.
-    """
-    date_max = datetime.date(datetime.MAXYEAR, 12, 31)
-    for other in others:
-        if check.pk == other.pk:
-            # No checks for the object we're validating
-            continue
-
-        max_start = max(check.since, other.since)
-        min_end = min(check.until or date_max, other.until or date_max)
-
-        if max_start == min_end and not can_equal:
-            return True
-        if max_start < min_end:
-            return True
-
-    return False
diff --git a/website/members/models/membership.py b/website/members/models/membership.py
--- a/website/members/models/membership.py
+++ b/website/members/models/membership.py
@@ -4,6 +4,8 @@
 from django.utils import timezone
 from django.utils.translation import pgettext_lazy, gettext_lazy as _
 
+from utils.snippets import overlaps
+
 
 class Membership(models.Model):
     MEMBER = "member"
@@ -57,32 +59,13 @@ def clean(self):
 
         if self.since is not None:
             memberships = self.user.membership_set.all()
-            for membership in memberships:
-                if membership.pk == self.pk:
-                    continue
-                if (
-                    (
-                        membership.until is None
-                        and (self.until is None or self.until > membership.since)
-                    )
-                    or (self.until is None and self.since < membership.until)
-                    or (
-                        self.until
-                        and membership.until
-                        and self.since < membership.until
-                        and self.until > membership.since
-                    )
-                ):
-                    errors.update(
-                        {
-                            "since": _(
-                                "A membership already " "exists for that period"
-                            ),
-                            "until": _(
-                                "A membership already " "exists for that period"
-                            ),
-                        }
-                    )
+            if overlaps(self, memberships):
+                errors.update(
+                    {
+                        "since": _("A membership already exists for that period"),
+                        "until": _("A membership already exists for that period"),
+                    }
+                )
 
         if errors:
             raise ValidationError(errors)
diff --git a/website/utils/snippets.py b/website/utils/snippets.py
--- a/website/utils/snippets.py
+++ b/website/utils/snippets.py
@@ -1,4 +1,5 @@
 """Provides various utilities that are useful across the project"""
+import datetime
 import hmac
 from _sha1 import sha1
 from base64 import urlsafe_b64decode, urlsafe_b64encode
@@ -89,3 +90,47 @@ def extract_date_range(request, allow_empty=False):
             raise ParseError(detail="end query parameter invalid") from e
 
     return start, end
+
+
+def overlaps(check, others, can_equal=False):
+    """Check for overlapping date ranges
+
+    This works by checking the maximum of the two `since` times, and the minimum of
+    the two `until` times. Because there are no infinite dates, the value date_max
+    is created for when the `until` value is None; this signifies a timespan that
+    has not ended yet and is the maximum possible date in Python's datetime.
+
+    The ranges overlap when the maximum start time is smaller than the minimum
+    end time, as can be seen in this example of two integer ranges:
+
+    check: . . . .[4]. . . . 9
+    other: . . 2 . .[5]. . . .
+
+    check: . . . .[4]. . . . 9
+    other: . . 2 . . . . . . . [date_max]
+
+    And when non overlapping:
+    check: . . . . . .[6] . . 9
+    other: . . 2 . .[5]. . . .
+
+    4 < 5 == True so these intervals overlap, while 6 < 5 == False so these intervals
+    don't overlap
+
+    The can_equal argument is used for boards, where the end date can't be the same
+    as the start date.
+    """
+    date_max = datetime.date(datetime.MAXYEAR, 12, 31)
+    for other in others:
+        if check.pk == other.pk:
+            # No checks for the object we're validating
+            continue
+
+        max_start = max(check.since, other.since)
+        min_end = min(check.until or date_max, other.until or date_max)
+
+        if max_start == min_end and not can_equal:
+            return True
+        if max_start < min_end:
+            return True
+
+    return False


Test patch: