docs: def publishing_clone_relations(self, src_obj):
          """
          Clone forward and reverse M2Ms.
  
          This code is difficult to follow because the logic it applies is
          confusing, but here is a summary that might help:
  
              - when a draft object is published, the "current" and definitive
                relationships are cloned to the published copy. The definitive
                relationships are the draft-to-draft ones, as set in the admin.
              - a "related draft" is the draft object at the other side of
                a draft-to-draft M2M relationship
              - if a related draft also has a published copy, a draft-to-
                published relationship is added to that published copy. This
                makes our newly-published item also "published" from the reverse
                direction
              - if our draft object has a related published copy without a
                correponding related draft -- that is, a draft-to-published
                relation without a definitive draft-to-draft relation -- then
                we remove that relation as it is no longer "current". This
                makes our newly-published item "unpublished" from the reverse
                direction when an admin removes the underlying relationship.
  
          An example case:
  
              - We have Event "E" (unpublished) and Program "P" (published)
              - We add an M2M relationship from E to P. Until the relationship
                change is published it only affects drafts. Relationships are:
                    E draft <-> P draft
              - We publish E, applying the relationship to published copies on
                both sides:
                    E draft <-> P draft
                    E published <-> P draft
                    P published <-> E draft
              - We remove the M2M relationship between E and P (We could do this
                from either side: remove E from P; or, remove P from E). The
                draft-to-draft relation is removed but published copy
                relationships are not affected:
                    E published <-> P draft
                    P published <-> E draft
              - We publish P (or E) to apply the relationshp removal to
                published copies on both sides. No relationships remain.
  
          To handle M2M relationships in general we iterate over entries in the
          through-table relationship table to clone these entries, or remove
          them, as appropriate. By processing the M2M relationships in this way
          we can handle both kinds of M2M relationship:
  
              - standard M2M relationships with no explicit through table defined
                (these get an auto-generated through table) which are easier to
                handle because we can add/remove items with the relationship's
                queryset directly
              - M2M relationships with an explicit through table defined, which
                are more difficult to handle because we must use the through
                model's manager to add/remove relationships.
  
          See unit tests in ``TestPublishingOfM2MRelationships``.
          """
  
          def clone_through_model_relationship(
                  manager, through_entry, dst_obj, rel_obj
          ):
              dst_obj_filter = build_filter_for_through_field(
                  manager, manager.source_field_name, dst_obj)
              rel_obj_filter = build_filter_for_through_field(
                  manager, manager.target_field_name, rel_obj)
              if manager.through.objects \
                      .filter(**dst_obj_filter) \
                      .filter(**rel_obj_filter) \
                      .exists():
                  return
              through_entry.pk = None
              setattr(through_entry, manager.source_field_name, dst_obj)
              setattr(through_entry, manager.target_field_name, rel_obj)
              through_entry.save()
  
          def delete_through_model_relationship(manager, src_obj, dst_obj):
              src_obj_filter = build_filter_for_through_field(
                  manager, manager.source_field_name, src_obj)
              dst_obj_filter = build_filter_for_through_field(
                  manager, manager.target_field_name, dst_obj)
              manager.through.objects \
                  .filter(**src_obj_filter) \
                  .filter(**dst_obj_filter) \
                  .delete()
  
          def build_filter_for_through_field(manager, field_name, obj):
              # If the field is a `GenericForeignKey` we need to build
              # a compatible filter dict against the field target's content type
              # and PK...
              field = getattr(manager.through, field_name)
              if isinstance(field, GenericForeignKey):
                  field_filter = {
                      getattr(field, 'fk_field'): obj.pk,
                      getattr(field, 'ct_field'):
                          ContentType.objects.get_for_model(obj)
                  }
              # ...otherwise standard FK fields can be handled simply
              else:
                  field_filter = {field_name: obj}
              return field_filter
  
          def clone(src_manager):
              if (
                  not hasattr(src_manager, 'source_field_name') or
                  not hasattr(src_manager, 'target_field_name')
              ):
                  raise PublishingException(
                      "Publishing requires many-to-many managers to have"
                      " 'source_field_name' and 'target_field_name' attributes"
                      " with the source and target field names that relate the"
                      " through model to the ends of the M2M relationship."
                      " If a non-standard manager does not provide these"
                      " attributes you must add them."
                  )
  
              src_obj_source_field_filter = build_filter_for_through_field(
                  src_manager, src_manager.source_field_name, src_obj)
              through_qs = src_manager.through.objects \
                  .filter(**src_obj_source_field_filter)
              published_rel_objs_maybe_obsolete = []
              current_draft_rel_pks = set()
              for through_entry in through_qs:
                  rel_obj = getattr(through_entry, src_manager.target_field_name)
                  # If the object referenced by the M2M is publishable we only
                  # clone the relationship if it is to a draft copy, not if it is
                  # to a published copy. If it is not a publishable object at
                  # all then we always clone the relationship (True by default).
                  if getattr(rel_obj, 'publishing_is_draft', True):
                      clone_through_model_relationship(
                          src_manager, through_entry, self, rel_obj)
                      # If the related draft object also has a published copy,
                      # we need to make sure the published copy also knows about
                      # this newly-published draft.
                      try:
                          # Get published copy for related object, if any
                          rel_obj_published = rel_obj.publishing_linked
                      except AttributeError:
                          pass  # Related item has no published copy
                      else:
                          if rel_obj_published:
                              clone_through_model_relationship(
                                  src_manager, through_entry,
                                  src_obj, rel_obj_published)
                      # Track IDs of related draft copies, so we can tell later
                      # whether relationshps with published copies are obsolete
                      current_draft_rel_pks.add(rel_obj.pk)
                  else:
                      # Track related published copies, in case they have
                      # become obsolete
                      published_rel_objs_maybe_obsolete.append(rel_obj)
              # If related published copies have no corresponding related
              # draft after all the previous processing, the relationship is
              # obsolete and must be removed.
              for published_rel_obj in published_rel_objs_maybe_obsolete:
                  draft = published_rel_obj.get_draft()
                  if not draft or draft.pk not in current_draft_rel_pks:
                      delete_through_model_relationship(
                          src_manager, src_obj, published_rel_obj)
  
          # Track the relationship through-tables we have processed to avoid
          # processing the same relationships in both forward and reverse
          # directions, which could otherwise happen in unusual cases like
          # for SFMOMA event M2M inter-relationships which are explicitly
          # defined both ways as a hack to expose form widgets.
          seen_rel_through_tables = set()
  
          # Forward.
          for field in src_obj._meta.many_to_many:
              src_manager = getattr(src_obj, field.name)
              clone(src_manager)
              seen_rel_through_tables.add(field.rel.through)
  
          # Reverse.
          for field in src_obj._meta.get_all_related_many_to_many_objects():
              # Skip reverse relationship we have already seen
              if field.field.rel.through in seen_rel_through_tables:
                  continue
              field_accessor_name = field.get_accessor_name()
              # M2M relationships with `self` don't have accessor names
              if not field_accessor_name:
                  continue
              src_manager = getattr(src_obj, field_accessor_name)
              clone(src_manager)