in src/olympia/addons/models.py [0:0]
def clean_slug(instance, slug_field='slug'):
"""Cleans a model instance slug.
This strives to be as generic as possible but is only used
by Add-ons at the moment.
:param instance: The instance to clean the slug for.
:param slug_field: The field where to get the currently set slug from.
"""
slug = getattr(instance, slug_field, None) or instance.name
if not slug:
# Initialize the slug with what we have available: a name translation
# or in last resort a random slug.
translations = Translation.objects.filter(id=instance.name_id)
if translations.exists():
slug = translations[0]
max_length = instance._meta.get_field(slug_field).max_length
# We have to account for slug being reduced to '' by slugify
slug = slugify(slug or '')[:max_length] or get_random_slug()
if DeniedSlug.blocked(slug):
slug = slug[: max_length - 1] + '~'
# The following trick makes sure we are using a manager that returns
# all the objects, as otherwise we could have a slug clash on our hands.
# Eg with the "Addon.objects" manager, which doesn't list deleted addons,
# we could have a "clean" slug which is in fact already assigned to an
# already existing (deleted) addon. Also, make sure we use the base class.
manager = models.Manager()
manager.model = instance._meta.proxy_for_model or instance.__class__
qs = manager.values_list(slug_field, flat=True) # Get list of all slugs.
if instance.id:
qs = qs.exclude(pk=instance.id) # Can't clash with itself.
# We first need to make sure there's a clash, before trying to find a
# suffix that is available. Eg, if there's a "foo-bar" slug, "foo" is still
# available.
clash = qs.filter(**{slug_field: slug})
if clash.exists():
max_postfix_length = len(str(MAX_SLUG_INCREMENT))
slug = slugify(slug)[: max_length - max_postfix_length]
# There is a clash, so find a suffix that will make this slug unique.
lookup = {'%s__startswith' % slug_field: slug}
clashes = qs.filter(**lookup)
prefix_len = len(slug)
used_slug_numbers = [value[prefix_len:] for value in clashes]
# find the next free slug number
slug_numbers = {int(i) for i in used_slug_numbers if i.isdigit()}
unused_numbers = SLUG_INCREMENT_SUFFIXES - slug_numbers
if unused_numbers:
num = min(unused_numbers)
elif max_length is None:
num = max(slug_numbers) + 1
else:
# This could happen. The current implementation (using
# ``[:max_length -2]``) only works for the first 100 clashes in the
# worst case (if the slug is equal to or longuer than
# ``max_length - 2`` chars).
# After that, {verylongslug}-100 will be trimmed down to
# {verylongslug}-10, which is already assigned, but it's the last
# solution tested.
raise RuntimeError(f'No suitable slug increment for {slug} found')
slug = f'{slug}{num}'
setattr(instance, slug_field, slug)
return instance