def m2m_changed_handler(sender, *args, **kwargs): """ A model's save() never gets called on ManyToManyField changes, m2m_changed-signal is sent. sender = dynamically generated model in m2m-table instance = parent related_instance = instance being m2m'ed """ action = kwargs['action'] instance = kwargs['instance'] logger.debug("m2m_changed: %s (%s) {%s}"%(sender, args, kwargs)) bulk = [] if is_df(instance) and (action in ['post_add', 'post_remove']): pk_set = list(kwargs.get('pk_set') or []) relation_name = sender._meta.db_table.replace(sender._meta.app_label + '_' + instance.__class__.__name__.lower() + '_', '') relations = {k.name:k for k in m2m_relations(instance)} field = relations[relation_name] for pk in pk_set: related_instance = get_relation(relations[relation_name]).objects.get(pk=pk) changes = {field.name: {'changed': [pk], 'changed_to_string': six.text_type(related_instance)}} # reflect change bulk.append(History.objects.add( action=action, changes=changes, model=instance, commit=False,)) # m2m to reflect on changes field = get_field(field) changes = {field.name: {'changed': [instance.pk], 'changed_to_string': six.text_type(instance), 'm2mpg': True,}} bulk.append(History.objects.add( action='add' if action in ['post_add'] else 'rem', changes=changes, model=related_instance, commit=False,)) if is_df(instance) and (action in ['pre_clear']): # "For the pre_clear and post_clear actions, this is None." # TODO: should defer this until post_clear is done to be sure it happened # TODO: background job, optional execution relations = instance._meta.get_all_related_many_to_many_objects() for relation in relations: instances = get_m2m_reverse_instances(instance, relation) field = get_model_relation_by_instance(kwargs['model'], instance) changes = {field.name: {'changed': [instance.pk], 'changed_to_string': six.text_type(instance)}} for k in instances: bulk.append(History.objects.add( action=action, changes=changes, model=k, commit=False,)) if bulk: History.objects.bulk_create(bulk)
def handle_m2m(sender, *args, **kwargs): """ A model's save() never gets called on ManyToManyField changes, m2m_changed-signal is sent. sender = dynamically generated model in m2m-table instance = parent related_instance = instance being m2m'ed """ action = kwargs['action'] instance = kwargs['instance'] if hasattr(instance, 'get_changes') and (action == 'post_add' or action == 'post_remove'): pk_set = list(kwargs['pk_set']) relation_name = sender._meta.db_table.replace(sender._meta.app_label + '_' + instance.__class__.__name__.lower() + '_', '') for pk in pk_set: relations = {k.name:k for k in instance.get_m2m_relations()} related_instance = get_relation(relations[relation_name]).objects.get(pk=pk) field = relations[relation_name] changes = {field.name: {'changed': [pk], 'changed_to_string': six.text_type(related_instance)}} # TODO: add meta information about relation # - parent, child History.objects.add( action=ACTION_MAP[action], changes=changes, model=instance,)
def add(self, action, changes, model, user=None, object_id=None): if not getattr(settings, 'DJANGO_HISTORY_TRACK', True): return request = get_current_request() if not user: if request: user = request.user user_id = user.pk if user else None model_ct = ContentType.objects.get_for_model(model) model_id = model_ct.pk object_id = object_id or model.pk # exclusion / none -checks excludes = get_setting('EXCLUDE_CHANGES') if excludes: excludes_for_model = excludes.get("{0}.{1}".format(model_ct.app_label, model_ct.model)) if excludes_for_model: for k,v in six.iteritems(copy.deepcopy(changes)): if k in excludes_for_model.get('fields', []): del changes[k] if not changes: return # for FKs, get old/new information fields = model._meta.local_fields def get_item(model, pk): value = None if isinstance(pk, models.Model): pk = copy.deepcopy(pk.pk) try: value = six.text_type(model.objects.get(pk=pk)) except Exception as e: if settings.DEBUG: print(e) return value def match_field(model, changed_field): try: field = model._meta.get_field(k) except: field = model._meta.get_field(k.replace('_id', '')) return field for k,v in six.iteritems(changes): field = match_field(model, k) v['verbose_name'] = six.text_type(field.verbose_name) if isinstance(field, models.ForeignKey): parent_model = get_relation(field) if v['new']: v['new_to_string'] = get_item(parent_model, v['new']) if isinstance(v['new'], models.Model): v['new'] = v['new'].pk if v['old']: v['old_to_string'] = get_item(parent_model, v['old']) if isinstance(v['old'], models.Model): v['old'] = v['old'].pk v['is_fk'] = True if isinstance(field, models.ManyToManyField): v['is_m2m'] = True v['m2m_css_class'] = 'old_change' if 'm2m.add' in action: v['m2m_css_class'] = 'new_change' if 'delete' in action:# M2M copied on delete for field in model._meta.local_many_to_many: pk_set = getattr(model, field.name).all() row = { 'changed': list([k.pk for k in pk_set]), 'is_m2m': True, 'm2m_css_class': 'old_change', 'changed_to_string': u", ".join([six.text_type(k) for k in pk_set]), 'verbose_name': six.text_type(field.verbose_name), } changes[field.name] = row changeset = { 'fields': changes, 'model': { 'to_string': six.text_type(model), 'verbose_name': six.text_type(model._meta.verbose_name), 'content_type': { 'id': model_ct.pk, 'app_label': model_ct.app_label, 'model': model_ct.model, } }, 'user': { 'to_string': six.text_type(user), } } history = self.model( action=action, changes=changeset, model=model_id, user=user_id, object_id=object_id,) history.save(force_insert=True)
def get_model_relation_by_instance(model, relation): return [k for k in m2m_relations(model) if \ isinstance(relation, get_relation(k))].pop()