Merge branch 'main' into feature/optionalization
commit
5975b4c3d9
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
// close all alerts after 2 seconds
|
||||
$('.alert').fadeTo(2000, 500).slideUp(500, function(){
|
||||
$("#success-alert").slideUp(500);
|
||||
});
|
|
@ -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 %}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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")
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -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())
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
|
||||
class PdfFromDocGeneratorTestCase(TestCase):
|
||||
...
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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.
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
|
@ -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)
|
|
@ -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'
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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()
|
|
@ -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")
|
|
@ -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)
|
|
@ -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.
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
|
@ -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()
|
|
@ -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")),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-25 08:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0006_remove_orderdocument_sent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="ProductImage",
|
||||
new_name="ProductTemplateImage",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-25 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0007_rename_productimage_producttemplateimage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="product",
|
||||
name="info",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="producttemplateimage",
|
||||
name="is_main",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="producttemplateimage",
|
||||
name="template",
|
||||
field=modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="template_images", to="store.producttemplate"
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ProductImage",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image", models.ImageField(upload_to="")),
|
||||
("is_main", models.BooleanField(default=False)),
|
||||
(
|
||||
"product",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="product_images", to="store.product"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-30 16:11
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0008_remove_product_info_producttemplateimage_is_main_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ProductCategoryParamValue",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("value", models.CharField(max_length=255)),
|
||||
(
|
||||
"param",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="param_values",
|
||||
to="store.productcategoryparam",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-30 16:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def copy_old_data(apps, schema_editor):
|
||||
TemplateParamValue = apps.get_model("store", "TemplateParamValue")
|
||||
ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue")
|
||||
|
||||
for param_value in TemplateParamValue.objects.all():
|
||||
ProductCategoryParamValue.objects.create(
|
||||
param=param_value.param,
|
||||
value=param_value.value
|
||||
)
|
||||
|
||||
|
||||
def remove_new_data(apps, schema_editor):
|
||||
ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue")
|
||||
ProductCategoryParamValue.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0009_productcategoryparam_order_productcategoryparamvalue"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(copy_old_data, remove_new_data),
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 4.1.9 on 2023-07-02 09:34
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0010_auto_20230630_1611"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ProductParam",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
(
|
||||
"param_value",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="store.productcategoryparamvalue"
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="product_params", to="store.product"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="TemplateParamValue",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="params",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, through="store.ProductParam", to="store.productcategoryparamvalue"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 4.1.9 on 2023-07-22 17:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0011_productparam_delete_templateparamvalue_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeliveryMethod",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
("price", models.FloatField(default=0)),
|
||||
("active", models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="uuid",
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="product",
|
||||
name="uuid",
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="delivery_method",
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to="store.deliverymethod"),
|
||||
),
|
||||
]
|
|
@ -1,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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-truck" viewBox="0 0 16 16">
|
||||
<path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h9A1.5 1.5 0 0 1 12 3.5V5h1.02a1.5 1.5 0 0 1 1.17.563l1.481 1.85a1.5 1.5 0 0 1 .329.938V10.5a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 1 1-4 0H5a2 2 0 1 1-3.998-.085A1.5 1.5 0 0 1 0 10.5v-7zm1.294 7.456A1.999 1.999 0 0 1 4.732 11h5.536a2.01 2.01 0 0 1 .732-.732V3.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .294.456zM12 10a2 2 0 0 1 1.732 1h.768a.5.5 0 0 0 .5-.5V8.35a.5.5 0 0 0-.11-.312l-1.48-1.85A.5.5 0 0 0 13.02 6H12v4zm-9 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm9 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 658 B |
|
@ -10,7 +10,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
const csrfToken = $(this).data('csrf-token');
|
||||
console.log(productID);
|
||||
formData.append('product_id', productID);
|
||||
formData.append('quantity', quantity); // Serialize the form data correctly
|
||||
formData.append('quantity', 1); // Serialize the form data correctly
|
||||
button.prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
|
@ -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
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
from mailings.models import OutgoingEmail
|
||||
from store.models import Product
|
||||
from store.admin import ProductAdmin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO - those should be modified to be celery tasks
|
||||
|
||||
def send_produt_request_email(variant_pk: int):
|
||||
try:
|
||||
variant = Product.objects.get(pk=variant_pk)
|
||||
except Product.DoesNotExist:
|
||||
logger.exception(f"Product with pk={variant_pk} does not exist")
|
||||
|
||||
try:
|
||||
admin_url = ProductAdmin().url_helper.get_action_url("edit", variant.pk)
|
||||
send = OutgoingEmail.objects.send(
|
||||
template_name="product_request",
|
||||
subject="Złożono zapytanie ofertowe",
|
||||
recipient=variant.template.author.email,
|
||||
context={"product": variant, "admin_url": admin_url},
|
||||
sender=settings.DEFAULT_FROM_EMAIL
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"Could not send email for variant pk={variant_pk}, exception: {e} has occured")
|
||||
else:
|
||||
if not send:
|
||||
logger.exception(f"Could not send email for variant pk={variant_pk}")
|
|
@ -9,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>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-5">
|
||||
|
||||
<div class="card-header text-center">
|
||||
<h2>{{template.title}}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<img src="{{template.main_image.image.url}}" class="img-fluid img-thumbnail" alt="Responsive image">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p>{{template.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<form action="" method="POST" class="mt-5">
|
||||
{% csrf_token %}
|
||||
<div class="row mt-5">
|
||||
{% for field in form %}
|
||||
<div class="col-6 text-center">
|
||||
<h3>{{field.label}}</h3>
|
||||
{{field}}
|
||||
</div>
|
||||
{% if forloop.counter|divisibleby:"2" %} </div><div class="row mt-5">{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6"></div>
|
||||
<div class="col-6 text-center">
|
||||
<button class="btn btn-lg btn-success" type="submit">Sprawdź dostępność</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,80 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="modal" tabindex="-1" role="dialog" id="addToCartModal" aria-labelledby="addToCartLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodano do koszyka</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Przedmiot dodany do koszyka</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-secondary" href="{{store_url}}">Kontynuuj zakupy</button>
|
||||
<a href="{% url 'cart' %}" class="btn btn-primary">Idź do koszyka</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="h-100">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<img src="{{variant.main_image.image.url}}"
|
||||
class="img-fluid img-thumbnail h-80" alt="Responsive image">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card mb-2 py-5">
|
||||
<div class="card-body">
|
||||
{% for value in params_values %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p class="text-muted mb-0">{{value}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not variant.available %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 card alert-danger text-center">
|
||||
Niestety skonfigurowany przez Ciebie wariant produktu nie jest jeszcze dostępny.
|
||||
Jeżeli jesteś zainteresowany tą konfiguracją złóż zapytanie ofertowe.
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end">
|
||||
<h3>Cena: {{variant.price}} zł</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-6 ">
|
||||
<a class="btn btn-outline-primary btn-lg" href="{% url 'product-configure' variant.template.pk %}">Wróć do konfiguratora</a>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
|
||||
{% if variant.available %}
|
||||
<button class="btn btn-outline-success btn-lg add-to-cart-button" data-product-id="{{variant.id}}"
|
||||
data-add-to-cart-url="{% url 'cart-action-add-product' %}" data-csrf-token='{{ csrf_token }}'
|
||||
data-bs-toggle="modal" data-bs-target="#addToCartModal">
|
||||
Zamów produkt
|
||||
</button>
|
||||
{% else %}
|
||||
<form method="POST" action="">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-outline-success btn-lg" type="submit">Złóż zapytanie ofertowe</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
{% with id=widget.attrs.id %}
|
||||
<div class="btn-group btn-group-toggle" role="group">
|
||||
{% for group, options, index in widget.optgroups %}
|
||||
{% for option in options %}
|
||||
<input type="radio" class="btn-check" name="{{option.name}}" id="{{id}}_{{option.index}}" autocomplete="off"
|
||||
value="{{option.value}}" required>
|
||||
<label class="btn btn-outline-primary" for="{{id}}_{{option.index}}">{{option.label}}</label>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
<!--
|
||||
{% with id=widget.attrs.id %}
|
||||
<div{% if id %} id="{{ id }}"{% endif %}
|
||||
{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %}
|
||||
>
|
||||
{% for group, options, index in widget.optgroups %}
|
||||
{% if group %}
|
||||
<div><label>{{ group }}</label>{% endif %}{% for option in options %}<div>
|
||||
{% include option.template_name with widget=option %}</div>{% endfor %}{% if group %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
-->
|
|
@ -8,9 +8,9 @@
|
|||
<div class="container pt-4">
|
||||
<div class="row">
|
||||
<div class="col-12 px-4">
|
||||
<h1>Twoje dane</h1>
|
||||
<hr class="mt-1" />
|
||||
<h2>Twoje dane</h2>
|
||||
</div>
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row mx-4">
|
||||
|
@ -42,12 +42,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 mx-4">
|
||||
|
||||
<div class="col-12">
|
||||
<label class="order-form-label">Dane Kontaktowe</label>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 px-4">
|
||||
<h2>Dane do wysyłki</h2>
|
||||
</div>
|
||||
<hr class="mt-1" />
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
<div class="row mt-3 mx-4">
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.street.label}}</label>
|
||||
|
@ -72,7 +73,30 @@
|
|||
{{form.zip_code}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 px-4">
|
||||
<h2>Płatność i wysyłka</h2>
|
||||
</div>
|
||||
<hr class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="row mt-3 mx-4">
|
||||
<div class="col-sm-12 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.payment_method.label}}</label>
|
||||
{{form.payment_method}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.delivery_method.label}}</label>
|
||||
{{form.delivery_method}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-end mt-3">
|
||||
<div class="form-outline">
|
||||
<input type="submit" value="Dalej" class="btn btn-success btn-lg">
|
||||
|
|
|
@ -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}} zł</h5>
|
||||
</div>
|
||||
<div class="col-sm-6 text-end">
|
||||
<form action="" method="POST">
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="card col-9 bg-white shadow-md p-5">
|
||||
<div class="mb-4 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-success" width="75" height="75"
|
||||
fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path
|
||||
d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1>Dziękujemy za zakup!</h1>
|
||||
<div>
|
||||
Twoje zamówienia zostały przekazane do realizacji.
|
||||
Oczekuj maili z dokumentami to zamówień.<br/>
|
||||
|
||||
<b>Twoje numery zamówień:</b>
|
||||
<div class="col-12 text-center">
|
||||
{% for order in orders %}
|
||||
{{order.order_number}}<br/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a class="btn btn-outline-primary mt-3" href="{{store_url}}">
|
||||
Powrót do sklepu
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -5,7 +5,7 @@
|
|||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{{item.product.main_image.url}}"
|
||||
src="{{item.product.main_image.image.url}}"
|
||||
class="img-fluid rounded-3">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-1">
|
||||
<div class="card-body p-4">
|
||||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{% static 'images/icons/truck.svg'%}"
|
||||
class="rounded mx-auto d-block">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
<p class="lead fw-normal mb-2">{{delivery.name}}</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-2 d-flex">
|
||||
1
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||
<h5 class="mb-0">{{delivery.price}} zł</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -3,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>
|
||||
|
|
|
@ -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}} ZŁ</h5>
|
||||
<h5 class="mb-0">{{item.product.price}} zł</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -3,26 +3,6 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="addToCartModal" aria-labelledby="addToCartLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodano do koszyka</h5>
|
||||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close" >
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Przedmiot został dodany do koszyka.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Kontynuuj zakupy</button>
|
||||
<a href="{% url 'cart' %}" class="btn btn-primary">Idź do koszyka</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mb-3 mt-5">
|
||||
<div class="row row-cols-3 g-4 mt-3">
|
||||
{% for item in items %}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -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.")
|
||||
|
|
@ -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')}"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
from django.test import TestCase
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from store.models import (
|
||||
ProductCategoryParam,
|
||||
ProductCategoryParamValue,
|
||||
CategoryParamTypeChoices
|
||||
)
|
||||
from store.tests.factories import (
|
||||
ProductTemplateFactory,
|
||||
ProductCategoryFactory,
|
||||
ProductFactory,
|
||||
ProductCategoryParamValueFactory
|
||||
)
|
||||
|
||||
|
||||
class ConfigureProductViewTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.category = ProductCategoryFactory()
|
||||
self.product_template = ProductTemplateFactory(category=self.category)
|
||||
# create template params and values for those params
|
||||
self.param1 = ProductCategoryParam.objects.create(
|
||||
key="Mocowanie", category=self.category,
|
||||
param_type=CategoryParamTypeChoices.STRING
|
||||
)
|
||||
self.param1_value1 = ProductCategoryParamValueFactory(param=self.param1)
|
||||
self.param1_value2 = ProductCategoryParamValueFactory(param=self.param1)
|
||||
self.param2 = ProductCategoryParam.objects.create(
|
||||
key="Format", category=self.category,
|
||||
param_type=CategoryParamTypeChoices.STRING
|
||||
)
|
||||
self.param2_value1 = ProductCategoryParamValueFactory(param=self.param2)
|
||||
self.param2_value2 = ProductCategoryParamValueFactory(param=self.param2)
|
||||
# create product variant
|
||||
self.variant1 = ProductFactory(
|
||||
template=self.product_template
|
||||
)
|
||||
self.variant1.params.set([self.param1_value1, self.param2_value1])
|
||||
self.variant1.save()
|
||||
|
||||
self.variant2 = ProductFactory(
|
||||
template=self.product_template,
|
||||
)
|
||||
self.variant2.params.set([self.param1_value2, self.param2_value2])
|
||||
self.variant2.save()
|
||||
|
||||
def test_get_success(self):
|
||||
response = self.client.get(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "store/configure_product.html")
|
||||
|
||||
def test_get_failure_wrong_pk(self):
|
||||
response = self.client.get(
|
||||
reverse("product-configure", args=[12312]),
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_post_success(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value1.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("configure-product-summary", args=[self.variant1.pk]))
|
||||
|
||||
def test_post_failure_not_existing_template(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value1.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[2137]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_post_not_existing_config(self):
|
||||
data = {
|
||||
self.param1.key: [str(self.param1_value2.pk)],
|
||||
self.param2.key: [str(self.param2_value1.pk)]
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("product-configure", args=[self.product_template.pk]),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class ConfigureProductSummaryViewTestCase(TestCase):
|
||||
...
|
|
@ -9,7 +9,10 @@ router.register("cart-action", store_views.CartActionView, "cart-action")
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path('product-configure/<int:pk>/', store_views.ConfigureProductView.as_view(), name='product-configure'),
|
||||
path('product-configure/summary/<int:variant_pk>/', store_views.ConfigureProductSummaryView.as_view(), name='configure-product-summary'),
|
||||
path('cart/', store_views.CartView.as_view(), name='cart'),
|
||||
path("order/", store_views.OrderView.as_view(), name="order"),
|
||||
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm")
|
||||
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm"),
|
||||
path("order/success/", store_views.OrderSuccessView.as_view(), name="order-success")
|
||||
] + router.urls
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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())
|
||||
|
|
Ładowanie…
Reference in New Issue