sforkowany z mtyton/comfy
Porównaj commity
20 Commity
ad0a0beeff
...
3c7b1fbc74
Autor | SHA1 | Data |
---|---|---|
Mi Klo | 3c7b1fbc74 | |
mtyton | a3c148fd70 | |
mtyton | a8b03dd1a1 | |
mtyton | dcae7a433f | |
mtyton | 2d464431ba | |
mtyton | 7364b24882 | |
mtyton | d8e81153ce | |
mtyton | f144ebf298 | |
mtyton | f310781ceb | |
mtyton | 783e04a134 | |
mtyton | 3580a3b1e1 | |
mtyton | 24f98cc3de | |
mtyton | 3a8ffd73c5 | |
mtyton | cffbc1fd41 | |
mtyton | 10b9527261 | |
mtyton | c7aa69448c | |
mtyton | c76e6fbc12 | |
mtyton | 540318ca48 | |
mtyton | bf33ef4ee9 | |
mtyton | 51c4ef9623 |
|
@ -21,6 +21,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
|
|||
|
||||
# -> GlitchTip error reporting
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN", ''),
|
||||
integrations=[DjangoIntegration()],
|
||||
auto_session_tracking=False,
|
||||
traces_sample_rate=0
|
||||
|
@ -212,3 +213,18 @@ EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", '')
|
|||
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
|
||||
EMAIL_USE_TLS = True
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'artel-sklep@tepewu.pl')
|
||||
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "WARNING",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@
|
|||
|
||||
{# Global javascript #}
|
||||
<script type="text/javascript" src="{% static 'js/jquery-3.6.4.min.js' %}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.7/dist/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/artel.js' %}"></script>
|
||||
<script src="{% static 'js/cart.js' %}"></script>
|
||||
|
|
|
@ -14,6 +14,7 @@ from search import views as search_views
|
|||
|
||||
handler404 = 'artel.views.my_custom_page_not_found_view'
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("django-admin/", admin.site.urls),
|
||||
path("admin/", include(wagtailadmin_urls)),
|
||||
|
|
|
@ -6,6 +6,10 @@ services:
|
|||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
comfy:
|
||||
restart: always
|
||||
depends_on:
|
||||
- smtp-server
|
||||
- db
|
||||
build:
|
||||
dockerfile: Dockerfile.local
|
||||
context: ./
|
||||
|
@ -13,7 +17,7 @@ services:
|
|||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
# - ./:/app
|
||||
- ./:/app
|
||||
- media:/app/media
|
||||
environment:
|
||||
- SECRET_KEY
|
||||
|
|
|
@ -40,12 +40,10 @@ class OutgoingMailAdmin(ModelAdmin):
|
|||
exclude_from_explorer = False
|
||||
list_display = (
|
||||
"subject",
|
||||
"to",
|
||||
"sent",
|
||||
)
|
||||
search_fields = (
|
||||
"subject",
|
||||
"to",
|
||||
)
|
||||
list_filter = (
|
||||
"subject",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import logging
|
||||
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
@ -11,6 +13,9 @@ from django.core.mail import EmailMessage
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Attachment:
|
||||
name: str
|
||||
|
@ -34,7 +39,11 @@ def send_mail(
|
|||
message.content_subtype = 'html'
|
||||
for attachment in attachments:
|
||||
message.attach(attachment.name, attachment.content, attachment.contenttype)
|
||||
return bool(message.send())
|
||||
|
||||
sent = bool(message.send())
|
||||
if not sent:
|
||||
logger.exception(f"Sending email to {to} with subject {subject} caused an exception")
|
||||
return sent
|
||||
|
||||
|
||||
class MailTemplate(models.Model):
|
||||
|
@ -54,6 +63,10 @@ class MailTemplate(models.Model):
|
|||
|
||||
def load_and_process_template(self, context: dict|Context):
|
||||
if not self.template:
|
||||
logger.exception(
|
||||
f"Template file is missing for template with "+
|
||||
f"pk={self.pk}, template_name={self.template_name}"
|
||||
)
|
||||
raise FileNotFoundError("Template file is missing")
|
||||
if isinstance(context, dict):
|
||||
context = Context(context)
|
||||
|
|
|
@ -42,6 +42,11 @@ class PaymentMethodAdmin(ModelAdmin):
|
|||
list_display = ("name", "active")
|
||||
|
||||
|
||||
class DeliveryMethodAdmin(ModelAdmin):
|
||||
model = models.DeliveryMethod
|
||||
list_display = ("name", "active")
|
||||
|
||||
|
||||
class DocumentTemplateAdmin(ModelAdmin):
|
||||
model = models.DocumentTemplate
|
||||
list_display = ("name", )
|
||||
|
@ -58,7 +63,8 @@ class StoreAdminGroup(ModelAdminGroup):
|
|||
ProductTemplateAdmin,
|
||||
ProductAdmin,
|
||||
DocumentTemplateAdmin,
|
||||
PaymentMethodAdmin
|
||||
PaymentMethodAdmin,
|
||||
DeliveryMethodAdmin
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import logging
|
||||
|
||||
from abc import (
|
||||
ABC,
|
||||
abstractmethod
|
||||
abstractmethod,
|
||||
abstractproperty
|
||||
)
|
||||
from typing import (
|
||||
List,
|
||||
|
@ -9,12 +12,16 @@ from typing import (
|
|||
from dataclasses import dataclass
|
||||
from django.http.request import HttpRequest
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
|
||||
from store.models import (
|
||||
Product,
|
||||
ProductAuthor
|
||||
ProductAuthor,
|
||||
DeliveryMethod
|
||||
)
|
||||
|
||||
logger = logging.getLogger("cart_logger")
|
||||
|
||||
|
||||
class BaseCart(ABC):
|
||||
|
||||
|
@ -33,27 +40,58 @@ class BaseCart(ABC):
|
|||
def update_item_quantity(self, item_id, change):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_items(self):
|
||||
@abstractproperty
|
||||
def display_items(self):
|
||||
...
|
||||
|
||||
|
||||
class SessionCart(BaseCart):
|
||||
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
|
||||
def _get_author_total_price(self, author_id: int):
|
||||
author_cart = self._cart[str(author_id)]
|
||||
author_price = 0
|
||||
product_ids = list(int(pk) for pk in author_cart.keys())
|
||||
queryset = Product.objects.filter(id__in=product_ids)
|
||||
for product in queryset:
|
||||
author_price += product.price * author_cart[str(product.id)]
|
||||
|
||||
if self._delivery_info:
|
||||
author_price += self._delivery_info.price
|
||||
|
||||
return author_price
|
||||
|
||||
def _prepare_display_items(self)-> List[dict[str, dict|str]]:
|
||||
items: List[dict[str, dict|str]] = []
|
||||
for author_id, cart_items in self._cart.items():
|
||||
author = ProductAuthor.objects.get(id=int(author_id))
|
||||
products = []
|
||||
for item_id, quantity in cart_items.items():
|
||||
product=Product.objects.get(id=int(item_id))
|
||||
products.append({"product": product, "quantity": quantity})
|
||||
items.append({
|
||||
"author": author,
|
||||
"products": products,
|
||||
"group_price": self._get_author_total_price(author_id)
|
||||
})
|
||||
return items
|
||||
|
||||
def __init__(self, request: HttpRequest, delivery: DeliveryMethod=None) -> None:
|
||||
super().__init__()
|
||||
self.session = request.session
|
||||
self._cart = self.session.get(settings.CART_SESSION_ID, None)
|
||||
if not self._cart:
|
||||
self._cart = {}
|
||||
self.session[settings.CART_SESSION_ID] = self._cart
|
||||
|
||||
self._delivery_info = delivery
|
||||
self._display_items = self._prepare_display_items()
|
||||
|
||||
def save_cart(self):
|
||||
self._display_items = self._prepare_display_items()
|
||||
self.session[settings.CART_SESSION_ID] = self._cart
|
||||
self.session.modified = True
|
||||
|
||||
def add_item(self, item_id: int, quantity: int) -> None:
|
||||
# TODO - add logging
|
||||
product = self.validate_and_get_product(item_id)
|
||||
author = product.author
|
||||
quantity = int(quantity)
|
||||
|
@ -75,8 +113,7 @@ class SessionCart(BaseCart):
|
|||
self._cart[str(author.id)].pop(str(item_id))
|
||||
self.save_cart()
|
||||
except KeyError:
|
||||
# TODO - add logging
|
||||
...
|
||||
logger.exception(f"Item {item_id} not found in cart")
|
||||
|
||||
def update_item_quantity(self, item_id: int, new_quantity: int) -> None:
|
||||
product = self.validate_and_get_product(item_id)
|
||||
|
@ -90,17 +127,14 @@ class SessionCart(BaseCart):
|
|||
self._cart[str(author.id)][str(product.id)] = new_quantity
|
||||
self.save_cart()
|
||||
|
||||
def get_items(self) -> List[dict[str, dict|str]]:
|
||||
items: List[dict[str, dict|str]] = []
|
||||
for author_id, cart_items in self._cart.items():
|
||||
author = ProductAuthor.objects.get(id=int(author_id))
|
||||
products = []
|
||||
for item_id, quantity in cart_items.items():
|
||||
product=Product.objects.get(id=int(item_id))
|
||||
products.append({"product": product, "quantity": quantity})
|
||||
items.append({"author": author, "products": products})
|
||||
return items
|
||||
@property
|
||||
def delivery_info(self):
|
||||
return self._delivery_info
|
||||
|
||||
@property
|
||||
def display_items(self) -> List[dict[str, dict|str]]:
|
||||
return self._display_items
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
total = 0
|
||||
|
@ -108,6 +142,8 @@ class SessionCart(BaseCart):
|
|||
for item_id, quantity in cart_items.items():
|
||||
product = Product.objects.get(id=int(item_id))
|
||||
total += product.price * quantity
|
||||
if self._delivery_info:
|
||||
total += self._delivery_info.price * len(self._cart.keys())
|
||||
return total
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
|
@ -116,3 +152,25 @@ class SessionCart(BaseCart):
|
|||
def clear(self) -> None:
|
||||
self._cart = {}
|
||||
self.save_cart()
|
||||
|
||||
|
||||
class CustomerData:
|
||||
|
||||
def _encrypt_data(self, data: dict[str, Any]) -> str:
|
||||
signer = signing.Signer()
|
||||
return signer.sign_object(data)
|
||||
|
||||
def _decrypt_data(self, data: str) -> dict[str, Any]:
|
||||
signer = signing.Signer()
|
||||
return signer.unsign_object(data)
|
||||
|
||||
def __init__(self, data: dict[str, Any]=None, encrypted_data: str=None) -> None:
|
||||
self._data = self._encrypt_data(data) if data else encrypted_data
|
||||
|
||||
@property
|
||||
def data(self) -> dict[str, Any]:
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def decrypted_data(self) -> dict[str, Any]:
|
||||
return self._decrypt_data(self._data)
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
from django import forms
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
# from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from django.db.models import Model
|
||||
|
||||
from store.models import (
|
||||
ProductTemplate,
|
||||
ProductCategoryParamValue,
|
||||
Product,
|
||||
PaymentMethod,
|
||||
DeliveryMethod
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -32,3 +42,49 @@ class CustomerDataForm(forms.Form):
|
|||
choices=(("PL", "Polska"), ), label="Kraj",
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
||||
)
|
||||
payment_method = forms.ModelChoiceField(
|
||||
queryset=PaymentMethod.objects.filter(active=True), label="Sposób płatności",
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
||||
)
|
||||
delivery_method = forms.ModelChoiceField(
|
||||
queryset=DeliveryMethod.objects.filter(active=True), label="Sposób dostawy",
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
"""Clean method should return JSON serializable"""
|
||||
new_cleaned_data = {}
|
||||
for key, value in self.cleaned_data.items():
|
||||
if isinstance(value, PhoneNumber):
|
||||
new_cleaned_data[key] = str(value)
|
||||
elif isinstance(value, Model):
|
||||
new_cleaned_data[key] = value.pk
|
||||
else:
|
||||
new_cleaned_data[key] = value
|
||||
return new_cleaned_data
|
||||
|
||||
|
||||
class ButtonToggleSelect(forms.RadioSelect):
|
||||
template_name = "store/forms/button_toggle_select.html"
|
||||
|
||||
|
||||
class ProductTemplateConfigForm(forms.Form):
|
||||
|
||||
def _create_dynamic_fields(self, template: ProductTemplate):
|
||||
category_params = template.category.category_params.all()
|
||||
for param in category_params:
|
||||
self.fields[param.key] = forms.ModelChoiceField(
|
||||
queryset=ProductCategoryParamValue.objects.filter(param=param),
|
||||
widget=ButtonToggleSelect(attrs={"class": "btn-group btn-group-toggle"}),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, template: ProductTemplate, *args, **kwargs
|
||||
):
|
||||
self.template = template
|
||||
super().__init__(*args, **kwargs)
|
||||
self._create_dynamic_fields(template)
|
||||
|
||||
def get_product(self):
|
||||
params = list(self.cleaned_data.values())
|
||||
return Product.objects.get_or_create_by_params(template=self.template, params=params)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-25 08:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0006_remove_orderdocument_sent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="ProductImage",
|
||||
new_name="ProductTemplateImage",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-25 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0007_rename_productimage_producttemplateimage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="product",
|
||||
name="info",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="producttemplateimage",
|
||||
name="is_main",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="producttemplateimage",
|
||||
name="template",
|
||||
field=modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="template_images", to="store.producttemplate"
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ProductImage",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image", models.ImageField(upload_to="")),
|
||||
("is_main", models.BooleanField(default=False)),
|
||||
(
|
||||
"product",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="product_images", to="store.product"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-30 16:11
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0008_remove_product_info_producttemplateimage_is_main_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ProductCategoryParamValue",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("value", models.CharField(max_length=255)),
|
||||
(
|
||||
"param",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="param_values",
|
||||
to="store.productcategoryparam",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-30 16:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copy_old_data(apps, schema_editor):
|
||||
TemplateParamValue = apps.get_model("store", "TemplateParamValue")
|
||||
ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue")
|
||||
|
||||
for param_value in TemplateParamValue.objects.all():
|
||||
ProductCategoryParamValue.objects.create(
|
||||
param=param_value.param,
|
||||
value=param_value.value
|
||||
)
|
||||
|
||||
|
||||
def remove_new_data(apps, schema_editor):
|
||||
ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue")
|
||||
ProductCategoryParamValue.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0009_productcategoryparam_order_productcategoryparamvalue"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_old_data, remove_new_data),
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 4.1.9 on 2023-07-02 09:34
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0010_auto_20230630_1611"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ProductParam",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
(
|
||||
"param_value",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="store.productcategoryparamvalue"
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="product_params", to="store.product"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="TemplateParamValue",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="params",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, through="store.ProductParam", to="store.productcategoryparamvalue"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 4.1.9 on 2023-07-22 17:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0011_productparam_delete_templateparamvalue_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeliveryMethod",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("price", models.FloatField(default=0)),
|
||||
("active", models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="uuid",
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="uuid",
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="delivery_method",
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to="store.deliverymethod"),
|
||||
),
|
||||
]
|
|
@ -1,8 +1,14 @@
|
|||
import pdfkit
|
||||
import datetime
|
||||
import builtins
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
from typing import (
|
||||
Any,
|
||||
Iterator
|
||||
)
|
||||
from django.db import models
|
||||
from django.core.paginator import (
|
||||
Paginator,
|
||||
|
@ -14,6 +20,8 @@ from django.template import (
|
|||
Template,
|
||||
Context
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models.signals import m2m_changed
|
||||
|
||||
from modelcluster.models import ClusterableModel
|
||||
from modelcluster.fields import ParentalKey
|
||||
|
@ -27,16 +35,23 @@ from taggit.managers import TaggableManager
|
|||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from num2words import num2words
|
||||
|
||||
from store.utils import (
|
||||
notify_user_about_order,
|
||||
notify_manufacturer_about_order
|
||||
)
|
||||
from mailings.models import (
|
||||
OutgoingEmail,
|
||||
Attachment
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseImageModel(models.Model):
|
||||
image = models.ImageField()
|
||||
is_main = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class PersonalData(models.Model):
|
||||
|
||||
class Meta:
|
||||
|
@ -92,6 +107,32 @@ class ProductCategoryParam(ClusterableModel):
|
|||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
panels = [
|
||||
FieldPanel("category"),
|
||||
FieldPanel("key"),
|
||||
FieldPanel("param_type"),
|
||||
InlinePanel("param_values")
|
||||
]
|
||||
|
||||
def get_available_values(self) -> Iterator[any]:
|
||||
for elem in self.param_values.all():
|
||||
yield elem.get_value()
|
||||
|
||||
|
||||
class ProductCategoryParamValue(ClusterableModel):
|
||||
param = ParentalKey(ProductCategoryParam, on_delete=models.CASCADE, related_name="param_values")
|
||||
value = models.CharField(max_length=255)
|
||||
|
||||
def get_value(self):
|
||||
try:
|
||||
func = getattr(builtins, self.param.param_type)
|
||||
return func(self.value)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.param.key}: {self.value}"
|
||||
|
||||
|
||||
class ProductTemplate(ClusterableModel):
|
||||
|
@ -106,45 +147,90 @@ class ProductTemplate(ClusterableModel):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def main_image(self):
|
||||
try:
|
||||
return self.template_images.get(is_main=True)
|
||||
except ProductImage.DoesNotExist:
|
||||
return self.template_images.first()
|
||||
|
||||
panels = [
|
||||
FieldPanel("category"),
|
||||
FieldPanel("author"),
|
||||
FieldPanel('title'),
|
||||
FieldPanel('code'),
|
||||
FieldPanel('description'),
|
||||
InlinePanel("images"),
|
||||
InlinePanel("template_images", label="Template Images"),
|
||||
FieldPanel("tags"),
|
||||
]
|
||||
|
||||
|
||||
class ProductImage(models.Model):
|
||||
class ProductTemplateImage(BaseImageModel):
|
||||
template = ParentalKey(
|
||||
ProductTemplate, on_delete=models.CASCADE, related_name="images"
|
||||
ProductTemplate, on_delete=models.CASCADE, related_name="template_images"
|
||||
)
|
||||
image = models.ImageField()
|
||||
is_main = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class ProductManager(models.Manager):
|
||||
|
||||
def get_or_create_by_params(self, params: list[ProductCategoryParamValue], template: ProductTemplate):
|
||||
products = self.filter(template=template)
|
||||
|
||||
for param in params:
|
||||
products = products.filter(params__pk=param.pk)
|
||||
|
||||
# There should be only one
|
||||
if not products.count() <= 1:
|
||||
logger.exception(
|
||||
f"There should be only one product with given set of params, detected: " +
|
||||
f"{products.count()}, params: {params}, template: {template}"
|
||||
)
|
||||
|
||||
product = products.first()
|
||||
if not product:
|
||||
product = self.create(
|
||||
name=f"{template.title} - AUTOGENERATED",
|
||||
template=template,
|
||||
price=0,
|
||||
available=False
|
||||
)
|
||||
for param in params:
|
||||
product.params.add(param)
|
||||
|
||||
return product
|
||||
|
||||
|
||||
class Product(ClusterableModel):
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
info = models.TextField(blank=True)
|
||||
template = models.ForeignKey(ProductTemplate, on_delete=models.CASCADE, related_name="products")
|
||||
params = models.ManyToManyField(
|
||||
ProductCategoryParamValue, blank=True, through="ProductParam"
|
||||
)
|
||||
price = models.FloatField()
|
||||
available = models.BooleanField(default=True)
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
|
||||
objects = ProductManager()
|
||||
|
||||
panels = [
|
||||
FieldPanel("template"),
|
||||
FieldPanel("price"),
|
||||
InlinePanel("param_values"),
|
||||
FieldPanel("params"),
|
||||
FieldPanel("available"),
|
||||
FieldPanel("name"),
|
||||
FieldPanel("info")
|
||||
InlinePanel("product_images", label="Variant Images"),
|
||||
]
|
||||
|
||||
@property
|
||||
def main_image(self):
|
||||
images = self.template.images.all()
|
||||
if images:
|
||||
return images.first().image
|
||||
try:
|
||||
return self.product_images.get(is_main=True)
|
||||
except ProductImage.DoesNotExist:
|
||||
if main_image := self.template.main_image:
|
||||
return main_image
|
||||
return self.product_images.first()
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
|
@ -163,10 +249,43 @@ class Product(ClusterableModel):
|
|||
return self.name or self.template.title
|
||||
|
||||
|
||||
class TemplateParamValue(models.Model):
|
||||
param = models.ForeignKey(ProductCategoryParam, on_delete=models.CASCADE)
|
||||
product = ParentalKey(Product, on_delete=models.CASCADE, related_name="param_values")
|
||||
value = models.CharField(max_length=255)
|
||||
class ProductImage(BaseImageModel):
|
||||
product = ParentalKey(
|
||||
"Product", on_delete=models.CASCADE, related_name="product_images"
|
||||
)
|
||||
|
||||
|
||||
class ProductParam(models.Model):
|
||||
product = ParentalKey(Product, on_delete=models.CASCADE, related_name="product_params")
|
||||
param_value = models.ForeignKey(ProductCategoryParamValue, on_delete=models.CASCADE)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
# SIGNALS
|
||||
def validate_param(sender, **kwargs):
|
||||
action = kwargs.pop("action")
|
||||
if action != "pre_add":
|
||||
return
|
||||
pk_set = kwargs.get("pk_set")
|
||||
product_instance = kwargs.get("instance")
|
||||
errors = []
|
||||
for pk in pk_set:
|
||||
try:
|
||||
param = ProductCategoryParamValue.objects.get(pk=pk).param
|
||||
except ProductCategoryParamValue.DoesNotExist as e:
|
||||
logger.exception(f"Product param validation failed with exception: {str(e)}")
|
||||
count = product_instance.params.filter(productparam__param_value__param=param).count()
|
||||
if count >= 1:
|
||||
errors.append(ValueError("Product param with this key already exists."))
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
m2m_changed.connect(validate_param, Product.params.through)
|
||||
|
||||
|
||||
class ProductListPage(Page):
|
||||
|
@ -177,9 +296,9 @@ class ProductListPage(Page):
|
|||
|
||||
def _get_items(self):
|
||||
if self.tags.all():
|
||||
return Product.objects.filter(available=True, template__tags__in=self.tags.all())
|
||||
return Product.objects.filter(available=True)
|
||||
|
||||
return ProductTemplate.objects.filter(tags__in=self.tags.all())
|
||||
return ProductTemplate.objects.all()
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
items = self._get_items()
|
||||
|
@ -207,7 +326,10 @@ class OrderProductManager(models.Manager):
|
|||
pks = []
|
||||
for item in items:
|
||||
if item["quantity"] < 1:
|
||||
# TODO - logging
|
||||
logger.exception(
|
||||
f"This is not possible to add less than one item to Order, omitting item: "+
|
||||
f"{item['product']}"
|
||||
)
|
||||
continue
|
||||
|
||||
pk = self.create(
|
||||
|
@ -307,8 +429,9 @@ class OrderManager(models.Manager):
|
|||
sent = self._send_notifications(order, author, customer_data, docs)
|
||||
|
||||
if not sent:
|
||||
# TODO - store data temporarily
|
||||
raise Exception("Error while sending emails")
|
||||
logger.exception(
|
||||
f"Error while sending emails, for order: {order.order_number}"
|
||||
)
|
||||
|
||||
return Order.objects.filter(pk__in=orders_pks)
|
||||
|
||||
|
@ -319,14 +442,29 @@ class PaymentMethod(models.Model):
|
|||
description = models.TextField(blank=True)
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class DeliveryMethod(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
price = models.FloatField(default=0)
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.CASCADE)
|
||||
delivery_method = models.ForeignKey(DeliveryMethod, on_delete=models.CASCADE, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
sent = models.BooleanField(default=False)
|
||||
order_number = models.CharField(max_length=255, null=True)
|
||||
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
objects = OrderManager()
|
||||
|
||||
@property
|
||||
|
@ -335,10 +473,12 @@ class Order(models.Model):
|
|||
|
||||
@property
|
||||
def total_price(self) -> Decimal:
|
||||
return sum(
|
||||
price = sum(
|
||||
[order_product.product.price * order_product.quantity
|
||||
for order_product in self.products.all()]
|
||||
)
|
||||
delivery_price = self.delivery_method.price if self.delivery_method else 5.0
|
||||
return price + delivery_price
|
||||
|
||||
@property
|
||||
def total_price_words(self) -> str:
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-truck" viewBox="0 0 16 16">
|
||||
<path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h9A1.5 1.5 0 0 1 12 3.5V5h1.02a1.5 1.5 0 0 1 1.17.563l1.481 1.85a1.5 1.5 0 0 1 .329.938V10.5a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 1 1-4 0H5a2 2 0 1 1-3.998-.085A1.5 1.5 0 0 1 0 10.5v-7zm1.294 7.456A1.999 1.999 0 0 1 4.732 11h5.536a2.01 2.01 0 0 1 .732-.732V3.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .294.456zM12 10a2 2 0 0 1 1.732 1h.768a.5.5 0 0 0 .5-.5V8.35a.5.5 0 0 0-.11-.312l-1.48-1.85A.5.5 0 0 0 13.02 6H12v4zm-9 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm9 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 658 B |
|
@ -10,7 +10,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
const csrfToken = $(this).data('csrf-token');
|
||||
console.log(productID);
|
||||
formData.append('product_id', productID);
|
||||
formData.append('quantity', quantity); // Serialize the form data correctly
|
||||
formData.append('quantity', 1); // Serialize the form data correctly
|
||||
button.prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
from mailings.models import OutgoingEmail
|
||||
from store.models import Product
|
||||
from store.admin import ProductAdmin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO - those should be modified to be celery tasks
|
||||
|
||||
def send_produt_request_email(variant_pk: int):
|
||||
try:
|
||||
variant = Product.objects.get(pk=variant_pk)
|
||||
except Product.DoesNotExist:
|
||||
logger.exception(f"Product with pk={variant_pk} does not exist")
|
||||
|
||||
try:
|
||||
admin_url = ProductAdmin().url_helper.get_action_url("edit", variant.pk)
|
||||
send = OutgoingEmail.objects.send(
|
||||
template_name="product_request",
|
||||
subject="Złożono zapytanie ofertowe",
|
||||
recipient=variant.template.author.email,
|
||||
context={"product": variant, "admin_url": admin_url},
|
||||
sender=settings.DEFAULT_FROM_EMAIL
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Could not send email for variant pk={variant_pk}, exception: {e} has occured")
|
||||
else:
|
||||
if not send:
|
||||
logger.exception(f"Could not send email for variant pk={variant_pk}")
|
|
@ -9,7 +9,7 @@
|
|||
<h3 class="fw-normal mb-0 text-black">Koszyk</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% for group in cart.get_items %}
|
||||
{% for group in cart.display_items %}
|
||||
{% if group.products %}
|
||||
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
||||
{% for item in group.products %}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-5">
|
||||
|
||||
<div class="card-header text-center">
|
||||
<h2>{{template.title}}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<img src="{{template.main_image.image.url}}" class="img-fluid img-thumbnail" alt="Responsive image">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p>{{template.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<form action="" method="POST" class="mt-5">
|
||||
{% csrf_token %}
|
||||
<div class="row mt-5">
|
||||
{% for field in form %}
|
||||
<div class="col-6 text-center">
|
||||
<h3>{{field.label}}</h3>
|
||||
{{field}}
|
||||
</div>
|
||||
{% if forloop.counter|divisibleby:"2" %} </div><div class="row mt-5">{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6"></div>
|
||||
<div class="col-6 text-center">
|
||||
<button class="btn btn-lg btn-success" type="submit">Sprawdź dostępność</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,80 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="modal" tabindex="-1" role="dialog" id="addToCartModal" aria-labelledby="addToCartLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodano do koszyka</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Przedmiot dodany do koszyka</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-secondary" href="{{store_url}}">Kontynuuj zakupy</button>
|
||||
<a href="{% url 'cart' %}" class="btn btn-primary">Idź do koszyka</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="h-100">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<img src="{{variant.main_image.image.url}}"
|
||||
class="img-fluid img-thumbnail h-80" alt="Responsive image">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card mb-2 py-5">
|
||||
<div class="card-body">
|
||||
{% for value in params_values %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p class="text-muted mb-0">{{value}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not variant.available %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 card alert-danger text-center">
|
||||
Niestety skonfigurowany przez Ciebie wariant produktu nie jest jeszcze dostępny.
|
||||
Jeżeli jesteś zainteresowany tą konfiguracją złóż zapytanie ofertowe.
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end">
|
||||
<h3>Cena: {{variant.price}} zł</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-6 ">
|
||||
<a class="btn btn-outline-primary btn-lg" href="{% url 'product-configure' variant.template.pk %}">Wróć do konfiguratora</a>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
|
||||
{% if variant.available %}
|
||||
<button class="btn btn-outline-success btn-lg add-to-cart-button" data-product-id="{{variant.id}}"
|
||||
data-add-to-cart-url="{% url 'cart-action-add-product' %}" data-csrf-token='{{ csrf_token }}'
|
||||
data-bs-toggle="modal" data-bs-target="#addToCartModal">
|
||||
Zamów produkt
|
||||
</button>
|
||||
{% else %}
|
||||
<form method="POST" action="">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-outline-success btn-lg" type="submit">Złóż zapytanie ofertowe</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
{% with id=widget.attrs.id %}
|
||||
<div class="btn-group btn-group-toggle" role="group">
|
||||
{% for group, options, index in widget.optgroups %}
|
||||
{% for option in options %}
|
||||
<input type="radio" class="btn-check" name="{{option.name}}" id="{{id}}_{{option.index}}" autocomplete="off"
|
||||
value="{{option.value}}" required>
|
||||
<label class="btn btn-outline-primary" for="{{id}}_{{option.index}}">{{option.label}}</label>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
<!--
|
||||
{% with id=widget.attrs.id %}
|
||||
<div{% if id %} id="{{ id }}"{% endif %}
|
||||
{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}
|
||||
>
|
||||
{% for group, options, index in widget.optgroups %}
|
||||
{% if group %}
|
||||
<div><label>{{ group }}</label>{% endif %}{% for option in options %}<div>
|
||||
{% include option.template_name with widget=option %}</div>{% endfor %}{% if group %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
-->
|
|
@ -8,9 +8,9 @@
|
|||
<div class="container pt-4">
|
||||
<div class="row">
|
||||
<div class="col-12 px-4">
|
||||
<h1>Twoje dane</h1>
|
||||
<hr class="mt-1" />
|
||||
<h2>Twoje dane</h2>
|
||||
</div>
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row mx-4">
|
||||
|
@ -42,12 +42,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 mx-4">
|
||||
|
||||
<div class="col-12">
|
||||
<label class="order-form-label">Dane Kontaktowe</label>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 px-4">
|
||||
<h2>Dane do wysyłki</h2>
|
||||
</div>
|
||||
<hr class="mt-1" />
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
<div class="row mt-3 mx-4">
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.street.label}}</label>
|
||||
|
@ -72,7 +73,30 @@
|
|||
{{form.zip_code}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 px-4">
|
||||
<h2>Płatność i wysyłka</h2>
|
||||
</div>
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 mx-4">
|
||||
<div class="col-sm-12 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.payment_method.label}}</label>
|
||||
{{form.payment_method}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.delivery_method.label}}</label>
|
||||
{{form.delivery_method}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end mt-3">
|
||||
<div class="form-outline">
|
||||
<input type="submit" value="Dalej" class="btn btn-success btn-lg">
|
||||
|
|
|
@ -57,20 +57,28 @@
|
|||
<h3 class="fw-normal mb-0 text-black">Zamówione przedmioty</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% for group in cart.get_items %}
|
||||
{% for group in cart.display_items %}
|
||||
{% if group.products %}
|
||||
<h4>Wykonawca: {{group.author.display_name}}</h4>
|
||||
{% for item in group.products %}
|
||||
{% include 'store/partials/summary_cart_item.html' %}
|
||||
{% endfor %}
|
||||
{% if cart.delivery_info %}
|
||||
{% with delivery=cart.delivery_info %}
|
||||
{% include 'store/partials/delivery_cart_item.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<div class="col-sm-11 text-end">
|
||||
<h5 class="fw-normal mb-0 pr-3text-black">W sumie: {{group.group_price}} zł</h5>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="card ">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h5 class="fw-normal mb-0 text-black">Do zapłaty: {{cart.total_price}}</h5>
|
||||
<h5 class="fw-normal mb-0 text-black">Do zapłaty: {{cart.total_price}} zł</h5>
|
||||
</div>
|
||||
<div class="col-sm-6 text-end">
|
||||
<form action="" method="POST">
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="card col-9 bg-white shadow-md p-5">
|
||||
<div class="mb-4 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-success" width="75" height="75"
|
||||
fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path
|
||||
d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1>Dziękujemy za zakup!</h1>
|
||||
<div>
|
||||
Twoje zamówienia zostały przekazane do realizacji.
|
||||
Oczekuj maili z dokumentami to zamówień.<br/>
|
||||
|
||||
<b>Twoje numery zamówień:</b>
|
||||
<div class="col-12 text-center">
|
||||
{% for order in orders %}
|
||||
{{order.order_number}}<br/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a class="btn btn-outline-primary mt-3" href="{{store_url}}">
|
||||
Powrót do sklepu
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -5,7 +5,7 @@
|
|||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{{item.product.main_image.url}}"
|
||||
src="{{item.product.main_image.image.url}}"
|
||||
class="img-fluid rounded-3">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-1">
|
||||
<div class="card-body p-4">
|
||||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{% static 'images/icons/truck.svg'%}"
|
||||
class="rounded mx-auto d-block">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
<p class="lead fw-normal mb-2">{{delivery.name}}</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-2 d-flex">
|
||||
1
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||
<h5 class="mb-0">{{delivery.price}} zł</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -3,23 +3,12 @@
|
|||
<div class="card h-100" >
|
||||
<div class="card-header text-truncate">{{item.title}}</div>
|
||||
<div class="card-body p-0">
|
||||
<img src="{{item.main_image.url}}"
|
||||
class="img-fluid img-thumbnail rounded mx-auto d-block mt-2 mb-2"
|
||||
style="width: 13rem; height: 15rem;" alt="{{item.title}}">
|
||||
<img src="{{item.main_image.image.url}}"
|
||||
class="img-fluid img-thumbnail rounded mx-auto d-block mt-2 mb-2" style="width: 13rem; height: 15rem;" alt="{{item.title}}">
|
||||
|
||||
<div class="card-footer row d-flex mt-3 m-0">
|
||||
<div class="col">
|
||||
<input type="number" id="quantity{{item.id}}" name="quantity" min="1" value="1" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button class="btn btn-outline-success add-to-cart-button"
|
||||
data-product-id="{{item.id}}"
|
||||
data-csrf-token="{{csrf_token}}"
|
||||
data-add-to-cart-url={% url "cart-action-add-product" %}
|
||||
data-bs-toggle="modal" data-bs-target="#addToCartModal"
|
||||
>
|
||||
<img src="{% static 'images/icons/cart.svg' %}" style="width: 1rem; height: 1rem;" alt="Koszyk"/>
|
||||
</button>
|
||||
<div class="card-footer row d-flex m-0">
|
||||
<div class="col text-center">
|
||||
<a href="{% url 'product-configure' item.id %}" class="btn btn-primary">Konfiguruj</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{{item.product.main_image.url}}"
|
||||
src="{{item.product.main_image.image.url}}"
|
||||
class="img-fluid rounded-3">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
|
@ -15,7 +15,7 @@
|
|||
{{item.quantity}}
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||
<h5 class="mb-0">{{item.product.price}} ZŁ</h5>
|
||||
<h5 class="mb-0">{{item.product.price}} zł</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -3,26 +3,6 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="addToCartModal" aria-labelledby="addToCartLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodano do koszyka</h5>
|
||||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close" >
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Przedmiot został dodany do koszyka.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kontynuuj zakupy</button>
|
||||
<a href="{% url 'cart' %}" class="btn btn-primary">Idź do koszyka</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mb-3 mt-5">
|
||||
<div class="row row-cols-3 g-4 mt-3">
|
||||
{% for item in items %}
|
||||
|
|
|
@ -40,6 +40,14 @@ class ProductCategoryParamFactory(DjangoModelFactory):
|
|||
param_type = 'str'
|
||||
|
||||
|
||||
class ProductCategoryParamValueFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductCategoryParamValue'
|
||||
|
||||
param = SubFactory(ProductCategoryParamFactory)
|
||||
value = Faker('name')
|
||||
|
||||
|
||||
class ProductTemplateFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductTemplate'
|
||||
|
@ -56,12 +64,19 @@ class ProductFactory(DjangoModelFactory):
|
|||
model = 'store.Product'
|
||||
|
||||
name = Faker('name')
|
||||
info = Faker('text')
|
||||
price = Faker('pydecimal', left_digits=5, right_digits=2, positive=True)
|
||||
available = Faker('boolean')
|
||||
template = SubFactory(ProductTemplateFactory)
|
||||
|
||||
|
||||
class ProductParamFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductParam'
|
||||
|
||||
product = SubFactory(ProductFactory)
|
||||
param = SubFactory(ProductCategoryParamFactory)
|
||||
|
||||
|
||||
class PaymentMethodFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.PaymentMethod'
|
||||
|
|
|
@ -3,15 +3,142 @@ from unittest.mock import patch
|
|||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
|
||||
from store.tests import factories
|
||||
from store import models as store_models
|
||||
from mailings.tests.factories import MailTemplateFactory
|
||||
|
||||
|
||||
# TODO - this is fine for now, but we'll want to use factoryboy for this:
|
||||
# https://factoryboy.readthedocs.io/en/stable/
|
||||
# TODO - test have to rewritten - I'll do it tommorow
|
||||
class ProductCategoryParamTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = factories.ProductCategoryFactory()
|
||||
self.param = factories.ProductCategoryParamFactory(
|
||||
category=self.category,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
|
||||
def test_get_available_values_no_values_success(self):
|
||||
available_values = [v for v in self.param.get_available_values()]
|
||||
self.assertEqual(available_values, [])
|
||||
|
||||
def test_get_available_values_one_value_success(self):
|
||||
factories.ProductCategoryParamValueFactory(param=self.param, value="23")
|
||||
available_values = [v for v in self.param.get_available_values()]
|
||||
self.assertEqual(available_values, [23])
|
||||
self.assertEqual(len(available_values), 1)
|
||||
|
||||
def test_get_available_values_multiple_values_success(self):
|
||||
factories.ProductCategoryParamValueFactory(param=self.param, value="23")
|
||||
factories.ProductCategoryParamValueFactory(param=self.param, value="24")
|
||||
factories.ProductCategoryParamValueFactory(param=self.param, value="25")
|
||||
available_values = [v for v in self.param.get_available_values()]
|
||||
self.assertEqual(available_values, [23, 24, 25])
|
||||
self.assertEqual(len(available_values), 3)
|
||||
|
||||
|
||||
class ProductCategoryParamValueTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = factories.ProductCategoryFactory()
|
||||
|
||||
|
||||
def test_get_value_success(self):
|
||||
param = factories.ProductCategoryParamFactory(
|
||||
category=self.category,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
|
||||
proper_value = param_value.get_value()
|
||||
self.assertEqual(proper_value, 23)
|
||||
|
||||
def test_get_value_failure_wrong_value(self):
|
||||
param = factories.ProductCategoryParamFactory(
|
||||
category=self.category,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param_value = factories.ProductCategoryParamValueFactory(param=param, value="wrong_value")
|
||||
proper_value = param_value.get_value()
|
||||
self.assertEqual(proper_value, None)
|
||||
|
||||
|
||||
class ProductTestCase(TestCase):
|
||||
|
||||
def test_category_params_one_value_success(self):
|
||||
product = factories.ProductFactory()
|
||||
param = factories.ProductCategoryParamFactory(
|
||||
category=product.template.category,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
|
||||
with transaction.atomic():
|
||||
product.params.add(param_value)
|
||||
product.save()
|
||||
self.assertEqual(product.params.count(), 1)
|
||||
self.assertEqual(product.params.first().get_value(), 23)
|
||||
|
||||
def test_category_params_multiple_values_failure(self):
|
||||
product = factories.ProductFactory()
|
||||
param = factories.ProductCategoryParamFactory(
|
||||
category=product.template.category,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
|
||||
sec_param_value = factories.ProductCategoryParamValueFactory(param=param, value="24")
|
||||
with self.assertRaises(ValidationError):
|
||||
with transaction.atomic():
|
||||
product.params.add(param_value)
|
||||
product.params.add(sec_param_value)
|
||||
self.assertEqual(product.params.count(), 0)
|
||||
|
||||
def test_get_or_create_by_params_success(self):
|
||||
product = factories.ProductFactory(available=True)
|
||||
value1 = factories.ProductCategoryParamValueFactory()
|
||||
value2 = factories.ProductCategoryParamValueFactory()
|
||||
product.params.add(value1)
|
||||
product.params.add(value2)
|
||||
product.save()
|
||||
prod = store_models.Product.objects.get_or_create_by_params(
|
||||
params=[value1, value2], template=product.template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertEqual(prod.pk, product.pk)
|
||||
self.assertTrue(prod.available)
|
||||
|
||||
def test_get_or_create_by_params_success_not_existing_product(self):
|
||||
product = factories.ProductFactory(available=True)
|
||||
value1 = factories.ProductCategoryParamValueFactory()
|
||||
value2 = factories.ProductCategoryParamValueFactory()
|
||||
product.params.add(value1)
|
||||
product.price = 13.0
|
||||
product.save()
|
||||
|
||||
prod = store_models.Product.objects.get_or_create_by_params(
|
||||
params=[value1, value2], template=product.template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertNotEqual(prod.pk, product.pk)
|
||||
self.assertFalse(prod.available)
|
||||
self.assertEqual(prod.price, 0)
|
||||
|
||||
def test_get_or_create_by_params_success_not_existing_product_no_other_products(self):
|
||||
template = factories.ProductTemplateFactory()
|
||||
value1 = factories.ProductCategoryParamValueFactory()
|
||||
value2 = factories.ProductCategoryParamValueFactory()
|
||||
|
||||
prod = store_models.Product.objects.get_or_create_by_params(
|
||||
params=[value1, value2], template=template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertFalse(prod.available)
|
||||
self.assertEqual(prod.price, 0)
|
||||
|
||||
|
||||
class OrderProductTestCase(TestCase):
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
from django.test import TestCase
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from store.models import (
|
||||
ProductCategoryParam,
|
||||
ProductCategoryParamValue,
|
||||
CategoryParamTypeChoices
|
||||
)
|
||||
from store.tests.factories import (
|
||||
ProductTemplateFactory,
|
||||
ProductCategoryFactory,
|
||||
ProductFactory,
|
||||
ProductCategoryParamValueFactory
|
||||
)
|
||||
|
||||
|
||||
class ConfigureProductViewTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = ProductCategoryFactory()
|
||||
self.product_template = ProductTemplateFactory(category=self.category)
|
||||
# create template params and values for those params
|
||||
self.param1 = ProductCategoryParam.objects.create(
|
||||
key="Mocowanie", category=self.category,
|
||||
param_type=CategoryParamTypeChoices.STRING
|
||||
)
|
||||
self.param1_value1 = ProductCategoryParamValueFactory(param=self.param1)
|
||||
self.param1_value2 = ProductCategoryParamValueFactory(param=self.param1)
|
||||
self.param2 = ProductCategoryParam.objects.create(
|
||||
key="Format", category=self.category,
|
||||
param_type=CategoryParamTypeChoices.STRING
|
||||
)
|
||||
self.param2_value1 = ProductCategoryParamValueFactory(param=self.param2)
|
||||
self.param2_value2 = ProductCategoryParamValueFactory(param=self.param2)
|
||||
# create product variant
|
||||
self.variant1 = ProductFactory(
|
||||
template=self.product_template
|
||||
)
|
||||
self.variant1.params.set([self.param1_value1, self.param2_value1])
|
||||
self.variant1.save()
|
||||
|
||||
self.variant2 = ProductFactory(
|
||||
template=self.product_template,
|
||||
)
|
||||
self.variant2.params.set([self.param1_value2, self.param2_value2])
|
||||
self.variant2.save()
|
||||
|
||||
def test_get_success(self):
|
||||
response = self.client.get(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "store/configure_product.html")
|
||||
|
||||
def test_get_failure_wrong_pk(self):
|
||||
response = self.client.get(
|
||||
reverse("product-configure", args=[12312]),
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_post_success(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value1.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("configure-product-summary", args=[self.variant1.pk]))
|
||||
|
||||
def test_post_failure_not_existing_template(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value1.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[2137]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_post_not_existing_config(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value2.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class ConfigureProductSummaryViewTestCase(TestCase):
|
||||
...
|
|
@ -9,7 +9,10 @@ router.register("cart-action", store_views.CartActionView, "cart-action")
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path('product-configure/<int:pk>/', store_views.ConfigureProductView.as_view(), name='product-configure'),
|
||||
path('product-configure/summary/<int:variant_pk>/', store_views.ConfigureProductSummaryView.as_view(), name='configure-product-summary'),
|
||||
path('cart/', store_views.CartView.as_view(), name='cart'),
|
||||
path("order/", store_views.OrderView.as_view(), name="order"),
|
||||
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm")
|
||||
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm"),
|
||||
path("order/success/", store_views.OrderSuccessView.as_view(), name="order-success")
|
||||
] + router.urls
|
||||
|
|
|
@ -4,7 +4,10 @@ from django.views.generic import (
|
|||
TemplateView,
|
||||
View
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import (
|
||||
render,
|
||||
get_object_or_404
|
||||
)
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib import messages
|
||||
|
@ -12,15 +15,24 @@ from rest_framework.viewsets import ViewSet
|
|||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from store.cart import SessionCart
|
||||
from store.tasks import send_produt_request_email
|
||||
from store.cart import (
|
||||
SessionCart,
|
||||
CustomerData
|
||||
)
|
||||
from store.serializers import (
|
||||
CartSerializer,
|
||||
CartProductAddSerializer
|
||||
)
|
||||
from store.forms import CustomerDataForm
|
||||
from store.forms import (
|
||||
CustomerDataForm,
|
||||
ProductTemplateConfigForm
|
||||
)
|
||||
from store.models import (
|
||||
Order,
|
||||
Product
|
||||
Product,
|
||||
ProductTemplate,
|
||||
ProductListPage
|
||||
)
|
||||
|
||||
|
||||
|
@ -46,7 +58,7 @@ class CartActionView(ViewSet):
|
|||
def list_products(self, request):
|
||||
# get cart items
|
||||
cart = SessionCart(self.request)
|
||||
items = cart.get_items()
|
||||
items = cart.display_items
|
||||
serializer = CartSerializer(instance=items, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -57,7 +69,7 @@ class CartActionView(ViewSet):
|
|||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=400)
|
||||
serializer.save(cart)
|
||||
items = cart.get_items()
|
||||
items = cart.display_items
|
||||
serializer = CartSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
@ -70,7 +82,7 @@ class CartActionView(ViewSet):
|
|||
except Product.DoesNotExist:
|
||||
return Response({"error": "Product does not exist"}, status=400)
|
||||
|
||||
items = cart.get_items()
|
||||
items = cart.display_items
|
||||
serializer = CartSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
@ -81,10 +93,62 @@ class CartActionView(ViewSet):
|
|||
cart.update_item_quantity(pk, int(request.data["quantity"]))
|
||||
except Product.DoesNotExist:
|
||||
return Response({"error": "Product does not exist"}, status=404)
|
||||
items = cart.get_items()
|
||||
items = cart.display_items
|
||||
serializer = CartSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class ConfigureProductView(View):
|
||||
template_name = "store/configure_product.html"
|
||||
|
||||
def get_context_data(self, pk: int, **kwargs: Any) -> Dict[str, Any]:
|
||||
template = get_object_or_404(ProductTemplate, pk=pk)
|
||||
form = ProductTemplateConfigForm(template=template)
|
||||
context = {
|
||||
"template": template,
|
||||
"form": form
|
||||
}
|
||||
return context
|
||||
|
||||
def get(self, request, pk: int, *args, **kwargs):
|
||||
context = self.get_context_data(pk)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request, pk: int, *args, **kwargs):
|
||||
# first select template
|
||||
template = get_object_or_404(ProductTemplate, pk=pk)
|
||||
form = ProductTemplateConfigForm(template=template, data=request.POST)
|
||||
if not form.is_valid():
|
||||
context = self.get_context_data(pk)
|
||||
context["form"] = form
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
product_variant = form.get_product()
|
||||
return HttpResponseRedirect(reverse("configure-product-summary", args=[product_variant.pk]))
|
||||
|
||||
class ConfigureProductSummaryView(View):
|
||||
template_name = "store/configure_product_summary.html"
|
||||
|
||||
def get_context_data(self, variant_pk):
|
||||
variant = get_object_or_404(Product, pk=variant_pk)
|
||||
return {
|
||||
"variant": variant,
|
||||
"params_values": variant.params.all(),
|
||||
"store_url": ProductListPage.objects.first().get_url()
|
||||
}
|
||||
|
||||
def get(self, request, variant_pk: int, *args, **kwargs):
|
||||
context = self.get_context_data(variant_pk)
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request, variant_pk: int, *args, **kwargs):
|
||||
# Here just send the email with product request
|
||||
variant = Product.objects.get(pk=variant_pk)
|
||||
send_produt_request_email(variant.pk)
|
||||
messages.success(request, "Zapytanie o produkt zostało wysłane")
|
||||
context = self.get_context_data(variant_pk)
|
||||
return HttpResponseRedirect(context["store_url"])
|
||||
|
||||
|
||||
class OrderView(View):
|
||||
template_name = "store/order.html"
|
||||
|
@ -112,9 +176,8 @@ class OrderView(View):
|
|||
context = self.get_context_data()
|
||||
context["form"] = form
|
||||
return render(request, self.template_name, context)
|
||||
customer_data = form.data
|
||||
# TODO - add encryption
|
||||
request.session["customer_data"] = customer_data
|
||||
customer_data = CustomerData(data=form.serialize())
|
||||
request.session["customer_data"] = customer_data.data
|
||||
return HttpResponseRedirect(reverse("order-confirm"))
|
||||
|
||||
|
||||
|
@ -122,27 +185,54 @@ class OrderConfirmView(View):
|
|||
template_name = "store/order_confirm.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
customer_data = self.request.session["customer_data"]
|
||||
|
||||
form = CustomerDataForm(
|
||||
data=CustomerData(
|
||||
encrypted_data=self.request.session["customer_data"]
|
||||
).decrypted_data
|
||||
)
|
||||
if not form.is_valid():
|
||||
raise Exception("Customer data is not valid")
|
||||
|
||||
customer_data = form.cleaned_data
|
||||
return {
|
||||
"cart": SessionCart(self.request),
|
||||
"customer_data": customer_data
|
||||
"cart": SessionCart(self.request, delivery=customer_data["delivery_method"]),
|
||||
"customer_data": customer_data
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cart = SessionCart(self.request)
|
||||
if cart.is_empty():
|
||||
# TODO - messages
|
||||
messages.error(request, "Twój koszyk jest pusty")
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def post(self, request):
|
||||
customer_data = request.session["customer_data"]
|
||||
customer_data = CustomerData(
|
||||
encrypted_data=self.request.session["customer_data"]
|
||||
).decrypted_data
|
||||
cart = SessionCart(self.request)
|
||||
Order.objects.create_from_cart(
|
||||
cart.get_items(),
|
||||
order = Order.objects.create_from_cart(
|
||||
cart.display_items,
|
||||
None, customer_data
|
||||
)
|
||||
request.session.pop("customer_data")
|
||||
cart.clear()
|
||||
messages.success(request, "Zamówienie zostało złożone, sprawdź swój email.")
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
request.session["order_uuids"] = [str(elem) for elem in order.values_list("uuid", flat=True)]
|
||||
return HttpResponseRedirect(reverse("order-success"))
|
||||
|
||||
|
||||
class OrderSuccessView(View):
|
||||
template_name = "store/order_success.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
"orders": Order.objects.filter(uuid__in=self.request.session.get("order_uuids")),
|
||||
"store_url": ProductListPage.objects.first().get_url()
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.request.session.get("order_uuids"):
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
|
Ładowanie…
Reference in New Issue