Porównaj commity
46 Commity
Autor | SHA1 | Data |
---|---|---|
mtyton | f8f3d35935 | |
mtyton | 6bc385e47d | |
mtyton | 345dbe3f46 | |
mtyton | a5e243c55c | |
mtyton | 1ab8537c64 | |
mtyton | d365a1ee98 | |
mtyton | 19078bd03f | |
mtyton | dd0998b96b | |
mtyton | b07561d34c | |
mtyton | 80ddd65ea6 | |
mtyton | c762438439 | |
mtyton | d98f1a42d2 | |
mtyton | f6f7878963 | |
mtyton | cc87725603 | |
mtyton | 46645bde17 | |
mtyton | b149b85f53 | |
mtyton | 2ab84212b6 | |
mtyton | dc25fa01a7 | |
mtyton | f509df6a9d | |
mtyton | 532180c605 | |
mtyton | cfee29d09d | |
KarolG | 22ce13904e | |
mtyton | 9bc46af370 | |
mtyton | 45a9918bda | |
mtyton | 0b99786301 | |
mtyton | a004ae4037 | |
mtyton | ec86deb2f2 | |
mtyton | 690af02153 | |
mtyton | b4edf01685 | |
mtyton | aae4396368 | |
mtyton | 7f4c00547f | |
mtyton | 75469ace5e | |
mtyton | d0d77956ad | |
mtyton | d66379c440 | |
gitesiek | b5165607f6 | |
mtyton | e000b5aa45 | |
mtyton | 074281257b | |
gitesiek | 0355d842d9 | |
gitesiek | e811abacb5 | |
mtyton | fdcd849a85 | |
mtyton | 6bdedb343d | |
mtyton | 315a80f66b | |
KarolG | 2e68e3ed6f | |
KarolG | 064c4cfa50 | |
KarolG | 750f709279 | |
KarolG | 2e80c0f9b3 |
|
@ -139,8 +139,8 @@ GitHub.sublime-settings
|
|||
#postgres pass files
|
||||
*.my_pgpass
|
||||
*.sql
|
||||
artel/static/
|
||||
wagtail_store/static/
|
||||
|
||||
# media
|
||||
artel/media/*
|
||||
artel/store/data/*
|
||||
wagtail_store/media/*
|
||||
wagtail_store/store/data/*
|
||||
|
|
53
README.md
53
README.md
|
@ -1 +1,52 @@
|
|||
# comfy-szop
|
||||
# ComfyShop
|
||||
|
||||
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)
|
||||
[![Release Version](https://img.shields.io/badge/Release%20Version-v0.2-blue)](https://forge.citizen4.eu/mtyton/comfy/releases/tag/0.2.0)
|
||||
## Requirements
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository using the following command:
|
||||
```git clone https://forge.citizen4.eu/mtyton/comfy.git```
|
||||
3. Build the Docker image:
|
||||
```docker-compose build```
|
||||
4. Run the Docker container for development:
|
||||
```docker-compose up```
|
||||
|
||||
|
||||
For production, use the following command to run the Docker container:
|
||||
```docker-compose -f docker-compose-prod.yml up -d```
|
||||
|
||||
|
||||
4. Access the application at [http://127.0.0.1:8001/](http://127.0.0.1:8001/)
|
||||
|
||||
## Usage
|
||||
|
||||
- Visit the home page to view the blog posts and shop items.
|
||||
|
||||
## Customization
|
||||
|
||||
- Access the admin panel at [http://127.0.0.1:8001/admin/](http://127.0.0.1:8001/admin/)
|
||||
- Log in with the superuser credentials (you can create a superuser within the Docker container if not already done).
|
||||
- Create and manage blog posts and products through the admin panel.
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to contribute to this project by opening issues or creating pull requests. Please follow the contribution guidelines and review the [CONTRIBUTING.md](CONTRIBUTING.md) file for details.
|
||||
|
||||
## Documentation
|
||||
|
||||
For more detailed information on how to use and customize the application, refer to the [documentation](https://forge.citizen4.eu/mtyton/comfy/wiki).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
|
@ -1,28 +0,0 @@
|
|||
{% load static %}
|
||||
{% load menu_tags %}
|
||||
|
||||
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3 mr-5">
|
||||
<img src="{{logo}}" class="img-fluid rounded mx-auto d-block mt-3" style="width: 10rem; height: 10 rem;" alt="Portal Logo"/>
|
||||
<hr>
|
||||
<ul class="nav navbar-nav">
|
||||
{% for item in menu_items %}
|
||||
<li class="{{ item.active_class }}">
|
||||
<a href="{{ item.href }}">{{ item.text }}</a>
|
||||
{% if item.has_children_in_menu %}
|
||||
<button class="btn btn-toggle" data-bs-target="#ddtoggle_{{ item.link_page.pk }}" data-bs-toggle="collapse"
|
||||
aria-expanded={% if item.active_class %}"true" {% else %} "false" {% endif %}
|
||||
aria-controls="#ddtoggle_{{ item.link_page.pk }}">
|
||||
<img src = "{% static 'images/icons/caret-down.svg' %}" alt="∨"/> </button>
|
||||
{% sub_menu item template="menu/custom_submenu.html" %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3 mr-5">
|
||||
<hr>
|
||||
{% if shop_enabled %}
|
||||
<a href={% url 'cart' %} alt="Koszyk" > Koszyk </a>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
{% load menu_tags %}
|
||||
<ul class={% if item.active_class %} "sub-menu collapse show fw-normal pb-1 small" {% else %} "sub-menu collapse fw-normal pb-1 small" {% endif %}
|
||||
id="ddtoggle_{{ item.link_page.pk }}">
|
||||
{% for sub_item in menu_items %}
|
||||
<li><a href="{{ sub_item.href }}" class="{{ sub_item.active_class }}">{{ sub_item.text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -1,57 +0,0 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_ROOT_PASSWORD
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_DB
|
||||
volumes:
|
||||
- ../postgres/:/var/lib/postgresql
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
comfy:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: ./
|
||||
user: "${UID}:${GID}"
|
||||
restart: always
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
environment:
|
||||
- SECRET_KEY
|
||||
- DATABASE_URL
|
||||
- DJANGO_SETTINGS_MODULE
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
web:
|
||||
image: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ../nginx/conf.d/:/etc/nginx/conf.d/
|
||||
- ./static/:/opt/services/comfy/static
|
||||
- ./media/:/opt/services/comfy/media
|
||||
ports:
|
||||
- "8000:80"
|
||||
environment:
|
||||
- NGINX_HOST=artel.tepewu.pl
|
||||
- NGINX_PORT=80
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
networks:
|
||||
nginx_network:
|
||||
driver: bridge
|
|
@ -13,7 +13,8 @@ steps:
|
|||
image: docker:24.0.6
|
||||
secrets: []
|
||||
commands:
|
||||
- docker compose -f artel/docker-compose-test.yml run --rm test_comfy
|
||||
- docker compose -f artel/docker-compose-test.yml run test_comfy
|
||||
- docker compose -f ./artel/docker-compose-test.yml down
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
when:
|
|
@ -28,6 +28,9 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
|
|||
# Install the application server.
|
||||
RUN pip install "gunicorn==20.0.4"
|
||||
|
||||
# Install gettext
|
||||
RUN apt-get update && apt-get install -y gettext
|
||||
|
||||
# Install the project requirements.
|
||||
COPY requirements.txt /
|
||||
RUN pip install -r /requirements.txt
|
|
@ -21,7 +21,10 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
|
|||
libwebp-dev \
|
||||
wkhtmltopdf \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# Install gettext
|
||||
RUN apt-get update && apt-get install -y gettext
|
||||
|
||||
# Install the project requirements.
|
||||
COPY requirements.txt /
|
||||
COPY requirements_dev.txt /
|
|
@ -0,0 +1,110 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_ROOT_PASSWORD
|
||||
- POSTGRES_USER
|
||||
- POSTGRES_PASSWORD
|
||||
- POSTGRES_DB
|
||||
volumes:
|
||||
- ../postgres/:/var/lib/postgresql
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
rabbit:
|
||||
hostname: rabbit
|
||||
image: rabbitmq:3.6.0
|
||||
environment:
|
||||
- RABBITMQ_DEFAULT_USER
|
||||
- RABBITMQ_DEFAULT_PASS
|
||||
ports:
|
||||
- "5672:5672" # We forward this port because it's useful for debugging
|
||||
- "15672:15672" # Here, we can access RabbitMQ management plugin
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
|
||||
comfy:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: ./
|
||||
user: "${UID}:${GID}"
|
||||
restart: always
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
environment:
|
||||
- SECRET_KEY
|
||||
- DATABASE_URL
|
||||
- DJANGO_SETTINGS_MODULE
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
web:
|
||||
image: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ../nginx/conf.d/:/etc/nginx/conf.d/
|
||||
- ./static/:/opt/services/comfy/static
|
||||
- ./media/:/opt/services/comfy/media
|
||||
ports:
|
||||
- "8000:80"
|
||||
environment:
|
||||
- NGINX_HOST=artel.tepewu.pl
|
||||
- NGINX_PORT=80
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
beat:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: celery -A artel beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
volumes:
|
||||
- ./:/app
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SECRET_KEY
|
||||
- DATABASE_URL
|
||||
depends_on:
|
||||
- comfy
|
||||
- rabbit
|
||||
- db
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: celery -A artel worker -l info
|
||||
volumes:
|
||||
- ./:/app
|
||||
- ./media:/app/media
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SECRET_KEY
|
||||
- DATABASE_URL
|
||||
depends_on:
|
||||
- comfy
|
||||
- rabbit
|
||||
- db
|
||||
- beat
|
||||
networks:
|
||||
- nginx_network
|
||||
|
||||
networks:
|
||||
nginx_network:
|
||||
driver: bridge
|
|
@ -20,9 +20,9 @@ services:
|
|||
environment:
|
||||
- RABBITMQ_DEFAULT_USER
|
||||
- RABBITMQ_DEFAULT_PASS
|
||||
ports:
|
||||
- "5672:5672" # We forward this port because it's useful for debugging
|
||||
- "15672:15672" # Here, we can access RabbitMQ management plugin
|
||||
# ports:
|
||||
# - "5672:5672" # We forward this port because it's useful for debugging
|
||||
# - "15672:15672" # Here, we can access RabbitMQ management plugin
|
||||
|
||||
smtp-server:
|
||||
image: mailhog/mailhog
|
|
@ -0,0 +1,45 @@
|
|||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin,
|
||||
ModelAdminGroup,
|
||||
modeladmin_register
|
||||
)
|
||||
|
||||
from dynamic_forms import models
|
||||
|
||||
class CustomEmailFormAdmin(ModelAdmin):
|
||||
model = models.CustomEmailForm
|
||||
menu_label = "Email Forms"
|
||||
menu_icon = 'mail'
|
||||
menu_order = 100
|
||||
add_to_settings_menu = False
|
||||
exclude_from_explorer = False
|
||||
list_display = (
|
||||
"title",
|
||||
"slug",
|
||||
)
|
||||
search_fields = (
|
||||
"title",
|
||||
"slug",
|
||||
)
|
||||
list_filter = (
|
||||
"title",
|
||||
"slug",
|
||||
)
|
||||
form_fields = (
|
||||
"slug",
|
||||
"intro",
|
||||
"thank_you_text",
|
||||
"from_address",
|
||||
"to_address",
|
||||
"subject",
|
||||
)
|
||||
|
||||
class CustomFormGroup(ModelAdminGroup):
|
||||
menu_label = "Custom Forms"
|
||||
menu_icon = 'tasks'
|
||||
menu_order = 100
|
||||
items = (
|
||||
CustomEmailFormAdmin,
|
||||
)
|
||||
|
||||
modeladmin_register(CustomFormGroup)
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DynamicFormsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "dynamic_forms"
|
|
@ -0,0 +1,99 @@
|
|||
from django import forms
|
||||
from dynamic_forms.widgets import (
|
||||
CheckboxSelectMultiple,
|
||||
CheckboxInput,
|
||||
RadioSelect
|
||||
)
|
||||
|
||||
|
||||
class MultipleFileInput(forms.ClearableFileInput):
|
||||
allow_multiple_selected = True
|
||||
|
||||
|
||||
class MultipleFileField(forms.FileField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("widget", MultipleFileInput())
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self, data, initial=None):
|
||||
single_file_clean = super().clean
|
||||
if isinstance(data, (list, tuple)):
|
||||
result = [single_file_clean(d, initial) for d in data]
|
||||
else:
|
||||
result = single_file_clean(data, initial)
|
||||
return result
|
||||
|
||||
|
||||
class HoneypotField(forms.BooleanField):
|
||||
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
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean(self, value):
|
||||
if cleaned_value := super().clean(value):
|
||||
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"})),
|
||||
"email": forms.EmailField(max_length=255, widget=forms.EmailInput(attrs={"class": "form-control"})),
|
||||
"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"})),
|
||||
"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"})),
|
||||
"date": forms.DateField(widget=forms.DateInput(attrs={"class": "form-control"})),
|
||||
"datetime": forms.DateTimeField(widget=forms.DateTimeInput(attrs={"class": "form-control"})),
|
||||
"hidden": forms.CharField(widget=forms.HiddenInput()),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
kwargs.pop("page", "")
|
||||
kwargs.pop("user", "")
|
||||
field_list = kwargs.pop("field_list")
|
||||
file_uploads = kwargs.pop("file_uploads", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in field_list:
|
||||
f = self.FIELD_TYPE_MAPPING[field.field_type]
|
||||
f.label = field.label
|
||||
if hasattr(f, "choices"):
|
||||
f.choices = [(v, v) for v in field.choices.split(",")]
|
||||
f.required = field.required
|
||||
f.help_text = field.help_text or ""
|
||||
self.fields[field.clean_name] = f
|
||||
if file_uploads:
|
||||
self.fields["attachments"] = MultipleFileField(
|
||||
required=True, widget=MultipleFileInput(
|
||||
attrs={"class": "form-control"}
|
||||
)
|
||||
)
|
||||
# add honeypot field
|
||||
self.fields["secret_honey"] = HoneypotField()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
new_cleaned_data = {}
|
||||
for key, value in cleaned_data.items():
|
||||
if isinstance(value, list):
|
||||
if isinstance(self.fields[key], MultipleFileField):
|
||||
continue
|
||||
|
||||
cleaned_data[key] = ",".join(value)
|
||||
if key=="secret_honey":
|
||||
continue
|
||||
|
||||
new_cleaned_data[key] = value
|
||||
|
||||
return new_cleaned_data
|
|
@ -0,0 +1,132 @@
|
|||
# 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
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("wagtailcore", "0083_workflowcontenttype"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CustomEmailForm",
|
||||
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",
|
||||
),
|
||||
),
|
||||
("intro", wagtail.fields.RichTextField(blank=True)),
|
||||
("thank_you_text", wagtail.fields.RichTextField(blank=True)),
|
||||
("allow_attachments", models.BooleanField(default=False)),
|
||||
("from_address", models.EmailField(blank=True, help_text="Sender email address", max_length=254)),
|
||||
("to_address", models.CharField(help_text="Comma separated list of recipients", max_length=255)),
|
||||
("subject", models.CharField(help_text="Subject of the email with data", max_length=255)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
bases=(wagtail.contrib.forms.models.FormMixin, "wagtailcore.page"),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EmailFormSubmission",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("form_data", models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder)),
|
||||
("submit_time", models.DateTimeField(auto_now_add=True, verbose_name="submit time")),
|
||||
("page", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="wagtailcore.page")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "form submission",
|
||||
"verbose_name_plural": "form submissions",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EmailFormField",
|
||||
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)),
|
||||
(
|
||||
"clean_name",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Safe name of the form field, the label converted to ascii_snake_case",
|
||||
max_length=255,
|
||||
verbose_name="name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"label",
|
||||
models.CharField(help_text="The label of the form field", max_length=255, verbose_name="label"),
|
||||
),
|
||||
(
|
||||
"field_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("singleline", "Single line text"),
|
||||
("multiline", "Multi-line text"),
|
||||
("email", "Email"),
|
||||
("number", "Number"),
|
||||
("url", "URL"),
|
||||
("checkbox", "Checkbox"),
|
||||
("checkboxes", "Checkboxes"),
|
||||
("dropdown", "Drop down"),
|
||||
("multiselect", "Multiple select"),
|
||||
("radio", "Radio buttons"),
|
||||
("date", "Date"),
|
||||
("datetime", "Date/time"),
|
||||
("hidden", "Hidden field"),
|
||||
],
|
||||
max_length=16,
|
||||
verbose_name="field type",
|
||||
),
|
||||
),
|
||||
("required", models.BooleanField(default=True, verbose_name="required")),
|
||||
(
|
||||
"choices",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Comma or new line separated list of choices. Only applicable in checkboxes, radio and dropdown.",
|
||||
verbose_name="choices",
|
||||
),
|
||||
),
|
||||
(
|
||||
"default_value",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Default value. Comma or new line separated values supported for checkboxes.",
|
||||
verbose_name="default value",
|
||||
),
|
||||
),
|
||||
("help_text", models.CharField(blank=True, max_length=255, verbose_name="help text")),
|
||||
(
|
||||
"form",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="form_fields",
|
||||
to="dynamic_forms.customemailform",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["sort_order"],
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,130 @@
|
|||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
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.contrib.forms.models import (
|
||||
AbstractFormField,
|
||||
FormMixin,
|
||||
Page,
|
||||
AbstractFormSubmission
|
||||
)
|
||||
|
||||
from mailings.models import (
|
||||
OutgoingEmail,
|
||||
Attachment
|
||||
)
|
||||
from dynamic_forms.forms import DynamicForm
|
||||
|
||||
|
||||
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")
|
||||
]
|
||||
|
||||
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()
|
||||
form_params.update(kwargs)
|
||||
form_params["field_list"] = self.get_form_fields()
|
||||
form_params["file_uploads"] = self.allow_attachments
|
||||
return form_class(*args, **form_params)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
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()
|
||||
return f"{form_slug}-{datetime.date.today()}-{case_number_daily}"
|
||||
|
||||
def send_mail(self, data):
|
||||
# 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", [])
|
||||
]
|
||||
subject = data.pop("subject")
|
||||
form_slug = data.pop("form_slug")
|
||||
from_address = data.pop("from_address", settings.DEFAULT_FROM_EMAIL)
|
||||
for address in to_addresses:
|
||||
OutgoingEmail.objects.send(
|
||||
subject=subject,
|
||||
template_name="form_mail",
|
||||
recipient=address,
|
||||
sender=from_address,
|
||||
context={"form_data": data, "submission_id": self.get_submission_id(form_slug)},
|
||||
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"
|
||||
)
|
||||
|
||||
template = "forms/email_form_page.html"
|
||||
|
||||
def get_submission_class(self):
|
||||
return EmailFormSubmission
|
||||
|
||||
def process_form_submission(self, form):
|
||||
attachments = form.cleaned_data.pop("attachments", [])
|
||||
submission = self.get_submission_class().objects.create(
|
||||
form_data=form.cleaned_data,
|
||||
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
|
||||
})
|
||||
submission.send_mail(data=mail_data)
|
||||
return submission
|
||||
|
||||
class EmailFormField(AbstractFormField):
|
||||
form = ParentalKey(
|
||||
"CustomEmailForm", related_name="form_fields", on_delete=models.CASCADE
|
||||
)
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load wagtailcore_tags wagtailimages_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
<p class="meta">{{ page.date }}</p>
|
||||
{% if form %}
|
||||
{{form.errors}}
|
||||
<div>{{ page.intro|richtext }}</div>
|
||||
<form enctype="multipart/form-data" action="{% pageurl page %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% if field.is_hidden %}
|
||||
<!--Empty space for reason, we don't want to show anything on empty fields-->
|
||||
{{field}}
|
||||
{% else %}
|
||||
<div class="form-group mt-3">
|
||||
<label for="{{field.id}}" class="form-label">
|
||||
{% trans field.label %}
|
||||
</label>
|
||||
{{field}}
|
||||
<small id="emailHelp" class="form-text text-muted">
|
||||
{% trans field.help_text %}
|
||||
</small>
|
||||
{% if field.error %}
|
||||
{{error}}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="text-end mt-3">
|
||||
<input class="btn btn-lg btn-success" type="submit" value='{% trans "Submit" %}'>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div>You can fill in the from only one time.</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<div class="card col-9 bg-white shadow-md p-5">
|
||||
<div class="mb-4 text-center">
|
||||
<svg class="text-success" width="75" height="75"
|
||||
fill="currentColor" class="bi bi-check-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path
|
||||
d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
{{page.thank_you_text|richtext}}
|
||||
</div>
|
||||
<a class="btn btn-outline-primary mt-3" href="/">
|
||||
{% trans "Homepage" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="{{ widget.attrs.id }}"
|
||||
name="{{ widget.name }}">
|
||||
<label class="form-check-label" for="{{ widget.attrs.id }}">
|
||||
{{ widget.name }}
|
||||
</label>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
{% for group, options, index in widget.optgroups %}
|
||||
{% if group %}
|
||||
<label>{{ group }}</label>
|
||||
{% endif %}
|
||||
{% for option in options %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ option.value }}" id="{{ option.attrs.id }}"
|
||||
name="{{ option.name }}">
|
||||
<label class="form-check-label" for="{{ option.attrs.id }}">
|
||||
{{ option.value }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,15 @@
|
|||
{% for group, options, index in widget.optgroups %}
|
||||
{% if group %}
|
||||
<label>{{ group }}</label>
|
||||
{% endif %}
|
||||
{% for option in options %}
|
||||
<div class="form-check">
|
||||
<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 }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,304 @@
|
|||
from wagtail.tests.utils import WagtailPageTests
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
from dynamic_forms.models import (
|
||||
CustomEmailForm,
|
||||
EmailFormField
|
||||
)
|
||||
from dynamic_forms.forms import DynamicForm
|
||||
|
||||
|
||||
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,
|
||||
from_address="comfy-test@egalitare.pl",
|
||||
to_address="comfy-dest@egalitare.pl",
|
||||
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="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
|
||||
)
|
||||
|
||||
def test_generate_html_form_from_model(self):
|
||||
html_form = self.form.get_form()
|
||||
self.assertIsInstance(html_form, DynamicForm)
|
||||
self.assertEqual(len(html_form.fields), 14)
|
||||
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
|
||||
)
|
||||
|
||||
def test_create_form_submission_success_without_files(self):
|
||||
form_data = {
|
||||
# generate data for this class self.form.get_form()
|
||||
"name": "Test",
|
||||
"message": "Test message",
|
||||
"email": "test@test.com",
|
||||
"number": 1,
|
||||
"url": "http://example.com",
|
||||
"checkbox": True,
|
||||
"checkboxes": ["a", "b"],
|
||||
"dropdown": "a",
|
||||
"multiselect": ["a", "b"],
|
||||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"hidden": "hidden",
|
||||
}
|
||||
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.required = False
|
||||
field.save()
|
||||
form = self.form.get_form(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
# it should also work without this field
|
||||
form_data.pop("name")
|
||||
form = self.form.get_form(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_create_form_submission_success_with_files(self):
|
||||
self.form.allow_attachments = True
|
||||
self.form.save()
|
||||
form_data = {
|
||||
# generate data for this class self.form.get_form()
|
||||
"name": "Test",
|
||||
"message": "Test message",
|
||||
"email": "test@test.com",
|
||||
"number": 1,
|
||||
"url": "http://example.com",
|
||||
"checkbox": True,
|
||||
"checkboxes": ["a", "b"],
|
||||
"dropdown": "a",
|
||||
"multiselect": ["a", "b"],
|
||||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"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()
|
||||
"message": "Test message",
|
||||
"email": "test@test.com",
|
||||
"number": 1,
|
||||
"url": "http://example.com",
|
||||
"checkbox": True,
|
||||
"checkboxes": ["a", "b"],
|
||||
"dropdown": "a",
|
||||
"multiselect": ["a", "b"],
|
||||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"hidden": "hidden",
|
||||
}
|
||||
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.'])
|
||||
|
||||
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.'])
|
||||
# make Field not required
|
||||
field = EmailFormField.objects.get(
|
||||
label="Hidden", form=self.form
|
||||
)
|
||||
field.required = False
|
||||
field.save()
|
||||
# it should also work without this field
|
||||
form_data.pop("hidden")
|
||||
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.'])
|
||||
|
||||
def test_create_form_submission_failure_with_files_missing_data(self):
|
||||
self.form.allow_attachments = True
|
||||
self.form.save()
|
||||
form_data = {
|
||||
# generate data for this class self.form.get_form()
|
||||
"message": "Test message",
|
||||
"email": "test@test.com",
|
||||
"number": 1,
|
||||
"url": "http://example.com",
|
||||
"checkbox": True,
|
||||
"checkboxes": ["a", "b"],
|
||||
"dropdown": "a",
|
||||
"multiselect": ["a", "b"],
|
||||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"hidden": "hidden",
|
||||
}
|
||||
files = {
|
||||
"attachments": [SimpleUploadedFile("test.txt", b"test")],
|
||||
}
|
||||
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.'])
|
||||
|
||||
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.'])
|
||||
# make Field not required
|
||||
field = EmailFormField.objects.get(
|
||||
label="Hidden", form=self.form
|
||||
)
|
||||
field.required = False
|
||||
field.save()
|
||||
# it should also work without this field
|
||||
form_data.pop("hidden")
|
||||
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.'])
|
||||
# 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.'])
|
||||
|
||||
def test_no_hidden_field_in_clean_data_success(self):
|
||||
form_data = {
|
||||
# generate data for this class self.form.get_form()
|
||||
"name": "Test",
|
||||
"message": "Test message",
|
||||
"email": "test@test.com",
|
||||
"number": 1,
|
||||
"url": "http://example.com",
|
||||
"checkbox": True,
|
||||
"checkboxes": ["a", "b"],
|
||||
"dropdown": "a",
|
||||
"multiselect": ["a", "b"],
|
||||
"radio": "a",
|
||||
"date": "2020-01-01",
|
||||
"datetime": "2020-01-01 00:00:00",
|
||||
"hidden": "hidden",
|
||||
}
|
||||
form = self.form.get_form(form_data)
|
||||
self.assertTrue(form.is_valid())
|
||||
cleaned_data = form.cleaned_data
|
||||
self.assertIn("hidden", cleaned_data)
|
||||
self.assertNotIn("secret_honey", cleaned_data)
|
||||
self.assertIn("hidden", form.fields)
|
||||
self.assertIn("secret_honey", form.fields)
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from django import forms
|
||||
|
||||
|
||||
class CheckboxInput(forms.CheckboxInput):
|
||||
template_name = "widgets/checkbox.html"
|
||||
|
||||
|
||||
class CheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
template_name = "widgets/checkbox_multiple.html"
|
||||
|
||||
|
||||
class RadioSelect(forms.RadioSelect):
|
||||
template_name = "widgets/radio_multiple.html"
|
Plik binarny nie jest wyświetlany.
|
@ -0,0 +1,189 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-20 13:43+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
|
||||
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
|
||||
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
|
||||
#: artel/templates/menu/custom_main_menu.html:33
|
||||
msgid "Cart"
|
||||
msgstr "Koszyk"
|
||||
|
||||
#: home/templates/home/welcome_page.html:6
|
||||
msgid "Visit the Wagtail website"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:15
|
||||
msgid "View the release notes"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:27
|
||||
msgid "Welcome to your new Wagtail site!"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:28
|
||||
msgid ""
|
||||
"Please feel free to <a href=\"https://github.com/wagtail/wagtail/wiki/Slack"
|
||||
"\">join our community on Slack</a>, or get started with one of the links "
|
||||
"below."
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:35
|
||||
msgid "Wagtail Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:36
|
||||
msgid "Topics, references, & how-tos"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:42
|
||||
msgid "Tutorial"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:43
|
||||
msgid "Build your first Wagtail site"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:49
|
||||
msgid "Admin Interface"
|
||||
msgstr ""
|
||||
|
||||
#: home/templates/home/welcome_page.html:50
|
||||
msgid "Create your superuser first!"
|
||||
msgstr ""
|
||||
|
||||
#: search/templates/search/search.html:10
|
||||
#: search/templates/search/search.html:14
|
||||
msgid "Search"
|
||||
msgstr "Wyszukaj"
|
||||
|
||||
#: search/templates/search/search.html:30
|
||||
#: store/templates/store/product_list_page.html:39
|
||||
msgid "Previous"
|
||||
msgstr "Poprzednia"
|
||||
|
||||
#: search/templates/search/search.html:34
|
||||
#: store/templates/store/product_list_page.html:45
|
||||
msgid "Next"
|
||||
msgstr "Następna"
|
||||
|
||||
#: search/templates/search/search.html:37
|
||||
msgid "No results found"
|
||||
msgstr "Nie znaleziono wyników"
|
||||
|
||||
#: store/forms.py:21
|
||||
msgid "Name"
|
||||
msgstr "Imię"
|
||||
|
||||
#: store/forms.py:25
|
||||
msgid "Surname"
|
||||
msgstr "Nazwisko"
|
||||
|
||||
#: store/forms.py:28 store/templates/store/order_confirm.html:42
|
||||
msgid "Address"
|
||||
msgstr "Adres"
|
||||
|
||||
#: store/forms.py:31
|
||||
msgid "City"
|
||||
msgstr "Miasto"
|
||||
|
||||
#: store/forms.py:34
|
||||
msgid "Zip-code"
|
||||
msgstr "Kod pocztowy"
|
||||
|
||||
#: store/forms.py:37
|
||||
msgid "E-mail"
|
||||
msgstr "Email"
|
||||
|
||||
#: store/forms.py:40
|
||||
msgid "Phone number"
|
||||
msgstr "Numer telefonu"
|
||||
|
||||
#: store/forms.py:43
|
||||
msgid "Polska"
|
||||
msgstr ""
|
||||
|
||||
#: store/forms.py:43
|
||||
msgid "Country"
|
||||
msgstr "Kraj"
|
||||
|
||||
#: store/templates/store/cart.html:9
|
||||
msgid "Shopping Cart"
|
||||
msgstr "Koszyk"
|
||||
|
||||
#: store/templates/store/cart.html:20
|
||||
#: store/templates/store/order_confirm.html:66
|
||||
msgid "To Pay:"
|
||||
msgstr "Do zapłaty:"
|
||||
|
||||
#: store/templates/store/cart.html:23
|
||||
msgid "Proceed to Pay"
|
||||
msgstr "Przejdź do płatności"
|
||||
|
||||
#: store/templates/store/order.html:12
|
||||
msgid "Order Data"
|
||||
msgstr "Dane zamówienia"
|
||||
|
||||
#: store/templates/store/order.html:13
|
||||
msgid "We won't share your data"
|
||||
msgstr "Nie udostępnimy Twoich danych"
|
||||
|
||||
#: store/templates/store/order.html:58
|
||||
msgid "Contact Details"
|
||||
msgstr "Dane kontaktowe"
|
||||
|
||||
#: store/templates/store/order.html:88
|
||||
#: store/templates/store/order_confirm.html:69
|
||||
msgid "Confirm"
|
||||
msgstr "Potwierdź"
|
||||
|
||||
#: store/templates/store/order_confirm.html:9
|
||||
msgid "Customer Data"
|
||||
msgstr "Dane klienta"
|
||||
|
||||
#: store/templates/store/order_confirm.html:15
|
||||
msgid "Full Name"
|
||||
msgstr "Pełne imię"
|
||||
|
||||
#: store/templates/store/order_confirm.html:24
|
||||
msgid "Email"
|
||||
msgstr "Email"
|
||||
|
||||
#: store/templates/store/order_confirm.html:33
|
||||
msgid "Phone"
|
||||
msgstr "Numer Telefonu"
|
||||
|
||||
#: store/templates/store/order_confirm.html:55
|
||||
msgid "Order Items"
|
||||
msgstr "Zamawiane przedmioty"
|
||||
|
||||
#: store/templates/store/product_list_page.html:10
|
||||
msgid "Added to cart"
|
||||
msgstr "Dodano do koszyka"
|
||||
|
||||
#: store/templates/store/product_list_page.html:16
|
||||
msgid "Item has been added to cart."
|
||||
msgstr "Przedmiot został dodany do koszyka"
|
||||
|
||||
#: store/templates/store/product_list_page.html:19
|
||||
msgid "Continue shopping"
|
||||
msgstr "Kontynuuj zakupy"
|
||||
|
||||
#: store/templates/store/product_list_page.html:20
|
||||
msgid "Go to cart"
|
||||
msgstr "Idź do koszyka"
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -3,7 +3,7 @@ import os
|
|||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "artel.settings.dev")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wagtail_store.settings.dev")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
Django>=4.1,<4.2
|
||||
wagtail>=4.2,<4.3
|
||||
Django==4.2.6
|
||||
wagtail==5.1.2
|
||||
wagtailmenus>=3.1.5,<=3.1.7
|
||||
psycopg2-binary>=2.9.5,<=2.9.6
|
||||
dj-database-url<=2.0.0
|
||||
|
@ -15,3 +15,4 @@ easy_thumbnails==2.8.5
|
|||
num2words==0.5.12
|
||||
sentry-sdk==1.28.0
|
||||
pandas==2.0.3
|
||||
wagtail-localize==1.5.2
|
|
@ -1,16 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static wagtailcore_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body_class %}template-searchresults{% endblock %}
|
||||
|
||||
{% block title %}Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Search</h1>
|
||||
<h1>{% trans "Search" %}</h1>
|
||||
|
||||
<form action="{% url 'search' %}" method="get">
|
||||
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
|
||||
<input type="submit" value="Search" class="button">
|
||||
<input type="submit" value=" {% trans "Search" %}" class="button">
|
||||
</form>
|
||||
|
||||
{% if search_results %}
|
||||
|
@ -26,13 +27,13 @@
|
|||
</ul>
|
||||
|
||||
{% if search_results.has_previous %}
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">Previous</a>
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">{% trans "Previous" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if search_results.has_next %}
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a>
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">{% trans "Next" %}</a>
|
||||
{% endif %}
|
||||
{% elif search_query %}
|
||||
No results found
|
||||
{% trans "No results found" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -11,41 +11,42 @@ from store.models import (
|
|||
DeliveryMethod
|
||||
)
|
||||
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class CustomerDataForm(forms.Form):
|
||||
|
||||
name = forms.CharField(
|
||||
max_length=255, label="Imię", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("Name"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
|
||||
surname = forms.CharField(
|
||||
max_length=255, label="Nazwisko", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("Surname"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
street = forms.CharField(
|
||||
max_length=255, label="Adres", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("Address"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
city = forms.CharField(
|
||||
max_length=255, label="Miasto", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("City"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
zip_code = forms.CharField(
|
||||
max_length=255, label="Kod pocztowy", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("Zip-code"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
email = forms.EmailField(
|
||||
max_length=255, label="E-mail", widget=forms.EmailInput(attrs={"class": "form-control"})
|
||||
max_length=255, label=_("E-mail"), widget=forms.EmailInput(attrs={"class": "form-control"})
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
region="PL", label="Numer telefonu", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
region="PL", label=_("Phone number"), widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
country = forms.ChoiceField(
|
||||
choices=(("PL", "Polska"), ), label="Kraj",
|
||||
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"})
|
||||
)
|
||||
|
||||
delivery_method = forms.ModelChoiceField(
|
||||
queryset=DeliveryMethod.objects.filter(active=True), label="Sposób dostawy",
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue