Formatted all files

feature/code_formatting
mtyton 2024-01-24 18:49:48 +01:00
rodzic 2fc4a7dd4f
commit 875dfe957d
133 zmienionych plików z 1098 dodań i 1424 usunięć

156
.gitignore vendored
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -8,4 +8,4 @@
<div>
{{ page.body|richtext }}
</div>
{% endblock %}
{% endblock %}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,5 +1,5 @@
from factory.django import DjangoModelFactory
from factory import Faker
from factory.django import DjangoModelFactory
class MailTemplateFactory(DjangoModelFactory):

Wyświetl plik

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

Wyświetl plik

@ -2,8 +2,7 @@
max-line-length = 120
[tool.black]
line-length = 119
line-length = 120
[tool.isort]
profile = "black"
multi_line_output = 3

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -9,6 +9,6 @@
</div>
{% block form %}
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% endblock %}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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