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).

enter image description here

But I am getting the Traceback ProductForm.__init__() missing 1 required positional argument: 'metal'.

Desired Output:

enter image description here

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')
0

There are 0 best solutions below