Merge branch 'main' into feature/optionalization

feature/optionalization
mtyton 2023-08-06 11:13:16 +02:00
commit 5975b4c3d9
78 zmienionych plików z 2649 dodań i 462 usunięć

Wyświetl plik

@ -46,6 +46,8 @@ COPY --chown=wagtail:wagtail . .
# Use user "wagtail" to run the build commands below and the server itself.
USER wagtail
RUN mkdir -p /app/media
# Collect static files.
RUN python manage.py collectstatic --noinput --clear

Wyświetl plik

@ -18,6 +18,19 @@ import json
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)
import sentry_sdk
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
)
SENTRY_ENVIRONMENT = os.environ.get("SENTRY_ENVIRONMENT", '')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
@ -27,6 +40,8 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
INSTALLED_APPS = [
"home",
"store",
"mailings",
"blog",
"search",
"setup",
@ -193,6 +208,19 @@ WAGTAILSEARCH_BACKENDS = {
# e.g. in notification emails. Don't include '/admin' or a trailing slash
WAGTAILADMIN_BASE_URL = "https://artel.tepewu.pl"
# Messages
from django.contrib import messages
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
MESSAGE_TAGS = {
messages.DEBUG: "debug",
messages.INFO: "info",
messages.SUCCESS: "success",
messages.WARNING: "warning",
messages.ERROR: "danger",
}
# STORE SETTINGS
PRODUCTS_PER_PAGE = 6
@ -205,4 +233,22 @@ EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", '')
EMAIL_PORT = os.environ.get('EMAIL_PORT', 587)
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')
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",
},
}
PRODUCTS_CSV_PATH = os.environ.get("PRODUCTS_CSV_PATH", "products.csv")

Wyświetl plik

@ -3,13 +3,20 @@ from .base import *
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-s7hlfa-#n7-v0#&-0ko3(efe+@^d@ie1_1-633e&jb1rh$)j1p"
# SECURITY WARNING: define the correct hosts in production!
ALLOWED_HOSTS = ["*"]
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = "smtp-server"
EMAIL_HOST_USER = None
EMAIL_HOST_PASSWORD = None
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl')
try:

Wyświetl plik

@ -7,10 +7,12 @@ ALLOWED_HOSTS = [
"localhost",
"0.0.0.0",
"127.0.0.1",
"artel.citizen4.eu",
"artel.tepewu.pl"
]
CSRF_TRUSTED_ORIGINS = [
"https://0.0.0.0", "http://0.0.0.0",
"https://localhost", "http://localhost",
"https://artel.tepewu.pl"
"https://artel.tepewu.pl",
"https://artel.citizen4.eu",
]

Wyświetl plik

@ -0,0 +1,5 @@
// close all alerts after 2 seconds
$('.alert').fadeTo(2000, 500).slideUp(500, function(){
$("#success-alert").slideUp(500);
});

Wyświetl plik

@ -49,7 +49,12 @@
</div>
{% endif %}
<div class="col-md-9 mt-5 ml-2">
{% block content %}{% endblock %}
{% for message in messages %}
<div class="alert alert-{{message.tags}}" role="alert">
{{message}}
</div>
{% endfor %}
{% block content %}{% endblock %}
</div>
{% if navbar_position == 'right' %}
<div class="col-md-3 mt-5 ml-2">
@ -60,10 +65,10 @@
</div>
{# Global javascript #}
<script type="text/javascript" src="{% static 'js/artel.js' %}"></script>
<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>
{% block extra_js %}
{# Override this in templates to add extra javascript #}
{% endblock %}

Wyświetl plik

@ -14,6 +14,9 @@ from search import views as search_views
from setup.views import SetupPageView as setup_view
handler404 = 'artel.views.my_custom_page_not_found_view'
urlpatterns = [
path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)),

Wyświetl plik

@ -0,0 +1,11 @@
from django.conf import settings
from django.http import HttpResponseNotFound
from sentry_sdk import capture_message
def my_custom_page_not_found_view(*args, **kwargs):
if settings.SENTRY_ENVIRONMENT != 'production':
capture_message("Page not found!", level="error")
# return any response here, e.g.:
return HttpResponseNotFound("Not found")

Wyświetl plik

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

Wyświetl plik

@ -1,44 +0,0 @@
from abc import (
ABC,
abstractmethod
)
from typing import (
Dict,
Any
)
from django.db.models import Model
from docxtpl import DocxTemplate
class DocumentGeneratorInterface(ABC):
@abstractmethod
def load_template(self, path: str):
...
@abstractmethod
def get_extra_context(self) -> Dict[str, Any]:
...
@abstractmethod
def generate_file(self, context: Dict[str, Any] = None):
...
class BaseDocumentGenerator(DocumentGeneratorInterface):
def __init__(self, instance: Model) -> None:
super().__init__()
self.instance = instance
def load_template(self, path: str):
return DocxTemplate(path)
def get_extra_context(self):
return {}
class PdfFromDocGenerator(BaseDocumentGenerator):
def generate_file(self, context: Dict[str, Any] = None):
template = self.load_template()
context.update(self.get_extra_context())

Wyświetl plik

@ -1,10 +0,0 @@
from django.db import models
from django.core.files.storage import Storage
class DocumentTemplate(models.Model):
name = models.CharField(max_length=255)
file = models.FileField(upload_to="doc_templates/", )
def __str__(self) -> str:
return self.name

Wyświetl plik

@ -1,5 +0,0 @@
from django.test import TestCase
class PdfFromDocGeneratorTestCase(TestCase):
...

Wyświetl plik

@ -20,9 +20,10 @@ services:
build:
dockerfile: Dockerfile
context: ./
user: "${UID}:${GID}"
restart: always
ports:
- "8001:8000"
- "8000:8000"
volumes:
- ./:/app
environment:
@ -38,6 +39,7 @@ services:
web:
image: nginx
restart: always
volumes:
- ../nginx/conf.d/:/etc/nginx/conf.d/
- ./static/:/opt/services/comfy/static

Wyświetl plik

@ -0,0 +1,93 @@
version: "3.8"
services:
db:
image: postgres
restart: always
environment:
- POSTGRES_ROOT_PASSWORD=${POSTGRES_ROOT_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- db:/var/lib/postgresql/data
networks:
- internal
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
comfy:
image: comfy
user: "${UID}:${GID}"
restart: always
ports:
- "8000"
volumes:
- media:/app/media
- static:/app/static
environment:
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=${DATABASE_URL}
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE}
- SENTRY_DSN=${SENTRY_DSN}
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
- EMAIL_HOST=${EMAIL_HOST}
- EMAIL_HOST_USER=${EMAIL_HOST_USER}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD}
- EMAIL_PORT=${EMAIL_PORT}
- EMAIL_USE_TLS=${EMAIL_USE_TLS}
- DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
depends_on:
- db
networks:
- internal
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
web:
image: nginx
restart: always
volumes:
- nginx:/etc/nginx/conf.d
- static:/opt/services/comfy/static
- media:/opt/services/comfy/media
ports:
- "80"
environment:
- NGINX_HOST=${NGINX_HOST}
- NGINX_PORT=${NGINX_PORT}
networks:
- internal
- web
deploy:
resources:
limits:
cpus: '1.0'
memory: 256M
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.artel.rule=Host(`${NGINX_HOST}`)"
- "traefik.http.routers.artel.entrypoints=websecure"
- "traefik.http.services.artel.loadbalancer.server.port=80"
- "traefik.http.routers.artel.tls=true"
- "traefik.http.routers.artel.tls.certresolver=ovh"
volumes:
db:
media:
static:
nginx:
networks:
internal:
web:
external:
name: web

Wyświetl plik

@ -0,0 +1,30 @@
version: "3.8"
services:
test_db:
image: postgres
restart: always
environment:
- POSTGRES_ROOT_PASSWORD
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
volumes:
- ../postgres/:/var/lib/postgresql
env_file:
- .env
test_comfy:
depends_on:
- test_db
build:
dockerfile: Dockerfile.local
context: ./
user: "${UID}:${GID}"
volumes:
- ./:/app
environment:
- SECRET_KEY
- DATABASE_URL
env_file:
- .env
command:
python manage.py test --noinput

Wyświetl plik

@ -1,13 +1,24 @@
version: "3.8"
services:
smtp-server:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
comfy:
restart: always
depends_on:
- smtp-server
- db
build:
dockerfile: Dockerfile.local
context: ./
user: "${UID}:${GID}"
ports:
- "8000:8000"
- "8001:8000"
volumes:
- ./:/app
- media:/app/media
environment:
- SECRET_KEY
- DATABASE_URL
@ -24,6 +35,17 @@ services:
- POSTGRES_PASSWORD
- POSTGRES_DB
volumes:
- ../postgres/:/var/lib/postgresql
- db:/var/lib/postgresql/data
env_file:
- .env
adminer:
image: adminer
restart: always
ports:
- "8002:8080"
volumes:
media:
db:

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,73 @@
from django.forms import fields
from wagtail.contrib.modeladmin.options import (
ModelAdmin,
ModelAdminGroup,
modeladmin_register
)
from mailings import models
class MailTemplateAdmin(ModelAdmin):
model = models.MailTemplate
menu_label = "Mail templates"
menu_icon = 'mail'
menu_order = 100
add_to_settings_menu = False
exclude_from_explorer = False
list_display = (
"template_name",
)
search_fields = (
"template_name",
)
list_filter = (
"template_name",
)
form_fields = (
"template_name",
"template",
)
class OutgoingMailAdmin(ModelAdmin):
model = models.OutgoingEmail
menu_label = "Outgoing mails"
menu_icon = 'mail'
menu_order = 100
add_to_settings_menu = False
exclude_from_explorer = False
list_display = (
"subject",
"sent",
)
search_fields = (
"subject",
)
list_filter = (
"subject",
"sender",
"recipient",
"template__template_name",
"sent",
)
readonly_fields = (
"subject",
"sender",
"recipient",
"sent"
)
class MailingGroup(ModelAdminGroup):
menu_label = "Mailings"
menu_icon = 'mail'
menu_order = 200
items = (
MailTemplateAdmin,
OutgoingMailAdmin
)
modeladmin_register(MailingGroup)

Wyświetl plik

@ -1,6 +1,6 @@
from django.apps import AppConfig
class DocgeneratorConfig(AppConfig):
class MailingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'docgenerator'
name = 'mailings'

Wyświetl plik

@ -0,0 +1,35 @@
# Generated by Django 4.1.9 on 2023-06-22 14:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="MailTemplate",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("template_name", models.CharField(max_length=255, unique=True)),
("template", models.FileField(upload_to="mail_templates")),
],
),
migrations.CreateModel(
name="OutgoingEmail",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("subject", models.CharField(max_length=255)),
("sender", models.EmailField(max_length=254)),
("recipient", models.EmailField(max_length=254)),
("sent", models.BooleanField(default=False)),
(
"template",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="mailings.mailtemplate"),
),
],
),
]

Wyświetl plik

@ -0,0 +1,111 @@
import logging
from typing import Any
from dataclasses import dataclass
from django.db import models
from django.db import transaction
from django.template import (
Template,
Context
)
from django.core.mail import EmailMessage
from django.conf import settings
logger = logging.getLogger(__name__)
@dataclass
class Attachment:
name: str
content: Any
contenttype: str
def send_mail(
to: list[str],
attachments: list[Attachment],
subject: str,
content: str,
sender_email: str = settings.DEFAULT_FROM_EMAIL
):
message = EmailMessage(
subject=subject,
body=content,
from_email=sender_email,
to=to
)
message.content_subtype = 'html'
for attachment in attachments:
message.attach(attachment.name, attachment.content, attachment.contenttype)
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):
template_name = models.CharField(max_length=255, unique=True)
template = models.FileField(
upload_to="mail_templates"
)
def delete(self, *args, **kwargs):
# delete file
super().delete(*args, **kwargs)
@transaction.on_commit
def remove_template_file(self):
self.template.delete()
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)
with open(self.template.path, "r", encoding="utf-8") as f:
content = f.read()
template = Template(content)
return template.render(context)
class OutgoingEmailManager(models.Manager):
def send(
self, template_name: str, subject: str,
recipient: str, context: dict | Context,
sender:str, attachments: list[Attachment] = None
):
template = MailTemplate.objects.get(template_name=template_name)
outgoing_email = self.create(
template=template, recipient=recipient, subject=subject,
sender=sender
)
attachments = attachments or []
# send email
sent = send_mail(
to=[recipient], sender_email=sender,
subject=subject, content=template.load_and_process_template(context),
attachments=attachments
)
outgoing_email.sent = sent
outgoing_email.save()
return outgoing_email
class OutgoingEmail(models.Model):
subject = models.CharField(max_length=255)
template = models.ForeignKey(MailTemplate, on_delete=models.CASCADE)
sender = models.EmailField()
recipient = models.EmailField()
sent = models.BooleanField(default=False)
objects = OutgoingEmailManager()

Wyświetl plik

@ -0,0 +1,10 @@
from factory.django import DjangoModelFactory
from factory import Faker
class MailTemplateFactory(DjangoModelFactory):
class Meta:
model = "mailings.MailTemplate"
template_name = Faker("name")
template = Faker("file_name", extension="html")

Wyświetl plik

@ -0,0 +1,66 @@
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core import mail
from mailings.models import (
MailTemplate,
OutgoingEmail,
)
class TestMailTemplate(TestCase):
def setUp(self) -> None:
super().setUp()
self.mail_template = MailTemplate.objects.create(
template_name="test_template",
template=SimpleUploadedFile(
"test_template.html", b"<html>{{test_var}}</html>"
)
)
def test_load_and_process_template_success(self):
content = self.mail_template.load_and_process_template({"test_var": "test"})
self.assertEqual(content, "<html>test</html>")
def test_load_and_process_template_missing_var_failure(self):
content = self.mail_template.load_and_process_template({})
self.assertEqual(content, "<html></html>")
def test_load_and_preprocess_template_no_template_file(self):
self.mail_template.template.delete()
self.mail_template.template = None
self.mail_template.save()
with self.assertRaises(FileNotFoundError):
self.mail_template.load_and_process_template({})
class TestOutgoingEmail(TestCase):
def setUp(self) -> None:
super().setUp()
self.mail_template = MailTemplate.objects.create(
template_name="test_template",
template=SimpleUploadedFile(
"test_template.html", b"<html>{{test_var}}</html>"
)
)
def test_send_success(self):
email = OutgoingEmail.objects.send(
template_name="test_template",
recipient="test@stardust.io", context={},
sender="sklep-test@stardust.io",
subject="Test subject"
)
self.assertEqual(email.sent, True)
self.assertEqual(mail.outbox[0].subject, "Test subject")
def test_send_missing_template_failure(self):
with self.assertRaises(MailTemplate.DoesNotExist):
OutgoingEmail.objects.send(
template_name="missing_template",
recipient="", sender="", context={},
subject="Test subject"
)
self.assertEqual(len(mail.outbox), 0)

Wyświetl plik

@ -1,10 +1,13 @@
Django>=4.1,<4.2
wagtail>=4.2,<4.3
wagtailmenus>=3.1.5,<=3.1.7
psycopg2>=2.9.5,<=2.9.6
psycopg2-binary>=2.9.5,<=2.9.6
dj-database-url<=2.0.0
djangorestframework==3.14.0
phonenumbers==8.13.13
django-phonenumber-field==7.1.0
factory-boy==3.2.1
pdfkit==1.0.0
pdfkit==1.0.0
num2words==0.5.12
sentry-sdk==1.28.0
pandas==2.0.3

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -26,19 +26,30 @@ class ProductCategoryParamAdmin(ModelAdmin):
class ProductTemplateAdmin(ModelAdmin):
menu_label = "Product design"
model = models.ProductTemplate
list_display = ("title", "code")
class ProductAdmin(ModelAdmin):
menu_label = "Product variant"
model = models.Product
list_display = ("title", "price")
class PaymentMethodAdmin(ModelAdmin):
model = models.PaymentMethod
list_display = ("name", "active")
class DeliveryMethodAdmin(ModelAdmin):
model = models.DeliveryMethod
list_display = ("name", "active")
class DocumentTemplateAdmin(ModelAdmin):
model = models.DocumentTemplate
list_display = ("name", "doc_type")
list_display = ("name", )
class StoreAdminGroup(ModelAdminGroup):
@ -51,7 +62,9 @@ class StoreAdminGroup(ModelAdminGroup):
ProductCategoryParamAdmin,
ProductTemplateAdmin,
ProductAdmin,
DocumentTemplateAdmin
DocumentTemplateAdmin,
PaymentMethodAdmin,
DeliveryMethodAdmin
)

Wyświetl plik

@ -1,24 +1,31 @@
import logging
from abc import (
ABC,
abstractmethod
abstractmethod,
abstractproperty
)
from typing import (
List,
Any
)
from typing import List
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
from store.models import (
Product,
ProductAuthor,
DeliveryMethod
)
@dataclass
class CartItem:
product: Product
quantity: int
logger = logging.getLogger("cart_logger")
class BaseCart(ABC):
def validate_item_id(self, item_id):
def validate_and_get_product(self, item_id):
return Product.objects.get(id=item_id)
@abstractmethod
@ -33,67 +40,137 @@ 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
if not self.session.get(settings.CART_SESSION_ID):
self.session[settings.CART_SESSION_ID] = {}
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
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
quantity = int(quantity)
item_id = int(item_id)
if not self.session[settings.CART_SESSION_ID].get(str(item_id)):
self.session[settings.CART_SESSION_ID][item_id] = quantity
self.session.modified = True
if not self._cart.get(str(author.id)):
self._cart[str(author.id)] = {str(item_id): quantity}
self.save_cart()
elif not self._cart[str(author.id)].get(str(item_id)):
self._cart[str(author.id)].update({str(item_id): quantity})
self.save_cart()
else:
self.update_item_quantity(item_id, quantity)
new_quantity = self._cart[str(author.id)][str(item_id)] + quantity
self.update_item_quantity(item_id, new_quantity)
def remove_item(self, item_id: int) -> None:
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
try:
self.session[settings.CART_SESSION_ID].pop(item_id)
self.session.modified = True
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:
self.validate_item_id(item_id)
product = self.validate_and_get_product(item_id)
author = product.author
if new_quantity < 1:
self.remove_item(item_id)
return
try:
self.session[settings.CART_SESSION_ID][str(item_id)] = new_quantity
self.session.modified = True
except KeyError:
# TODO - add logging
if not self._cart.get(str(author.id)):
self.add_item(item_id, new_quantity)
return
self._cart[str(author.id)][str(product.id)] = new_quantity
self.save_cart()
def get_items(self) -> List[CartItem]:
_items = []
for item_id, quantity in self.session[settings.CART_SESSION_ID].items():
_items.append(CartItem(quantity=quantity, product=Product.objects.get(id=item_id)))
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
for item in self.get_items():
total += item.product.price * int(item.quantity)
for _, cart_items in self._cart.items():
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:
return not bool(self.session[settings.CART_SESSION_ID].items())
return not bool(self._cart.items())
def clear(self) -> None:
self.session[settings.CART_SESSION_ID] = {}
self.session.modified = True
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,19 +1,20 @@
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 (
CustomerData,
ProductTemplate,
ProductCategoryParamValue,
Product,
PaymentMethod,
DeliveryMethod
)
class CustomerDataForm(forms.ModelForm):
class Meta:
model = CustomerData
fields = [
"name", "surname", "email", "phone",
"street", "city", "zip_code"
]
class CustomerDataForm(forms.Form):
name = forms.CharField(
max_length=255, label="Imię", widget=forms.TextInput(attrs={"class": "form-control"})
@ -41,3 +42,49 @@ class CustomerDataForm(forms.ModelForm):
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,76 @@
import logging
import requests
import pandas as pd
from django.core import files
from store.models import (
ProductTemplate,
ProductCategoryParamValue,
Product,
ProductImage
)
logger = logging.getLogger(__name__)
class BaseLoader:
def __init__(self, path):
self.path = path
def load_data(self):
return pd.read_csv(self.path)
class TemplateLoader(BaseLoader):
...
class ProductLoader(BaseLoader):
def _get_images(self, row) -> list[files.ContentFile]:
urls = row["images"]
images = []
for url in urls:
response = requests.get(url, stream=True)
if response.status_code == 200:
data = response.raw
file_name = url.split("/")[-1]
image = files.ContentFile(data, name=file_name)
images.append(image)
return images
def _process_row(self, row):
template = ProductTemplate.objects.get(code=row["template"])
price = float(row["price"])
name = row["name"]
available = bool(row["available"])
params = []
for param in row["params"]:
key, value = param
param = ProductCategoryParamValue.objects.get(param__key=key, value=value)
params.append(param)
product = Product.objects.get_or_create_by_params(template=template, params=params)
product.price = price
product.name = name
product.available = available
images = self._get_images(row)
for i, image in enumerate(images):
ProductImage.objects.create(product=product, image=image, is_main=bool(i==0))
product.save()
return product
def process(self):
data = self.load_data()
products = []
for _, row in data.iterrows():
try:
product = self._process_row(row)
except Exception as e:
# catch any error and log it, GlitchTip will catch it
logger.exception(str(e))
else:
products.append(product)
logger.info(f"Loaded {len(products)} products")

Wyświetl plik

@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from django.conf import settings
from store.loader import ProductLoader
class Command(BaseCommand):
help = "Load products from csv file"
def handle(self, *args, **options):
loader = ProductLoader(settings.PRODUCTS_CSV_PATH)
loader.process()

Wyświetl plik

@ -1,53 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-01 19:00
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("store", "0003_product_info_product_name_and_more"),
]
operations = [
migrations.CreateModel(
name="CustomerData",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("surname", models.CharField(max_length=255)),
("email", models.EmailField(max_length=254)),
("phone", phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)),
("street", models.CharField(max_length=255)),
("city", models.CharField(max_length=255)),
("zip_code", models.CharField(max_length=120)),
("country", models.CharField(max_length=120)),
],
),
migrations.CreateModel(
name="Order",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("sent", models.BooleanField(default=False)),
("customer", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.customerdata")),
],
),
migrations.CreateModel(
name="OrderProduct",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("quantity", models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.order"
),
),
("product", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.product")),
],
),
]

Wyświetl plik

@ -0,0 +1,129 @@
# Generated by Django 4.1.9 on 2023-06-16 15:33
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("store", "0003_product_info_product_name_and_more"),
]
operations = [
migrations.CreateModel(
name="DocumentTemplate",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("file", models.FileField(upload_to="documents")),
(
"doc_type",
models.CharField(
choices=[("agreement", "Agreement"), ("receipt", "Receipt")], max_length=255, unique=True
),
),
("created_at", models.DateTimeField(auto_now_add=True, null=True)),
],
),
migrations.CreateModel(
name="Order",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("sent", models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name="PaymentMethod",
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)),
("active", models.BooleanField(default=True)),
],
),
migrations.AddField(
model_name="productauthor",
name="city",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="country",
field=models.CharField(blank=True, max_length=120),
),
migrations.AddField(
model_name="productauthor",
name="display_name",
field=models.CharField(blank=True, max_length=255, unique=True),
),
migrations.AddField(
model_name="productauthor",
name="email",
field=models.EmailField(blank=True, max_length=254),
),
migrations.AddField(
model_name="productauthor",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None),
),
migrations.AddField(
model_name="productauthor",
name="street",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="surname",
field=models.CharField(blank=True, max_length=255),
),
migrations.AddField(
model_name="productauthor",
name="zip_code",
field=models.CharField(blank=True, max_length=120),
),
migrations.AlterField(
model_name="productauthor",
name="name",
field=models.CharField(blank=True, max_length=255),
),
migrations.CreateModel(
name="OrderProduct",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("quantity", models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.order"
),
),
("product", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.product")),
],
),
migrations.CreateModel(
name="OrderDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("sent", models.BooleanField(default=False)),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="store.order"
),
),
(
"template",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.documenttemplate"),
),
],
),
migrations.AddField(
model_name="order",
name="payment_method",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.paymentmethod"),
),
]

Wyświetl plik

@ -1,41 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-04 09:38
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("store", "0004_customerdata_order_orderproduct"),
]
operations = [
migrations.CreateModel(
name="DocumentTemplate",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=255)),
("file", models.FileField(upload_to="documents")),
(
"doc_type",
models.CharField(choices=[("agreement", "Agreement"), ("receipt", "Receipt")], max_length=255),
),
],
),
migrations.CreateModel(
name="OrderDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="store.order"
),
),
(
"template",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.documenttemplate"),
),
],
),
]

Wyświetl plik

@ -0,0 +1,17 @@
# Generated by Django 4.1.9 on 2023-06-18 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("store", "0004_documenttemplate_order_paymentmethod_and_more"),
]
operations = [
migrations.AddField(
model_name="order",
name="order_number",
field=models.CharField(max_length=255, null=True),
),
]

Wyświetl plik

@ -1,22 +0,0 @@
# Generated by Django 4.1.9 on 2023-06-07 23:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("store", "0005_documenttemplate_orderdocument"),
]
operations = [
migrations.AddField(
model_name="documenttemplate",
name="created_at",
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name="orderdocument",
name="sent",
field=models.BooleanField(default=False),
),
]

Wyświetl plik

@ -0,0 +1,16 @@
# Generated by Django 4.1.9 on 2023-06-22 16:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("store", "0005_order_order_number"),
]
operations = [
migrations.RemoveField(
model_name="orderdocument",
name="sent",
),
]

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,4 +1,14 @@
import pdfkit
import datetime
import builtins
import uuid
import logging
from decimal import Decimal
from typing import (
Any,
Iterator
)
from django.db import models
from django.core.paginator import (
Paginator,
@ -10,6 +20,9 @@ from django.template import (
Template,
Context
)
from django.core.exceptions import ValidationError
from django.db.models.signals import m2m_changed
from django.forms import CheckboxSelectMultiple
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
@ -21,18 +34,53 @@ from wagtail.models import Page
from wagtail import fields as wagtail_fields
from taggit.managers import TaggableManager
from phonenumber_field.modelfields import PhoneNumberField
from num2words import num2words
from store.utils import (
send_mail
from mailings.models import (
OutgoingEmail,
Attachment
)
class ProductAuthor(models.Model):
name = models.CharField(max_length=255)
# TODO - add author contact data
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:
abstract = True
name = models.CharField(max_length=255, blank=True)
surname = models.CharField(max_length=255, blank=True)
email = models.EmailField(blank=True)
phone = PhoneNumberField(blank=True)
street = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=255, blank=True)
zip_code = models.CharField(max_length=120, blank=True)
country = models.CharField(max_length=120, blank=True)
@property
def full_name(self):
return f"{self.name} {self.surname}"
@property
def full_address(self):
return f"{self.street}, {self.zip_code} {self.city}, {self.country}"
class ProductAuthor(PersonalData):
display_name = models.CharField(max_length=255, unique=True, blank=True)
def __str__(self):
return self.name
return self.display_name
class ProductCategory(ClusterableModel):
@ -60,6 +108,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):
@ -68,57 +142,107 @@ class ProductTemplate(ClusterableModel):
title = models.CharField(max_length=255)
code = models.CharField(max_length=255)
description = models.TextField(blank=True)
# TODO - add mechanism for enabling params
tags = TaggableManager()
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",
limit_choices_to=models.Q(param__category=models.F("product__template__category"))
)
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", widget=CheckboxSelectMultiple),
FieldPanel("available"),
FieldPanel("name"),
FieldPanel("info")
InlinePanel("product_images", label="Variant Images"),
]
@property
def main_image(self):
images = self.template.images.all()
print(images)
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):
return self.template.tags.all()
@property
def author(self):
return self.template.author
@property
def description(self):
return self.info or self.template.description
@ -128,10 +252,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):
@ -141,10 +298,12 @@ class ProductListPage(Page):
tags = TaggableManager(blank=True)
def _get_items(self):
if not self.pk:
return ProductTemplate.objects.all()
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()
@ -166,33 +325,25 @@ class ProductListPage(Page):
FieldPanel("tags")
]
class CustomerData(models.Model):
name = models.CharField(max_length=255)
surname = models.CharField(max_length=255)
email = models.EmailField()
phone = PhoneNumberField()
street = models.CharField(max_length=255)
city = models.CharField(max_length=255)
zip_code = models.CharField(max_length=120)
country = models.CharField(max_length=120)
@property
def full_name(self):
return f"{self.name} {self.surname}"
@property
def full_address(self):
return f"{self.street}, {self.zip_code} {self.city}, {self.country}"
class OrderProductManager(models.Manager):
def create_from_cart(self, cart, order):
for item in cart.get_items():
self.create(
product=item.product,
def create_from_cart(self, items: dict[str, Product|int], order: models.Model):
pks = []
for item in items:
if item["quantity"] < 1:
logger.exception(
f"This is not possible to add less than one item to Order, omitting item: "+
f"{item['product']}"
)
continue
pk = self.create(
product=item["product"],
order=order,
quantity=item.quantity
)
quantity=item["quantity"]
).pk
pks.append(pk)
return self.filter(pk__in=pks)
class OrderProduct(models.Model):
@ -204,41 +355,143 @@ class OrderProduct(models.Model):
class OrderManager(models.Manager):
def create_from_cart(self, cart, customer_data):
order = self.create(customer=customer_data)
OrderProduct.objects.create_from_cart(cart, order)
# create proper documents
# NOTE - this is temporary
# agreement_template = DocumentTemplate.objects.filter(
# doc_type=DocumentTypeChoices.AGREEMENT
# ).order_by("-created_at").first()
# receipt_template = DocumentTemplate.objects.filter(
# doc_type=DocumentTypeChoices.RECEIPT
# ).order_by("-created_at").first()
# agreement = OrderDocument.objects.create(
# order=order,
# template=agreement_template
# )
# receipt = OrderDocument.objects.create(
# order=order,
# template=receipt_template
# )
#send_mail(agreement)
#send_mail(receipt)
return order
def _get_order_number(self, author: ProductAuthor):
number_of_prev_orders = OrderProduct.objects.filter(
product__template__author=author
).values("order").distinct().count()
number_of_prev_orders += 1
year = datetime.datetime.now().year
return f"{author.id}/{number_of_prev_orders:06}/{year}"
def _send_notifications(
self, order: models.Model, author: ProductAuthor,
customer_data: dict[str, Any], docs: list[models.Model]
):
# for user
attachments = [
Attachment(
content=doc.generate_document({"customer_data": customer_data}),
contenttype="application/pdf",
name=f"{doc.template.doc_type}_{order.order_number}.pdf"
) for doc in docs
]
mail_subject = f"Wygenerowano umowę numer {order.order_number} z dnia {order.created_at.strftime('%d.%m.%Y')}"
user_mail = OutgoingEmail.objects.send(
recipient=customer_data["email"],
subject=mail_subject,
context = {
"docs": docs,
"order_number": order.order_number,
"customer_email": customer_data["email"],
}, sender=settings.DEFAULT_FROM_EMAIL,
template_name="order_created_user",
attachments=attachments
)
# for author
author_mail = OutgoingEmail.objects.send(
recipient=author.email,
subject=mail_subject,
context = {
"docs": docs,
"order_number": order.order_number,
"manufacturer_email": author.email,
}, sender=settings.DEFAULT_FROM_EMAIL,
template_name="order_created_author",
attachments=attachments
)
return user_mail is not None and author_mail is not None
def create_from_cart(
self, cart_items: list[dict[str, str|dict]],
payment_method: models.Model| None,
customer_data: dict[str, Any]
) -> models.QuerySet:
# split cart
orders_pks = []
payment_method = payment_method or PaymentMethod.objects.first()
doc_templates = DocumentTemplate.objects.filter(
doc_type__in=[DocumentTypeChoices.AGREEMENT, DocumentTypeChoices.RECEIPT]
)
for item in cart_items:
author = item["author"]
author_products = item["products"]
order = self.create(
payment_method=payment_method,
order_number=self._get_order_number(author)
)
OrderProduct.objects.create_from_cart(author_products, order)
orders_pks.append(order.pk)
docs = []
for template in doc_templates:
doc = OrderDocument.objects.create(
order=order, template=template
)
docs.append(doc)
sent = self._send_notifications(order, author, customer_data, docs)
if not sent:
logger.exception(
f"Error while sending emails, for order: {order.order_number}"
)
return Order.objects.filter(pk__in=orders_pks)
class PaymentMethod(models.Model):
name = models.CharField(max_length=255)
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):
customer = models.ForeignKey(CustomerData, on_delete=models.CASCADE)
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
def order_number(self) -> str:
return f"{self.id:06}/{self.created_at.year}"
def manufacturer(self) -> str:
return self.products.first().product.author
@property
def total_price(self) -> Decimal:
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:
return num2words(self.total_price, lang="pl", to="currency", currency="PLN")
@property
def payment_date(self) -> datetime.date:
return self.created_at.date() + datetime.timedelta(days=7)
class DocumentTypeChoices(models.TextChoices):
@ -249,7 +502,8 @@ class DocumentTypeChoices(models.TextChoices):
class DocumentTemplate(models.Model):
name = models.CharField(max_length=255)
file = models.FileField(upload_to="documents")
doc_type = models.CharField(max_length=255, choices=DocumentTypeChoices.choices)
# there may be only one document of each type
doc_type = models.CharField(max_length=255, choices=DocumentTypeChoices.choices, unique=True)
created_at = models.DateTimeField(auto_now_add=True, null=True)
def __str__(self):
@ -259,21 +513,23 @@ class DocumentTemplate(models.Model):
class OrderDocument(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="documents")
template = models.ForeignKey(DocumentTemplate, on_delete=models.CASCADE)
sent = models.BooleanField(default=False)
def get_document_context(self):
_context = {
"order": self.order,
"customer": self.order.customer,
"products": self.order.products.all(),
"author": self.order.manufacturer,
"order_products": self.order.products.all(),
"payment_data": self.order.payment_method,
}
return Context(_context)
@property
def document(self):
with open(self.template.file.path, "rb") as f:
def generate_document(self, extra_context: dict = None):
extra_context = extra_context or {}
context = self.get_document_context()
context.update(extra_context)
with open(self.template.file.path, "r", encoding="utf-8") as f:
content = f.read()
template = Template(content)
context = self.get_document_context()
content = template.render(context)
return pdfkit.from_string(content, False)

Wyświetl plik

@ -1,6 +1,9 @@
from rest_framework import serializers
from store.models import Product
from store.models import (
Product,
ProductAuthor
)
class TagSerializer(serializers.Serializer):
@ -21,6 +24,17 @@ class CartProductSerializer(serializers.Serializer):
quantity = serializers.IntegerField()
class ProductAuthorSerializer(serializers.Serializer):
class Meta:
model = ProductAuthor
fields = ["display_name"]
class CartSerializer(serializers.Serializer):
author = ProductAuthorSerializer()
products = CartProductSerializer(many=True)
class CartProductAddSerializer(serializers.Serializer):
product_id = serializers.IntegerField()

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',
@ -133,7 +133,9 @@ $(document).on('click', '.add-to-cart-button', function(event) {
data: formData, // Use the serialized form data
headers: { 'X-CSRFToken': csrfToken },
dataType: 'json',
success: location.reload(),
success: function(data) {
setTimeout(location.reload(), 500)
},
processData: false, // Prevent jQuery from processing the data
contentType: false, // Let the browser set the content type
});

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,15 +9,20 @@
<h3 class="fw-normal mb-0 text-black">Koszyk</h3>
</div>
</div>
{% for item in cart.get_items %}
{% include 'store/partials/cart_item.html' %}
{% for group in cart.display_items %}
{% if group.products %}
<h4>Wykonawca: {{group.author.display_name}}</h4>
{% for item in group.products %}
{% include 'store/partials/cart_item.html' %}
{% endfor %}
{% endif %}
{% endfor %}
<div class="card ">
<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">W sumie do zapłaty: {{cart.total_price}}</h5>
</div>
<div class="col-sm-6 text-end">
<a href="{% url 'order' %}" class="btn btn-success btn-block btn-lg">Dalej</a>

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

@ -14,7 +14,7 @@
<p class="mb-0">Imię i Nazwisko</p>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">{{customer_data.full_name}}</p>
<p class="text-muted mb-0">{{customer_data.name}} {{customer_data.surname}}</p>
</div>
</div>
<hr>
@ -41,7 +41,10 @@
<p class="mb-0">Adres</p>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">{{customer_data.full_address}}</p>
<p class="text-muted mb-0">
{{customer_data.city}}, {{customer_data.zip_code}}<br/>
{{customer_data.street}}
</p>
</div>
</div>
</div>
@ -54,15 +57,28 @@
<h3 class="fw-normal mb-0 text-black">Zamówione przedmioty</h3>
</div>
</div>
{% for item in cart.get_items %}
{% include 'store/partials/summary_cart_item.html' %}
{% 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,21 +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 rounded mx-auto d-block" style="width: 10rem; 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

@ -1,11 +1,11 @@
{% load static %}
<div class="card rounded-3 mb-4">
<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="{{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

@ -1,16 +1,17 @@
from factory import (
Faker,
SubFactory
SubFactory,
Factory
)
from factory.django import (
FileField,
DjangoModelFactory
DjangoModelFactory,
)
class CustomerDataFactory(DjangoModelFactory):
class ProductAuthorFactory(DjangoModelFactory):
class Meta:
model = 'store.CustomerData'
model = 'store.ProductAuthor'
name = Faker('name')
surname = Faker('name')
@ -20,13 +21,76 @@ class CustomerDataFactory(DjangoModelFactory):
city = Faker('city')
zip_code = Faker('postcode')
country = Faker('country')
display_name = Faker('name')
class ProductCategoryFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductCategory'
name = Faker('name')
class ProductCategoryParamFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductCategoryParam'
key = Faker('name')
category = SubFactory(ProductCategoryFactory)
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'
title = Faker('name')
description = Faker('text')
code = Faker('name')
author = SubFactory(ProductAuthorFactory)
category = SubFactory(ProductCategoryFactory)
class ProductFactory(DjangoModelFactory):
class Meta:
model = 'store.Product'
name = Faker('name')
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'
name = Faker('name')
description = Faker('text')
active = Faker('boolean')
class OrderFactory(DjangoModelFactory):
class Meta:
model = 'store.Order'
customer = SubFactory(CustomerDataFactory)
payment_method = SubFactory(PaymentMethodFactory)
created_at = Faker('date_time')
updated_at = Faker('date_time')
sent = Faker('boolean')
@ -38,4 +102,4 @@ class DocumentTemplateFactory(DjangoModelFactory):
name = Faker('name')
file = FileField(filename="doc.odt")
doc_type = "AGREEMENT"
doc_type = "agreement"

Wyświetl plik

@ -0,0 +1,120 @@
from rest_framework.test import APITestCase
from django.urls import reverse
from django.conf import settings
from store.tests import factories
class SessionCartTestCase(APITestCase):
def setUp(self):
super().setUp()
self.product = factories.ProductFactory(price=100)
self.second_product = factories.ProductFactory(price=200)
def test_add_item_simple_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
def test_add_item_complex_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
2
)
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.second_product.id, "quantity": 5},
)
final_dict = {
str(self.product.author.id): {
str(self.product.id): 2,
}
}
final_dict.update({
str(self.second_product.author.id): {
str(self.second_product.id): 5,
}
})
self.assertDictEqual(
self.client.session[settings.CART_SESSION_ID], final_dict
)
def test_add_item_invalid_product_id(self):
response = self.client.post(
reverse("cart-action-add-product"),
{"product_id": 999, "quantity": 1},
)
self.assertEqual(response.status_code, 400)
def test_add_item_invalid_quantity(self):
response = self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": "invalid"},
)
self.assertEqual(response.status_code, 400)
def test_remove_item_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.post(
reverse("cart-action-remove-product"),
{"product_id": self.product.id},
)
self.assertEqual(self.client.session[settings.CART_SESSION_ID], {str(self.product.author.id): {}})
def test_remove_item_invalid_product_id(self):
response = self.client.post(
reverse("cart-action-remove-product"),
{"product_id": 999},
)
self.assertEqual(response.status_code, 400)
def test_update_item_quantity_success(self):
self.client.post(
reverse("cart-action-add-product"),
{"product_id": self.product.id, "quantity": 1},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
1
)
self.client.put(
reverse("cart-action-update-product", kwargs={"pk": self.product.id}),
{"quantity": 5},
)
self.assertEqual(
self.client.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)],
5
)
def test_update_item_quantity_invalid_product_id(self):
response = self.client.put(
reverse("cart-action-update-product", kwargs={"pk": 2137}),
{"quantity": 5},
)
self.assertEqual(response.status_code, 404)

Wyświetl plik

@ -0,0 +1,97 @@
import pandas as pd
from django.test import TestCase
from unittest.mock import patch
from store.tests import factories
from store.loader import ProductLoader
class TestProductLoader(TestCase):
def setUp(self) -> None:
self.category = factories.ProductCategoryFactory()
self.template = factories.ProductTemplateFactory(category=self.category)
self.category_params = [factories.ProductCategoryParamFactory(category=self.category) for _ in range(3)]
self.category_param_values = [factories.ProductCategoryParamValueFactory(param=param) for param in self.category_params]
def test_load_products_single_product_success(self):
fake_df = pd.DataFrame({
"template": [self.template.code],
"price": [10.0],
"name": ["Test product"],
"available": [True],
"params": [[
(self.category_params[0].key, self.category_param_values[0].value),
(self.category_params[1].key, self.category_param_values[1].value),
(self.category_params[2].key, self.category_param_values[2].value),
]]
})
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
loader = ProductLoader("fake_path")
loader.process()
self.assertEqual(self.template.products.count(), 1)
product = self.template.products.first()
self.assertEqual(product.price, 10.0)
self.assertEqual(product.name, "Test product")
self.assertEqual(product.available, True)
@patch("store.loader.logger")
def test_load_incorrect_data_types_failure(self, mock_logger):
fake_df = pd.DataFrame({
"template": [self.template.code],
"price": ["FASDSADQAW"],
"name": ["Test product"],
"available": [True],
"params": [[
(self.category_params[0].key, self.category_param_values[0].value),
(self.category_params[1].key, self.category_param_values[1].value),
(self.category_params[2].key, self.category_param_values[2].value),
]]
})
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
loader = ProductLoader("fake_path")
loader.process()
self.assertEqual(self.template.products.count(), 0)
mock_logger.exception.assert_called_with("could not convert string to float: 'FASDSADQAW'")
@patch("store.loader.logger")
def test_load_no_existing_template_code_failure(self, mock_logger):
fake_df = pd.DataFrame({
"template": ["NOTEEXISTINGTEMPLATE"],
"price": [10.0],
"name": ["Test product"],
"available": [True],
"params": [[
(self.category_params[0].key, self.category_param_values[0].value),
(self.category_params[1].key, self.category_param_values[1].value),
(self.category_params[2].key, self.category_param_values[2].value),
]]
})
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
loader = ProductLoader("fake_path")
loader.process()
self.assertEqual(self.template.products.count(), 0)
mock_logger.exception.assert_called_with("ProductTemplate matching query does not exist.")
@patch("store.loader.logger")
def test_not_existing_params_key_value_pairs_failure(self, mock_logger):
fake_df = pd.DataFrame({
"template": [self.template.code],
"price": [10.0],
"name": ["Test product"],
"available": [True],
"params": [[
(self.category_params[0].key, self.category_param_values[2].value),
(self.category_params[1].key, self.category_param_values[0].value),
(self.category_params[2].key, self.category_param_values[1].value),
]]
})
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
loader = ProductLoader("fake_path")
loader.process()
self.assertEqual(self.template.products.count(), 0)
mock_logger.exception.assert_called_with("ProductCategoryParamValue matching query does not exist.")

Wyświetl plik

@ -1,41 +1,253 @@
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 OrderDocumentTestCase(TestCase):
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):
def setUp(self):
super().setUp()
self.author = factories.ProductAuthorFactory()
self.order = factories.OrderFactory()
self.document_template = factories.DocumentTemplateFactory(file__data="test")
def test_generate_document_success(self):
order_doc = store_models.OrderDocument.objects.create(
order=self.order,
template=self.document_template
)
document = order_doc.document
self.assertIsInstance(document, bytes)
def test_get_document_context_success(self):
order_doc = store_models.OrderDocument.objects.create(
order=self.order,
template=self.document_template
)
context = order_doc.get_document_context()
self.assertIsInstance(context, store_models.Context)
self.assertEqual(context["order"].id, self.order.id)
self.assertEqual(context["customer"].id, self.order.customer.id)
self.assertEqual(context["products"].count(), 0)
self.product = factories.ProductFactory(template__author=self.author, price=100)
self.second_product = factories.ProductFactory(template__author=self.author, price=200)
def test_send_order_document_mail_success(self):
...
def test_send_order_document_mail_failure_wrong_email(self):
...
def test_create_from_cart_single_product_success(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[{"product": self.product, "quantity": 1}],
order=self.order
)
self.assertEqual(products.count(), 1)
def test_create_from_cart_multiple_products_success(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[
{"product": self.product, "quantity": 1},
{"product": self.second_product, "quantity": 1}
],
order=self.order
)
self.assertEqual(products.count(), 2)
def test_create_from_cart_wrong_quanitity_failure(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[{"product": self.product, "quantity": -123}],
order=self.order
)
self.assertEqual(products.count(), 0)
def test_create_from_cart_empty_data_failure(self):
products = store_models.OrderProduct.objects.create_from_cart(
items=[],
order=self.order
)
self.assertEqual(products.count(), 0)
class OrderTestCase(TestCase):
def setUp(self) -> None:
super().setUp()
self.author = factories.ProductAuthorFactory()
self.second_author = factories.ProductAuthorFactory()
self.customer_data = {
"first_name": "Jan",
"last_name": "Kowalski",
"email": "jan.kowalski@tepewu.pl",
"phone": "",
"address": "",
"postal_code": "",
"city": "",
"country": "",
}
self.payment_method = factories.PaymentMethodFactory()
factories.DocumentTemplateFactory()
factories.DocumentTemplateFactory(doc_type="receipt")
MailTemplateFactory(template_name="order_created_user")
MailTemplateFactory(template_name="order_created_author")
@patch("mailings.models.MailTemplate.load_and_process_template", return_value="test")
def test_create_from_cart_success_single_author(self, mocked_load):
product = factories.ProductFactory(template__author=self.author, price=100)
cart_items = [{
"author": self.author,
"products": [{"product": product, "quantity": 1}]
}]
orders = store_models.Order.objects.create_from_cart(
cart_items=cart_items,
customer_data=self.customer_data,
payment_method=self.payment_method
)
self.assertEqual(orders.count(), 1)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(
mail.outbox[0].subject,
f"Wygenerowano umowę numer {orders[0].order_number} z dnia {orders[0].created_at.strftime('%d.%m.%Y')}"
)
@patch("mailings.models.MailTemplate.load_and_process_template", return_value="test")
def test_create_from_cart_success_multpile_authors(self, mocked_load):
product = factories.ProductFactory(template__author=self.second_author, price=100)
cart_items = [
{
"author": self.author,
"products": [{"product": product, "quantity": 1}]
}, {
"author": self.second_author,
"products": [{"product": product, "quantity": 1}]
}
]
orders = store_models.Order.objects.create_from_cart(
cart_items=cart_items,
customer_data=self.customer_data,
payment_method=self.payment_method
)
self.assertEqual(orders.count(), 2)
self.assertEqual(len(mail.outbox), 4)
self.assertEqual(
mail.outbox[0].subject,
f"Wygenerowano umowę numer {orders[0].order_number} z dnia {orders[0].created_at.strftime('%d.%m.%Y')}"
)
self.assertEqual(
mail.outbox[2].subject,
f"Wygenerowano umowę numer {orders[1].order_number} z dnia {orders[1].created_at.strftime('%d.%m.%Y')}"
)

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

@ -1,18 +1,39 @@
from typing import Any
from django.core.mail import EmailMessage
from django.conf import settings
from django.db.models import QuerySet
# TODO - add celery task for sending not sent earlier
def send_mail(order_doc):
order = order_doc.order
def send_mail(
to: list[str], docs: Any, order_number: str,
subject: str, body: str
):
message = EmailMessage(
subject=f"Zamówienie {order.order_number}",
body="Dokumenty dla Twojego zamówienia",
subject=subject,
body=body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[order.customer.email]
to=to
)
message.attach(f"{order.order_number}.pdf", order_doc.document, "application/pdf")
sent = bool(message.send())
order_doc.sent = sent
order_doc.save()
return sent
for doc in docs:
message.attach(f"{order_number}.pdf", doc, "application/pdf")
return bool(message.send())
def notify_user_about_order(customer_email, docs, order_number):
return send_mail(
to=[customer_email],
docs=docs,
order_number=order_number,
subject=f"Zamówienie {order_number}",
body="Dokumenty dla Twojego zamówienia"
)
def notify_manufacturer_about_order(manufacturer_email, docs, order_number):
return send_mail(
to=[manufacturer_email],
docs=docs,
order_number=order_number,
subject=f"Złożono zamówienie {order_number}",
body="Dokumenty dla złożonego zamówienia"
)

Wyświetl plik

@ -4,25 +4,35 @@ 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
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 (
CartProductSerializer,
CartSerializer,
CartProductAddSerializer
)
from store.forms import CustomerDataForm
from store.forms import (
CustomerDataForm,
ProductTemplateConfigForm
)
from store.models import (
CustomerData,
Order,
OrderProduct,
OrderDocument,
DocumentTemplate
Product,
ProductTemplate,
ProductListPage
)
@ -43,12 +53,13 @@ class CartView(TemplateView):
class CartActionView(ViewSet):
# TODO - test this, currently not in use
@action(detail=False, methods=["get"], url_path="list-products")
def list_products(self, request):
# get cart items
cart = SessionCart(self.request)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
items = cart.display_items
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data)
@action(detail=False, methods=["post"])
@ -58,29 +69,86 @@ class CartActionView(ViewSet):
if not serializer.is_valid():
return Response(serializer.errors, status=400)
serializer.save(cart)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
items = cart.display_items
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
@action(detail=False, methods=["post"])
def remove_product(self, request):
cart = SessionCart(self.request)
product_id = request.POST.get("product_id")
cart.remove_item(product_id)
try:
cart.remove_item(product_id)
except Product.DoesNotExist:
return Response({"error": "Product does not exist"}, status=400)
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
items = cart.display_items
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
@action(detail=True, methods=["put"])
def update_product(self, request, pk):
cart = SessionCart(self.request)
cart.update_item_quantity(pk, int(request.data["quantity"]))
items = cart.get_items()
serializer = CartProductSerializer(instance=items, many=True)
try:
cart.update_item_quantity(pk, int(request.data["quantity"]))
except Product.DoesNotExist:
return Response({"error": "Product does not exist"}, status=404)
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"
@ -93,24 +161,23 @@ class OrderView(View):
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, *args, **kwargs):
# TODO - messages
cart = SessionCart(self.request)
if cart.is_empty():
messages.error(request, "Twój koszyk jest pusty")
return HttpResponseRedirect(reverse("cart"))
form = CustomerDataForm(request.POST)
if not form.is_valid():
print(form.errors)
context = self.get_context_data()
context["form"] = form
return render(request, self.template_name, context)
customer_data = form.save()
request.session["customer_data_id"] = customer_data.id
# TODO - add this page
customer_data = CustomerData(data=form.serialize())
request.session["customer_data"] = customer_data.data
return HttpResponseRedirect(reverse("order-confirm"))
@ -118,26 +185,54 @@ class OrderConfirmView(View):
template_name = "store/order_confirm.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
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 = CustomerData.objects.get(id=self.request.session["customer_data_id"])
customer_data = CustomerData(
encrypted_data=self.request.session["customer_data"]
).decrypted_data
cart = SessionCart(self.request)
order = Order.objects.create_from_cart(
cart, customer_data
cart.display_items,
None, customer_data
)
self.request.session.pop("customer_data_id")
request.session.pop("customer_data")
cart.clear()
# TODO - messages
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())