diff --git a/makeabilitylab/settings.py b/makeabilitylab/settings.py index 06ed0306..006d28fb 100644 --- a/makeabilitylab/settings.py +++ b/makeabilitylab/settings.py @@ -72,8 +72,8 @@ ALLOWED_HOSTS = ['*'] # Makeability Lab Global Variables, including Makeability Lab version -ML_WEBSITE_VERSION = "1.9.9.9" # Keep this updated with each release and also change the short description below -ML_WEBSITE_VERSION_DESCRIPTION = "Fixes poster upload bugs" +ML_WEBSITE_VERSION = "2.0" # Keep this updated with each release and also change the short description below +ML_WEBSITE_VERSION_DESCRIPTION = "Generates unique url_names for duplicate member names to avoid conflicts" DATE_MAKEABILITYLAB_FORMED = datetime.date(2012, 1, 1) # Date Makeability Lab was formed MAX_BANNERS = 7 # Maximum number of banners on a page diff --git a/website/models/person.py b/website/models/person.py index 7325607b..cafd9141 100644 --- a/website/models/person.py +++ b/website/models/person.py @@ -623,8 +623,20 @@ def save(self, *args, **kwargs): if bool(re.search('[^a-zA-Z]', c)) and c in special_chars: url_name_cleaned = url_name_cleaned.replace(c, special_chars.get(c)) - # Finally, clean remaining characters (EX: dashes, periods). + # Clean remaining characters (EX: dashes, periods). url_name_cleaned = re.sub('[^a-zA-Z]', '', url_name_cleaned) + + # Check for collisions and append numeric suffix if needed + # We exclude the current person (by pk) when checking for duplicates to allow updates + base_url_name = url_name_cleaned + counter = 2 + + # Keep incrementing counter until we find a unique url_name + while Person.objects.filter(url_name=url_name_cleaned).exclude(pk=self.pk).exists(): + url_name_cleaned = f"{base_url_name}{counter}" + counter += 1 + _logger.debug(f"URL name collision detected for {self.get_full_name()}. Trying {url_name_cleaned}") + self.url_name = url_name_cleaned # Next, automatically set the bio_date_modified field diff --git a/website/views/member.py b/website/views/member.py index b3f7bfda..e777700e 100644 --- a/website/views/member.py +++ b/website/views/member.py @@ -3,6 +3,7 @@ import website.utils.ml_utils as ml_utils from django.shortcuts import render, get_object_or_404, redirect from django.db.models import Q +from django.core.exceptions import MultipleObjectsReturned # For logging import time @@ -36,6 +37,13 @@ def member(request, member_name=None, member_id=None): try: # Try a case-insensitive exact match person = get_object_or_404(Person, url_name__iexact=member_name) + except MultipleObjectsReturned: + # This should not happen if url_name uniqueness is working correctly + # Log error and return the most recently modified person as fallback + _logger.error(f"Multiple people found with url_name={member_name}! This indicates url_name uniqueness is broken. Returning most recent.") + person = Person.objects.filter(url_name__iexact=member_name).order_by('-modified_date').first() + if person is None: + raise Http404("No person matches the given query.") except Http404: _logger.debug(f"{member_name} not found for url_name, looking for closest match in database") closest_urlname = get_closest_urlname_in_database(member_name)