I am working on a web shop using Django-Oscar. I want to customise the page which creates a new product, and show different sets of attributes on the page depending on the product's metal type (which has been selected before this page is created - see below image).
But I am getting the Traceback ProductForm.__init__() missing 1 required positional argument: 'metal'.
Desired Output:
Traceback
Traceback (most recent call last):
File "/usr/local/lib/python3.10/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.10/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.10/contextlib.py", line 79, in inner
return func(*args, **kwds)
File "/usr/local/lib/python3.10/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
return view_func(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/generic/base.py", line 70, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/oscar/apps/dashboard/catalogue/views.py", line 218, in dispatch
resp = super().dispatch(
File "/usr/local/lib/python3.10/site-packages/django/views/generic/base.py", line 98, in dispatch
return handler(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/generic/edit.py", line 190, in get
return super().get(request, *args, **kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/generic/edit.py", line 133, in get
return self.render_to_response(self.get_context_data())
File "/usr/local/lib/python3.10/site-packages/oscar/apps/dashboard/catalogue/views.py", line 274, in get_context_data
ctx = super().get_context_data(**kwargs)
File "/usr/local/lib/python3.10/site-packages/django/views/generic/edit.py", line 66, in get_context_data
kwargs['form'] = self.get_form()
File "/usr/local/lib/python3.10/site-packages/django/views/generic/edit.py", line 33, in get_form
return form_class(**self.get_form_kwargs())
Exception Type: TypeError at /dashboard/catalogue/products/create/earrings/gold/diamond
Exception Value: ProductForm.__init__() missing 1 required positional argument: 'metal'
Link to Oscar Code: https://github.com/django-oscar/django-oscar/tree/master/src/oscar
Views
from django.views.generic import CreateView, ListView, UpdateView, DetailView, DeleteView
from django.urls import reverse, reverse_lazy
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.shortcuts import get_object_or_404
from oscar.core.loading import get_classes
from oscar.apps.dashboard.catalogue.views import \
ProductListView as CoreProductListView, \
ProductCreateRedirectView as CoreProductCreateRedirectView, \
ProductCreateUpdateView as CoreProductCreateUpdateView
from oscar.apps.catalogue.models import ProductClass
from myapps.catalogue.models import Metal, Gemstone, ProductAttribute, Product
from myapps.dashboard.catalogue.forms import MetalSelectForm, GemstoneSelectForm
class ProductCreateUpdateView(CoreProductCreateUpdateView):
def get_object(self, queryset=None):
"""
This parts allows generic.UpdateView to handle creating products as
well. The only distinction between an UpdateView and a CreateView
is that self.object is None. We emulate this behavior.
This method is also responsible for setting self.product_class, self.metal
and self.parent.
"""
self.creating = 'pk' not in self.kwargs
if self.creating:
# Specifying a parent product is only done when creating a child
# product.
parent_pk = self.kwargs.get('parent_pk')
if parent_pk is None:
self.parent = None
# A product class needs to be specified when creating a
# standalone product.
product_class_slug = self.kwargs.get('product_class_slug')
metal_slug = self.kwargs.get('metal_slug')
self.product_class = get_object_or_404(
ProductClass, slug=product_class_slug)
self.metal = get_object_or_404(
Metal, slug=metal_slug)
else:
self.parent = get_object_or_404(Product, pk=parent_pk)
self.product_class = self.parent.product_class
self.metal = self.parent.metal
return None # success
else:
product = super().get_object(queryset)
self.product_class = product.get_product_class()
self.parent = product.parent
self.metal = product.get_metal_type()
return product
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['metal'] = self.metal
return ctx
def get_page_title(self):
if self.creating:
if self.parent is None:
return _('Create new %(product_class)s product of metal %(metal)') % {
'product_class': self.product_class.name, 'metal': self.metal.name}
else:
return _('Create new variant of %(parent_product)s') % {
'parent_product': self.parent.title}
else:
if self.object.title or not self.parent:
return self.object.title
else:
return _('Editing variant of %(parent_product)s') % {
'parent_product': self.parent.title}
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['metal'] = self.metal
return kwargs
Forms
from django import forms
from django.core import exceptions
from django.utils.translation import gettext_lazy as _
from oscar.apps.dashboard.catalogue.forms import ProductForm as CoreProductForm
from myapps.catalogue.models import Metal, Gemstone, ProductAttribute, Product
class ProductForm(CoreProductForm):
class Meta:
fields = [
'title', 'upc', 'description', 'is_public', 'is_discountable', 'structure', 'slug', 'meta_title',
'meta_description', 'metal', 'primary_gemstone']
def __init__(self, product_class, metal, data=None, parent=None, *args, **kwargs):
self.set_initial(product_class, metal, parent, kwargs)
super().__init__(data, *args, **kwargs)
if parent:
self.instance.parent = parent
# We need to set the correct product structures explicitly to pass
# attribute validation and child product validation. Note that
# those changes are not persisted.
self.instance.structure = Product.CHILD
self.instance.parent.structure = Product.PARENT
self.delete_non_child_fields()
else:
# Only set product class and metal type for non-child products
self.instance.product_class = product_class
self.instance.metal = metal
self.add_attribute_fields(product_class, metal, self.instance.is_parent)
if 'slug' in self.fields:
self.fields['slug'].required = False
self.fields['slug'].help_text = _('Leave blank to generate from product title')
if 'title' in self.fields:
self.fields['title'].widget = forms.TextInput(
attrs={'autocomplete': 'off'})
def set_initial(self, product_class, metal, parent, kwargs):
"""
Set initial data for the form. Sets the correct product structure
and fetches initial values for the dynamically constructed attribute
fields.
"""
if 'initial' not in kwargs:
kwargs['initial'] = {}
self.set_initial_attribute_values(product_class, metal, kwargs)
if parent:
kwargs['initial']['structure'] = Product.CHILD
def set_initial_attribute_values(self, product_class, metal, kwargs):
"""
Update the kwargs['initial'] value to have the initial values based on
the product instance's attributes
"""
super().set_initial_attribute_values(product_class, kwargs)
instance = kwargs.get('instance')
if instance is None:
return
for attribute in metal.product_attribute.all():
try:
value = instance.attribute_values.get(
attribute=attribute).value
except exceptions.ObjectDoesNotExist:
pass
else:
kwargs['initial']['attr_%s' % attribute.code] = value
def add_attribute_fields(self, product_class, metal, is_parent=False):
"""
For each attribute specified by the product class, metal type,
this method dynamically adds form fields to the product form.
"""
super().add_attribute_fields(product_class, is_parent=False)
for attribute in metal.product_attribute.all():
field = self.get_attribute_field(attribute)
if field:
self.fields['attr_%s' % attribute.code].required = False
Models
from operator import xor
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from oscar.core.utils import slugify
from oscar.apps.catalogue.abstract_models import AbstractProductAttribute, AbstractProduct
from oscar.models.fields import AutoSlugField
class ProductAttribute(AbstractProductAttribute):
"""
Subclass of the AbstractProductAttribute class, adding metal and primary_gemstone fields.
"""
metal = models.ForeignKey(
'catalogue.Metal', on_delete=models.PROTECT, related_name='product_attribute',
verbose_name='Metal Type', blank=True, null=True)
primary_gemstone = models.ForeignKey(
'catalogue.Gemstone', on_delete=models.PROTECT, related_name='product_attribute',
verbose_name='Primary Gemstone Type', blank=True, null=True)
common_attribute = models.BooleanField(verbose_name='Common Attribute (Y/N)')
def get_absolute_url(self):
return reverse('dashboard:attribute_detail', args=[str(self.id)])
def clean(self):
super().clean()
if (
(
self.common_attribute and
(self.metal or self.primary_gemstone)
)
or
(
not self.common_attribute and
not xor(bool(self.metal), bool(self.primary_gemstone))
)
):
raise ValidationError(_("Product Attribute can belong to exactly one category: "
"Metal, Gemstone or Common Attribute."))
class Product(AbstractProduct):
"""
Subclass of the AbstractProduct class, adding metal and primary_gemstone fields.
"""
metal = models.ForeignKey(
'catalogue.Metal', on_delete=models.PROTECT, related_name='product',
verbose_name='Metal Type', null=True)
primary_gemstone = models.ForeignKey(
'catalogue.Gemstone', on_delete=models.PROTECT, related_name='product',
verbose_name='Primary Gemstone Type', null=True)
def get_metal_type(self):
"""
Return a product's metal type. Child products inherit their parent's.
"""
if self.is_child:
return self.parent.metal
else:
return self.metal
get_metal_type.short_description = _('Metal type')

