Formatted all files
rodzic
2fc4a7dd4f
commit
875dfe957d
|
@ -6,10 +6,10 @@ __pycache__
|
|||
db.sqlite3
|
||||
media
|
||||
|
||||
# Backup files #
|
||||
*.bak
|
||||
# Backup files #
|
||||
*.bak
|
||||
|
||||
# If you are using PyCharm #
|
||||
# If you are using PyCharm #
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
|
@ -45,95 +45,95 @@ out/
|
|||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Python #
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# Python #
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
.Python build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
*.manifest
|
||||
*.spec
|
||||
# Distribution / packaging
|
||||
.Python build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery
|
||||
celerybeat-schedule.*
|
||||
# celery
|
||||
celerybeat-schedule.*
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Sublime Text #
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
# Sublime Text #
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
# Package control specific files Package
|
||||
Control.last-run
|
||||
Control.ca-list
|
||||
Control.ca-bundle
|
||||
Control.system-ca-bundle
|
||||
GitHub.sublime-settings
|
||||
# Package control specific files Package
|
||||
Control.last-run
|
||||
Control.ca-list
|
||||
Control.ca-bundle
|
||||
Control.system-ca-bundle
|
||||
GitHub.sublime-settings
|
||||
|
||||
# Visual Studio Code #
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
# Visual Studio Code #
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history
|
||||
|
||||
#postgres pass files
|
||||
|
|
18
README.md
18
README.md
|
@ -2,11 +2,11 @@
|
|||
|
||||
ComfyShop is an open source project that combines a blog and shop using Django and Wagtail. It provides an easy-to-use interface for managing blog posts and products.
|
||||
|
||||
|
||||
[![Python](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python&logoColor=blue)](https://www.python.org)
|
||||
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)
|
||||
|
||||
[![Woodpecker CI](https://ci.citizen4.eu/api/badges/21/status.svg)](https://ci.citizen4.eu/repos/21)
|
||||
[![Python](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python&logoColor=blue)](https://www.python.org)
|
||||
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)
|
||||
|
||||
[![Woodpecker CI](https://ci.citizen4.eu/api/badges/21/status.svg)](https://ci.citizen4.eu/repos/21)
|
||||
[![Release Version](https://img.shields.io/badge/Release%20Version-v0.2-blue)](https://forge.citizen4.eu/mtyton/comfy/releases/tag/0.2.0)
|
||||
## Requirements
|
||||
|
||||
|
@ -15,15 +15,15 @@ ComfyShop is an open source project that combines a blog and shop using Django a
|
|||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository using the following command:
|
||||
1. Clone the repository using the following command:
|
||||
```git clone https://forge.citizen4.eu/mtyton/comfy.git```
|
||||
3. Build the Docker image:
|
||||
3. Build the Docker image:
|
||||
```docker-compose build```
|
||||
4. Run the Docker container for development:
|
||||
4. Run the Docker container for development:
|
||||
```docker-compose up```
|
||||
|
||||
|
||||
For production, use the following command to run the Docker container:
|
||||
For production, use the following command to run the Docker container:
|
||||
```docker-compose -f docker-compose-prod.yml up -d```
|
||||
|
||||
|
||||
|
@ -49,4 +49,4 @@ For more detailed information on how to use and customize the application, refer
|
|||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
This project is licensed under the [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe]
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: debug-statements
|
||||
language_version: python3
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
|
@ -5,7 +5,7 @@ steps:
|
|||
commands:
|
||||
- docker compose -f wagtail_store/docker-compose-test.yml build --no-cache
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
when:
|
||||
event: pull_request
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
@ -14,9 +14,9 @@ steps:
|
|||
secrets: []
|
||||
commands:
|
||||
- docker compose -f wagtail_store/docker-compose-test.yml run test_comfy
|
||||
- docker compose -f ./wagtail_store/docker-compose-test.yml down
|
||||
- docker compose -f ./wagtail_store/docker-compose-test.yml down
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
when:
|
||||
event: pull_request
|
||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||
|
|
|
@ -1,48 +1,75 @@
|
|||
# Generated by Django 4.1.8 on 2023-04-28 20:41
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0083_workflowcontenttype'),
|
||||
('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
|
||||
("wagtailcore", "0083_workflowcontenttype"),
|
||||
("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogPage',
|
||||
name="BlogPage",
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
('create_date', models.DateField(auto_now_add=True)),
|
||||
('edit_date', models.DateField(auto_now=True)),
|
||||
('body', wagtail.fields.RichTextField()),
|
||||
(
|
||||
"page_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="wagtailcore.page",
|
||||
),
|
||||
),
|
||||
("create_date", models.DateField(auto_now_add=True)),
|
||||
("edit_date", models.DateField(auto_now=True)),
|
||||
("body", wagtail.fields.RichTextField()),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BlogPageGalleryImage',
|
||||
name="BlogPageGalleryImage",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
|
||||
('caption', models.CharField(blank=True, max_length=250)),
|
||||
('order', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1000)])),
|
||||
('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailimages.image')),
|
||||
('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='gallery_images', to='blog.blogpage')),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("sort_order", models.IntegerField(blank=True, editable=False, null=True)),
|
||||
("caption", models.CharField(blank=True, max_length=250)),
|
||||
(
|
||||
"order",
|
||||
models.IntegerField(
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(0),
|
||||
django.core.validators.MaxValueValidator(1000),
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"image",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="+", to="wagtailimages.image"
|
||||
),
|
||||
),
|
||||
(
|
||||
"page",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="gallery_images", to="blog.blogpage"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'ordering': ['sort_order'],
|
||||
'abstract': False,
|
||||
"ordering": ["sort_order"],
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.core.validators import (
|
||||
MinValueValidator,
|
||||
MaxValueValidator
|
||||
)
|
||||
|
||||
from wagtail.models import Page, Orderable
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.admin.panels import (
|
||||
FieldPanel,
|
||||
InlinePanel
|
||||
)
|
||||
from wagtail.search import index
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.models import Orderable, Page
|
||||
from wagtail.search import index
|
||||
|
||||
|
||||
class BlogPage(Page):
|
||||
|
@ -20,28 +13,18 @@ class BlogPage(Page):
|
|||
|
||||
body = RichTextField()
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("body"),
|
||||
InlinePanel("gallery_images")
|
||||
]
|
||||
content_panels = Page.content_panels + [FieldPanel("body"), InlinePanel("gallery_images")]
|
||||
|
||||
|
||||
class BlogPageGalleryImage(Orderable):
|
||||
page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name='gallery_images')
|
||||
image = models.ForeignKey(
|
||||
'wagtailimages.Image', on_delete=models.CASCADE, related_name='+'
|
||||
)
|
||||
page = ParentalKey(BlogPage, on_delete=models.CASCADE, related_name="gallery_images")
|
||||
image = models.ForeignKey("wagtailimages.Image", on_delete=models.CASCADE, related_name="+")
|
||||
caption = models.CharField(blank=True, max_length=250)
|
||||
|
||||
order = models.IntegerField(
|
||||
validators=[
|
||||
MinValueValidator(0),
|
||||
MaxValueValidator(1000)
|
||||
]
|
||||
)
|
||||
order = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(1000)])
|
||||
|
||||
panels = [
|
||||
FieldPanel('order'),
|
||||
FieldPanel('image'),
|
||||
FieldPanel('caption'),
|
||||
FieldPanel("order"),
|
||||
FieldPanel("image"),
|
||||
FieldPanel("caption"),
|
||||
]
|
||||
|
|
|
@ -56,7 +56,7 @@ services:
|
|||
volumes:
|
||||
- ../nginx/conf.d/:/etc/nginx/conf.d/
|
||||
- ./static/:/opt/services/comfy/static
|
||||
- ./media/:/opt/services/comfy/media
|
||||
- ./media/:/opt/services/comfy/media
|
||||
ports:
|
||||
- "8000:80"
|
||||
environment:
|
||||
|
|
|
@ -12,12 +12,12 @@ services:
|
|||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internal
|
||||
deploy:
|
||||
- internal
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 512M
|
||||
memory: 512M
|
||||
|
||||
comfy:
|
||||
image: comfy
|
||||
|
@ -33,22 +33,22 @@ services:
|
|||
- DATABASE_URL=${DATABASE_URL}
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE}
|
||||
- SENTRY_DSN=${SENTRY_DSN}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
- 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}
|
||||
- DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- internal
|
||||
deploy:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 512M
|
||||
memory: 512M
|
||||
|
||||
web:
|
||||
image: nginx
|
||||
|
@ -56,7 +56,7 @@ services:
|
|||
volumes:
|
||||
- nginx:/etc/nginx/conf.d
|
||||
- static:/opt/services/comfy/static
|
||||
- media:/opt/services/comfy/media
|
||||
- media:/opt/services/comfy/media
|
||||
ports:
|
||||
- "80"
|
||||
environment:
|
||||
|
@ -64,20 +64,20 @@ services:
|
|||
- NGINX_PORT=${NGINX_PORT}
|
||||
networks:
|
||||
- internal
|
||||
- web
|
||||
deploy:
|
||||
- web
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1.0'
|
||||
memory: 256M
|
||||
memory: 256M
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.http.routers.wagtail_store.rule=Host(`${NGINX_HOST}`)"
|
||||
- "traefik.http.routers.wagtail_store.entrypoints=websecure"
|
||||
- "traefik.http.services.wagtail_store.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.wagtail_store.tls=true"
|
||||
- "traefik.http.routers.wagtail_store.tls.certresolver=ovh"
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.http.routers.wagtail_store.rule=Host(`${NGINX_HOST}`)"
|
||||
- "traefik.http.routers.wagtail_store.entrypoints=websecure"
|
||||
- "traefik.http.services.wagtail_store.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.wagtail_store.tls=true"
|
||||
- "traefik.http.routers.wagtail_store.tls.certresolver=ovh"
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
@ -89,5 +89,4 @@ networks:
|
|||
internal:
|
||||
web:
|
||||
external:
|
||||
name: web
|
||||
|
||||
name: web
|
||||
|
|
|
@ -8,7 +8,7 @@ services:
|
|||
- POSTGRES_USER=comfy
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=comfy_shop
|
||||
|
||||
|
||||
test_rabbit:
|
||||
hostname: rabbit
|
||||
image: rabbitmq:3.6.0
|
||||
|
@ -57,4 +57,4 @@ services:
|
|||
depends_on:
|
||||
- test_comfy
|
||||
- test_rabbit
|
||||
- test_beat
|
||||
- test_beat
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
@ -29,7 +29,7 @@ services:
|
|||
ports:
|
||||
- "1025:1025"
|
||||
- "8025:8025"
|
||||
|
||||
|
||||
comfy:
|
||||
restart: always
|
||||
depends_on:
|
||||
|
@ -94,4 +94,4 @@ services:
|
|||
volumes:
|
||||
media:
|
||||
db:
|
||||
rabbitmq_data:
|
||||
rabbitmq_data:
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
modeladmin_register,
|
||||
)
|
||||
|
||||
from dynamic_forms import models
|
||||
|
||||
|
||||
class CustomEmailFormAdmin(ModelAdmin):
|
||||
model = models.CustomEmailForm
|
||||
menu_label = "Email Forms"
|
||||
menu_icon = 'mail'
|
||||
menu_icon = "mail"
|
||||
menu_order = 100
|
||||
add_to_settings_menu = False
|
||||
exclude_from_explorer = False
|
||||
|
@ -34,12 +35,12 @@ class CustomEmailFormAdmin(ModelAdmin):
|
|||
"subject",
|
||||
)
|
||||
|
||||
|
||||
class CustomFormGroup(ModelAdminGroup):
|
||||
menu_label = "Custom Forms"
|
||||
menu_icon = 'tasks'
|
||||
menu_icon = "tasks"
|
||||
menu_order = 100
|
||||
items = (
|
||||
CustomEmailFormAdmin,
|
||||
)
|
||||
items = (CustomEmailFormAdmin,)
|
||||
|
||||
|
||||
modeladmin_register(CustomFormGroup)
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
from django import forms
|
||||
from dynamic_forms.widgets import (
|
||||
CheckboxSelectMultiple,
|
||||
CheckboxInput,
|
||||
RadioSelect
|
||||
)
|
||||
|
||||
from dynamic_forms.widgets import CheckboxInput, CheckboxSelectMultiple, RadioSelect
|
||||
|
||||
|
||||
class MultipleFileInput(forms.ClearableFileInput):
|
||||
|
@ -25,24 +22,21 @@ class MultipleFileField(forms.FileField):
|
|||
|
||||
|
||||
class HoneypotField(forms.BooleanField):
|
||||
default_widget = forms.HiddenInput(
|
||||
{'style': 'display:none !important;', 'tabindex': '-1', 'autocomplete': 'off'}
|
||||
)
|
||||
default_widget = forms.HiddenInput({"style": "display:none !important;", "tabindex": "-1", "autocomplete": "off"})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('widget', HoneypotField.default_widget)
|
||||
kwargs['required'] = False
|
||||
kwargs.setdefault("widget", HoneypotField.default_widget)
|
||||
kwargs["required"] = False
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def clean(self, value):
|
||||
if cleaned_value := super().clean(value):
|
||||
raise forms.ValidationError('')
|
||||
raise forms.ValidationError("")
|
||||
else:
|
||||
return cleaned_value
|
||||
|
||||
|
||||
class DynamicForm(forms.Form):
|
||||
|
||||
FIELD_TYPE_MAPPING = {
|
||||
"singleline": forms.CharField(max_length=50, widget=forms.TextInput(attrs={"class": "form-control"})),
|
||||
"multiline": forms.CharField(max_length=255, widget=forms.Textarea(attrs={"class": "form-control"})),
|
||||
|
@ -50,7 +44,9 @@ class DynamicForm(forms.Form):
|
|||
"number": forms.IntegerField(widget=forms.NumberInput(attrs={"class": "form-control"})),
|
||||
"url": forms.URLField(max_length=255, widget=forms.URLInput(attrs={"class": "form-control"})),
|
||||
"checkbox": forms.BooleanField(required=False, widget=CheckboxInput(attrs={"class": "form-check"})),
|
||||
"checkboxes": forms.MultipleChoiceField(required=False, widget=CheckboxSelectMultiple(attrs={"class": "form-check"})),
|
||||
"checkboxes": forms.MultipleChoiceField(
|
||||
required=False, widget=CheckboxSelectMultiple(attrs={"class": "form-check"})
|
||||
),
|
||||
"dropdown": forms.ChoiceField(widget=forms.Select(attrs={"class": "form-control"})),
|
||||
"multiselect": forms.MultipleChoiceField(widget=forms.SelectMultiple(attrs={"class": "form-control"})),
|
||||
"radio": forms.ChoiceField(widget=RadioSelect(attrs={"class": "form-control"})),
|
||||
|
@ -75,9 +71,7 @@ class DynamicForm(forms.Form):
|
|||
self.fields[field.clean_name] = f
|
||||
if file_uploads:
|
||||
self.fields["attachments"] = MultipleFileField(
|
||||
required=True, widget=MultipleFileInput(
|
||||
attrs={"class": "form-control"}
|
||||
)
|
||||
required=True, widget=MultipleFileInput(attrs={"class": "form-control"})
|
||||
)
|
||||
# add honeypot field
|
||||
self.fields["secret_honey"] = HoneypotField()
|
||||
|
@ -91,9 +85,9 @@ class DynamicForm(forms.Form):
|
|||
continue
|
||||
|
||||
cleaned_data[key] = ",".join(value)
|
||||
if key=="secret_honey":
|
||||
if key == "secret_honey":
|
||||
continue
|
||||
|
||||
|
||||
new_cleaned_data[key] = value
|
||||
|
||||
return new_cleaned_data
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 4.1.11 on 2023-10-12 17:23
|
||||
|
||||
import django.core.serializers.json
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
import wagtail.contrib.forms.models
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,51 +1,49 @@
|
|||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.formats import date_format
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.admin.panels import (
|
||||
FieldPanel, FieldRowPanel,
|
||||
InlinePanel, MultiFieldPanel
|
||||
)
|
||||
from wagtail.fields import RichTextField
|
||||
from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
|
||||
from wagtail.contrib.forms.models import (
|
||||
AbstractFormField,
|
||||
AbstractFormSubmission,
|
||||
FormMixin,
|
||||
Page,
|
||||
AbstractFormSubmission
|
||||
)
|
||||
from wagtail.fields import RichTextField
|
||||
|
||||
from mailings.models import (
|
||||
OutgoingEmail,
|
||||
Attachment
|
||||
)
|
||||
from dynamic_forms.forms import DynamicForm
|
||||
from mailings.models import Attachment, OutgoingEmail
|
||||
|
||||
|
||||
class Form(FormMixin, Page):
|
||||
intro = RichTextField(blank=True)
|
||||
thank_you_text = RichTextField(blank=True)
|
||||
allow_attachments = models.BooleanField(default=False)
|
||||
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel('intro'),
|
||||
InlinePanel('form_fields', label="Form fields"),
|
||||
FieldPanel('thank_you_text'),
|
||||
MultiFieldPanel([
|
||||
FieldRowPanel([
|
||||
FieldPanel('from_address', classname="col6"),
|
||||
FieldPanel('to_address', classname="col6"),
|
||||
]),
|
||||
FieldPanel('subject'),
|
||||
], "Email"),
|
||||
FieldPanel("allow_attachments")
|
||||
FieldPanel("intro"),
|
||||
InlinePanel("form_fields", label="Form fields"),
|
||||
FieldPanel("thank_you_text"),
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldRowPanel(
|
||||
[
|
||||
FieldPanel("from_address", classname="col6"),
|
||||
FieldPanel("to_address", classname="col6"),
|
||||
]
|
||||
),
|
||||
FieldPanel("subject"),
|
||||
],
|
||||
"Email",
|
||||
),
|
||||
FieldPanel("allow_attachments"),
|
||||
]
|
||||
|
||||
def get_form_class(self):
|
||||
return DynamicForm
|
||||
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
form_class = self.get_form_class()
|
||||
form_params = self.get_form_parameters()
|
||||
|
@ -59,7 +57,6 @@ class Form(FormMixin, Page):
|
|||
|
||||
|
||||
class EmailFormSubmission(AbstractFormSubmission):
|
||||
|
||||
# TODO - make this optional, allow to set pattern in admin
|
||||
def get_submission_id(self, form_slug):
|
||||
case_number_daily = EmailFormSubmission.objects.filter(submit_time__date=datetime.date.today()).count()
|
||||
|
@ -69,10 +66,7 @@ class EmailFormSubmission(AbstractFormSubmission):
|
|||
# modify this, get proper template
|
||||
to_addresses = data.pop("to_address").split(",")
|
||||
attachments = [
|
||||
Attachment(
|
||||
file.name, file.file.read(), file.content_type
|
||||
)
|
||||
for file in data.pop("attachments", [])
|
||||
Attachment(file.name, file.file.read(), file.content_type) for file in data.pop("attachments", [])
|
||||
]
|
||||
subject = data.pop("subject")
|
||||
form_slug = data.pop("form_slug")
|
||||
|
@ -84,23 +78,14 @@ class EmailFormSubmission(AbstractFormSubmission):
|
|||
recipient=address,
|
||||
sender=from_address,
|
||||
context={"form_data": data, "submission_id": self.get_submission_id(form_slug)},
|
||||
attachments=attachments
|
||||
attachments=attachments,
|
||||
)
|
||||
|
||||
|
||||
class CustomEmailForm(Form):
|
||||
from_address = models.EmailField(
|
||||
blank=True,
|
||||
help_text="Sender email address"
|
||||
)
|
||||
to_address = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Comma separated list of recipients"
|
||||
)
|
||||
subject = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Subject of the email with data"
|
||||
)
|
||||
from_address = models.EmailField(blank=True, help_text="Sender email address")
|
||||
to_address = models.CharField(max_length=255, help_text="Comma separated list of recipients")
|
||||
subject = models.CharField(max_length=255, help_text="Subject of the email with data")
|
||||
|
||||
template = "forms/email_form_page.html"
|
||||
|
||||
|
@ -114,17 +99,18 @@ class CustomEmailForm(Form):
|
|||
page=self,
|
||||
)
|
||||
mail_data = form.cleaned_data.copy()
|
||||
mail_data.update({
|
||||
"from_address": self.from_address,
|
||||
"to_address": self.to_address,
|
||||
"subject": self.subject,
|
||||
"attachments": attachments,
|
||||
"form_slug": self.slug
|
||||
})
|
||||
mail_data.update(
|
||||
{
|
||||
"from_address": self.from_address,
|
||||
"to_address": self.to_address,
|
||||
"subject": self.subject,
|
||||
"attachments": attachments,
|
||||
"form_slug": self.slug,
|
||||
}
|
||||
)
|
||||
submission.send_mail(data=mail_data)
|
||||
return submission
|
||||
|
||||
|
||||
class EmailFormField(AbstractFormField):
|
||||
form = ParentalKey(
|
||||
"CustomEmailForm", related_name="form_fields", on_delete=models.CASCADE
|
||||
)
|
||||
form = ParentalKey("CustomEmailForm", related_name="form_fields", on_delete=models.CASCADE)
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
{% endfor %}
|
||||
<div class="text-end mt-3">
|
||||
<input class="btn btn-lg btn-success" type="submit" value='{% trans "Submit" %}'>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div>You can fill in the from only one time.</div>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
{% endif %}
|
||||
{% for option in options %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio"
|
||||
value="{{ option.value }}" id="{{ option.attrs.id }}"
|
||||
<input class="form-check-input" type="radio"
|
||||
value="{{ option.value }}" id="{{ option.attrs.id }}"
|
||||
name="{{ option.name }}">
|
||||
<label class="form-check-label" for="{{ option.attrs.id }}">
|
||||
{{ option.value }}
|
||||
|
|
|
@ -1,105 +1,45 @@
|
|||
from wagtail.tests.utils import WagtailPageTests
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from wagtail.tests.utils import WagtailPageTests
|
||||
|
||||
from dynamic_forms.models import (
|
||||
CustomEmailForm,
|
||||
EmailFormField
|
||||
)
|
||||
from dynamic_forms.forms import DynamicForm
|
||||
from dynamic_forms.models import CustomEmailForm, EmailFormField
|
||||
|
||||
|
||||
class CustomEmailFormTestCase(WagtailPageTests):
|
||||
def setUp(self):
|
||||
self.form = CustomEmailForm.objects.create(
|
||||
slug="test", title="Test Form", path="test",
|
||||
depth=0, numchild=0, live=True, has_unpublished_changes=False,
|
||||
slug="test",
|
||||
title="Test Form",
|
||||
path="test",
|
||||
depth=0,
|
||||
numchild=0,
|
||||
live=True,
|
||||
has_unpublished_changes=False,
|
||||
from_address="comfy-test@egalitare.pl",
|
||||
to_address="comfy-dest@egalitare.pl",
|
||||
subject="Test Form", allow_attachments=False,
|
||||
subject="Test Form",
|
||||
allow_attachments=False,
|
||||
)
|
||||
EmailFormField.objects.create(label="Name", field_type="singleline", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="Message", field_type="multiline", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="Email", field_type="email", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="Number", field_type="number", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="URL", field_type="url", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="Checkbox", field_type="checkbox", required=True, form=self.form)
|
||||
EmailFormField.objects.create(
|
||||
label="Checkboxes", field_type="checkboxes", required=True, choices="a,b,c", form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Name",
|
||||
field_type="singleline",
|
||||
required=True,
|
||||
form=self.form
|
||||
label="Dropdown", field_type="dropdown", required=True, choices="a,b,c", form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Message",
|
||||
field_type="multiline",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Email",
|
||||
field_type="email",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Number",
|
||||
field_type="number",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="URL",
|
||||
field_type="url",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Checkbox",
|
||||
field_type="checkbox",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Checkboxes",
|
||||
field_type="checkboxes",
|
||||
required=True,
|
||||
choices="a,b,c",
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Dropdown",
|
||||
field_type="dropdown",
|
||||
required=True,
|
||||
choices="a,b,c",
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="MultiSelect",
|
||||
field_type="multiselect",
|
||||
required=True,
|
||||
choices="a,b,c",
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Radio",
|
||||
field_type="radio",
|
||||
required=True,
|
||||
choices="a,b,c",
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Date",
|
||||
field_type="date",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="DateTime",
|
||||
field_type="datetime",
|
||||
required=True,
|
||||
form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(
|
||||
label="Hidden",
|
||||
field_type="hidden",
|
||||
required=True,
|
||||
form=self.form
|
||||
label="MultiSelect", field_type="multiselect", required=True, choices="a,b,c", form=self.form
|
||||
)
|
||||
EmailFormField.objects.create(label="Radio", field_type="radio", required=True, choices="a,b,c", form=self.form)
|
||||
EmailFormField.objects.create(label="Date", field_type="date", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="DateTime", field_type="datetime", required=True, form=self.form)
|
||||
EmailFormField.objects.create(label="Hidden", field_type="hidden", required=True, form=self.form)
|
||||
|
||||
def test_generate_html_form_from_model(self):
|
||||
html_form = self.form.get_form()
|
||||
|
@ -108,22 +48,10 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
self.assertEqual(html_form.fields["name"].label, "Name")
|
||||
self.assertEqual(html_form.fields["name"].required, True)
|
||||
self.assertEqual(html_form.fields["name"].widget.attrs["class"], "form-control")
|
||||
self.assertIsInstance(
|
||||
html_form.fields["name"],
|
||||
forms.CharField
|
||||
)
|
||||
self.assertIsInstance(
|
||||
html_form.fields["message"],
|
||||
forms.CharField
|
||||
)
|
||||
self.assertIsInstance(
|
||||
html_form.fields["email"].widget,
|
||||
forms.EmailInput
|
||||
)
|
||||
self.assertIsInstance(
|
||||
html_form.fields["number"].widget,
|
||||
forms.NumberInput
|
||||
)
|
||||
self.assertIsInstance(html_form.fields["name"], forms.CharField)
|
||||
self.assertIsInstance(html_form.fields["message"], forms.CharField)
|
||||
self.assertIsInstance(html_form.fields["email"].widget, forms.EmailInput)
|
||||
self.assertIsInstance(html_form.fields["number"].widget, forms.NumberInput)
|
||||
|
||||
def test_create_form_submission_success_without_files(self):
|
||||
form_data = {
|
||||
|
@ -145,9 +73,7 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
form = self.form.get_form(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
# change field not to be required
|
||||
field = EmailFormField.objects.get(
|
||||
label="Name", form=self.form
|
||||
)
|
||||
field = EmailFormField.objects.get(label="Name", form=self.form)
|
||||
field.required = False
|
||||
field.save()
|
||||
form = self.form.get_form(form_data)
|
||||
|
@ -174,14 +100,14 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"hidden": "hidden"
|
||||
"hidden": "hidden",
|
||||
}
|
||||
files = {
|
||||
"attachments": [SimpleUploadedFile("test.txt", b"test")],
|
||||
}
|
||||
form = self.form.get_form(form_data, files=files)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
|
||||
def test_create_form_submission_failure_without_files_missing_data(self):
|
||||
form_data = {
|
||||
# generate data for this class self.form.get_form()
|
||||
|
@ -201,18 +127,16 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
form = self.form.get_form(form_data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 1)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
|
||||
form_data.pop("url")
|
||||
form = self.form.get_form(form_data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 2)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["url"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
self.assertEqual(form.errors["url"], ["This field is required."])
|
||||
# make Field not required
|
||||
field = EmailFormField.objects.get(
|
||||
label="Hidden", form=self.form
|
||||
)
|
||||
field = EmailFormField.objects.get(label="Hidden", form=self.form)
|
||||
field.required = False
|
||||
field.save()
|
||||
# it should also work without this field
|
||||
|
@ -220,8 +144,8 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
form = self.form.get_form(form_data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 2)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["url"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
self.assertEqual(form.errors["url"], ["This field is required."])
|
||||
|
||||
def test_create_form_submission_failure_with_files_missing_data(self):
|
||||
self.form.allow_attachments = True
|
||||
|
@ -247,18 +171,16 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
form = self.form.get_form(form_data, files=files)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 1)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
|
||||
form_data.pop("url")
|
||||
form = self.form.get_form(form_data, files=files)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 2)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["url"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
self.assertEqual(form.errors["url"], ["This field is required."])
|
||||
# make Field not required
|
||||
field = EmailFormField.objects.get(
|
||||
label="Hidden", form=self.form
|
||||
)
|
||||
field = EmailFormField.objects.get(label="Hidden", form=self.form)
|
||||
field.required = False
|
||||
field.save()
|
||||
# it should also work without this field
|
||||
|
@ -266,15 +188,15 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
form = self.form.get_form(form_data, files=files)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 2)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["url"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
self.assertEqual(form.errors["url"], ["This field is required."])
|
||||
# Now try without files
|
||||
form = self.form.get_form(form_data, files={})
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form.errors), 3)
|
||||
self.assertEqual(form.errors["name"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["url"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["attachments"], ['This field is required.'])
|
||||
self.assertEqual(form.errors["name"], ["This field is required."])
|
||||
self.assertEqual(form.errors["url"], ["This field is required."])
|
||||
self.assertEqual(form.errors["attachments"], ["This field is required."])
|
||||
|
||||
def test_no_hidden_field_in_clean_data_success(self):
|
||||
form_data = {
|
||||
|
@ -300,5 +222,3 @@ class CustomEmailFormTestCase(WagtailPageTests):
|
|||
self.assertNotIn("secret_honey", cleaned_data)
|
||||
self.assertIn("hidden", form.fields)
|
||||
self.assertIn("secret_honey", form.fields)
|
||||
|
||||
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
# Generated by Django 4.1.8 on 2023-04-23 13:34
|
||||
|
||||
from django.db import migrations
|
||||
import wagtail.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0002_create_homepage'),
|
||||
("home", "0002_create_homepage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='body',
|
||||
model_name="homepage",
|
||||
name="body",
|
||||
field=wagtail.fields.RichTextField(blank=True),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
from django.db import models
|
||||
|
||||
from wagtail.models import Page
|
||||
from wagtail import fields
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
class HomePage(Page):
|
||||
body = fields.RichTextField(blank=True)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel('body'),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
<div>
|
||||
{{ page.body|richtext }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
from django.forms import fields
|
||||
|
||||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
modeladmin_register,
|
||||
)
|
||||
|
||||
from mailings import models
|
||||
|
@ -12,19 +11,13 @@ from mailings import models
|
|||
class MailTemplateAdmin(ModelAdmin):
|
||||
model = models.MailTemplate
|
||||
menu_label = "Mail templates"
|
||||
menu_icon = 'mail'
|
||||
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",
|
||||
)
|
||||
list_display = ("template_name",)
|
||||
search_fields = ("template_name",)
|
||||
list_filter = ("template_name",)
|
||||
form_fields = (
|
||||
"template_name",
|
||||
"template",
|
||||
|
@ -34,7 +27,7 @@ class MailTemplateAdmin(ModelAdmin):
|
|||
class OutgoingMailAdmin(ModelAdmin):
|
||||
model = models.OutgoingEmail
|
||||
menu_label = "Outgoing mails"
|
||||
menu_icon = 'mail'
|
||||
menu_icon = "mail"
|
||||
menu_order = 100
|
||||
add_to_settings_menu = False
|
||||
exclude_from_explorer = False
|
||||
|
@ -42,9 +35,7 @@ class OutgoingMailAdmin(ModelAdmin):
|
|||
"subject",
|
||||
"sent",
|
||||
)
|
||||
search_fields = (
|
||||
"subject",
|
||||
)
|
||||
search_fields = ("subject",)
|
||||
list_filter = (
|
||||
"subject",
|
||||
"sender",
|
||||
|
@ -52,22 +43,14 @@ class OutgoingMailAdmin(ModelAdmin):
|
|||
"template__template_name",
|
||||
"sent",
|
||||
)
|
||||
readonly_fields = (
|
||||
"subject",
|
||||
"sender",
|
||||
"recipient",
|
||||
"sent"
|
||||
)
|
||||
readonly_fields = ("subject", "sender", "recipient", "sent")
|
||||
|
||||
|
||||
class MailingGroup(ModelAdminGroup):
|
||||
menu_label = "Mailings"
|
||||
menu_icon = 'mail'
|
||||
menu_icon = "mail"
|
||||
menu_order = 200
|
||||
items = (
|
||||
MailTemplateAdmin,
|
||||
OutgoingMailAdmin
|
||||
)
|
||||
items = (MailTemplateAdmin, OutgoingMailAdmin)
|
||||
|
||||
|
||||
modeladmin_register(MailingGroup)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-22 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import logging
|
||||
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db import models, transaction
|
||||
from django.template import Context, Template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,22 +18,17 @@ class Attachment:
|
|||
|
||||
|
||||
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'
|
||||
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")
|
||||
|
@ -49,23 +38,20 @@ def send_mail(
|
|||
class MailTemplate(models.Model):
|
||||
template_name = models.CharField(max_length=255, unique=True)
|
||||
|
||||
template = models.FileField(
|
||||
upload_to="mail_templates"
|
||||
)
|
||||
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):
|
||||
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}"
|
||||
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):
|
||||
|
@ -77,23 +63,25 @@ class MailTemplate(models.Model):
|
|||
|
||||
|
||||
class OutgoingEmailManager(models.Manager):
|
||||
|
||||
def send(
|
||||
self, template_name: str, subject: str,
|
||||
recipient: str, context: dict | Context,
|
||||
sender:str, attachments: list[Attachment] = None
|
||||
):
|
||||
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
|
||||
)
|
||||
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
|
||||
to=[recipient],
|
||||
sender_email=sender,
|
||||
subject=subject,
|
||||
content=template.load_and_process_template(context),
|
||||
attachments=attachments,
|
||||
)
|
||||
outgoing_email.sent = sent
|
||||
outgoing_email.save()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from factory.django import DjangoModelFactory
|
||||
from factory import Faker
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
|
||||
class MailTemplateFactory(DjangoModelFactory):
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
from django.test import TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core import mail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
|
||||
from mailings.models import (
|
||||
MailTemplate,
|
||||
OutgoingEmail,
|
||||
)
|
||||
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>"
|
||||
)
|
||||
template=SimpleUploadedFile("test_template.html", b"<html>{{test_var}}</html>"),
|
||||
)
|
||||
|
||||
def test_load_and_process_template_success(self):
|
||||
|
@ -36,22 +30,20 @@ class TestMailTemplate(TestCase):
|
|||
|
||||
|
||||
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>"
|
||||
)
|
||||
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={},
|
||||
recipient="test@stardust.io",
|
||||
context={},
|
||||
sender="sklep-test@stardust.io",
|
||||
subject="Test subject"
|
||||
subject="Test subject",
|
||||
)
|
||||
self.assertEqual(email.sent, True)
|
||||
self.assertEqual(mail.outbox[0].subject, "Test subject")
|
||||
|
@ -59,8 +51,6 @@ class TestOutgoingEmail(TestCase):
|
|||
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"
|
||||
template_name="missing_template", recipient="", sender="", context={}, subject="Test subject"
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
max-line-length = 120
|
||||
|
||||
[tool.black]
|
||||
line-length = 119
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FLAKE8>=6.0.0
|
||||
pre-commit>=3.3.1
|
||||
pre-commit>=3.6.0
|
||||
isort>=5.12
|
||||
black>=23.3.0
|
||||
ipdb==0.12.3
|
||||
ipdb==0.12.3
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from wagtail.models import Page
|
||||
from wagtail.search.models import Query
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
modeladmin_register,
|
||||
)
|
||||
|
||||
from setup.models import ComfyConfig
|
||||
|
||||
|
||||
class ConfigAdmin(ModelAdmin):
|
||||
model = ComfyConfig
|
||||
list_display = ("updated", )
|
||||
list_display = ("updated",)
|
||||
|
||||
|
||||
class SetupModelAdminGroup(ModelAdminGroup):
|
||||
menu_label = "Setup"
|
||||
menu_icon = 'folder-open-inverse'
|
||||
menu_icon = "folder-open-inverse"
|
||||
menu_order = 200
|
||||
items = (
|
||||
ConfigAdmin,
|
||||
)
|
||||
items = (ConfigAdmin,)
|
||||
|
||||
|
||||
modeladmin_register(SetupModelAdminGroup)
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import logging
|
||||
|
||||
from typing import Any, Mapping, Optional, Type, Union
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.forms.utils import ErrorList
|
||||
|
||||
|
||||
from setup.models import (
|
||||
ComfyConfig,
|
||||
NavbarPosition
|
||||
)
|
||||
from store import SHOP_ESSENTIAL_MAIL_TEMPLATES
|
||||
from mailings.models import MailTemplate
|
||||
|
||||
from setup.models import ComfyConfig, NavbarPosition
|
||||
from store import SHOP_ESSENTIAL_MAIL_TEMPLATES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -21,42 +16,39 @@ logger = logging.getLogger(__name__)
|
|||
class SiteConfigurationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ComfyConfig
|
||||
fields = [
|
||||
"logo", "navbar_position", "shop_enabled"
|
||||
]
|
||||
fields = ["logo", "navbar_position", "shop_enabled"]
|
||||
widgets = {
|
||||
"logo": forms.FileInput(attrs={"class": "form-control"}),
|
||||
"navbar_position": forms.Select(attrs={"class": "form-control"}),
|
||||
"shop_enabled": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||
}
|
||||
|
||||
|
||||
navbar_position = forms.ChoiceField(
|
||||
choices=NavbarPosition.choices,
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
initial=NavbarPosition.LEFT.value
|
||||
initial=NavbarPosition.LEFT.value,
|
||||
)
|
||||
|
||||
|
||||
class MailTemplatesFileUploadForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
for field_name, desc in SHOP_ESSENTIAL_MAIL_TEMPLATES.items():
|
||||
label = field_name.replace("_", " ").capitalize()
|
||||
self.fields[field_name] = forms.FileField(
|
||||
validators=[FileExtensionValidator(allowed_extensions=["html"])],
|
||||
help_text=desc, label=label, widget=forms.FileInput(attrs={"class": "form-control"})
|
||||
help_text=desc,
|
||||
label=label,
|
||||
widget=forms.FileInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
|
||||
def save(self):
|
||||
counter = 0
|
||||
for filename, file in self.files.items():
|
||||
obj, _created = MailTemplate.objects.get_or_create(
|
||||
template_name=filename
|
||||
)
|
||||
obj, _created = MailTemplate.objects.get_or_create(template_name=filename)
|
||||
obj.template = file
|
||||
obj.save()
|
||||
if _created:
|
||||
counter +=1
|
||||
counter += 1
|
||||
logger.info(f"Created {counter} mail templates")
|
||||
return MailTemplate.objects.count() >= len(SHOP_ESSENTIAL_MAIL_TEMPLATES.keys())
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import logging
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from store.models import ProductListPage
|
||||
from setup.models import ComfyConfig
|
||||
from store.models import ProductListPage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -18,16 +19,20 @@ class CheckSetupMiddleware(object):
|
|||
try:
|
||||
config = ComfyConfig.objects.get(active=True)
|
||||
except ComfyConfig.DoesNotExist:
|
||||
if (not request.path_info.startswith('/setup') and not request.path_info.startswith('/admin')
|
||||
and not request.path_info.startswith('/media') and not request.path_info.startswith('/static')):
|
||||
return redirect('/setup/')
|
||||
if (
|
||||
not request.path_info.startswith("/setup")
|
||||
and not request.path_info.startswith("/admin")
|
||||
and not request.path_info.startswith("/media")
|
||||
and not request.path_info.startswith("/static")
|
||||
):
|
||||
return redirect("/setup/")
|
||||
except ComfyConfig.MultipleObjectsReturned:
|
||||
config = ComfyConfig.objects.first()
|
||||
logger.exception("Multiple ComfyConfig objects found. Using first one.")
|
||||
|
||||
if config and request.path_info.startswith('/setup'):
|
||||
return redirect('/')
|
||||
|
||||
|
||||
if config and request.path_info.startswith("/setup"):
|
||||
return redirect("/")
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
@ -37,7 +42,7 @@ class CheckShopMiddleware(object):
|
|||
self.get_response = get_response
|
||||
|
||||
def _check_if_store_request(self, request):
|
||||
if request.path_info.startswith('/store-app/'):
|
||||
if request.path_info.startswith("/store-app/"):
|
||||
return True
|
||||
if request.path_info == "/":
|
||||
return False
|
||||
|
@ -47,11 +52,9 @@ class CheckShopMiddleware(object):
|
|||
config = ComfyConfig.objects.filter(active=True).first()
|
||||
if config and not config.shop_enabled and self._check_if_store_request(request):
|
||||
if settings.DEBUG:
|
||||
return HttpResponse(
|
||||
status=500, content="Store is not enabled please turn it on in admin panel"
|
||||
)
|
||||
return HttpResponse(status=500, content="Store is not enabled please turn it on in admin panel")
|
||||
else:
|
||||
return redirect('/')
|
||||
|
||||
return redirect("/")
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
|
|
@ -2,21 +2,18 @@ from django.db import models
|
|||
|
||||
|
||||
class NavbarPosition(models.TextChoices):
|
||||
TOP = 'top'
|
||||
LEFT = 'left'
|
||||
RIGHT = 'right'
|
||||
TOP = "top"
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
|
||||
|
||||
class ComfyConfig(models.Model):
|
||||
logo = models.ImageField(upload_to='images')
|
||||
navbar_position = models.CharField(
|
||||
max_length=20,
|
||||
choices=NavbarPosition.choices, default=NavbarPosition.TOP
|
||||
)
|
||||
logo = models.ImageField(upload_to="images")
|
||||
navbar_position = models.CharField(max_length=20, choices=NavbarPosition.choices, default=NavbarPosition.TOP)
|
||||
shop_enabled = models.BooleanField(default=False)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
active = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f'Comfy Config - updated: {self.updated}'
|
||||
return f"Comfy Config - updated: {self.updated}"
|
||||
|
|
|
@ -6,6 +6,4 @@ from setup.models import ComfyConfig
|
|||
class ConfigSerializers(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ComfyConfig
|
||||
fields = [
|
||||
'logo', 'navbar_position', 'shop_enabled'
|
||||
]
|
||||
fields = ["logo", "navbar_position", "shop_enabled"]
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
<h1 class="text-center">Your Config</h1>
|
||||
<div class="row mb-3 mt-5">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
Logo:
|
||||
Logo:
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<h1 class="text-center">Configure Your Comfy Basics</h1>
|
||||
<div class="row mb-3 mt-5">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3>
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
{% for field in form %}
|
||||
<div class="row mb-3 mt-5">
|
||||
<div class="col-1">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h3>
|
||||
<label for="id_logo" class="form-label">{{field.label}}</label>
|
||||
<img src = "{% static 'images/icons/question-circle.svg' %}" alt="?"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top" title="{{field.help_text}}"/>
|
||||
<img src = "{% static 'images/icons/question-circle.svg' %}" alt="?"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top" title="{{field.help_text}}"/>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
</div>
|
||||
{% block form %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
from mailings.models import MailTemplate
|
||||
from setup import models as setup_models
|
||||
from store import SHOP_ESSENTIAL_MAIL_TEMPLATES
|
||||
from mailings.models import MailTemplate
|
||||
|
||||
|
||||
TEST_IMAGE = (
|
||||
b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04'
|
||||
b'\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02'
|
||||
b'\x02\x4c\x01\x00\x3b'
|
||||
b"\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04"
|
||||
b"\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02"
|
||||
b"\x02\x4c\x01\x00\x3b"
|
||||
)
|
||||
|
||||
|
||||
class SetupTestCase(TestCase):
|
||||
def test_get_setup_first_step_get_success(self):
|
||||
response = self.client.get(reverse('setup-page'))
|
||||
response = self.client.get(reverse("setup-page"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'setup/config.html')
|
||||
|
||||
self.assertTemplateUsed(response, "setup/config.html")
|
||||
|
||||
def test_post_setup_first_step_post_success_shop_enabled(self):
|
||||
response = self.client.post(reverse('setup-page'), data={
|
||||
"logo": SimpleUploadedFile('filename.png', content=TEST_IMAGE, content_type='image/jpeg'),
|
||||
"navbar_position": setup_models.NavbarPosition.LEFT.value,
|
||||
"shop_enabled": True
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("setup-page"),
|
||||
data={
|
||||
"logo": SimpleUploadedFile("filename.png", content=TEST_IMAGE, content_type="image/jpeg"),
|
||||
"navbar_position": setup_models.NavbarPosition.LEFT.value,
|
||||
"shop_enabled": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('setup-mailings'))
|
||||
self.assertEqual(response.url, reverse("setup-mailings"))
|
||||
self.assertEqual(setup_models.ComfyConfig.objects.count(), 1)
|
||||
config = setup_models.ComfyConfig.objects.first()
|
||||
self.assertEqual(config.navbar_position, setup_models.NavbarPosition.LEFT.value)
|
||||
|
@ -36,13 +38,16 @@ class SetupTestCase(TestCase):
|
|||
self.assertFalse(config.active)
|
||||
|
||||
def test_post_setup_first_step_post_success_shop_disabled(self):
|
||||
response = self.client.post(reverse('setup-page'), data={
|
||||
"logo": SimpleUploadedFile('filename.png', content=TEST_IMAGE, content_type='image/jpeg'),
|
||||
"navbar_position": setup_models.NavbarPosition.LEFT.value,
|
||||
"shop_enabled": False
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("setup-page"),
|
||||
data={
|
||||
"logo": SimpleUploadedFile("filename.png", content=TEST_IMAGE, content_type="image/jpeg"),
|
||||
"navbar_position": setup_models.NavbarPosition.LEFT.value,
|
||||
"shop_enabled": False,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('setup-complete'))
|
||||
self.assertEqual(response.url, reverse("setup-complete"))
|
||||
self.assertEqual(setup_models.ComfyConfig.objects.count(), 1)
|
||||
config = setup_models.ComfyConfig.objects.first()
|
||||
self.assertEqual(config.navbar_position, setup_models.NavbarPosition.LEFT.value)
|
||||
|
@ -51,36 +56,36 @@ class SetupTestCase(TestCase):
|
|||
self.assertFalse(config.active)
|
||||
|
||||
def test_post_setup_first_step_post_failure(self):
|
||||
response = self.client.post(reverse('setup-page'), data={
|
||||
"logo": "",
|
||||
"navbar_position": setup_models.NavbarPosition.LEFT.value,
|
||||
"shop_enabled": True
|
||||
})
|
||||
response = self.client.post(
|
||||
reverse("setup-page"),
|
||||
data={"logo": "", "navbar_position": setup_models.NavbarPosition.LEFT.value, "shop_enabled": True},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(setup_models.ComfyConfig.objects.count(), 0)
|
||||
self.assertTemplateUsed(response, 'setup/config.html')
|
||||
self.assertFormError(response, 'form', 'logo', 'This field is required.')
|
||||
self.assertTemplateUsed(response, "setup/config.html")
|
||||
self.assertFormError(response, "form", "logo", "This field is required.")
|
||||
|
||||
def test_get_email_config_success(self):
|
||||
response = self.client.get(reverse('setup-mailings'))
|
||||
response = self.client.get(reverse("setup-mailings"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'setup/mailing.html')
|
||||
self.assertTemplateUsed(response, "setup/mailing.html")
|
||||
|
||||
def test_post_email_config_success(self):
|
||||
response = self.client.post(reverse('setup-mailings'), data={
|
||||
key: SimpleUploadedFile(
|
||||
f'{key}.html', content=b'<html></html>', content_type='text/html'
|
||||
) for key, _ in SHOP_ESSENTIAL_MAIL_TEMPLATES.items()
|
||||
}
|
||||
response = self.client.post(
|
||||
reverse("setup-mailings"),
|
||||
data={
|
||||
key: SimpleUploadedFile(f"{key}.html", content=b"<html></html>", content_type="text/html")
|
||||
for key, _ in SHOP_ESSENTIAL_MAIL_TEMPLATES.items()
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse('setup-complete'))
|
||||
self.assertEqual(response.url, reverse("setup-complete"))
|
||||
self.assertEqual(MailTemplate.objects.count(), 3)
|
||||
self.assertEqual(MailTemplate.objects.filter(template_name__in=SHOP_ESSENTIAL_MAIL_TEMPLATES.keys()).count(), 3)
|
||||
|
||||
def test_post_email_config_failure(self):
|
||||
response = self.client.post(reverse('setup-mailings'))
|
||||
response = self.client.post(reverse("setup-mailings"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(MailTemplate.objects.count(), 0)
|
||||
self.assertTemplateUsed(response, 'setup/mailing.html')
|
||||
self.assertFormError(response, 'form', None, None, 'This field is required.')
|
||||
self.assertTemplateUsed(response, "setup/mailing.html")
|
||||
self.assertFormError(response, "form", None, None, "This field is required.")
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
from django.urls import (
|
||||
path
|
||||
)
|
||||
from django.urls import path
|
||||
|
||||
from setup import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.SetupPageView.as_view(), name='setup-page'),
|
||||
path('mailings/', views.SetupMailingView.as_view(), name='setup-mailings'),
|
||||
path('complete/', views.SetupCompleteView.as_view(), name='setup-complete'),
|
||||
path("", views.SetupPageView.as_view(), name="setup-page"),
|
||||
path("mailings/", views.SetupMailingView.as_view(), name="setup-mailings"),
|
||||
path("complete/", views.SetupCompleteView.as_view(), name="setup-complete"),
|
||||
]
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
import logging
|
||||
import typing
|
||||
|
||||
from django.shortcuts import (
|
||||
render,
|
||||
redirect
|
||||
)
|
||||
from django.views import View
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import redirect, render
|
||||
from django.views import View
|
||||
|
||||
from setup.forms import (
|
||||
SiteConfigurationForm,
|
||||
MailTemplatesFileUploadForm
|
||||
)
|
||||
from setup.forms import MailTemplatesFileUploadForm, SiteConfigurationForm
|
||||
from setup.models import ComfyConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -25,9 +18,7 @@ class BaseSetupView(View):
|
|||
step = None
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"form": self.form_class()
|
||||
}
|
||||
return {"form": self.form_class()}
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
@ -37,67 +28,65 @@ class BaseSetupView(View):
|
|||
if form.is_valid():
|
||||
return form.save(), {}
|
||||
context = self.get_context_data()
|
||||
context['form'] = form
|
||||
context["form"] = form
|
||||
return None, context
|
||||
|
||||
def get_redirect(self, form_result: typing.Any=None):
|
||||
|
||||
def get_redirect(self, form_result: typing.Any = None):
|
||||
return redirect(self.next_step_view)
|
||||
|
||||
def post(self, request: HttpRequest):
|
||||
form_result, ctx = self.handle_posted_form(request)
|
||||
if form_result:
|
||||
return self.get_redirect(form_result)
|
||||
|
||||
|
||||
return render(request, self.template_name, ctx)
|
||||
|
||||
|
||||
class SetupPageView(BaseSetupView):
|
||||
template_name = 'setup/config.html'
|
||||
next_step_view = 'setup-mailings'
|
||||
template_name = "setup/config.html"
|
||||
next_step_view = "setup-mailings"
|
||||
form_class = SiteConfigurationForm
|
||||
step = 1
|
||||
|
||||
def get_redirect(self, form_result: typing.Any=None):
|
||||
def get_redirect(self, form_result: typing.Any = None):
|
||||
if form_result.shop_enabled:
|
||||
return super().get_redirect(form_result)
|
||||
return redirect('setup-complete')
|
||||
|
||||
return redirect("setup-complete")
|
||||
|
||||
def handle_posted_form(self, request: HttpRequest):
|
||||
result, ctx = super().handle_posted_form(request)
|
||||
if not result:
|
||||
return result, ctx
|
||||
|
||||
request.session['config_id'] = result.id
|
||||
|
||||
request.session["config_id"] = result.id
|
||||
return result, ctx
|
||||
|
||||
|
||||
class SetupMailingView(BaseSetupView):
|
||||
template_name = 'setup/mailing.html'
|
||||
next_step_view = 'setup-complete'
|
||||
template_name = "setup/mailing.html"
|
||||
next_step_view = "setup-complete"
|
||||
form_class = MailTemplatesFileUploadForm
|
||||
step = 2
|
||||
|
||||
|
||||
class SetupCompleteView(BaseSetupView):
|
||||
template_name = 'setup/complete.html'
|
||||
template_name = "setup/complete.html"
|
||||
step = 3
|
||||
|
||||
def _get_config(self, request: HttpRequest):
|
||||
config_id = request.session.get('config_id', None)
|
||||
config_id = request.session.get("config_id", None)
|
||||
if config_id is None:
|
||||
return redirect('setup-page')
|
||||
|
||||
return redirect("setup-page")
|
||||
|
||||
return ComfyConfig.objects.get(id=config_id)
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"config": self._get_config(self.request)
|
||||
}
|
||||
|
||||
return {"config": self._get_config(self.request)}
|
||||
|
||||
def post(self, request: HttpRequest):
|
||||
config = self._get_config(request)
|
||||
config.active = True
|
||||
config.save()
|
||||
|
||||
request.session.flush()
|
||||
return redirect('/')
|
||||
return redirect("/")
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
SHOP_ESSENTIAL_MAIL_TEMPLATES = {
|
||||
"order_created_author": "Mail template to be send to "+
|
||||
"product manufacturer after order has been created",
|
||||
"order_created_customer": "Mail template to be send to "+
|
||||
"customer after order has been created",
|
||||
"product_request": "Mail template to be send to "+
|
||||
"product manufacturer after product request has been created",
|
||||
"order_created_author": "Mail template to be send to " + "product manufacturer after order has been created",
|
||||
"order_created_customer": "Mail template to be send to " + "customer after order has been created",
|
||||
"product_request": "Mail template to be send to " + "product manufacturer after product request has been created",
|
||||
}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
from django.forms import fields
|
||||
|
||||
from wagtail.admin.forms.models import WagtailAdminModelForm
|
||||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
modeladmin_register,
|
||||
)
|
||||
from wagtail.admin.forms.models import WagtailAdminModelForm
|
||||
|
||||
from store import models
|
||||
|
||||
|
||||
class ProductAuthorAdmin(ModelAdmin):
|
||||
model = models.ProductAuthor
|
||||
list_display = ("name", )
|
||||
list_display = ("name",)
|
||||
|
||||
|
||||
class ProductCategoryAdmin(ModelAdmin):
|
||||
model = models.ProductCategory
|
||||
list_display = ("name", )
|
||||
list_display = ("name",)
|
||||
|
||||
|
||||
class ProductTemplateParamAdmin(ModelAdmin):
|
||||
|
@ -49,22 +48,22 @@ class DeliveryMethodAdmin(ModelAdmin):
|
|||
|
||||
class DocumentTemplateAdmin(ModelAdmin):
|
||||
model = models.DocumentTemplate
|
||||
list_display = ("name", )
|
||||
list_display = ("name",)
|
||||
|
||||
|
||||
class StoreAdminGroup(ModelAdminGroup):
|
||||
menu_label = "Store"
|
||||
menu_icon = 'folder-open-inverse'
|
||||
menu_icon = "folder-open-inverse"
|
||||
menu_order = 200
|
||||
items = (
|
||||
ProductAuthorAdmin,
|
||||
ProductCategoryAdmin,
|
||||
ProductAuthorAdmin,
|
||||
ProductCategoryAdmin,
|
||||
ProductTemplateParamAdmin,
|
||||
ProductTemplateAdmin,
|
||||
ProductAdmin,
|
||||
DocumentTemplateAdmin,
|
||||
PaymentMethodAdmin,
|
||||
DeliveryMethodAdmin
|
||||
DeliveryMethodAdmin,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,37 +1,25 @@
|
|||
import logging
|
||||
|
||||
from abc import (
|
||||
ABC,
|
||||
abstractmethod,
|
||||
abstractproperty
|
||||
)
|
||||
from typing import (
|
||||
List,
|
||||
Any
|
||||
)
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from dataclasses import dataclass
|
||||
from django.http.request import HttpRequest
|
||||
from typing import Any, List
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from store.models import (
|
||||
Product,
|
||||
ProductAuthor,
|
||||
DeliveryMethod
|
||||
)
|
||||
from store.models import DeliveryMethod, Product, ProductAuthor
|
||||
|
||||
logger = logging.getLogger("cart_logger")
|
||||
|
||||
|
||||
class BaseCart(ABC):
|
||||
|
||||
def validate_and_get_product(self, item_id):
|
||||
return Product.objects.get(id=item_id)
|
||||
|
||||
@abstractmethod
|
||||
def add_item(self, item_id, quantity):
|
||||
...
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def remove_item(self, item_id):
|
||||
...
|
||||
|
@ -39,14 +27,13 @@ class BaseCart(ABC):
|
|||
@abstractmethod
|
||||
def update_item_quantity(self, item_id, change):
|
||||
...
|
||||
|
||||
|
||||
@abstractproperty
|
||||
def display_items(self):
|
||||
...
|
||||
|
||||
|
||||
class SessionCart(BaseCart):
|
||||
|
||||
def _get_author_total_price(self, author_id: int):
|
||||
author_cart = self._cart[str(author_id)]
|
||||
author_price = 0
|
||||
|
@ -57,25 +44,23 @@ class SessionCart(BaseCart):
|
|||
|
||||
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]] = []
|
||||
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))
|
||||
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)
|
||||
})
|
||||
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:
|
||||
def __init__(self, request: HttpRequest, delivery: DeliveryMethod = None) -> None:
|
||||
super().__init__()
|
||||
self.session = request.session
|
||||
self._cart = self.session.get(settings.CART_SESSION_ID, None)
|
||||
|
@ -84,7 +69,7 @@ class SessionCart(BaseCart):
|
|||
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
|
||||
|
@ -113,7 +98,7 @@ class SessionCart(BaseCart):
|
|||
self.save_cart()
|
||||
except KeyError:
|
||||
logger.exception(f"Item {item_id} not found in cart")
|
||||
|
||||
|
||||
def update_item_quantity(self, item_id: int, new_quantity: int) -> None:
|
||||
product = self.validate_and_get_product(item_id)
|
||||
author = product.author
|
||||
|
@ -131,9 +116,9 @@ class SessionCart(BaseCart):
|
|||
return self._delivery_info
|
||||
|
||||
@property
|
||||
def display_items(self) -> List[dict[str, dict|str]]:
|
||||
def display_items(self) -> List[dict[str, dict | str]]:
|
||||
return self._display_items
|
||||
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
total = 0
|
||||
|
@ -144,7 +129,7 @@ class SessionCart(BaseCart):
|
|||
if self._delivery_info:
|
||||
total += self._delivery_info.price * len(self._cart.keys())
|
||||
return total
|
||||
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return not bool(self._cart.items())
|
||||
|
||||
|
@ -154,7 +139,6 @@ class SessionCart(BaseCart):
|
|||
|
||||
|
||||
class CustomerData:
|
||||
|
||||
def _encrypt_data(self, data: dict[str, Any]) -> str:
|
||||
signer = signing.Signer()
|
||||
return signer.sign_object(data)
|
||||
|
@ -163,13 +147,13 @@ class CustomerData:
|
|||
signer = signing.Signer()
|
||||
return signer.unsign_object(data)
|
||||
|
||||
def __init__(self, data: dict[str, Any]=None, encrypted_data: str=None) -> None:
|
||||
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,24 +1,20 @@
|
|||
from django import forms
|
||||
from django.db.models import Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
from django.db.models import Model
|
||||
|
||||
from store.models import (
|
||||
DeliveryMethod,
|
||||
PaymentMethod,
|
||||
Product,
|
||||
ProductTemplate,
|
||||
ProductTemplateParamValue,
|
||||
Product,
|
||||
PaymentMethod,
|
||||
DeliveryMethod
|
||||
)
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CustomerDataForm(forms.Form):
|
||||
|
||||
name = forms.CharField(
|
||||
max_length=255, label=_("Name"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
name = forms.CharField(max_length=255, label=_("Name"), widget=forms.TextInput(attrs={"class": "form-control"}))
|
||||
|
||||
surname = forms.CharField(
|
||||
max_length=255, label=_("Surname"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
|
@ -26,9 +22,7 @@ class CustomerDataForm(forms.Form):
|
|||
street = forms.CharField(
|
||||
max_length=255, label=_("Address"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
city = forms.CharField(
|
||||
max_length=255, label=_("City"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
city = forms.CharField(max_length=255, label=_("City"), widget=forms.TextInput(attrs={"class": "form-control"}))
|
||||
zip_code = forms.CharField(
|
||||
max_length=255, label=_("Zip-code"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
|
@ -39,17 +33,18 @@ class CustomerDataForm(forms.Form):
|
|||
region="PL", label=_("Phone number"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
country = forms.ChoiceField(
|
||||
choices=(("PL", _("Polska")), ), label=_("Country"),
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
||||
choices=(("PL", _("Polska")),), label=_("Country"), 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"})
|
||||
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"})
|
||||
queryset=DeliveryMethod.objects.filter(active=True),
|
||||
label="Sposób dostawy",
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
def serialize(self):
|
||||
|
@ -70,7 +65,6 @@ class ButtonToggleSelect(forms.RadioSelect):
|
|||
|
||||
|
||||
class ProductTemplateConfigForm(forms.Form):
|
||||
|
||||
def _create_dynamic_fields(self, template: ProductTemplate):
|
||||
template_params = template.template_params.all()
|
||||
for param in template_params:
|
||||
|
@ -83,10 +77,8 @@ class ProductTemplateConfigForm(forms.Form):
|
|||
queryset=queryset,
|
||||
widget=widget,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, template: ProductTemplate, *args, **kwargs
|
||||
):
|
||||
|
||||
def __init__(self, template: ProductTemplate, *args, **kwargs):
|
||||
self.template = template
|
||||
super().__init__(*args, **kwargs)
|
||||
self._create_dynamic_fields(template)
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import logging
|
||||
import time
|
||||
import requests
|
||||
import pandas as pd
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
import pandas as pd
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from store.models import (
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductTemplate,
|
||||
ProductTemplateParam,
|
||||
ProductTemplateParamValue,
|
||||
Product,
|
||||
ProductImage
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -30,7 +30,6 @@ class TemplateLoader(BaseLoader):
|
|||
|
||||
|
||||
class ProductLoader(BaseLoader):
|
||||
|
||||
def _clear(self):
|
||||
Product.objects.all().delete()
|
||||
|
||||
|
@ -43,9 +42,7 @@ class ProductLoader(BaseLoader):
|
|||
def _get_images(self, row) -> list[ContentFile]:
|
||||
url = row["images"]
|
||||
images = []
|
||||
response = requests.get(
|
||||
url+"/preview", stream=True
|
||||
)
|
||||
response = requests.get(url + "/preview", stream=True)
|
||||
if response.status_code == 200:
|
||||
data = response.content
|
||||
image = ContentFile(data, name=row["template"])
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from django.core.management import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from store.loader import ProductLoader
|
||||
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Load products from csv file"
|
||||
|
||||
|
|
|
@ -1,91 +1,115 @@
|
|||
# Generated by Django 4.1.9 on 2023-05-10 19:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
name="Product",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.FloatField()),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("price", models.FloatField()),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductAuthor',
|
||||
name="ProductAuthor",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductCategory',
|
||||
name="ProductCategory",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductCategoryParam',
|
||||
name="ProductCategoryParam",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=200)),
|
||||
('param_type', models.CharField(choices=[('int', 'Int'), ('str', 'String'), ('float', 'Float')], max_length=200)),
|
||||
('category', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_params', to='store.productcategory')),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("key", models.CharField(max_length=200)),
|
||||
(
|
||||
"param_type",
|
||||
models.CharField(choices=[("int", "Int"), ("str", "String"), ("float", "Float")], max_length=200),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="category_params",
|
||||
to="store.productcategory",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TemplateParamValue',
|
||||
name="TemplateParamValue",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(max_length=255)),
|
||||
('param', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.productcategoryparam')),
|
||||
('product', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='param_values', to='store.product')),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("value", models.CharField(max_length=255)),
|
||||
(
|
||||
"param",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.productcategoryparam"),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="param_values", to="store.product"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductTemplate',
|
||||
name="ProductTemplate",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('code', models.CharField(max_length=255)),
|
||||
('description', models.TextField()),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.productauthor')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.productcategory')),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("code", models.CharField(max_length=255)),
|
||||
("description", models.TextField()),
|
||||
("author", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.productauthor")),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.productcategory"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductImage',
|
||||
name="ProductImage",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(upload_to='')),
|
||||
('template', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='store.producttemplate')),
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("image", models.ImageField(upload_to="")),
|
||||
(
|
||||
"template",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="images", to="store.producttemplate"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='store.producttemplate'),
|
||||
model_name="product",
|
||||
name="template",
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.producttemplate"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,45 +1,70 @@
|
|||
# Generated by Django 4.1.9 on 2023-05-13 21:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('taggit', '0005_auto_20220424_2025'),
|
||||
('wagtailcore', '0083_workflowcontenttype'),
|
||||
('store', '0001_initial'),
|
||||
("taggit", "0005_auto_20220424_2025"),
|
||||
("wagtailcore", "0083_workflowcontenttype"),
|
||||
("store", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='available',
|
||||
model_name="product",
|
||||
name="available",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='producttemplate',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'),
|
||||
model_name="producttemplate",
|
||||
name="tags",
|
||||
field=taggit.managers.TaggableManager(
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='store.producttemplate'),
|
||||
model_name="product",
|
||||
name="template",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.producttemplate"
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductListPage',
|
||||
name="ProductListPage",
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
('description', wagtail.fields.RichTextField(blank=True)),
|
||||
('tags', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
|
||||
(
|
||||
"page_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="wagtailcore.page",
|
||||
),
|
||||
),
|
||||
("description", wagtail.fields.RichTextField(blank=True)),
|
||||
(
|
||||
"tags",
|
||||
taggit.managers.TaggableManager(
|
||||
blank=True,
|
||||
help_text="A comma-separated list of tags.",
|
||||
through="taggit.TaggedItem",
|
||||
to="taggit.Tag",
|
||||
verbose_name="Tags",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# 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
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Generated by Django 4.1.10 on 2023-08-15 10:44
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Generated by Django 4.1.10 on 2023-09-09 16:42
|
||||
|
||||
from django.db import migrations
|
||||
import easy_thumbnails.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -19,5 +19,5 @@ class Migration(migrations.Migration):
|
|||
model_name="producttemplateimage",
|
||||
name="image",
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(upload_to=""),
|
||||
)
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,51 +1,34 @@
|
|||
import pdfkit
|
||||
import datetime
|
||||
import builtins
|
||||
import uuid
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from typing import (
|
||||
Any,
|
||||
Iterator
|
||||
)
|
||||
from django.db import models
|
||||
from django.core.paginator import (
|
||||
Paginator,
|
||||
EmptyPage
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
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 django.dispatch import receiver
|
||||
from typing import Any, Iterator
|
||||
|
||||
from modelcluster.models import ClusterableModel
|
||||
from modelcluster.fields import ParentalKey
|
||||
from wagtail.admin.panels import (
|
||||
FieldPanel,
|
||||
InlinePanel
|
||||
)
|
||||
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
|
||||
import pdfkit
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.paginator import EmptyPage, Paginator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.template import Context, Template
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
from easy_thumbnails.signals import saved_file
|
||||
from modelcluster.fields import ParentalKey
|
||||
from modelcluster.models import ClusterableModel
|
||||
from num2words import num2words
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from taggit.managers import TaggableManager
|
||||
from wagtail import fields as wagtail_fields
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel
|
||||
from wagtail.models import Page
|
||||
|
||||
from mailings.models import (
|
||||
OutgoingEmail,
|
||||
Attachment
|
||||
)
|
||||
from mailings.models import Attachment, OutgoingEmail
|
||||
from wagtail_store.tasks import generate_thumbnails
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -58,7 +41,6 @@ class BaseImageModel(models.Model):
|
|||
|
||||
|
||||
class PersonalData(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
@ -74,7 +56,7 @@ class PersonalData(models.Model):
|
|||
@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}"
|
||||
|
@ -93,9 +75,7 @@ class ProductCategory(ClusterableModel):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
panels = [
|
||||
FieldPanel("name")
|
||||
]
|
||||
panels = [FieldPanel("name")]
|
||||
|
||||
|
||||
class ProductTemplate(ClusterableModel):
|
||||
|
@ -106,7 +86,7 @@ class ProductTemplate(ClusterableModel):
|
|||
description = models.TextField(blank=True)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
@ -120,19 +100,17 @@ class ProductTemplate(ClusterableModel):
|
|||
panels = [
|
||||
FieldPanel("category"),
|
||||
FieldPanel("author"),
|
||||
FieldPanel('title'),
|
||||
FieldPanel('code'),
|
||||
FieldPanel('description'),
|
||||
FieldPanel("title"),
|
||||
FieldPanel("code"),
|
||||
FieldPanel("description"),
|
||||
InlinePanel("template_images", label="Template Images"),
|
||||
FieldPanel("tags"),
|
||||
InlinePanel("template_params")
|
||||
InlinePanel("template_params"),
|
||||
]
|
||||
|
||||
|
||||
class ProductTemplateImage(BaseImageModel):
|
||||
template = ParentalKey(
|
||||
ProductTemplate, on_delete=models.CASCADE, related_name="template_images"
|
||||
)
|
||||
template = ParentalKey(ProductTemplate, on_delete=models.CASCADE, related_name="template_images")
|
||||
image = ThumbnailerImageField()
|
||||
is_main = models.BooleanField(default=False)
|
||||
|
||||
|
@ -150,13 +128,8 @@ class ProductTemplateParam(ClusterableModel):
|
|||
|
||||
def __str__(self):
|
||||
return self.key
|
||||
|
||||
panels = [
|
||||
FieldPanel("template"),
|
||||
FieldPanel("key"),
|
||||
FieldPanel("param_type"),
|
||||
InlinePanel("param_values")
|
||||
]
|
||||
|
||||
panels = [FieldPanel("template"), FieldPanel("key"), FieldPanel("param_type"), InlinePanel("param_values")]
|
||||
|
||||
def get_available_values(self) -> Iterator[any]:
|
||||
for elem in self.param_values.all():
|
||||
|
@ -166,7 +139,7 @@ class ProductTemplateParam(ClusterableModel):
|
|||
class ProductTemplateParamValue(ClusterableModel):
|
||||
param = ParentalKey(ProductTemplateParam, 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)
|
||||
|
@ -179,31 +152,25 @@ class ProductTemplateParamValue(ClusterableModel):
|
|||
|
||||
|
||||
class ProductManager(models.Manager):
|
||||
|
||||
def get_or_create_by_params(self, params: list[ProductTemplateParam], 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}"
|
||||
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
|
||||
)
|
||||
product = self.create(name=f"{template.title} - AUTOGENERATED", template=template, price=0, available=False)
|
||||
for param in params:
|
||||
product.params.add(param)
|
||||
|
||||
|
||||
return product
|
||||
|
||||
|
||||
|
@ -211,13 +178,15 @@ class Product(ClusterableModel):
|
|||
name = models.CharField(max_length=255, blank=True)
|
||||
template = models.ForeignKey(ProductTemplate, on_delete=models.CASCADE, related_name="products")
|
||||
params = models.ManyToManyField(
|
||||
ProductTemplateParamValue, blank=True, through="ProductParam",
|
||||
limit_choices_to=models.Q(param__template=models.F("product__template"))
|
||||
ProductTemplateParamValue,
|
||||
blank=True,
|
||||
through="ProductParam",
|
||||
limit_choices_to=models.Q(param__template=models.F("product__template")),
|
||||
)
|
||||
price = models.FloatField()
|
||||
available = models.BooleanField(default=True)
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
|
||||
|
||||
objects = ProductManager()
|
||||
|
||||
panels = [
|
||||
|
@ -256,9 +225,7 @@ class Product(ClusterableModel):
|
|||
|
||||
|
||||
class ProductImage(BaseImageModel):
|
||||
product = ParentalKey(
|
||||
"Product", on_delete=models.CASCADE, related_name="product_images"
|
||||
)
|
||||
product = ParentalKey("Product", on_delete=models.CASCADE, related_name="product_images")
|
||||
|
||||
|
||||
class ProductParam(models.Model):
|
||||
|
@ -286,10 +253,10 @@ def validate_param(sender, **kwargs):
|
|||
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)
|
||||
|
||||
|
@ -306,13 +273,13 @@ class ProductListPage(Page):
|
|||
if self.tags.all():
|
||||
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()
|
||||
if not items:
|
||||
return context
|
||||
|
||||
|
||||
paginator = Paginator(items, settings.PRODUCTS_PER_PAGE)
|
||||
page_number = request.GET.get("page", 1)
|
||||
try:
|
||||
|
@ -323,33 +290,25 @@ class ProductListPage(Page):
|
|||
context["page"] = page
|
||||
return context
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("description"),
|
||||
FieldPanel("tags")
|
||||
]
|
||||
content_panels = Page.content_panels + [FieldPanel("description"), FieldPanel("tags")]
|
||||
|
||||
|
||||
class OrderProductManager(models.Manager):
|
||||
def create_from_cart(self, items: dict[str, Product|int], order: models.Model):
|
||||
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']}"
|
||||
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"]
|
||||
).pk
|
||||
|
||||
pk = self.create(product=item["product"], order=order, quantity=item["quantity"]).pk
|
||||
pks.append(pk)
|
||||
return self.filter(pk__in=pks)
|
||||
|
||||
|
||||
class OrderProduct(models.Model):
|
||||
class OrderProduct(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
order = models.ForeignKey("Order", on_delete=models.CASCADE, related_name="products")
|
||||
quantity = models.IntegerField(validators=[MinValueValidator(1)])
|
||||
|
@ -358,58 +317,60 @@ class OrderProduct(models.Model):
|
|||
|
||||
|
||||
class OrderManager(models.Manager):
|
||||
|
||||
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 = (
|
||||
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]
|
||||
):
|
||||
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
|
||||
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 = {
|
||||
context={
|
||||
"docs": docs,
|
||||
"order_number": order.order_number,
|
||||
"customer_email": customer_data["email"],
|
||||
}, sender=settings.DEFAULT_FROM_EMAIL,
|
||||
},
|
||||
sender=settings.DEFAULT_FROM_EMAIL,
|
||||
template_name="order_created_user",
|
||||
attachments=attachments
|
||||
attachments=attachments,
|
||||
)
|
||||
# for author
|
||||
author_mail = OutgoingEmail.objects.send(
|
||||
recipient=author.email,
|
||||
subject=mail_subject,
|
||||
context = {
|
||||
context={
|
||||
"docs": docs,
|
||||
"order_number": order.order_number,
|
||||
"manufacturer_email": author.email,
|
||||
}, sender=settings.DEFAULT_FROM_EMAIL,
|
||||
},
|
||||
sender=settings.DEFAULT_FROM_EMAIL,
|
||||
template_name="order_created_author",
|
||||
attachments=attachments
|
||||
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:
|
||||
self,
|
||||
cart_items: list[dict[str, str | dict]],
|
||||
payment_method: models.Model | None,
|
||||
customer_data: dict[str, Any],
|
||||
) -> models.QuerySet:
|
||||
# split cart
|
||||
orders_pks = []
|
||||
|
||||
|
@ -422,25 +383,18 @@ class OrderManager(models.Manager):
|
|||
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)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
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}"
|
||||
)
|
||||
logger.exception(f"Error while sending emails, for order: {order.order_number}")
|
||||
|
||||
return Order.objects.filter(pk__in=orders_pks)
|
||||
|
||||
|
@ -472,7 +426,7 @@ class Order(models.Model):
|
|||
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()
|
||||
|
||||
|
@ -482,10 +436,7 @@ class Order(models.Model):
|
|||
|
||||
@property
|
||||
def total_price(self) -> Decimal:
|
||||
price = sum(
|
||||
[order_product.product.price * order_product.quantity
|
||||
for order_product in self.products.all()]
|
||||
)
|
||||
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
|
||||
|
||||
|
@ -548,7 +499,7 @@ class StorePage(Page):
|
|||
|
||||
class AllProductsListPage(Page):
|
||||
store = models.OneToOneField("store_api.Store", on_delete=models.CASCADE, related_name="all_products_list")
|
||||
|
||||
|
||||
|
||||
class GroupListPage(Page):
|
||||
store = models.ForeignKey("store_api.Store", on_delete=models.CASCADE, related_name="product_lists")
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from store.models import (
|
||||
Product,
|
||||
ProductAuthor
|
||||
)
|
||||
from store.models import Product, ProductAuthor
|
||||
|
||||
|
||||
class TagSerializer(serializers.Serializer):
|
||||
|
@ -36,7 +33,6 @@ class CartSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class CartProductAddSerializer(serializers.Serializer):
|
||||
|
||||
product_id = serializers.IntegerField()
|
||||
quantity = serializers.IntegerField()
|
||||
|
||||
|
|
|
@ -1,3 +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>
|
||||
</svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 658 B Po Szerokość: | Wysokość: | Rozmiar: 659 B |
|
@ -1,17 +1,17 @@
|
|||
|
||||
$(document).on('click', '.add-to-cart-button', function(event) {
|
||||
|
||||
|
||||
event.preventDefault();
|
||||
const button = $(this);
|
||||
const formData = new FormData();
|
||||
const productID = parseInt($(this).data('product-id'));
|
||||
const quantity = parseInt($('#quantity'+productID).val());
|
||||
const quantity = parseInt($('#quantity'+productID).val());
|
||||
const addToCartURL = $(this).data('add-to-cart-url');
|
||||
const csrfToken = $(this).data('csrf-token');
|
||||
console.log(productID);
|
||||
formData.append('product_id', productID);
|
||||
formData.append('quantity', 1); // Serialize the form data correctly
|
||||
button.prop('disabled', true);
|
||||
button.prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: addToCartURL,
|
||||
|
@ -22,7 +22,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
contentType: false, // Let the browser set the content type
|
||||
success: function(data) {
|
||||
// Show the options block
|
||||
|
||||
|
||||
//$('#addToCartModal').show();
|
||||
//createShadedOverlay()
|
||||
button.prop('disabled', false);
|
||||
|
@ -42,14 +42,14 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
optionsdiv.classList.add('unshaded-overlay');
|
||||
body.appendChild(optionsdiv);
|
||||
}
|
||||
|
||||
|
||||
function removeShadedOverlay() {
|
||||
const overlay = document.querySelector('.shaded-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const cartButton = document.getElementById('cart-button');
|
||||
const cartDropdown = document.getElementById('cart-dropdown');
|
||||
|
||||
|
@ -62,11 +62,11 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
const cartItemsList = document.getElementById('cart-items');
|
||||
const csrf_token = xcsrf_token
|
||||
cartItemsList.innerHTML = ''; // Clear existing cart items
|
||||
|
||||
|
||||
cartItems.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `Product ID: ${item.product_id}, Quantity: `;
|
||||
|
||||
|
||||
const quantityInput = document.createElement('input');
|
||||
quantityInput.type = 'number';
|
||||
quantityInput.classList.add('quantity-input');
|
||||
|
@ -76,7 +76,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
quantityInput.dataset.productId = item.product_id;
|
||||
quantityInput.dataset.csrfToken = csrf_token;
|
||||
li.appendChild(quantityInput);
|
||||
|
||||
|
||||
li.appendChild(document.createTextNode(' '));
|
||||
|
||||
const removeButton = document.createElement('a');
|
||||
|
@ -102,7 +102,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
type: 'POST',
|
||||
url: url,
|
||||
data: {"product_id": productId},
|
||||
headers: { 'X-CSRFToken': csrfToken },
|
||||
headers: { 'X-CSRFToken': csrfToken },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
alert("Item has been removed");
|
||||
|
@ -126,7 +126,7 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
formData.append('product_id', productID);
|
||||
formData.append('quantity', newQuantity);
|
||||
console.log(input.val())
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'PUT',
|
||||
url: url,
|
||||
|
@ -137,6 +137,6 @@ $(document).on('click', '.add-to-cart-button', function(event) {
|
|||
setTimeout(location.reload(), 500)
|
||||
},
|
||||
processData: false, // Prevent jQuery from processing the data
|
||||
contentType: false, // Let the browser set the content type
|
||||
contentType: false, // Let the browser set the content type
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import logging
|
||||
|
||||
import celery
|
||||
from django.conf import settings
|
||||
from easy_thumbnails.files import generate_all_aliases
|
||||
|
||||
from mailings.models import OutgoingEmail
|
||||
from store.models import Product
|
||||
from store.admin import ProductAdmin
|
||||
|
||||
from store.models import Product
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -25,10 +25,10 @@ def send_produt_request_email(variant_pk: int):
|
|||
subject="Złożono zapytanie ofertowe",
|
||||
recipient=variant.template.author.email,
|
||||
context={"product": variant, "admin_url": admin_url},
|
||||
sender=settings.DEFAULT_FROM_EMAIL
|
||||
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:
|
||||
else:
|
||||
if not send:
|
||||
logger.exception(f"Could not send email for variant pk={variant_pk}")
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<h3>{{field.label}}</h3>
|
||||
{{field}}
|
||||
</div>
|
||||
{% if forloop.counter|divisibleby:"2" %} </div><div class="row mt-5">{% endif %}
|
||||
{% if forloop.counter|divisibleby:"2" %} </div><div class="row mt-5">{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<img src="{{variant.main_image.image.url}}"
|
||||
<img src="{{variant.main_image.image.url}}"
|
||||
class="img-fluid img-thumbnail h-80" alt="Responsive image">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
|
@ -59,10 +59,10 @@
|
|||
<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 }}'
|
||||
<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>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
{% with id=widget.attrs.id %}
|
||||
<div class="btn-group btn-group-toggle" role="group">
|
||||
{% for group, options, index in widget.optgroups %}
|
||||
<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"
|
||||
<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 %}
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
{% 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>
|
||||
<h5 class="fw-normal mb-0 pr-3text-black">W sumie: {{group.group_price}} zł</h5>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-4">
|
||||
<div class="card-body p-4">
|
||||
|
@ -18,8 +18,8 @@
|
|||
</button>
|
||||
|
||||
<input id="form1" min="0" name="quantity" value="{{item.quantity}}" type="number"
|
||||
class="form-control form-control-sm quantity-input"
|
||||
data-product-id="{{item.product.id}}"
|
||||
class="form-control form-control-sm quantity-input"
|
||||
data-product-id="{{item.product.id}}"
|
||||
data-csrf-token="{{csrf_token}}"
|
||||
data-update-cart-url="{% url 'cart-action-update-product' item.product.id %}"/>
|
||||
|
||||
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="col-md-1 col-lg-1 col-xl-1 text-end">
|
||||
<a href="#!" class="text-danger remove-from-cart-button"
|
||||
data-product-id="{{item.product.id}}"
|
||||
data-product-id="{{item.product.id}}"
|
||||
data-csrf-token="{{csrf_token}}"
|
||||
data-remove-from-cart-url={% url "cart-action-remove-product" %}>
|
||||
<img src="{% static 'images/icons/trash.svg'%}" />
|
||||
|
@ -42,4 +42,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-1">
|
||||
<div class="card-body p-4">
|
||||
|
@ -22,4 +22,3 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<img src="{{ item.main_image.image|thumbnail_url:'image_60_90' }}" class="d-none d-sm-block d-md-none img-fluid rounded mx-auto" alt="{{item.title}}">
|
||||
|
||||
<img src="{{ item.main_image.image|thumbnail_url:'image_80_120' }}" class="d-none d-md-block d-lg-none img-fluid rounded mx-auto" alt="{{item.title}}">
|
||||
|
||||
|
||||
<img src="{{ item.main_image.image|thumbnail_url:'image_120_180' }}" class="d-none d-lg-block d-xl-none img-fluid rounded mx-auto" alt="{{item.title}}">
|
||||
|
||||
<img src="{{ item.main_image.image|thumbnail_url:'image_160_240' }}" class="d-none d-xl-block img-fluid rounded mx-auto" alt="{{item.title}}">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-1">
|
||||
<div class="card-body p-4">
|
||||
|
@ -22,4 +22,3 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,105 +1,98 @@
|
|||
from factory import (
|
||||
Faker,
|
||||
SubFactory,
|
||||
Factory
|
||||
)
|
||||
from factory.django import (
|
||||
FileField,
|
||||
DjangoModelFactory,
|
||||
)
|
||||
from factory import Factory, Faker, SubFactory
|
||||
from factory.django import DjangoModelFactory, FileField
|
||||
|
||||
|
||||
class ProductAuthorFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductAuthor'
|
||||
model = "store.ProductAuthor"
|
||||
|
||||
name = Faker('name')
|
||||
surname = Faker('name')
|
||||
email = Faker('email')
|
||||
phone = Faker('phone_number')
|
||||
street = Faker('street_address')
|
||||
city = Faker('city')
|
||||
zip_code = Faker('postcode')
|
||||
country = Faker('country')
|
||||
display_name = Faker('name')
|
||||
name = Faker("name")
|
||||
surname = Faker("name")
|
||||
email = Faker("email")
|
||||
phone = Faker("phone_number")
|
||||
street = Faker("street_address")
|
||||
city = Faker("city")
|
||||
zip_code = Faker("postcode")
|
||||
country = Faker("country")
|
||||
display_name = Faker("name")
|
||||
|
||||
|
||||
class ProductCategoryFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductCategory'
|
||||
model = "store.ProductCategory"
|
||||
|
||||
name = Faker('name')
|
||||
name = Faker("name")
|
||||
|
||||
|
||||
class ProductTemplateFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductTemplate'
|
||||
model = "store.ProductTemplate"
|
||||
|
||||
title = Faker('name')
|
||||
description = Faker('text')
|
||||
code = Faker('name')
|
||||
title = Faker("name")
|
||||
description = Faker("text")
|
||||
code = Faker("name")
|
||||
author = SubFactory(ProductAuthorFactory)
|
||||
category = SubFactory(ProductCategoryFactory)
|
||||
|
||||
|
||||
class ProductTemplateParamFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductTemplateParam'
|
||||
model = "store.ProductTemplateParam"
|
||||
|
||||
key = Faker('name')
|
||||
key = Faker("name")
|
||||
template = SubFactory(ProductTemplateFactory)
|
||||
param_type = 'str'
|
||||
param_type = "str"
|
||||
|
||||
|
||||
class ProductTemplateParamValueFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.ProductTemplateParamValue'
|
||||
model = "store.ProductTemplateParamValue"
|
||||
|
||||
param = SubFactory(ProductTemplateParamFactory)
|
||||
value = Faker('name')
|
||||
value = Faker("name")
|
||||
|
||||
|
||||
class ProductFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.Product'
|
||||
model = "store.Product"
|
||||
|
||||
name = Faker('name')
|
||||
price = Faker('pydecimal', left_digits=5, right_digits=2, positive=True)
|
||||
available = Faker('boolean')
|
||||
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'
|
||||
|
||||
model = "store.ProductParam"
|
||||
|
||||
product = SubFactory(ProductFactory)
|
||||
param = SubFactory(ProductTemplateParamValueFactory)
|
||||
|
||||
|
||||
class PaymentMethodFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.PaymentMethod'
|
||||
model = "store.PaymentMethod"
|
||||
|
||||
name = Faker('name')
|
||||
description = Faker('text')
|
||||
active = Faker('boolean')
|
||||
name = Faker("name")
|
||||
description = Faker("text")
|
||||
active = Faker("boolean")
|
||||
|
||||
|
||||
class OrderFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.Order'
|
||||
model = "store.Order"
|
||||
|
||||
payment_method = SubFactory(PaymentMethodFactory)
|
||||
created_at = Faker('date_time')
|
||||
updated_at = Faker('date_time')
|
||||
sent = Faker('boolean')
|
||||
created_at = Faker("date_time")
|
||||
updated_at = Faker("date_time")
|
||||
sent = Faker("boolean")
|
||||
|
||||
|
||||
class DocumentTemplateFactory(DjangoModelFactory):
|
||||
class Meta:
|
||||
model = 'store.DocumentTemplate'
|
||||
model = "store.DocumentTemplate"
|
||||
|
||||
name = Faker('name')
|
||||
name = Faker("name")
|
||||
file = FileField(filename="doc.odt")
|
||||
doc_type = "agreement"
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
from rest_framework.test import APITestCase
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from store.tests import factories
|
||||
from wagtail_store.tests import BaseComfyTestCaseMixin
|
||||
|
||||
|
||||
|
||||
class SessionCartTestCase(BaseComfyTestCaseMixin, APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.product = factories.ProductFactory(price=100)
|
||||
|
@ -20,8 +18,7 @@ class SessionCartTestCase(BaseComfyTestCaseMixin, APITestCase):
|
|||
{"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.session[settings.CART_SESSION_ID][str(self.product.author.id)][str(self.product.id)], 1
|
||||
)
|
||||
|
||||
def test_add_item_complex_success(self):
|
||||
|
@ -30,34 +27,32 @@ class SessionCartTestCase(BaseComfyTestCaseMixin, APITestCase):
|
|||
{"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.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.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,
|
||||
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
|
||||
)
|
||||
self.assertDictEqual(self.client.session[settings.CART_SESSION_ID], final_dict)
|
||||
|
||||
def test_add_item_invalid_product_id(self):
|
||||
response = self.client.post(
|
||||
|
@ -79,15 +74,14 @@ class SessionCartTestCase(BaseComfyTestCaseMixin, APITestCase):
|
|||
{"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.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"),
|
||||
|
@ -101,21 +95,16 @@ class SessionCartTestCase(BaseComfyTestCaseMixin, APITestCase):
|
|||
{"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.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
|
||||
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(
|
||||
f'en/{reverse("cart-action-update-product", kwargs={"pk": 2137})}',
|
||||
{"quantity": 5}
|
||||
)
|
||||
response = self.client.put(f'en/{reverse("cart-action-update-product", kwargs={"pk": 2137})}', {"quantity": 5})
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import pandas as pd
|
||||
from django.test import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from store.tests import factories
|
||||
import pandas as pd
|
||||
from django.test import TestCase
|
||||
|
||||
from store.loader import ProductLoader
|
||||
from store.tests import factories
|
||||
from wagtail_store.tests import BaseComfyTestCaseMixin
|
||||
|
||||
|
||||
|
@ -12,18 +13,22 @@ class TestProductLoader(BaseComfyTestCaseMixin, TestCase):
|
|||
self.category = factories.ProductCategoryFactory()
|
||||
self.template = factories.ProductTemplateFactory(category=self.category)
|
||||
self.template_params = [factories.ProductTemplateParamFactory(template=self.template) for _ in range(3)]
|
||||
self.templat_params_values = [factories.ProductTemplateParamValueFactory(param=param) for param in self.template_params]
|
||||
|
||||
self.templat_params_values = [
|
||||
factories.ProductTemplateParamValueFactory(param=param) for param in self.template_params
|
||||
]
|
||||
|
||||
def test_load_products_single_product_success(self):
|
||||
fake_df = pd.DataFrame({
|
||||
"template": [self.template.code],
|
||||
"price": str(10.0),
|
||||
"name": ["Test product"],
|
||||
"available": [True],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value
|
||||
})
|
||||
fake_df = pd.DataFrame(
|
||||
{
|
||||
"template": [self.template.code],
|
||||
"price": str(10.0),
|
||||
"name": ["Test product"],
|
||||
"available": [True],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value,
|
||||
}
|
||||
)
|
||||
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
|
||||
loader = ProductLoader("fake_path", [p.key for p in self.template_params])
|
||||
loader.process()
|
||||
|
@ -36,15 +41,17 @@ class TestProductLoader(BaseComfyTestCaseMixin, TestCase):
|
|||
|
||||
@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],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value
|
||||
})
|
||||
fake_df = pd.DataFrame(
|
||||
{
|
||||
"template": [self.template.code],
|
||||
"price": ["FASDSADQAW"],
|
||||
"name": ["Test product"],
|
||||
"available": [True],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value,
|
||||
}
|
||||
)
|
||||
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
|
||||
loader = ProductLoader("fake_path", [p.key for p in self.template_params])
|
||||
loader.process()
|
||||
|
@ -54,19 +61,20 @@ class TestProductLoader(BaseComfyTestCaseMixin, TestCase):
|
|||
|
||||
@patch("store.loader.logger")
|
||||
def test_load_no_existing_template_code_failure(self, mock_logger):
|
||||
fake_df = pd.DataFrame({
|
||||
"template": ["NOTEEXISTINGTEMPLATE"],
|
||||
"price": str(10.0),
|
||||
"name": ["Test product"],
|
||||
"available": [True],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value
|
||||
})
|
||||
fake_df = pd.DataFrame(
|
||||
{
|
||||
"template": ["NOTEEXISTINGTEMPLATE"],
|
||||
"price": str(10.0),
|
||||
"name": ["Test product"],
|
||||
"available": [True],
|
||||
self.template_params[0].key: self.templat_params_values[0].value,
|
||||
self.template_params[1].key: self.templat_params_values[1].value,
|
||||
self.template_params[2].key: self.templat_params_values[2].value,
|
||||
}
|
||||
)
|
||||
with patch("store.loader.BaseLoader.load_data", return_value=fake_df):
|
||||
loader = ProductLoader("fake_path", [p.key for p in self.template_params])
|
||||
loader.process()
|
||||
|
||||
|
||||
self.assertEqual(self.template.products.count(), 0)
|
||||
mock_logger.exception.assert_called_with("ProductTemplate matching query does not exist.")
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
|
||||
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 django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from store.tests import factories
|
||||
from store import models as store_models
|
||||
from mailings.tests.factories import MailTemplateFactory
|
||||
from store import models as store_models
|
||||
from store.tests import factories
|
||||
from wagtail_store.tests import BaseComfyTestCaseMixin
|
||||
|
||||
|
||||
|
@ -17,12 +17,8 @@ class ProductCategoryParamTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
super().setUp()
|
||||
self.category = factories.ProductCategoryFactory()
|
||||
self.template = factories.ProductTemplateFactory(category=self.category)
|
||||
self.param = factories.ProductTemplateParamFactory(
|
||||
template=self.template,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
|
||||
self.param = factories.ProductTemplateParamFactory(template=self.template, 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, [])
|
||||
|
@ -47,38 +43,24 @@ class ProductTemplateParamValueTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
super().setUp()
|
||||
self.category = factories.ProductCategoryFactory()
|
||||
self.template = factories.ProductTemplateFactory(category=self.category)
|
||||
|
||||
|
||||
def test_get_value_success(self):
|
||||
param = factories.ProductTemplateParamFactory(
|
||||
template=self.template,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param = factories.ProductTemplateParamFactory(template=self.template, param_type="int", key="test_param")
|
||||
param_value = factories.ProductTemplateParamValueFactory(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.ProductTemplateParamFactory(
|
||||
template=self.template,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param = factories.ProductTemplateParamFactory(template=self.template, param_type="int", key="test_param")
|
||||
param_value = factories.ProductTemplateParamValueFactory(param=param, value="wrong_value")
|
||||
proper_value = param_value.get_value()
|
||||
self.assertEqual(proper_value, None)
|
||||
|
||||
|
||||
class ProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
||||
|
||||
def test_template_params_one_value_success(self):
|
||||
product = factories.ProductFactory()
|
||||
param = factories.ProductTemplateParamFactory(
|
||||
template=product.template,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param = factories.ProductTemplateParamFactory(template=product.template, param_type="int", key="test_param")
|
||||
param_value = factories.ProductTemplateParamValueFactory(param=param, value="23")
|
||||
with transaction.atomic():
|
||||
product.params.add(param_value)
|
||||
|
@ -88,11 +70,7 @@ class ProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
|
||||
def test_template_params_multiple_values_failure(self):
|
||||
product = factories.ProductFactory()
|
||||
param = factories.ProductTemplateParamFactory(
|
||||
template=product.template,
|
||||
param_type="int",
|
||||
key="test_param"
|
||||
)
|
||||
param = factories.ProductTemplateParamFactory(template=product.template, param_type="int", key="test_param")
|
||||
param_value = factories.ProductTemplateParamValueFactory(param=param, value="23")
|
||||
sec_param_value = factories.ProductTemplateParamValueFactory(param=param, value="24")
|
||||
with self.assertRaises(ValidationError):
|
||||
|
@ -109,7 +87,8 @@ class ProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
product.params.add(value2)
|
||||
product.save()
|
||||
prod = store_models.Product.objects.get_or_create_by_params(
|
||||
params=[value1, value2], template=product.template,
|
||||
params=[value1, value2],
|
||||
template=product.template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertEqual(prod.pk, product.pk)
|
||||
|
@ -122,9 +101,10 @@ class ProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
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,
|
||||
params=[value1, value2],
|
||||
template=product.template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertNotEqual(prod.pk, product.pk)
|
||||
|
@ -135,9 +115,10 @@ class ProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
template = factories.ProductTemplateFactory()
|
||||
value1 = factories.ProductTemplateParamValueFactory()
|
||||
value2 = factories.ProductTemplateParamValueFactory()
|
||||
|
||||
|
||||
prod = store_models.Product.objects.get_or_create_by_params(
|
||||
params=[value1, value2], template=template,
|
||||
params=[value1, value2],
|
||||
template=template,
|
||||
)
|
||||
self.assertIsNotNone(prod)
|
||||
self.assertFalse(prod.available)
|
||||
|
@ -152,37 +133,27 @@ class OrderProductTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
self.product = factories.ProductFactory(template__author=self.author, price=100)
|
||||
self.second_product = factories.ProductFactory(template__author=self.author, price=200)
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
)
|
||||
products = store_models.OrderProduct.objects.create_from_cart(items=[], order=self.order)
|
||||
self.assertEqual(products.count(), 0)
|
||||
|
||||
|
||||
|
@ -200,7 +171,6 @@ class OrderTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
"postal_code": "",
|
||||
"city": "",
|
||||
"country": "",
|
||||
|
||||
}
|
||||
self.payment_method = factories.PaymentMethodFactory()
|
||||
factories.DocumentTemplateFactory()
|
||||
|
@ -211,46 +181,34 @@ class OrderTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
@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}]
|
||||
}]
|
||||
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
|
||||
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')}"
|
||||
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}]
|
||||
}
|
||||
{"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
|
||||
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')}"
|
||||
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')}"
|
||||
mail.outbox[2].subject,
|
||||
f"Wygenerowano umowę numer {orders[1].order_number} z dnia {orders[1].created_at.strftime('%d.%m.%Y')}",
|
||||
)
|
||||
|
|
|
@ -1,43 +1,38 @@
|
|||
from django.test import TestCase
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from store.models import (
|
||||
ProductTemplateParam,
|
||||
ProductTemplateParamValue,
|
||||
TemplateParamValueChoices
|
||||
TemplateParamValueChoices,
|
||||
)
|
||||
from store.tests.factories import (
|
||||
ProductTemplateFactory,
|
||||
ProductCategoryFactory,
|
||||
ProductFactory,
|
||||
ProductTemplateParamValueFactory
|
||||
ProductTemplateFactory,
|
||||
ProductTemplateParamValueFactory,
|
||||
)
|
||||
from wagtail_store.tests import BaseComfyTestCaseMixin
|
||||
|
||||
|
||||
class ConfigureProductViewTestCase(BaseComfyTestCaseMixin, 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 = ProductTemplateParam.objects.create(
|
||||
key="Mocowanie", template=self.product_template,
|
||||
param_type=TemplateParamValueChoices.STRING
|
||||
key="Mocowanie", template=self.product_template, param_type=TemplateParamValueChoices.STRING
|
||||
)
|
||||
self.param1_value1 = ProductTemplateParamValueFactory(param=self.param1)
|
||||
self.param1_value2 = ProductTemplateParamValueFactory(param=self.param1)
|
||||
self.param2 = ProductTemplateParam.objects.create(
|
||||
key="Format", template=self.product_template,
|
||||
param_type=TemplateParamValueChoices.STRING
|
||||
key="Format", template=self.product_template, param_type=TemplateParamValueChoices.STRING
|
||||
)
|
||||
self.param2_value1 = ProductTemplateParamValueFactory(param=self.param2)
|
||||
self.param2_value2 = ProductTemplateParamValueFactory(param=self.param2)
|
||||
# create product variant
|
||||
self.variant1 = ProductFactory(
|
||||
template=self.product_template
|
||||
)
|
||||
self.variant1 = ProductFactory(template=self.product_template)
|
||||
self.variant1.params.set([self.param1_value1, self.param2_value1])
|
||||
self.variant1.save()
|
||||
|
||||
|
@ -61,37 +56,19 @@ class ConfigureProductViewTestCase(BaseComfyTestCaseMixin, TestCase):
|
|||
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
|
||||
)
|
||||
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(
|
||||
f"en/{reverse('product-configure', args=[123123])}",
|
||||
data=data
|
||||
)
|
||||
data = {self.param1.key: [str(self.param1_value1.pk)], self.param2.key: [str(self.param2_value1.pk)]}
|
||||
response = self.client.post(f"en/{reverse('product-configure', args=[123123])}", 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
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -3,16 +3,19 @@ from rest_framework.routers import DefaultRouter
|
|||
|
||||
from store import views as store_views
|
||||
|
||||
|
||||
router = DefaultRouter()
|
||||
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("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/success/", store_views.OrderSuccessView.as_view(), name="order-success")
|
||||
path("order/success/", store_views.OrderSuccessView.as_view(), name="order-success"),
|
||||
] + router.urls
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
from typing import Any
|
||||
from django.core.mail import EmailMessage
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
def send_mail(
|
||||
to: list[str], docs: Any, order_number: str,
|
||||
subject: str, body: str
|
||||
):
|
||||
message = EmailMessage(
|
||||
subject=subject,
|
||||
body=body,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=to
|
||||
)
|
||||
def send_mail(to: list[str], docs: Any, order_number: str, subject: str, body: str):
|
||||
message = EmailMessage(subject=subject, body=body, from_email=settings.DEFAULT_FROM_EMAIL, to=to)
|
||||
for doc in docs:
|
||||
message.attach(f"{order_number}.pdf", doc, "application/pdf")
|
||||
return bool(message.send())
|
||||
|
@ -25,7 +18,7 @@ def notify_user_about_order(customer_email, docs, order_number):
|
|||
docs=docs,
|
||||
order_number=order_number,
|
||||
subject=f"Zamówienie {order_number}",
|
||||
body="Dokumenty dla Twojego zamówienia"
|
||||
body="Dokumenty dla Twojego zamówienia",
|
||||
)
|
||||
|
||||
|
||||
|
@ -35,5 +28,5 @@ def notify_manufacturer_about_order(manufacturer_email, docs, order_number):
|
|||
docs=docs,
|
||||
order_number=order_number,
|
||||
subject=f"Złożono zamówienie {order_number}",
|
||||
body="Dokumenty dla złożonego zamówienia"
|
||||
)
|
||||
body="Dokumenty dla złożonego zamówienia",
|
||||
)
|
||||
|
|
|
@ -1,49 +1,29 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from django.views.generic import (
|
||||
TemplateView,
|
||||
View
|
||||
)
|
||||
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 django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import TemplateView, View
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from store.cart import CustomerData, SessionCart
|
||||
from store.forms import CustomerDataForm, ProductTemplateConfigForm
|
||||
from store.models import Order, Product, ProductListPage, ProductTemplate
|
||||
from store.serializers import CartProductAddSerializer, CartSerializer
|
||||
from store.tasks import send_produt_request_email
|
||||
from store.cart import (
|
||||
SessionCart,
|
||||
CustomerData
|
||||
)
|
||||
from store.serializers import (
|
||||
CartSerializer,
|
||||
CartProductAddSerializer
|
||||
)
|
||||
from store.forms import (
|
||||
CustomerDataForm,
|
||||
ProductTemplateConfigForm
|
||||
)
|
||||
from store.models import (
|
||||
Order,
|
||||
Product,
|
||||
ProductTemplate,
|
||||
ProductListPage
|
||||
)
|
||||
|
||||
|
||||
class CartView(TemplateView):
|
||||
"""
|
||||
This view should simply render cart with initial data, it'll do that each refresh, for
|
||||
making actions on cart (using jquery) we will use CartActionView, which will
|
||||
be prepared to return JsonResponse.
|
||||
This view should simply render cart with initial data, it'll do that each refresh, for
|
||||
making actions on cart (using jquery) we will use CartActionView, which will
|
||||
be prepared to return JsonResponse.
|
||||
"""
|
||||
template_name = 'store/cart.html'
|
||||
|
||||
template_name = "store/cart.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -52,7 +32,6 @@ class CartView(TemplateView):
|
|||
|
||||
|
||||
class CartActionView(ViewSet):
|
||||
|
||||
# NOTE - currently not in use
|
||||
@action(detail=False, methods=["get"], url_path="list-products")
|
||||
def list_products(self, request):
|
||||
|
@ -61,7 +40,7 @@ class CartActionView(ViewSet):
|
|||
items = cart.display_items
|
||||
serializer = CartSerializer(instance=items, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
def add_product(self, request):
|
||||
cart = SessionCart(self.request)
|
||||
|
@ -72,7 +51,7 @@ class CartActionView(ViewSet):
|
|||
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)
|
||||
|
@ -104,10 +83,7 @@ class ConfigureProductView(View):
|
|||
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
|
||||
}
|
||||
context = {"template": template, "form": form}
|
||||
return context
|
||||
|
||||
def get(self, request, pk: int, *args, **kwargs):
|
||||
|
@ -122,19 +98,20 @@ class ConfigureProductView(View):
|
|||
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()
|
||||
"store_url": ProductListPage.objects.first().get_url(),
|
||||
}
|
||||
|
||||
def get(self, request, variant_pk: int, *args, **kwargs):
|
||||
|
@ -170,7 +147,7 @@ class OrderView(View):
|
|||
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():
|
||||
context = self.get_context_data()
|
||||
|
@ -185,19 +162,14 @@ class OrderConfirmView(View):
|
|||
template_name = "store/order_confirm.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
|
||||
form = CustomerDataForm(
|
||||
data=CustomerData(
|
||||
encrypted_data=self.request.session["customer_data"]
|
||||
).decrypted_data
|
||||
)
|
||||
form = CustomerDataForm(data=CustomerData(encrypted_data=self.request.session["customer_data"]).decrypted_data)
|
||||
if not form.is_valid():
|
||||
raise Exception("Customer data is not valid")
|
||||
|
||||
|
||||
customer_data = form.cleaned_data
|
||||
return {
|
||||
"cart": SessionCart(self.request, delivery=customer_data["delivery_method"]),
|
||||
"customer_data": customer_data
|
||||
"customer_data": customer_data,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -208,14 +180,9 @@ class OrderConfirmView(View):
|
|||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def post(self, request):
|
||||
customer_data = CustomerData(
|
||||
encrypted_data=self.request.session["customer_data"]
|
||||
).decrypted_data
|
||||
customer_data = CustomerData(encrypted_data=self.request.session["customer_data"]).decrypted_data
|
||||
cart = SessionCart(self.request)
|
||||
order = Order.objects.create_from_cart(
|
||||
cart.display_items,
|
||||
None, customer_data
|
||||
)
|
||||
order = Order.objects.create_from_cart(cart.display_items, None, customer_data)
|
||||
request.session.pop("customer_data")
|
||||
cart.clear()
|
||||
request.session["order_uuids"] = [str(elem) for elem in order.values_list("uuid", flat=True)]
|
||||
|
@ -228,11 +195,11 @@ class OrderSuccessView(View):
|
|||
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()
|
||||
"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())
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from wagtail.models import Page
|
||||
from django.db import models
|
||||
from django.dispatch import receiver
|
||||
from easy_thumbnails.signals import saved_file
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
from wagtail.models import Page
|
||||
|
||||
from wagtail_store.tasks import generate_thumbnails
|
||||
|
||||
|
||||
class PersonalData(models.Model):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
@ -24,7 +24,7 @@ class PersonalData(models.Model):
|
|||
@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}"
|
||||
|
@ -35,23 +35,20 @@ class StoreUser(PersonalData, models.Model):
|
|||
|
||||
|
||||
class StoreMembership(models.Model):
|
||||
store = models.ForeignKey('Store', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey('StoreUser', on_delete=models.CASCADE)
|
||||
store = models.ForeignKey("Store", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey("StoreUser", on_delete=models.CASCADE)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
|
||||
class Store(models.Model):
|
||||
owner = models.ForeignKey(StoreUser, on_delete=models.CASCADE)
|
||||
members = models.ManyToManyField(
|
||||
StoreUser, related_name='stores', blank=True, through=StoreMembership
|
||||
)
|
||||
members = models.ManyToManyField(StoreUser, related_name="stores", blank=True, through=StoreMembership)
|
||||
|
||||
|
||||
class ProductGroup(models.Model):
|
||||
|
||||
store = models.ForeignKey(Store, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
|
||||
|
||||
# Products
|
||||
class Product(models.Model):
|
||||
|
@ -65,12 +62,12 @@ class Product(models.Model):
|
|||
|
||||
|
||||
class ProductImage(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
|
||||
image = models.ImageField(upload_to='products', blank=True)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="images")
|
||||
image = models.ImageField(upload_to="products", blank=True)
|
||||
|
||||
|
||||
class ProductVariant(models.Model):
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='variants')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="variants")
|
||||
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
sku = models.CharField(max_length=255, blank=True)
|
||||
|
@ -85,7 +82,4 @@ class ProductVariant(models.Model):
|
|||
|
||||
@receiver(saved_file)
|
||||
def generate_thumbnails_async(sender, fieldfile, **kwargs):
|
||||
generate_thumbnails.delay(
|
||||
model=sender, pk=fieldfile.instance.pk,
|
||||
field=fieldfile.field.name
|
||||
)
|
||||
generate_thumbnails.delay(model=sender, pk=fieldfile.instance.pk, field=fieldfile.field.name)
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
from django.db import models
|
||||
from wagtail.models import Page
|
||||
from wagtail import fields as wagtail_fields
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from taggit.managers import TaggableManager
|
||||
from wagtail import fields as wagtail_fields
|
||||
from wagtail.models import Page
|
||||
|
||||
from store_api.models import Product
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class PaginationMixin:
|
||||
def get_paginator(self):
|
||||
return LimitOffsetPagination()
|
||||
|
||||
def paginate(self, queryset):
|
||||
paginator = self.get_paginator()
|
||||
return paginator.paginate_queryset(queryset, self.request)
|
||||
|
||||
|
||||
class StorePage(Page):
|
||||
|
@ -13,16 +21,23 @@ class StorePage(Page):
|
|||
description = wagtail_fields.RichTextField(blank=True)
|
||||
tags = TaggableManager(blank=True)
|
||||
|
||||
|
||||
class AllProductsListPage(Page):
|
||||
store = models.OneToOneField("store_api.Store", on_delete=models.CASCADE, related_name="all_products_list")
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context["items"] = Product.objects.filter(store=self.store)
|
||||
# FIXME getting all products may cause website to crash
|
||||
# context["products"] = Product.objects.filter(store=self.store)
|
||||
context["groups"] = self.store.product_groups.all()
|
||||
return context
|
||||
|
||||
|
||||
class GroupListPage(Page):
|
||||
class AllProductsListPage(PaginationMixin, Page):
|
||||
store = models.OneToOneField("store_api.Store", on_delete=models.CASCADE, related_name="all_products_list")
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context["products"] = self.paginate(Product.objects.filter(store=self.store))
|
||||
return context
|
||||
|
||||
|
||||
class GroupListPage(PaginationMixin, Page):
|
||||
store = models.ForeignKey("store_api.Store", on_delete=models.CASCADE, related_name="product_lists")
|
||||
group = models.OneToOneField("store_api.ProductGroup", on_delete=models.CASCADE, related_name="page")
|
||||
|
|
|
@ -11,4 +11,4 @@ processes = 5
|
|||
|
||||
socket = %(socket_dir)/%(project).sock
|
||||
vacuum = true
|
||||
daemonize = /var/log/uwsgi/project.log
|
||||
daemonize = /var/log/uwsgi/project.log
|
||||
|
|
|
@ -19,15 +19,15 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
|
|||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
# -> GlitchTip error reporting
|
||||
# -> GlitchTip error reporting
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get("SENTRY_DSN", ''),
|
||||
dsn=os.environ.get("SENTRY_DSN", ""),
|
||||
integrations=[DjangoIntegration()],
|
||||
auto_session_tracking=False,
|
||||
traces_sample_rate=0
|
||||
)
|
||||
traces_sample_rate=0,
|
||||
)
|
||||
|
||||
SENTRY_ENVIRONMENT = os.environ.get("SENTRY_ENVIRONMENT", '')
|
||||
SENTRY_ENVIRONMENT = os.environ.get("SENTRY_ENVIRONMENT", "")
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
|
@ -58,7 +58,7 @@ INSTALLED_APPS = [
|
|||
"wagtail.images",
|
||||
"wagtail.search",
|
||||
"wagtail.admin",
|
||||
'wagtail.contrib.modeladmin',
|
||||
"wagtail.contrib.modeladmin",
|
||||
"wagtail.contrib.settings",
|
||||
"wagtail",
|
||||
"wagtailmenus",
|
||||
|
@ -74,7 +74,7 @@ INSTALLED_APPS = [
|
|||
"phonenumber_field",
|
||||
"django_celery_results",
|
||||
"django_celery_beat",
|
||||
"easy_thumbnails"
|
||||
"easy_thumbnails",
|
||||
]
|
||||
|
||||
|
||||
|
@ -88,8 +88,8 @@ MIDDLEWARE = [
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware"
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "wagtail_store.urls"
|
||||
|
@ -103,13 +103,13 @@ TEMPLATES = [
|
|||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
'setup.context_processors.config_context_processor',
|
||||
"setup.context_processors.config_context_processor",
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
'django.template.context_processors.i18n',
|
||||
"django.template.context_processors.i18n",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
'wagtailmenus.context_processors.wagtailmenus',
|
||||
"wagtailmenus.context_processors.wagtailmenus",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -122,11 +122,7 @@ WSGI_APPLICATION = "wagtail_store.wsgi.application"
|
|||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
import dj_database_url as db_url
|
||||
|
||||
DATABASES = {
|
||||
"default": db_url.parse(
|
||||
os.environ.get("DATABASE_URL", "postgres://comfy:password@db/comfy_shop")
|
||||
)
|
||||
}
|
||||
DATABASES = {"default": db_url.parse(os.environ.get("DATABASE_URL", "postgres://comfy:password@db/comfy_shop"))}
|
||||
|
||||
|
||||
# Password validation
|
||||
|
@ -164,12 +160,12 @@ USE_TZ = True
|
|||
WAGTAIL_I18N_ENABLED = True
|
||||
|
||||
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
|
||||
('en', "English"),
|
||||
('pl', "Polish"),
|
||||
]
|
||||
("en", "English"),
|
||||
("pl", "Polish"),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
os.path.join(BASE_DIR, "locale"),
|
||||
]
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
@ -229,16 +225,16 @@ MESSAGE_TAGS = {
|
|||
PRODUCTS_PER_PAGE = 6
|
||||
|
||||
# CART settings
|
||||
CART_SESSION_ID = 'cart'
|
||||
CART_SESSION_ID = "cart"
|
||||
|
||||
# EMAIL settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
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)
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
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)
|
||||
EMAIL_USE_TLS = True
|
||||
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'wagtail_store-sklep@tepewu.pl')
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "wagtail_store-sklep@tepewu.pl")
|
||||
|
||||
# CELERY settings
|
||||
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND")
|
||||
|
@ -247,20 +243,22 @@ CELERY_TIMEZONE = os.environ.get("CELERY_TIMEZONE")
|
|||
CELERY_TASK_TRACK_STARTED = os.environ.get("CELERY_TASK_TRACK_STARTED")
|
||||
CELERY_TASK_TIME_LIMIT = os.environ.get("CELERY_TASK_TIME_LIMIT")
|
||||
# CELERY_RESULT_BACKEND_DB = f'db+mysql+pymysql://{os.environ.get("MYSQL_USER")}:{os.environ.get("MYSQL_PASSWORD")}@db/{os.environ.get("MYSQL_DATABASE")}'
|
||||
CELERY_BROKER_URL = f'amqp://{os.environ.get("RABBITMQ_DEFAULT_USER")}:{os.environ.get("RABBITMQ_DEFAULT_PASS")}@rabbit//'
|
||||
CELERY_BROKER_URL = (
|
||||
f'amqp://{os.environ.get("RABBITMQ_DEFAULT_USER")}:{os.environ.get("RABBITMQ_DEFAULT_PASS")}@rabbit//'
|
||||
)
|
||||
CELERY_TASK_RESULT_EXPIRES = os.environ.get("CELERY_TASK_RESULT_EXPIRES")
|
||||
CELERY_ACCEPT_CONTENT = ['pickle'] #add this to your env
|
||||
CELERY_ACCEPT_CONTENT = ["pickle"] # add this to your env
|
||||
|
||||
# EASY_THUMBNAILS settings
|
||||
|
||||
THUMBNAIL_DEFAULT_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
THUMBNAIL_DEFAULT_STORAGE = "django.core.files.storage.FileSystemStorage"
|
||||
THUMBNAIL_ALIASES = {
|
||||
'': {
|
||||
'image_40_60': {'size': (40, 60), 'crop': True},
|
||||
'image_60_90': {'size': (60, 90), 'crop': True},
|
||||
'image_80_120': {'size': (80, 120), 'crop': True},
|
||||
'image_120_180': {'size': (120, 180), 'crop': True},
|
||||
'image_160_240': {'size': (160, 240), 'crop': True},
|
||||
"": {
|
||||
"image_40_60": {"size": (40, 60), "crop": True},
|
||||
"image_60_90": {"size": (60, 90), "crop": True},
|
||||
"image_80_120": {"size": (80, 120), "crop": True},
|
||||
"image_120_180": {"size": (120, 180), "crop": True},
|
||||
"image_160_240": {"size": (160, 240), "crop": True},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -5048,4 +5048,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-grid.css.map */
|
||||
/*# sourceMappingURL=bootstrap-grid.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5047,4 +5047,4 @@
|
|||
display: none !important;
|
||||
}
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-grid.rtl.css.map */
|
||||
/*# sourceMappingURL=bootstrap-grid.rtl.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -482,4 +482,4 @@ progress {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -479,4 +479,4 @@ progress {
|
|||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4863,4 +4863,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-utilities.css.map */
|
||||
/*# sourceMappingURL=bootstrap-utilities.css.map */
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue