mirror of
https://github.com/Crocmagnon/checkout.git
synced 2024-11-22 08:08:04 +01:00
Implement basic baskets features
This commit is contained in:
parent
eead96359b
commit
d97030da6f
16 changed files with 249 additions and 23 deletions
33
poetry.lock
generated
33
poetry.lock
generated
|
@ -122,6 +122,21 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
|
||||||
[package.extras]
|
[package.extras]
|
||||||
toml = ["tomli"]
|
toml = ["tomli"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crispy-bootstrap5"
|
||||||
|
version = "0.6"
|
||||||
|
description = "Bootstrap5 template pack for django-crispy-forms"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=2.2"
|
||||||
|
django-crispy-forms = ">=1.13.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest", "pytest-django"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curtsies"
|
name = "curtsies"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
@ -192,6 +207,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-crispy-forms"
|
||||||
|
version = "1.14.0"
|
||||||
|
description = "Best way to have Django DRY forms"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-csp"
|
name = "django-csp"
|
||||||
version = "3.7"
|
version = "3.7"
|
||||||
|
@ -642,7 +665,7 @@ brotli = ["brotli"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "86cec3b5cbae86ec8a2e77f3e77139e12c48bcc8aa79918daed41f56f48597f4"
|
content-hash = "41c39a9ccca46b60a7ad37f03ca80ee2787f0eba1a07a74ee2eb0722db1b656e"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
asgiref = [
|
asgiref = [
|
||||||
|
@ -789,6 +812,10 @@ coverage = [
|
||||||
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
|
{file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
|
||||||
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
|
{file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
|
||||||
]
|
]
|
||||||
|
crispy-bootstrap5 = [
|
||||||
|
{file = "crispy-bootstrap5-0.6.tar.gz", hash = "sha256:e8c50daaf266d6127c7414f5669d5f2467b640d08b045ed9dd57e351d30609ae"},
|
||||||
|
{file = "crispy_bootstrap5-0.6-py3-none-any.whl", hash = "sha256:ca5d2afc2a5d1cf12b8d057886a81002cdf7793afcbe0ce006c2294069c1f387"},
|
||||||
|
]
|
||||||
curtsies = [
|
curtsies = [
|
||||||
{file = "curtsies-0.3.10.tar.gz", hash = "sha256:11efbb153d9cb22223dd9a44041ea0c313b8411e246e7f684aa843f6aa9c1600"},
|
{file = "curtsies-0.3.10.tar.gz", hash = "sha256:11efbb153d9cb22223dd9a44041ea0c313b8411e246e7f684aa843f6aa9c1600"},
|
||||||
]
|
]
|
||||||
|
@ -839,6 +866,10 @@ django-cleanup = [
|
||||||
{file = "django-cleanup-5.2.0.tar.gz", hash = "sha256:909d10ff574f5ce1a40fa63bd5c94c9ed866fd7ae770994c46cdf66c3db3e846"},
|
{file = "django-cleanup-5.2.0.tar.gz", hash = "sha256:909d10ff574f5ce1a40fa63bd5c94c9ed866fd7ae770994c46cdf66c3db3e846"},
|
||||||
{file = "django_cleanup-5.2.0-py2.py3-none-any.whl", hash = "sha256:193cf69de54b9fc0a0f4547edbb3a63bbe01728cb029f9f4b7912098cc1bced7"},
|
{file = "django_cleanup-5.2.0-py2.py3-none-any.whl", hash = "sha256:193cf69de54b9fc0a0f4547edbb3a63bbe01728cb029f9f4b7912098cc1bced7"},
|
||||||
]
|
]
|
||||||
|
django-crispy-forms = [
|
||||||
|
{file = "django-crispy-forms-1.14.0.tar.gz", hash = "sha256:35887b8851a931374dd697207a8f56c57a9c5cb9dbf0b9fa54314da5666cea5b"},
|
||||||
|
{file = "django_crispy_forms-1.14.0-py3-none-any.whl", hash = "sha256:bc4d2037f6de602d39c0bc452ac3029d1f5d65e88458872cc4dbc01c3a400604"},
|
||||||
|
]
|
||||||
django-csp = [
|
django-csp = [
|
||||||
{file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"},
|
{file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"},
|
||||||
{file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"},
|
{file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"},
|
||||||
|
|
|
@ -17,6 +17,8 @@ django-extensions = "^3.1.5"
|
||||||
bpython = "^0.22.1"
|
bpython = "^0.22.1"
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
Pillow = "^9.1.0"
|
Pillow = "^9.1.0"
|
||||||
|
django-crispy-forms = "^1.14.0"
|
||||||
|
crispy-bootstrap5 = "^0.6"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pre-commit = "^2.7"
|
pre-commit = "^2.7"
|
||||||
|
|
|
@ -82,6 +82,8 @@ INSTALLED_APPS = [
|
||||||
"django_cleanup.apps.CleanupConfig",
|
"django_cleanup.apps.CleanupConfig",
|
||||||
"common",
|
"common",
|
||||||
"purchase",
|
"purchase",
|
||||||
|
"crispy_forms",
|
||||||
|
"crispy_bootstrap5",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -195,7 +197,7 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
# CSP
|
# CSP
|
||||||
CSP_DEFAULT_SRC = ("'none'",)
|
CSP_DEFAULT_SRC = ("'none'",)
|
||||||
CSP_IMG_SRC = ("'self'",)
|
CSP_IMG_SRC = ("'self'",)
|
||||||
CSP_SCRIPT_SRC = ("'self'",)
|
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'")
|
||||||
CSP_CONNECT_SRC = ("'self'",)
|
CSP_CONNECT_SRC = ("'self'",)
|
||||||
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
|
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
|
||||||
CSP_MANIFEST_SRC = ("'self'",)
|
CSP_MANIFEST_SRC = ("'self'",)
|
||||||
|
@ -204,3 +206,6 @@ CSP_BASE_URI = ("'none'",)
|
||||||
CSP_FORM_ACTION = ("'self'",)
|
CSP_FORM_ACTION = ("'self'",)
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||||
|
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||||
|
|
|
@ -15,7 +15,7 @@ Including another URLconf
|
||||||
"""
|
"""
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import include, path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from checkout import settings
|
from checkout import settings
|
||||||
|
@ -27,7 +27,9 @@ urlpatterns = [
|
||||||
template_name="common/robots.txt", content_type="text/plain"
|
template_name="common/robots.txt", content_type="text/plain"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
path("", include("common.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("purchase/", include("purchase.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
23
src/common/templates/common/base.html
Normal file
23
src/common/templates/common/base.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Checkout</title>
|
||||||
|
<link href="{% static "vendor/bootstrap-5.1.3-dist/css/bootstrap.min.css" %}"
|
||||||
|
rel="stylesheet">
|
||||||
|
{% block extrahead %}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{% url "admin:index" %}" class="btn btn-outline-secondary">Admin</a>
|
||||||
|
<a href="{% url "admin:purchase_basket_changelist" %}" class="btn btn-outline-secondary">Baskets</a>
|
||||||
|
{% endif %}
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<script src="{% static "vendor/bootstrap-5.1.3-dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
src/common/urls.py
Normal file
8
src/common/urls.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from common.views import home
|
||||||
|
|
||||||
|
app_name = "common"
|
||||||
|
urlpatterns = [
|
||||||
|
path("", home, name="home"),
|
||||||
|
]
|
|
@ -1,3 +1,5 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
def home(request):
|
||||||
|
return redirect("purchase:new")
|
||||||
|
|
|
@ -25,18 +25,18 @@ class BasketItemInline(admin.TabularInline):
|
||||||
extra = 0
|
extra = 0
|
||||||
readonly_fields = ["price"]
|
readonly_fields = ["price"]
|
||||||
|
|
||||||
def price(self, instance) -> float:
|
def price(self, instance) -> str:
|
||||||
return instance.price / 100
|
return instance.price_display
|
||||||
|
|
||||||
|
|
||||||
@register(Basket)
|
@register(Basket)
|
||||||
class BasketAdmin(admin.ModelAdmin):
|
class BasketAdmin(admin.ModelAdmin):
|
||||||
list_display = ["id", "status", "payment_method", "created_at", "price"]
|
list_display = ["id", "payment_method", "created_at", "price"]
|
||||||
fields = ["created_at", "status", "payment_method"]
|
fields = ["created_at", "payment_method"]
|
||||||
list_filter = ["status", "payment_method"]
|
list_filter = ["payment_method"]
|
||||||
date_hierarchy = "created_at"
|
date_hierarchy = "created_at"
|
||||||
readonly_fields = ["created_at"]
|
readonly_fields = ["created_at"]
|
||||||
inlines = [BasketItemInline]
|
inlines = [BasketItemInline]
|
||||||
|
|
||||||
def price(self, instance) -> float:
|
def price(self, instance) -> str:
|
||||||
return instance.price / 100
|
return instance.price_display
|
||||||
|
|
59
src/purchase/forms.py
Normal file
59
src/purchase/forms.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Div, Field, Layout, Submit
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from purchase.layout import BasketItemField
|
||||||
|
from purchase.models import Basket, Product
|
||||||
|
|
||||||
|
PREFIX = "product-"
|
||||||
|
|
||||||
|
|
||||||
|
class BasketForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Basket
|
||||||
|
fields = ["payment_method"]
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ["purchase/js/basket_form.js"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.add_input(Submit("submit", "Submit"))
|
||||||
|
self.helper.layout = Layout()
|
||||||
|
products = {}
|
||||||
|
basket = kwargs.get("instance")
|
||||||
|
if basket:
|
||||||
|
for item in basket.items.all():
|
||||||
|
products[item.product] = item.quantity
|
||||||
|
fields = []
|
||||||
|
for product in Product.objects.all():
|
||||||
|
field_name = f"{PREFIX}{product.id}"
|
||||||
|
self.fields.update(
|
||||||
|
{
|
||||||
|
field_name: forms.IntegerField(
|
||||||
|
label=product.name,
|
||||||
|
min_value=0,
|
||||||
|
initial=products.get(product, 0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fields.append(BasketItemField(field_name, product=product))
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Div(*fields, css_class="row"),
|
||||||
|
Field("payment_method"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
instance: Basket = super().save(commit=True)
|
||||||
|
name: str
|
||||||
|
for name, value in self.cleaned_data.items():
|
||||||
|
if name.startswith(PREFIX):
|
||||||
|
product_id = int(name.removeprefix(PREFIX))
|
||||||
|
if value > 0:
|
||||||
|
instance.items.update_or_create(
|
||||||
|
product_id=product_id, defaults={"quantity": value}
|
||||||
|
)
|
||||||
|
if value == 0:
|
||||||
|
instance.items.filter(product_id=product_id).delete()
|
||||||
|
return instance
|
15
src/purchase/layout.py
Normal file
15
src/purchase/layout.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from crispy_forms.layout import Field
|
||||||
|
|
||||||
|
|
||||||
|
class BasketItemField(Field):
|
||||||
|
template = "purchase/basket_item.html"
|
||||||
|
|
||||||
|
def __init__(self, *args, product, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.product = product
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
extra_context = kwargs.get("extra_context", {})
|
||||||
|
extra_context.update({"product": self.product})
|
||||||
|
kwargs["extra_context"] = extra_context
|
||||||
|
return super().render(*args, **kwargs)
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
|
@ -34,12 +35,6 @@ class Product(Model):
|
||||||
|
|
||||||
|
|
||||||
class Basket(Model):
|
class Basket(Model):
|
||||||
STATUS_DRAFT = "DRAFT"
|
|
||||||
STATUS_COMPLETE = "COMPLETE"
|
|
||||||
_STATUS_CHOICES = [
|
|
||||||
(STATUS_DRAFT, "Draft"),
|
|
||||||
(STATUS_COMPLETE, "Complete"),
|
|
||||||
]
|
|
||||||
payment_method = models.ForeignKey(
|
payment_method = models.ForeignKey(
|
||||||
to=PaymentMethod,
|
to=PaymentMethod,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -47,9 +42,6 @@ class Basket(Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
|
||||||
max_length=20, choices=_STATUS_CHOICES, default=STATUS_DRAFT
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Panier #{self.id}"
|
return f"Panier #{self.id}"
|
||||||
|
@ -58,6 +50,14 @@ class Basket(Model):
|
||||||
def price(self) -> int:
|
def price(self) -> int:
|
||||||
return sum(item.price for item in self.items.all())
|
return sum(item.price for item in self.items.all())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_display(self) -> str:
|
||||||
|
price = self.price / 100
|
||||||
|
return f"{price}€"
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("purchase:update", args=(self.pk,))
|
||||||
|
|
||||||
|
|
||||||
class BasketItem(Model):
|
class BasketItem(Model):
|
||||||
product = models.ForeignKey(
|
product = models.ForeignKey(
|
||||||
|
@ -71,3 +71,8 @@ class BasketItem(Model):
|
||||||
@property
|
@property
|
||||||
def price(self) -> int:
|
def price(self) -> int:
|
||||||
return self.product.unit_price_cents * self.quantity
|
return self.product.unit_price_cents * self.quantity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price_display(self) -> str:
|
||||||
|
price = self.price / 100
|
||||||
|
return f"{price}€"
|
||||||
|
|
14
src/purchase/static/purchase/js/basket_form.js
Normal file
14
src/purchase/static/purchase/js/basket_form.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
window.incrementValue = function (id) {
|
||||||
|
let value = parseInt(document.getElementById(id).value);
|
||||||
|
value = isNaN(value) ? 0 : value;
|
||||||
|
value++;
|
||||||
|
document.getElementById(id).value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.decrementValue = function (id) {
|
||||||
|
let value = parseInt(document.getElementById(id).value);
|
||||||
|
value = isNaN(value) ? 0 : value;
|
||||||
|
value--;
|
||||||
|
value = value < 0 ? 0 : value;
|
||||||
|
document.getElementById(id).value = value;
|
||||||
|
};
|
16
src/purchase/templates/purchase/basket_form.html
Normal file
16
src/purchase/templates/purchase/basket_form.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "common/base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block content %}
|
||||||
|
{% if object %}
|
||||||
|
<h1>{{ object }}</h1>
|
||||||
|
{% else %}
|
||||||
|
<h1>New basket</h1>
|
||||||
|
{% endif %}
|
||||||
|
{% crispy form %}
|
||||||
|
{% if object %}
|
||||||
|
<div>
|
||||||
|
Price: {{ object.price_display }}
|
||||||
|
</div>
|
||||||
|
<a href="{% url "purchase:new" %}" class="btn btn-secondary">New</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
16
src/purchase/templates/purchase/basket_item.html
Normal file
16
src/purchase/templates/purchase/basket_item.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% load crispy_forms_field %}
|
||||||
|
<div class="col-sm-3 mb-3 mt-3">
|
||||||
|
<div class="card">
|
||||||
|
{% if product.image %}
|
||||||
|
<img src="{{ product.image.url }}" class="card-img-top">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ product.name }}</h5>
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-danger" type="button" onclick="decrementValue('{{ field.id_for_label }}')">-</button>
|
||||||
|
{% crispy_field field 'class' 'form-control' %}
|
||||||
|
<button class="btn btn-success" type="button" onclick="incrementValue('{{ field.id_for_label }}')">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
src/purchase/urls.py
Normal file
9
src/purchase/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from purchase.views import NewBasketView, UpdateBasketView
|
||||||
|
|
||||||
|
app_name = "purchase"
|
||||||
|
urlpatterns = [
|
||||||
|
path("new/", NewBasketView.as_view(), name="new"),
|
||||||
|
path("update/<int:pk>/", UpdateBasketView.as_view(), name="update"),
|
||||||
|
]
|
|
@ -1,3 +1,22 @@
|
||||||
from django.shortcuts import render
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views.generic import CreateView, UpdateView
|
||||||
|
|
||||||
# Create your views here.
|
from purchase.forms import BasketForm
|
||||||
|
from purchase.models import Basket
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectedViewsMixin(PermissionRequiredMixin, LoginRequiredMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NewBasketView(ProtectedViewsMixin, CreateView):
|
||||||
|
permission_required = ["purchase.add_basket"]
|
||||||
|
model = Basket
|
||||||
|
form_class = BasketForm
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateBasketView(ProtectedViewsMixin, UpdateView):
|
||||||
|
permission_required = ["purchase.change_basket", "purchase.view_basket"]
|
||||||
|
model = Basket
|
||||||
|
form_class = BasketForm
|
||||||
|
|
Loading…
Reference in a new issue