Porównaj commity

...

20 Commity

Autor SHA1 Wiadomość Data
Mi Klo 3c7b1fbc74 merge upstream
ci/woodpecker/tag/build Pipeline was successful Szczegóły
ci/woodpecker/tag/test Pipeline was successful Szczegóły
ci/woodpecker/tag/push Pipeline was successful Szczegóły
2023-07-29 07:46:40 +02:00
mtyton a3c148fd70 Merge pull request 'feature/error_handling' (#3) from feature/error_handling into main
Reviewed-on: mtyton/comfy#3
2023-07-25 22:33:52 +00:00
mtyton a8b03dd1a1 Added proper logging for the whole store and mailing application 2023-07-26 00:31:10 +02:00
mtyton dcae7a433f updated mail context 2023-07-23 19:16:08 +02:00
mtyton 2d464431ba small changes 2023-07-23 18:27:15 +02:00
mtyton 7364b24882 Merge branch 'main' of github.com:mtyton/ComfyShop 2023-07-23 18:19:39 +02:00
mtyton d8e81153ce
Merge pull request #22 from mtyton/feature/product_models_refactor
prepared some basic model modification for configurator implementation
2023-07-23 18:16:42 +02:00
mtyton f144ebf298
Merge branch 'main' into feature/product_models_refactor 2023-07-23 18:16:32 +02:00
mtyton f310781ceb finished styling 2023-07-23 18:14:09 +02:00
mtyton 783e04a134 added delivery feature and payment method 2023-07-23 00:48:09 +02:00
mtyton 3580a3b1e1 added data encrption for customer 2023-07-22 20:39:12 +02:00
mtyton 24f98cc3de added product_request mechanism 2023-07-22 18:53:18 +02:00
mtyton 3a8ffd73c5 configurator is finished summary page has been added 2023-07-21 21:34:37 +02:00
mtyton cffbc1fd41 added sepparate docker-compose file for tests 2023-07-20 18:32:10 +02:00
mtyton 10b9527261 [WIP] added offer request 2023-07-20 17:49:12 +02:00
mtyton c7aa69448c [WIP] trying formset attempt 2023-07-16 16:22:09 +02:00
mtyton c76e6fbc12 configurator is almost done 2023-07-10 22:36:11 +02:00
mtyton 540318ca48 finished preview version of configurator 2023-07-07 23:29:42 +02:00
mtyton bf33ef4ee9 added correct ProductParam validation 2023-07-03 21:51:24 +02:00
mtyton 51c4ef9623 prepared some basic model modification for configurator implementation 2023-07-02 15:51:28 +02:00
37 zmienionych plików z 1205 dodań i 127 usunięć

Wyświetl plik

@ -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",
},
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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)),

Wyświetl plik

@ -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

Wyświetl plik

@ -40,12 +40,10 @@ class OutgoingMailAdmin(ModelAdmin):
exclude_from_explorer = False
list_display = (
"subject",
"to",
"sent",
)
search_fields = (
"subject",
"to",
)
list_filter = (
"subject",

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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",
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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),
]

Wyświetl plik

@ -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"
),
),
]

Wyświetl plik

@ -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"),
),
]

Wyświetl plik

@ -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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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',

Wyświetl plik

@ -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}")

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}
-->

Wyświetl plik

@ -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">

Wyświetl plik

@ -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}}</h5>
</div>
<div class="col-sm-6 text-end">
<form action="" method="POST">

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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">

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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}} </h5>
<h5 class="mb-0">{{item.product.price}} </h5>
</div>
</div>

Wyświetl plik

@ -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">&times;</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 %}

Wyświetl plik

@ -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'

Wyświetl plik

@ -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):

Wyświetl plik

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

Wyświetl plik

@ -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

Wyświetl plik

@ -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())