LINUX.ORG.RU

Отслеживание изменений в m2m поле модели

 ,


1

2

Добрый день, Коллеги! Вот застрял на такой задаче: Имеется тип, имеется категория, сущности относятся друг к другу как м2м. Категории имеют иерархию, реализованную через mptt.

Требуется при добавлении через админку связи тип-категория добавить к данному типу связь со всеми дочерними категориями данной.

Нашёл в интернете совет явно указать таблицу связи посредством through, и в метод save данной таблицы добавить свою логику, но этот метод не вызывается. В чём причина - я не знаю.

Код:

class AdType(models.Model):
    """
    Класс для типов
    """
    name = models.CharField(_(u'Название'), max_length=100)
    slug = models.SlugField(_(u'Слаг'), max_length=50, unique=True, help_text=_(u'Наименование в URL'))
    created_date = models.DateTimeField(_(u'Дата создания'), auto_now_add=True)
    updated_date = models.DateTimeField(_(u'Дата обновления'), auto_now=True)
    active = models.BooleanField(default=True, verbose_name=_(u'Включён'))
    position = models.PositiveIntegerField(_(u'Позиция'), default=1, help_text=_(u'Влияет на порядок отображения списка типов'))
    sites = models.ManyToManyField(Site, verbose_name=_(u'Сайты'), help_text=_(u'Отображать тип на следующих сайтах'))
    categories = models.ManyToManyField(Category, verbose_name=_(u'Категории'), help_text=_(u'Категории для данного типа объявлений'), related_name='types', through='AdTypeToCategories')

    objects = BatchManager()

    class Meta:
        ordering = ['position']
        verbose_name = _(u'тип')
        verbose_name_plural = _(u'Типы')

    def __unicode__(self):
        return self.name


class AdTypeToCategories(models.Model):
    ad_type = models.ForeignKey(AdType)
    category = models.ForeignKey(Category)

    def save(self, *args, **kwargs):
        self.add_category()
        super(AdTypeToCategories, self).save(*args, **kwargs)

    def add_category(self):
        """
        При выборе категорий выбирает и все дочерние категории
        При снятие категории, снимает и со всех дочерних
        """

        # Если это новая категория
        if not self.pk:
            # То добавим все её дочерние категории
            child_categories = self.category.get_children()
            self.ad_type.categories.add(child_categories)
            self.ad_type.save()

    class Meta:
        db_table = 'catalog_adtype_categories'
        auto_created = AdType
Есть идеи как решить задачу? И почему метод save AdTypeToCategories не вызывается?

Помню, в Django есть оптимизация для массового редактирования, возможно на неё я и попал, а как её явно обойти - не помню.



Последнее исправление: whitemaster (всего исправлений: 1)

Еще возможно документацию mptt стоит почитать, там вроде было что-то связанное со своими особенностями.

ei-grad ★★★★★
()
Ответ на: комментарий от ei-grad

Нашёл решение первой части своей задачи: для отслеживания изменения в m2m полях в Django предусмотрен сигнал m2m_changed: http://djbook.ru/rel1.8/ref/signals.html#django.db.models.signals.m2m_changed Я реализовал его приёмник и сделал добавление дочерних категорий:

    @receiver(m2m_changed, sender=AdType.categories.through)
    def ad_type_categories_changed(sender, instance, action, reverse, pk_set, *args, **kwargs):
        if action == 'pre_add' and not reverse:
    
            # Добавляем связь с типом всем дочерним категориям при добавление данной
            categories = Category.objects.filter(pk__in=pk_set)
            child_categories = []
            for category in categories:
                child_categories.extend(category.get_descendants())
            new_pk_set = []
            for category in child_categories:
                new_pk_set.append(category.pk)
    
            pk_set.update(new_pk_set)
        
    m2m_changed.connect(ad_type_categories_changed, sender=AdType.categories.through)
А вот с удалением всё несколько сложнее. Дело в том, что сигнал с описанным в документации действием pre_remove не приходит (https://code.djangoproject.com/ticket/16073), и получить список ранее связанных категорий - не получается. Попытки обратиться к instanse в обработчике для действия pre_add так-же безрезультатны:
    # Удаляем все дочерние категории при удаление данной
    old_categories = instance.categories.all() # пусто
    for category in old_categories:
        if category.pk not in pk_set:
            pass # удаляем
так что задача решена только на половину.

whitemaster
() автор топика
Ответ на: комментарий от whitemaster

После долгих поисков наконец решил свою задачу!

для отслеживания изменения в m2m полях в Django предусмотрен сигнал m2m_changed: http://djbook.ru/rel1.8/ref/signals.html#django.db.models.signals.m2m_changed Я реализовал его приёмник и сделал добавление дочерних категорий.

С удалением всё несколько сложнее. Дело в том, что сигнал с описанным в документации действием pre_remove не приходит (https://code.djangoproject.com/ticket/16073) Дело в том, что django работает несколько иначе, чем написано в документации - в действительности никакого удаления не происходит, просто в методе clear очищается старый список связей и в методе add формируется новый, поэтому мне приходится брать старый список значений до очистки в действие pre_clear и в действие pre_add сверять с новым, выявляя удалённые. Далее представляю полный код моего алгоритма обработки сигнала m2m_changed для представленных выше в вопросе моделей:

    @receiver(m2m_changed, sender=AdType.categories.through)
    def ad_type_categories_changed(sender, instance, action, reverse, pk_set, *args, **kwargs):
        """
        Алгоритм для добавления дочерних категорий:
        1) Получаем список всех новых категорий через список их primary_key (pk_set)
        2) Для каждой из категорий, посредством метода django_mmpt get_descendants получаем всех потомков объединяя их всех в единый список child_categories
        3) Формируем new_pk_set из списка child_categories
        4) Добавляем в pk_set все значения из new_pk_set
    
        Алгоритм удаления дочерних категорий:
        1) В действие pre_clear получаем старый список связанных категорий, сохраняем его в instanse
        2) Собираем список кетегорий, которые были удалены
        3) Собираем список всех дочерних категорий, для списка удалённых категорий
        4) # Исключаем дочерние категории удалённых из списка новых категорий (pk_set)
        
        Сложность данного алгоритма в том, что django не удаляет связанные объекты, поэтому нельзя просто обратиться к действию pre_remove, описанному в документации,
        вместо этого каждый раз при сохранение связанных, django пересоздаёт список связанных объектов
        """
    
        if action == 'pre_add' and not reverse:
    
            # Добавляем связь с типом всем дочерним категориям при добавление данной
            categories = Category.objects.filter(pk__in=pk_set)
            child_categories = []
            for category in categories:
                child_categories.extend(category.get_descendants())
            new_pk_set = []
            for category in child_categories:
                new_pk_set.append(category.pk)
    
            pk_set.update(new_pk_set)
    
            # Собираем список кетегорий, которые были удалены
            removed_categories = [] # cписок удалённых категорий
            for category in instance.old_categories:
                if category.pk not in pk_set:
                    removed_categories.append(category)
    
            # Собираем список всех дочерних категорий, для списка удалённых категорий
            child_categories = []
            for category in removed_categories:
                child_categories.extend(category.get_descendants())
            new_pk_set = []
            for category in child_categories:
                new_pk_set.append(category.pk)
    
            # Исключаем дочерние категории удалённых из списка новых категорий
            for pk in new_pk_set:
                if pk in pk_set:
                    pk_set.remove(pk)
    
        # Собираем старый набор категорий
        if action == 'pre_clear':
            instance.old_categories = list(instance.categories.all())
    
    
    m2m_changed.connect(ad_type_categories_changed, sender=AdType.categories.through)

whitemaster
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.