First of all, thank you for maintaining this amazing package 👍
I just updated django-filter 1.1.0 to 2.0.0 and now the following CBV fails at django startup:
class ForventetRegnskab(ABC, LoginRequiredMixin, UserPassesTestMixin, FilterView):
"""
Abstract base class for all "brugervisninger".
Each subclass must set the appropriate filterset_class and queryset.
"""
login_url = 'home'
raise_exception = True
context_object_name = 'context'
template_name = 'forventet_regnskab/content.html'
@property
@abstractmethod
def filterset_class(self) -> Optional[FilterView.filterset_class]:
pass
@property
@abstractmethod
def queryset(self):
pass
@has_sted1_permission
def user_passes_test(self, request, kwargs) -> bool:
"""
Deny a request with a permission error if the method returns False.
:param request: The request object
:param kwargs: self.kwargs
"""
pass
def test_func(self):
return self.user_passes_test(self.request, self.kwargs)
def get_context_data(self, **kwargs):
context = super(ForventetRegnskab, self).get_context_data(**kwargs)
filter_object = self.filterset_class(self.request.GET, queryset=self.get_queryset())
# Add context variables
context['title'] = constants.FORVENTET_REGNSKAB_TITLE
context['filter_sum'] = self.filter_sum(filter_queryset=filter_object.qs)
return context
def get_queryset(self):
aar = self.request.GET.get('aar')
maaned = self.request.GET.get('maaned')
kasse = self.request.GET.get('kasse')
if aar and maaned and kasse:
query = self.queryset.filter(sted1=self.kwargs['sted1']) \
.annotate(
forbrugsprocent=Case(
When(Q(korrigeret_budget__gt=0),
then=(F('akkumuleret_regnskab') / F('korrigeret_budget')) * 100),
default=0,
output_field=IntegerField()),
mer_mindre_forbrug=Func(
F('forventet_regnskab'), function='ABS') - Func(F('korrigeret_budget'), function='ABS'))
else:
query = self.queryset.none()
return query
@staticmethod
def filter_sum(filter_queryset: QuerySet) -> QuerySet:
"""
Aggregate the filtered queryset.
"""
filter_sum = filter_queryset.aggregate(
korrigeret_budget_sum=Sum('korrigeret_budget'),
regnskab_sum=Sum('regnskab'),
akkumuleret_regnskab_sum=Sum('akkumuleret_regnskab'),
forventet_regnskab_sum=Sum('forventet_regnskab'),
forbrugsprocent_sum=Case(
When(Q(korrigeret_budget_sum__gt=0),
then=(Sum('akkumuleret_regnskab') / Sum('korrigeret_budget')) * 100),
default=0,
output_field=IntegerField()),
mer_mindre_forbrug_sum=Func(
Sum('forventet_regnskab'), function='ABS') - Func(Sum('korrigeret_budget'), function='ABS'))
return filter_sum
Each subclass implements its own filterset_class and queryset. Here is an example:
class SpecialBrugervisning(ForventetRegnskab):
filterset_class = filters.SpecialBrugervisning
queryset = models.SpecialBrugervisning.objects.all()
I get this error at startup:
File "[...]\oekonomistyring\forventet_regnskab\views.py", line 18, in
class ForventetRegnskab(ABC, LoginRequiredMixin, UserPassesTestMixin, FilterView):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Hmmm... !
Is this behavior wanted or is this a bug?
What happens if you drop the ABC
inheritance?
(More: any chance you could reduce this to a minimal example so I can have a play myself?)
Hi! It works without the ABC inheritance 👍
The minimal example:
class ForventetRegnskabNoABC(LoginRequiredMixin, UserPassesTestMixin, FilterView):
login_url = 'home'
raise_exception = True
context_object_name = 'context'
template_name = 'content.html'
filterset_class = filters.my_filterset
queryset = models.my_queryset
def test_func(self):
return True
def get_context_data(self, **kwargs):
context = super(ForventetRegnskabNoABC, self).get_context_data(**kwargs)
filter_object = self.filterset_class(self.request.GET, queryset=self.get_queryset())
# Add context variables
context['filter_sum'] = self.filter_sum(filter_queryset=filter_object.qs)
return context
def get_queryset(self):
return self.queryset
@staticmethod
def filter_sum(filter_queryset: QuerySet) -> QuerySet:
filter_sum = filter_queryset.aggregate()
return filter_sum
A minimal example to recreate the TypeError
is:
class Foo(abc.ABC, FilterView):
pass
This doesn't look specific to django-filter, more that a class cannot inherit two separate metaclasses.
class AMeta(type): pass
class BMeta(type): pass
class A(metaclass=AMeta): pass
class B(metaclass=BMeta): pass
# fails with metaclass error
class C(A, B): pass
# passes
class CMeta(AMeta, BMeta): pass
class C(A, B, metaclass=CMeta): pass
The relevant change in 2.x is that FilterMixin
uses the FilterMixinRenames
metaclass, creating the conflict with ABCMeta
. You should be able to create a metclass without directly referencing the view's metaclass.
```python
class MyMeta(ABCMeta, type(FilterView)):
pass
class ForventetRegnskab(LoginRequiredMixin, UserPassesTestMixin, FilterView, metaclass=MyMeta):
pass
``
Thank you for the write-up @rpkilby!
Hi both of you!
@rpkilby very clear answer - and your solution works for the record! 🥇
All the best, Henrik
Hi again!
I Hope you have the time to clarify one thing: Why do you use type(FilterView) and not just FilterView?
Will it be interpreted as "metaclass MyABC IS metaclass FilterView"?
The type of a class is it's metaclass. i.e., the default metaclass is type
.
type(FilterView) creates a FilterView metaclass
Not exactly, it just gets the metaclass (not creating a new metaclass). This is an alternative to referencing FilterMixinRenames
explicitly.
The keyword is used to assign 1) as the only metaclass for all subclasses?
It would apply to the class and its subclasses, unless overridden by another metaclass.
Thanks again, that was helpful.
Everything in Python is an object, and everything is constructed from a class. i.e., the class of an instance is its class, the class of a class is its metaclass. The only special case is the base type
, which has a type of type
.
If you say that a class can't inherit two separate metaclasses, it makes sence to "merge" them into one and use that one in a subclass?
I mistyped. A class cannot be created from two metaclasses. It can however, be created from a metaclass that inherits multiple other metaclasses. This is similar to how you cannot instantiate an object with two classes. You would need to create one class that inherits both and instantiate the object from that. Class creation is the same way.
I assume that the metaclass of FilterClass is inherited from FilterMixinRenames...
It's metaclass is FilterMixinRenames
. You can verify this with type(FilterView)
.
Thank you! Everything makes sense now - yes, I did the type checking it adds up - it is not magic :relaxed:
Most helpful comment
This doesn't look specific to django-filter, more that a class cannot inherit two separate metaclasses.
The relevant change in 2.x is that
FilterMixin
uses theFilterMixinRenames
metaclass, creating the conflict withABCMeta
. You should be able to create a metclass without directly referencing the view's metaclass.```python
class MyMeta(ABCMeta, type(FilterView)):
pass
class ForventetRegnskab(LoginRequiredMixin, UserPassesTestMixin, FilterView, metaclass=MyMeta):
pass
``