commit
81c5926261
|
@ -139,6 +139,7 @@ GitHub.sublime-settings
|
|||
#postgres pass files
|
||||
*.my_pgpass
|
||||
*.sql
|
||||
artel/static/
|
||||
|
||||
# media
|
||||
artel/media/*
|
||||
|
|
|
@ -23,7 +23,9 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r
|
|||
|
||||
# Install the project requirements.
|
||||
COPY requirements.txt /
|
||||
COPY requirements_dev.txt /
|
||||
RUN pip install -r /requirements.txt
|
||||
RUN pip install -r /requirements_dev.txt
|
||||
|
||||
# Use /app folder as a directory where the source code is stored.
|
||||
WORKDIR /app
|
||||
|
|
|
@ -49,6 +49,8 @@ INSTALLED_APPS = [
|
|||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"phonenumber_field",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -173,3 +175,6 @@ WAGTAILADMIN_BASE_URL = "http://example.com"
|
|||
|
||||
# STORE SETTINGS
|
||||
PRODUCTS_PER_PAGE = 6
|
||||
|
||||
# CART settings
|
||||
CART_SESSION_ID = 'cart'
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
.shaded-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.unshaded-overlay{
|
||||
position: absolute;
|
||||
top: 25%;
|
||||
left: 35%;
|
||||
width: 30%;
|
||||
height: 30%;
|
||||
background-color: rgb(255,255,255);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
max-width: 50px;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
||||
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 553 B |
|
@ -51,7 +51,7 @@
|
|||
<script type="text/javascript" src="{% static 'js/jquery-3.6.4.min.js' %}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.7/dist/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/cart.js' %}"></script>
|
||||
{% block extra_js %}
|
||||
{# Override this in templates to add extra javascript #}
|
||||
{% endblock %}
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3 mr-5" style="width: 280px;">
|
||||
<hr>
|
||||
</div>
|
||||
<a href={% url 'cart' %} alt="Koszyk" > Koszyk </a>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
from django.conf import settings
|
||||
from django.urls import include, path
|
||||
from django.urls import (
|
||||
include,
|
||||
path
|
||||
)
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
@ -12,7 +16,8 @@ urlpatterns = [
|
|||
path("django-admin/", admin.site.urls),
|
||||
path("admin/", include(wagtailadmin_urls)),
|
||||
path("documents/", include(wagtaildocs_urls)),
|
||||
path("search/", search_views.search, name="search")
|
||||
path("search/", search_views.search, name="search"),
|
||||
path("store-app/", include("store.urls"))
|
||||
]
|
||||
|
||||
|
||||
|
@ -22,7 +27,8 @@ if settings.DEBUG:
|
|||
|
||||
# Serve static and media files from development server
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns = urlpatterns + [
|
||||
# For anything not caught by a more specific rule above, hand over to
|
||||
|
|
|
@ -13,6 +13,8 @@ services:
|
|||
- DATABASE_URL
|
||||
env_file:
|
||||
- .env
|
||||
stdin_open: true
|
||||
tty: true
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
|
|
@ -3,3 +3,6 @@ wagtail>=4.2,<4.3
|
|||
wagtailmenus>=3.1.5,<=3.1.7
|
||||
psycopg2>=2.9.5,<=2.9.6
|
||||
dj-database-url<=2.0.0
|
||||
djangorestframework==3.14.0
|
||||
phonenumbers==8.13.13
|
||||
django-phonenumber-field==7.1.0
|
||||
|
|
|
@ -2,3 +2,4 @@ FLAKE8>=6.0.0
|
|||
pre-commit>=3.3.1
|
||||
isort>=5.12
|
||||
black>=23.3.0
|
||||
ipdb==0.12.3
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
from abc import (
|
||||
ABC,
|
||||
abstractmethod
|
||||
)
|
||||
from typing import List
|
||||
from dataclasses import dataclass
|
||||
from django.http.request import HttpRequest
|
||||
from django.conf import settings
|
||||
|
||||
from store.models import Product
|
||||
|
||||
|
||||
@dataclass
|
||||
class CartItem:
|
||||
product: Product
|
||||
quantity: int
|
||||
|
||||
|
||||
class BaseCart(ABC):
|
||||
|
||||
def validate_item_id(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):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def update_item_quantity(self, item_id, change):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_items(self):
|
||||
...
|
||||
|
||||
|
||||
class SessionCart(BaseCart):
|
||||
|
||||
def __init__(self, request: HttpRequest) -> None:
|
||||
super().__init__()
|
||||
self.session = request.session
|
||||
if not self.session.get(settings.CART_SESSION_ID):
|
||||
self.session[settings.CART_SESSION_ID] = {}
|
||||
|
||||
def add_item(self, item_id: int, quantity: int) -> None:
|
||||
# TODO - add logging
|
||||
self.validate_item_id(item_id)
|
||||
quantity = int(quantity)
|
||||
item_id = int(item_id)
|
||||
if not self.session[settings.CART_SESSION_ID].get(str(item_id)):
|
||||
self.session[settings.CART_SESSION_ID][item_id] = quantity
|
||||
self.session.modified = True
|
||||
else:
|
||||
self.update_item_quantity(item_id, quantity)
|
||||
|
||||
def remove_item(self, item_id: int) -> None:
|
||||
self.validate_item_id(item_id)
|
||||
try:
|
||||
self.session[settings.CART_SESSION_ID].pop(item_id)
|
||||
self.session.modified = True
|
||||
except KeyError:
|
||||
# TODO - add logging
|
||||
...
|
||||
|
||||
def update_item_quantity(self, item_id: int, change: int) -> None:
|
||||
self.validate_item_id(item_id)
|
||||
try:
|
||||
self.session[settings.CART_SESSION_ID][str(item_id)] += change
|
||||
self.session.modified = True
|
||||
except KeyError:
|
||||
# TODO - add logging
|
||||
self.add_item(item_id, change)
|
||||
|
||||
def get_items(self) -> List[CartItem]:
|
||||
_items = []
|
||||
for item_id, quantity in self.session[settings.CART_SESSION_ID].items():
|
||||
_items.append(CartItem(quantity=quantity, product=Product.objects.get(id=item_id)))
|
||||
return _items
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
total = 0
|
||||
for item in self.get_items():
|
||||
total += item.product.price * int(item.quantity)
|
||||
return total
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return not bool(self.session[settings.CART_SESSION_ID].items())
|
||||
|
||||
def clear(self) -> None:
|
||||
self.session[settings.CART_SESSION_ID] = {}
|
||||
self.session.modified = True
|
|
@ -0,0 +1,43 @@
|
|||
from django import forms
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
# from phonenumber_field.widgets import PhoneNumberPrefixWidget
|
||||
|
||||
from store.models import (
|
||||
CustomerData,
|
||||
)
|
||||
|
||||
|
||||
class CustomerDataForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = CustomerData
|
||||
fields = [
|
||||
"name", "surname", "email", "phone",
|
||||
"street", "city", "zip_code"
|
||||
]
|
||||
|
||||
name = forms.CharField(
|
||||
max_length=255, label="Imię", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
|
||||
surname = forms.CharField(
|
||||
max_length=255, label="Nazwisko", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
street = forms.CharField(
|
||||
max_length=255, label="Adres", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
city = forms.CharField(
|
||||
max_length=255, label="Miasto", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
zip_code = forms.CharField(
|
||||
max_length=255, label="Kod pocztowy", widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
email = forms.EmailField(
|
||||
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"})
|
||||
)
|
||||
country = forms.ChoiceField(
|
||||
choices=(("PL", "Polska"), ), label="Kraj",
|
||||
widget=forms.Select(attrs={"class": "form-control"})
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.1.9 on 2023-05-28 11:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('store', '0002_product_available_producttemplate_tags_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='info',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='producttemplate',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 4.1.9 on 2023-06-01 19:00
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import phonenumber_field.modelfields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("store", "0003_product_info_product_name_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CustomerData",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("surname", models.CharField(max_length=255)),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
("phone", phonenumber_field.modelfields.PhoneNumberField(max_length=128, region=None)),
|
||||
("street", models.CharField(max_length=255)),
|
||||
("city", models.CharField(max_length=255)),
|
||||
("zip_code", models.CharField(max_length=120)),
|
||||
("country", models.CharField(max_length=120)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Order",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("sent", models.BooleanField(default=False)),
|
||||
("customer", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.customerdata")),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="OrderProduct",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("quantity", models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="products", to="store.order"
|
||||
),
|
||||
),
|
||||
("product", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.product")),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -4,6 +4,7 @@ from django.core.paginator import (
|
|||
EmptyPage
|
||||
)
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
from modelcluster.models import ClusterableModel
|
||||
from modelcluster.fields import ParentalKey
|
||||
|
@ -14,6 +15,7 @@ from wagtail.admin.panels import (
|
|||
from wagtail.models import Page
|
||||
from wagtail import fields as wagtail_fields
|
||||
from taggit.managers import TaggableManager
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
|
||||
class ProductAuthor(models.Model):
|
||||
|
@ -56,7 +58,7 @@ class ProductTemplate(ClusterableModel):
|
|||
author = models.ForeignKey(ProductAuthor, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=255)
|
||||
code = models.CharField(max_length=255)
|
||||
description = models.TextField()
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
tags = TaggableManager()
|
||||
|
||||
|
@ -82,6 +84,8 @@ class ProductImage(models.Model):
|
|||
|
||||
|
||||
class Product(ClusterableModel):
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
info = models.TextField(blank=True)
|
||||
template = models.ForeignKey(ProductTemplate, on_delete=models.CASCADE, related_name="products")
|
||||
price = models.FloatField()
|
||||
available = models.BooleanField(default=True)
|
||||
|
@ -90,7 +94,9 @@ class Product(ClusterableModel):
|
|||
FieldPanel("template"),
|
||||
FieldPanel("price"),
|
||||
InlinePanel("param_values"),
|
||||
FieldPanel("available")
|
||||
FieldPanel("available"),
|
||||
FieldPanel("name"),
|
||||
FieldPanel("info")
|
||||
]
|
||||
|
||||
@property
|
||||
|
@ -100,9 +106,17 @@ class Product(ClusterableModel):
|
|||
if images:
|
||||
return images.first().image
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self.template.tags.all()
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.info or self.template.description
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return f"{self.template.title} - {self.price}"
|
||||
return self.name or self.template.title
|
||||
|
||||
|
||||
class TemplateParamValue(models.Model):
|
||||
|
@ -142,3 +156,46 @@ class ProductListPage(Page):
|
|||
FieldPanel("description"),
|
||||
FieldPanel("tags")
|
||||
]
|
||||
|
||||
class CustomerData(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
surname = models.CharField(max_length=255)
|
||||
email = models.EmailField()
|
||||
phone = PhoneNumberField()
|
||||
street = models.CharField(max_length=255)
|
||||
city = models.CharField(max_length=255)
|
||||
zip_code = models.CharField(max_length=120)
|
||||
country = models.CharField(max_length=120)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.name} {self.surname}"
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
return f"{self.street}, {self.zip_code} {self.city}, {self.country}"
|
||||
|
||||
|
||||
class OrderProductManager(models.Manager):
|
||||
def create_from_cart(self, cart, order):
|
||||
for item in cart:
|
||||
self.create(
|
||||
product=item.product,
|
||||
order=order,
|
||||
quantity=item.quantity
|
||||
)
|
||||
|
||||
|
||||
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)])
|
||||
|
||||
objects = OrderProductManager()
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
customer = models.ForeignKey(CustomerData, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
sent = models.BooleanField(default=False)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from store.models import Product
|
||||
|
||||
|
||||
class TagSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ["title", "description", "price", "available", "tags"]
|
||||
|
||||
tags = TagSerializer(many=True)
|
||||
|
||||
|
||||
class CartProductSerializer(serializers.Serializer):
|
||||
product = ProductSerializer()
|
||||
quantity = serializers.IntegerField()
|
||||
|
||||
|
||||
class CartProductAddSerializer(serializers.Serializer):
|
||||
|
||||
product_id = serializers.IntegerField()
|
||||
quantity = serializers.IntegerField()
|
||||
|
||||
def validate_product_id(self, value):
|
||||
try:
|
||||
Product.objects.get(id=value)
|
||||
except Product.DoesNotExist:
|
||||
raise serializers.ValidationError("Unable to add not existing product")
|
||||
return value
|
||||
|
||||
def save(self, cart):
|
||||
cart.add_item(self.validated_data["product_id"], self.validated_data["quantity"])
|
|
@ -0,0 +1,138 @@
|
|||
|
||||
$(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 addToCartURL = $(this).data('add-to-cart-url');
|
||||
const csrfToken = $(this).data('csrf-token');
|
||||
console.log(productID);
|
||||
formData.append('product_id', productID);
|
||||
formData.append('quantity', quantity); // Serialize the form data correctly
|
||||
button.prop('disabled', true);
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: addToCartURL,
|
||||
data: formData, // Use the serialized form data
|
||||
headers: { 'X-CSRFToken': csrfToken },
|
||||
dataType: 'json',
|
||||
processData: false, // Prevent jQuery from processing the data
|
||||
contentType: false, // Let the browser set the content type
|
||||
success: function(data) {
|
||||
// Show the options block
|
||||
|
||||
//$('#addToCartModal').show();
|
||||
//createShadedOverlay()
|
||||
button.prop('disabled', false);
|
||||
},
|
||||
error: function() {
|
||||
button.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function createShadedOverlay() {
|
||||
const body = document.body;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.classList.add('shaded-overlay');
|
||||
body.appendChild(overlay);
|
||||
const optionsdiv = document.getElementById('addToCartModal');
|
||||
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');
|
||||
|
||||
|
||||
function fetchCartItems(xcsrf_token){
|
||||
fetch('/store/cart-action/list-products')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const cartItems = data.cart_items;
|
||||
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');
|
||||
quantityInput.value = item.quantity;
|
||||
quantityInput.min = '1';
|
||||
quantityInput.step = '1';
|
||||
quantityInput.dataset.productId = item.product_id;
|
||||
quantityInput.dataset.csrfToken = csrf_token;
|
||||
li.appendChild(quantityInput);
|
||||
|
||||
li.appendChild(document.createTextNode(' '));
|
||||
|
||||
const removeButton = document.createElement('a');
|
||||
removeButton.href = '#';
|
||||
removeButton.classList.add('remove-from-cart-button');
|
||||
removeButton.dataset.productId = item.product_id;
|
||||
removeButton.dataset.csrfToken = csrf_token;
|
||||
removeButton.textContent = 'Remove';
|
||||
li.appendChild(removeButton);
|
||||
|
||||
cartItemsList.appendChild(li);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.remove-from-cart-button', function(event) {
|
||||
event.preventDefault();
|
||||
const button = $(this);
|
||||
const productId = button.data('product-id');
|
||||
const csrfToken = button.data('csrf-token');
|
||||
const url = button.data('remove-from-cart-url');
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: {"product_id": productId},
|
||||
headers: { 'X-CSRFToken': csrfToken },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
alert("Item has been removed");
|
||||
location.reload();
|
||||
},
|
||||
error: function() {
|
||||
alert("Error occurred while removing the item from the cart.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$(document).on('change', '.quantity-input', function(event) {
|
||||
event.preventDefault();
|
||||
const input = $(this);
|
||||
const formData = new FormData();
|
||||
const productID = $(this).data('product-id');
|
||||
const newQuantity = input.val();
|
||||
const csrfToken = $(this).data('csrf-token');
|
||||
formData.append('product_id', productID);
|
||||
formData.append('quantity', newQuantity);
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/../../cart/item/',
|
||||
data: formData, // Use the serialized form data
|
||||
headers: { 'X-CSRFToken': csrfToken },
|
||||
dataType: 'json',
|
||||
processData: false, // Prevent jQuery from processing the data
|
||||
contentType: false, // Let the browser set the content type
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<section class="h-100">
|
||||
<div class="container h-100 py-5">
|
||||
<div class="row d-flex justify-content-center align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-normal mb-0 text-black">Shopping Cart</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% for item in cart.get_items %}
|
||||
{% include 'store/partials/cart_item.html' %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="card ">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h5 class="fw-normal mb-0 text-black">To Pay: {{cart.total_price}}</h5>
|
||||
</div>
|
||||
<div class="col-sm-6 text-end">
|
||||
<a href="{% url 'order' %}" class="btn btn-success btn-block btn-lg">Proceed to Pay</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,94 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<section class="order-form m-4">
|
||||
<div class="container pt-4">
|
||||
<div class="row">
|
||||
<div class="col-12 px-4">
|
||||
<h1>Order Data</h1>
|
||||
<span>We won't share your data</span>
|
||||
<hr class="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="row mx-4">
|
||||
<div class="col-sm-6">
|
||||
<label class="order-form-label">{{form.name.label}}</label>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="order-form-label">{{form.surname.label}}</label>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-outline">
|
||||
{{form.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-2 mt-sm-0">
|
||||
<div class="form-outline">
|
||||
{{form.surname}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mx-4">
|
||||
<div class="col-sm-6">
|
||||
<label class="order-form-label">{{form.email.label}}</label>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="order-form-label">{{form.phone.label}}</label>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="form-outline">
|
||||
{{form.email}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-2 mt-sm-0">
|
||||
<div class="form-outline">
|
||||
{{form.phone}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 mx-4">
|
||||
|
||||
<div class="col-12">
|
||||
<label class="order-form-label">Dane Kontaktowe</label>
|
||||
</div>
|
||||
<hr class="mt-1" />
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.street.label}}</label>
|
||||
{{form.street}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="{{form.street.id}}">{{form.country.label}}</label>
|
||||
{{form.country}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="form7">{{form.city.label}}</label>
|
||||
{{form.city}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-2 pe-sm-2">
|
||||
<div class="form-outline">
|
||||
<label class="form-label" for="form7">{{form.zip_code.label}}</label>
|
||||
{{form.zip_code}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 text-end mt-3">
|
||||
<div class="form-outline">
|
||||
<input type="submit" value="Confirm" class="btn btn-success btn-lg">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,77 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<section class="h-100">
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-normal mb-0 text-black">Customer Data</h3>
|
||||
</div>
|
||||
<div class="card mb-2 py-5">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<p class="mb-0">Full Name</p>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p class="text-muted mb-0">{{customer_data.full_name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<p class="mb-0">Email</p>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p class="text-muted mb-0">{{customer_data.email}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<p class="mb-0">Phone</p>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p class="text-muted mb-0">{{customer_data.phone}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<p class="mb-0">Address</p>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p class="text-muted mb-0">{{customer_data.full_address}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row d-flex justify-content-center align-items-center">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-normal mb-0 text-black">Order Items</h3>
|
||||
</div>
|
||||
</div>
|
||||
{% for item in cart.get_items %}
|
||||
{% include 'store/partials/summary_cart_item.html' %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="card ">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h5 class="fw-normal mb-0 text-black">To Pay: {{cart.total_price}}</h5>
|
||||
</div>
|
||||
<div class="col-sm-6 text-end">
|
||||
<a href="" class="btn btn-success btn-block btn-lg">Confirm</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,42 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{{item.product.main_image.url}}"
|
||||
class="img-fluid rounded-3">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
<p class="lead fw-normal mb-2">{{item.product.title}}</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-2 d-flex">
|
||||
<button class="btn btn-link px-2"
|
||||
onclick="this.parentNode.querySelector('input[type=number]').stepDown()">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
|
||||
<input id="form1" min="0" name="quantity" value="{{item.quantity}}" type="number"
|
||||
class="form-control form-control-sm" />
|
||||
|
||||
<button class="btn btn-link px-2"
|
||||
onclick="this.parentNode.querySelector('input[type=number]').stepUp()">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||
<h5 class="mb-0">{{item.product.price}} ZŁ</h5>
|
||||
</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-csrf-token="{{csrf_token}}"
|
||||
data-remove-from-cart-url={% url "cart-action-remove-product" %}>
|
||||
<img src="{% static 'images/icons/trash.svg'%}" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,18 +1,25 @@
|
|||
{% load static %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
<div class="card h-100" style="width: 15rem;">
|
||||
<div class="card h-100" style="width: 15rem;">
|
||||
<div class="card-header">{{item.title}}</div>
|
||||
<div class="card-body">
|
||||
<img src="{{item.main_image.url}}" class="rounded mx-auto d-block" style="width: 10rem; height: 10 rem;">
|
||||
<p class="card-text">{{item.description}}</p>
|
||||
|
||||
<div class="text-end">
|
||||
<a href="#" class="btn btn-outline-primary">Details</a>
|
||||
<a href="#" class="btn btn-outline-success">
|
||||
<img src = "{% static 'images/icons/cart.svg' %}" alt="Koszyk"/>
|
||||
</a>
|
||||
|
||||
<div class="row d-flex mt-3">
|
||||
<div class="col">
|
||||
<input type="number" id="quantity{{item.id}}" name="quantity" min="1" value="1" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button class="btn btn-outline-success add-to-cart-button"
|
||||
data-product-id="{{item.id}}"
|
||||
data-csrf-token="{{csrf_token}}"
|
||||
data-add-to-cart-url={% url "cart-action-add-product" %}
|
||||
data-bs-toggle="modal" data-bs-target="#addToCartModal"
|
||||
>
|
||||
<img src="{% static 'images/icons/cart.svg' %}" alt="Koszyk"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="card rounded-3 mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row d-flex justify-content-between align-items-center">
|
||||
<div class="col-md-2 col-lg-2 col-xl-2">
|
||||
<img
|
||||
src="{{item.product.main_image.url}}"
|
||||
class="img-fluid rounded-3">
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-3">
|
||||
<p class="lead fw-normal mb-2">{{item.product.title}}</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-3 col-xl-2 d-flex">
|
||||
{{item.quantity}}
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 offset-lg-1">
|
||||
<h5 class="mb-0">{{item.product.price}} ZŁ</h5>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,8 +1,28 @@
|
|||
{% extends 'base.html'%}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="addToCartModal" aria-labelledby="addToCartLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Added to cart</h5>
|
||||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close" >
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Item has been added to cart.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Continue shopping</button>
|
||||
<a href="{% url 'cart' %}" class="btn btn-primary">Go to cart</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mb-3">
|
||||
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||
{% for item in items %}
|
||||
|
@ -11,7 +31,6 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<nav aria-label="Page navigation example">
|
||||
|
@ -27,4 +46,5 @@
|
|||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,3 +1,36 @@
|
|||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from store.models import ProductAuthor, ProductCategory, ProductTemplate, Product
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
# TODO - this is fine for now, but we'll want to use factoryboy for this:
|
||||
# https://factoryboy.readthedocs.io/en/stable/
|
||||
# TODO - test have to rewritten - I'll do it tommorow
|
||||
class CartTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.productid = 1
|
||||
self.author = ProductAuthor.objects.create(name='Test Author')
|
||||
self.category = ProductCategory.objects.create(name='Test Category')
|
||||
self.template = ProductTemplate.objects.create(category=self.category,
|
||||
author=self.author,
|
||||
title='Test title',
|
||||
code='Test code',
|
||||
description='Test description'
|
||||
)
|
||||
self.product = Product.objects.create(template=self.template,
|
||||
price=10.99)
|
||||
self.cart_url = reverse('view_cart')
|
||||
|
||||
def test_add_to_cart(self):
|
||||
response = self.client.post(reverse('add_to_cart',
|
||||
args=[self.productid]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_remove_from_cart(self):
|
||||
response = self.client.post(reverse('remove_from_cart',
|
||||
args=[self.productid]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_view_cart(self):
|
||||
response = self.client.get(self.cart_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from django.urls import path
|
||||
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('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"),
|
||||
] + router.urls
|
|
@ -0,0 +1,144 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from django.views.generic import (
|
||||
TemplateView,
|
||||
View
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from rest_framework.viewsets import ViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from store.cart import SessionCart
|
||||
from store.serializers import (
|
||||
CartProductSerializer,
|
||||
CartProductAddSerializer
|
||||
)
|
||||
from store.forms import CustomerDataForm
|
||||
from store.models import (
|
||||
CustomerData,
|
||||
Order,
|
||||
OrderProduct
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
template_name = 'store/cart.html'
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["cart"] = SessionCart(self.request)
|
||||
return context
|
||||
|
||||
|
||||
class CartActionView(ViewSet):
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="list-products")
|
||||
def list_products(self, request):
|
||||
# get cart items
|
||||
cart = SessionCart(self.request)
|
||||
items = cart.get_items()
|
||||
serializer = CartProductSerializer(instance=items, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
def add_product(self, request):
|
||||
cart = SessionCart(self.request)
|
||||
serializer = CartProductAddSerializer(data=request.POST)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=400)
|
||||
serializer.save(cart)
|
||||
|
||||
items = cart.get_items()
|
||||
serializer = CartProductSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
def remove_product(self, request):
|
||||
cart = SessionCart(self.request)
|
||||
product_id = request.POST.get("product_id")
|
||||
cart.remove_item(product_id)
|
||||
|
||||
items = cart.get_items()
|
||||
serializer = CartProductSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
@action(detail=True, methods=["put"])
|
||||
def update_product(self, request, product_id):
|
||||
cart = SessionCart(self.request)
|
||||
product_id = request.POST.get("product_id")
|
||||
cart.update_item_quantity(product_id, request.PUT["quantity"])
|
||||
items = cart.get_items()
|
||||
serializer = CartProductSerializer(instance=items, many=True)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
class OrderView(View):
|
||||
template_name = "store/order.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = {}
|
||||
context["form"] = CustomerDataForm()
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cart = SessionCart(self.request)
|
||||
if cart.is_empty():
|
||||
# TODO - messages
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# TODO - messages
|
||||
cart = SessionCart(self.request)
|
||||
if cart.is_empty():
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
form = CustomerDataForm(request.POST)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data()
|
||||
context["form"] = form
|
||||
return render(request, self.template_name, context)
|
||||
customer_data = form.save()
|
||||
request.session["customer_data_id"] = customer_data.id
|
||||
# TODO - add this page
|
||||
return HttpResponseRedirect(reverse("order-confirm"))
|
||||
|
||||
|
||||
class OrderConfirmView(View):
|
||||
template_name = "store/order_confirm.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
|
||||
return {
|
||||
"cart": SessionCart(self.request),
|
||||
"customer_data": customer_data
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
cart = SessionCart(self.request)
|
||||
if cart.is_empty():
|
||||
# TODO - messages
|
||||
return HttpResponseRedirect(reverse("cart"))
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def post(self, request):
|
||||
customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"])
|
||||
cart = SessionCart(self.request)
|
||||
order = Order.objects.create(
|
||||
customer=customer_data,
|
||||
)
|
||||
OrderProduct.objects.create_from_cart(order, cart)
|
||||
cart.clear()
|
||||
self.request.session.pop("customer_data_id")
|
||||
# TODO - to be tested
|
||||
# TODO - messages
|
||||
return HttpResponseRedirect(reverse("cart"))
|
Ładowanie…
Reference in New Issue