Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using m2m_fields in inheritated models #1242

Closed
bacheric opened this issue Sep 4, 2023 · 4 comments · Fixed by #1243
Closed

Using m2m_fields in inheritated models #1242

bacheric opened this issue Sep 4, 2023 · 4 comments · Fixed by #1243

Comments

@bacheric
Copy link

bacheric commented Sep 4, 2023

Problem Statement
Access many-to-many fields of base model when using inheritance

Describe the solution you'd like
I have a question about using django-simple-history with models that support inheritation.

Basic example:

class Application(PolymorphicModel):
     users = ManyToManyField("User")
class SpecialApplication(Application):
     history=HistoricalRecords(m2m_fields=["users"]

This results in AttributeError: 'str' object has no attribute 'name'

I also tried using m2m_fields=[Application.users] but received AttributeError: 'ManyToManyDescriptor' object has no attribute 'name'.

How can I solve this?

Additional context

Edit: I am using Django Polymorphic for my base model, I don't know if this makes a difference, but I put it in the example.

The documentation says:

You may also define these fields in a model attribute (by default on _history_m2m_fields). This is mainly used for inherited models. You can override the attribute name by setting your own m2m_fields_model_field_name argument on the HistoricalRecord instance.

But I have no real clue how to use this. Is there an example somewhere?

@legau
Copy link
Contributor

legau commented Sep 4, 2023

Hi, I made a PR(#1243) to allow declaring these fields by their name.

While waiting for the new release you can subclass HistoricalRecords with my changes.

@bacheric
Copy link
Author

bacheric commented Sep 5, 2023

Hey @legau , many thanks for your response. However, I have trouble getting it to work. I created a very basic django project to make sure it is not an issue coming from my current code project. This file holds my models:

from django.conf import settings
from django.db import models
from polymorphic.models import PolymorphicModel

from mysite.polls.models.historical_records2 import HistoricalRecords2

class Application(PolymorphicModel):
     responsible = models.ManyToManyField(settings.AUTH_USER_MODEL,blank=True)
     
class SpecialApplication(Application):
    information = models.CharField(max_length=100)
    history = HistoricalRecords2(m2m_fields=["responsible"])
    

HistoricalRecords 2:

from simple_history.models import HistoricalRecords


class HistoricalRecords2(HistoricalRecords):
    def get_m2m_fields_from_model(self, model):
        m2m_fields = set(self.m2m_fields)
        try:
            m2m_fields.update(getattr(model, self.m2m_fields_model_field_name))
        except AttributeError:
            pass
        field_names = [
            field if isinstance(field, str) else field.name for field in m2m_fields
        ]
        return [getattr(model, field_name).field for field_name in field_names]

Now, whenever I try to save a SpecialApplication instance (inside django admin for example), I receive an error with the following stacktrace:

Environment:


Request Method: POST
Request URL: http://localhost:8000/admin/polls/specialapplication/add/

Django Version: 4.2.5
Python Version: 3.11.1
Installed Applications:
['mysite.polls',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'polymorphic',
 'simple_history']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'simple_history.middleware.HistoryRequestMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 688, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/utils/decorators.py", line 134, in _wrapper_view
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/views/decorators/cache.py", line 62, in _wrapper_view_func
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/sites.py", line 242, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1886, in add_view
    return self.changeform_view(request, None, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/utils/decorators.py", line 134, in _wrapper_view
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1747, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1798, in _changeform_view
    self.save_model(request, new_object, form, not add)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1227, in save_model
    obj.save()
    ^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/polymorphic/models.py", line 87, in save
    return super().save(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/base.py", line 814, in save
    self.save_base(
    ^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/base.py", line 892, in save_base
    post_save.send(
    ^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/dispatch/dispatcher.py", line 176, in send
    return [
           
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/dispatch/dispatcher.py", line 177, in <listcomp>
    (receiver, receiver(signal=self, sender=sender, **named))
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/simple_history/models.py", line 637, in post_save
    self.create_historical_record(instance, created and "+" or "~", using=using)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/simple_history/models.py", line 736, in create_historical_record
    self.create_historical_record_m2ms(history_instance, instance)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/simple_history/models.py", line 673, in create_historical_record_m2ms
    rows = through_model.objects.filter(**{through_field_name: instance})
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1436, in filter
    return self._filter_or_exclude(False, args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1454, in _filter_or_exclude
    clone._filter_or_exclude_inplace(negate, args, kwargs)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1461, in _filter_or_exclude_inplace
    self._query.add_q(Q(*args, **kwargs))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1545, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1576, in _add_q
    child_clause, needed_inner = self.build_filter(
                                 
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1426, in build_filter
    lookups, parts, reffed_expression = self.solve_lookup_type(arg, summarize)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1236, in solve_lookup_type
    _, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eric/repos/m2mtest/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1724, in names_to_path
    raise FieldError(
    ^

Exception Type: FieldError at /admin/polls/specialapplication/add/
Exception Value: Cannot resolve keyword 'specialapplication' into field. Choices are: application, application_id, id, user, user_id

I attached the sample project to this comment. I would be very grateful if you could take a look at it to see whats wrong. :)

mysite.zip

@legau
Copy link
Contributor

legau commented Sep 5, 2023

After some tries against dsh master branch it looks like you also need #1218 which was merged 2 weeks ago

@bacheric
Copy link
Author

bacheric commented Sep 5, 2023

Many thanks, after adding the changes of both #1218 and #1243 it works as expected!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants