Repository: django-oscar/django-oscar

Issue:
Overriding AddToBasketForm is not Simple
When I want to customize my basket - `AddToBasketForm` in particular, for example: I want to have just one hard-coded field in the form, I do following:

1. run `manage.py oscar_fork_app basket ./whatever` and rest as per [docs](http://docs.oscarcommerce.com/en/latest/topics/customisation.html)
2. create file `./whatever/forms.py` with following content
```python
from django import forms
from oscar.apps.basket.forms import  AddToBasketForm as CoreAddToBasketForm

class AddToBasketForm(CoreAddToBasketForm): 
    def _create_product_fields(self, product):
        self.fields['myonlyfield'] = forms.CharField(label='My only field', required=False)
```
3. I don't get my form on the product detail page :'(


### Problem

Product detail page uses [`basket_form`](https://github.com/django-oscar/django-oscar/blob/master/src/oscar/templatetags/basket_tags.py#L15) template tag which defaults to "simple" or so called "compact" style, which means using `SimpleAddToBasketForm`.

[`SimpleAddToBasketForm`'s purpose is to hide quantity form field.](https://github.com/django-oscar/django-oscar/blob/d7b2515ea30962b8160e6e50576e6c190ec95873/src/oscar/apps/basket/forms.py#L271)

Problem is that it's inherited from `AddToBasketForm` localy resolved in that particular file, without dynamic class lookup.

### Workaround

So for my *simple* `AddToBasketForm` override I need to override `SimpleAddToBasketForm` as well. Pun intended.

At the end of `./whatever/forms.py` add (bluntly copy paste from `oscar.apps.basket.forms`):
```python

class SimpleAddToBasketForm(AddToBasketForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if 'quantity' in self.fields:
            self.fields['quantity'].initial = 1
            self.fields['quantity'].widget = forms.HiddenInput()
```

Pleas note that repeating code may not be necessary, but it was easier than getting my head around `__init__`+`super()` combos with dynamic class loading magic, that I like about django-oscar, but not  yet fully understand. :) Nice work, thanks!

### Fix

This may be as simple as using dynamic class loading in [`SimpleAddToBasketForm` definition](https://github.com/django-oscar/django-oscar/blob/d7b2515ea30962b8160e6e50576e6c190ec95873/src/oscar/apps/basket/forms.py#L271).

Let's discuss it in the pull request
dynamic parent class for SimpleAddToBasketForm
Fixing https://github.com/django-oscar/django-oscar/issues/3435

I don't know `get_class()` internals well, but this small change is working to simplify overriding `AddToBasketForm` as described in the issue.

Is this a good idea?


Discussion:

# [Codecov](https://codecov.io/gh/django-oscar/django-oscar/pull/3436?src=pr&el=h1) Report
> Merging [#3436](https://codecov.io/gh/django-oscar/django-oscar/pull/3436?src=pr&el=desc) into [master](https://codecov.io/gh/django-oscar/django-oscar/commit/d7b2515ea30962b8160e6e50576e6c190ec95873&el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.


```diff
@@           Coverage Diff           @@
##           master    #3436   +/-   ##
=======================================
  Coverage   85.10%   85.10%           
=======================================
  Files         289      289           
  Lines       15511    15511           
=======================================
  Hits        13200    13200           
  Misses       2311     2311           
```

| [Impacted Files](https://codecov.io/gh/django-oscar/django-oscar/pull/3436?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/oscar/apps/basket/forms.py](https://codecov.io/gh/django-oscar/django-oscar/pull/3436/diff?src=pr&el=tree#diff-c3JjL29zY2FyL2FwcHMvYmFza2V0L2Zvcm1zLnB5) | `84.00% <100.00%> (ø)` | |

This would cause a circular import error if a project has overridden both `AddToBasketForm` and `SimpleAddToBasketForm`.

The issue you raise can be addressed another way - by moving that logic into a separate class, something like this:

```python
class SimpleAddToBasketMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if 'quantity' in self.fields:
            self.fields['quantity'].initial = 1
            self.fields['quantity'].widget = forms.HiddenInput()


class SimpleAddToBasketForm(SimpleAddToBasketMixin, AddToBasketForm):
    pass
```

So that a project just has to subclass `SimpleAddToBasketMixin` instead of duplicating it.

Resolving patch:
diff --git a/src/oscar/apps/basket/forms.py b/src/oscar/apps/basket/forms.py
--- a/src/oscar/apps/basket/forms.py
+++ b/src/oscar/apps/basket/forms.py
@@ -299,17 +299,22 @@ def cleaned_options(self):
         return options
 
 
-class SimpleAddToBasketForm(AddToBasketForm):
-    """
-    Simplified version of the add to basket form where the quantity is
-    defaulted to 1 and rendered in a hidden widget
-
-    Most of the time, you won't need to override this class. Just change
-    AddToBasketForm to change behaviour in both forms at once.
-    """
-
+class SimpleAddToBasketMixin:
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if 'quantity' in self.fields:
             self.fields['quantity'].initial = 1
             self.fields['quantity'].widget = forms.HiddenInput()
+
+
+class SimpleAddToBasketForm(SimpleAddToBasketMixin, AddToBasketForm):
+    """
+    Simplified version of the add to basket form where the quantity is
+    defaulted to 1 and rendered in a hidden widget
+
+    If you changed `AddToBasketForm`, you'll need to override this class
+    as well by doing:
+
+    class SimpleAddToBasketForm(SimpleAddToBasketMixin, AddToBasketForm):
+        pass
+    """


Test patch: