feat: update order payment methods and refine profile edit functionality
All checks were successful
Build and Push Docker Image / build (push) Successful in 43s
All checks were successful
Build and Push Docker Image / build (push) Successful in 43s
- Replaced `Order.PaymentMethod` options with "cash" and "card" while adjusting related code and templates - Set "cash" as the default payment method for orders and checkout forms - Added profile editing functionality with `ProfileForm`, allowing users to update names - Improved feedback messages across profile and order management templates - Updated catalog to handle empty filter results with a "reset filters" option
This commit is contained in:
2
.env
2
.env
@@ -12,7 +12,7 @@ SITE_NAME=SantehRay
|
||||
LOGIN_URL=/users/login/
|
||||
LOGIN_REDIRECT_URL=/users/profile/
|
||||
LOGOUT_REDIRECT_URL=/users/login/
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
||||
EMAIL_HOST=smtp.example.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=
|
||||
|
||||
@@ -266,6 +266,16 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
||||
|
||||
price_filter_action = _build_catalog_url(selected_category.id)
|
||||
|
||||
filters_active = any([
|
||||
search_query,
|
||||
min_price_raw,
|
||||
max_price_raw,
|
||||
filter_sale,
|
||||
filter_new,
|
||||
selected_color_id_raw,
|
||||
])
|
||||
reset_filters_url = _build_catalog_url(selected_category.id, selected_subcategory.id)
|
||||
|
||||
sidebar_sale_items = (
|
||||
Item.objects.filter(discount_price__isnull=False, is_available=True)
|
||||
.prefetch_related('images')
|
||||
@@ -294,6 +304,8 @@ def index(request: HttpRequest, category: str | None = None) -> HttpResponse:
|
||||
'all_items_url': all_items_url,
|
||||
'selected_category_url': selected_category_url,
|
||||
'price_filter_action': price_filter_action,
|
||||
'filters_active': filters_active,
|
||||
'reset_filters_url': reset_filters_url,
|
||||
'current_search_query': search_query,
|
||||
'current_min_price': min_price_raw,
|
||||
'current_max_price': max_price_raw,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from orders.models import Order
|
||||
|
||||
|
||||
PHONE_RE = re.compile(r"^\+?[\d\s\-()]{7,20}$")
|
||||
|
||||
|
||||
class CheckoutForm(forms.Form):
|
||||
delivery_method = forms.ChoiceField(
|
||||
choices=Order.DeliveryMethod.choices,
|
||||
@@ -14,7 +20,7 @@ class CheckoutForm(forms.Form):
|
||||
address = forms.CharField(max_length=512, required=False, label="Адрес")
|
||||
payment_method = forms.ChoiceField(
|
||||
choices=Order.PaymentMethod.choices,
|
||||
initial=Order.PaymentMethod.CARD,
|
||||
initial=Order.PaymentMethod.CASH,
|
||||
widget=forms.RadioSelect,
|
||||
label="Способ оплаты",
|
||||
)
|
||||
@@ -35,7 +41,10 @@ class CheckoutForm(forms.Form):
|
||||
return self.cleaned_data["customer_name"].strip()
|
||||
|
||||
def clean_phone(self):
|
||||
return self.cleaned_data["phone"].strip()
|
||||
phone = self.cleaned_data["phone"].strip()
|
||||
if not PHONE_RE.match(phone):
|
||||
raise ValidationError("Укажите корректный номер телефона.")
|
||||
return phone
|
||||
|
||||
def clean_region(self):
|
||||
return self.cleaned_data["region"].strip()
|
||||
|
||||
18
orders/migrations/0004_alter_order_payment_method.py
Normal file
18
orders/migrations/0004_alter_order_payment_method.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-29 16:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0003_cartitem_selected_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='payment_method',
|
||||
field=models.CharField(choices=[('cash', 'Наличкой при получении'), ('card', 'Картой при получении')], default='cash', max_length=16, verbose_name='Способ оплаты'),
|
||||
),
|
||||
]
|
||||
@@ -71,9 +71,8 @@ class Order(models.Model):
|
||||
PICKUP = "pickup", "Самовывоз"
|
||||
|
||||
class PaymentMethod(models.TextChoices):
|
||||
CARD = "card", "Банковская карта"
|
||||
CREDIT = "credit", "Кредит"
|
||||
INSTALLMENT = "installment", "Рассрочка"
|
||||
CASH = "cash", "Наличкой при получении"
|
||||
CARD = "card", "Картой при получении"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
PAID = "paid", "Оплачен"
|
||||
@@ -98,7 +97,7 @@ class Order(models.Model):
|
||||
payment_method = models.CharField(
|
||||
max_length=16,
|
||||
choices=PaymentMethod.choices,
|
||||
default=PaymentMethod.CARD,
|
||||
default=PaymentMethod.CASH,
|
||||
verbose_name="Способ оплаты",
|
||||
)
|
||||
promo_code = models.CharField(max_length=64, blank=True, verbose_name="Промокод")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ def checkout(request: HttpRequest) -> HttpResponse:
|
||||
except Exception:
|
||||
messages.error(request, "Заказ создан, но письмо с деталями не удалось отправить. Мы свяжемся с вами.")
|
||||
else:
|
||||
messages.success(request, "Оплата прошла успешно. Детали заказа отправлены на почту.")
|
||||
messages.success(request, "Заказ успешно оформлен. Детали отправлены на почту.")
|
||||
|
||||
return redirect("orders:success", order_id=order.id)
|
||||
|
||||
|
||||
@@ -50,6 +50,34 @@ aside {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.catalog-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.catalog-empty__reset {
|
||||
display: inline-block;
|
||||
min-height: 42px;
|
||||
padding: 12px 28px;
|
||||
border: 1px solid #63768d;
|
||||
background: #63768d;
|
||||
color: #ffffff;
|
||||
font-family: 'Noah', sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.catalog-empty__reset:hover {
|
||||
background: #554971;
|
||||
border-color: #554971;
|
||||
}
|
||||
|
||||
.catalog-filter-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -461,6 +489,7 @@ aside {
|
||||
}
|
||||
|
||||
.product-summary__color-name {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
color: #554971;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,10 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.auth-form .auth-actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.register-auth-actions {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<main class="item-list">
|
||||
{% include "_fragments/item-grid.html" with items=page_obj empty_message="В этой категории пока нет товаров." %}
|
||||
{% if page_obj.object_list %}
|
||||
{% include "_fragments/item-grid.html" with items=page_obj empty_message="В этой категории пока нет товаров." %}
|
||||
{% elif filters_active %}
|
||||
<div class="catalog-empty">
|
||||
<p class="item-list-empty">По выбранным фильтрам ничего не найдено.</p>
|
||||
<a class="catalog-empty__reset" href="{{ reset_filters_url }}">Сбросить фильтры</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "_fragments/item-grid.html" with items=page_obj empty_message="В этой категории пока нет товаров." %}
|
||||
{% endif %}
|
||||
|
||||
{% if paginator.num_pages > 1 %}
|
||||
<nav class="catalog-pagination">
|
||||
|
||||
@@ -112,11 +112,11 @@
|
||||
<legend>СПОСОБ ДОСТАВКИ</legend>
|
||||
<div class="checkout-options checkout-options--two">
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="delivery_method" value="courier" {% if checkout_form.delivery_method.value == "courier" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<input type="radio" name="delivery_method" value="courier" required {% if checkout_form.delivery_method.value == "courier" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>Курьерская доставка</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="delivery_method" value="pickup" {% if checkout_form.delivery_method.value == "pickup" %}checked{% endif %}>
|
||||
<input type="radio" name="delivery_method" value="pickup" required {% if checkout_form.delivery_method.value == "pickup" %}checked{% endif %}>
|
||||
<span>Самовывоз</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -141,18 +141,14 @@
|
||||
|
||||
<fieldset class="checkout-fieldset">
|
||||
<legend>СПОСОБ ОПЛАТЫ</legend>
|
||||
<div class="checkout-options checkout-options--three">
|
||||
<div class="checkout-options checkout-options--two">
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="card" {% if checkout_form.payment_method.value == "card" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>Банковская карта</span>
|
||||
<input type="radio" name="payment_method" value="cash" required {% if checkout_form.payment_method.value == "cash" or not checkout_form.is_bound %}checked{% endif %}>
|
||||
<span>Наличкой при получении</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="credit" {% if checkout_form.payment_method.value == "credit" %}checked{% endif %}>
|
||||
<span>Кредит</span>
|
||||
</label>
|
||||
<label class="checkout-radio">
|
||||
<input type="radio" name="payment_method" value="installment" {% if checkout_form.payment_method.value == "installment" %}checked{% endif %}>
|
||||
<span>Рассрочка</span>
|
||||
<input type="radio" name="payment_method" value="card" required {% if checkout_form.payment_method.value == "card" %}checked{% endif %}>
|
||||
<span>Картой при получении</span>
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.payment_method.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
@@ -163,15 +159,15 @@
|
||||
<div class="checkout-grid checkout-grid--contacts">
|
||||
<label class="checkout-input checkout-input--wide">
|
||||
<span>ФИО*</span>
|
||||
<input type="text" name="customer_name" value="{{ checkout_form.customer_name.value|default:'' }}" placeholder="ФИО*" autocomplete="name">
|
||||
<input type="text" name="customer_name" value="{{ checkout_form.customer_name.value|default:'' }}" placeholder="ФИО*" autocomplete="name" required>
|
||||
</label>
|
||||
<label class="checkout-input">
|
||||
<span>Телефон*</span>
|
||||
<input type="tel" name="phone" value="{{ checkout_form.phone.value|default:'' }}" placeholder="Телефон*" autocomplete="tel">
|
||||
<input type="tel" name="phone" value="{{ checkout_form.phone.value|default:'' }}" placeholder="Телефон*" autocomplete="tel" inputmode="tel" pattern="\+?[\d\s\-()]{7,20}" required>
|
||||
</label>
|
||||
<label class="checkout-input">
|
||||
<span>E-mail*</span>
|
||||
<input type="email" name="email" value="{{ checkout_form.email.value|default:'' }}" placeholder="E-mail*" autocomplete="email">
|
||||
<input type="email" name="email" value="{{ checkout_form.email.value|default:'' }}" placeholder="E-mail*" autocomplete="email" required>
|
||||
</label>
|
||||
</div>
|
||||
{% for error in checkout_form.customer_name.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
@@ -201,7 +197,7 @@
|
||||
<button class="checkout-submit" type="submit">ОФОРМИТЬ ЗАКАЗ</button>
|
||||
|
||||
<label class="checkout-checkbox checkout-checkbox--terms">
|
||||
<input type="checkbox" name="terms_accepted" value="on" {% if checkout_form.terms_accepted.value %}checked{% endif %}>
|
||||
<input type="checkbox" name="terms_accepted" value="on" required {% if checkout_form.terms_accepted.value %}checked{% endif %}>
|
||||
<span>Согласен с условиями продажи, оплаты и возврата</span>
|
||||
</label>
|
||||
{% for error in checkout_form.terms_accepted.errors %}<p class="field-error">{{ error }}</p>{% endfor %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div style="max-width:640px;margin:0 auto;background:#ffffff;border:1px solid #d6dde6;padding:32px;font-family:Arial,sans-serif;color:#36213e;">
|
||||
<h1 style="margin:0 0 16px;font-size:28px;">Заказ №{{ order.id }} оплачен</h1>
|
||||
<h1 style="margin:0 0 16px;font-size:28px;">Заказ №{{ order.id }} оформлен</h1>
|
||||
<p>Здравствуйте, {{ order.customer_name }}!</p>
|
||||
<p>Ваш заказ успешно оформлен и оплачен.</p>
|
||||
<p>Ваш заказ успешно оформлен.</p>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;margin:24px 0;">
|
||||
<thead>
|
||||
@@ -26,7 +26,7 @@
|
||||
<p><strong>Скидка:</strong> {{ order.discount_total }} руб.</p>
|
||||
<p><strong>Доставка:</strong> {{ order.delivery_total }} руб.</p>
|
||||
<p><strong>Подъём на этаж:</strong> {{ order.floor_lift_total }} руб.</p>
|
||||
<p style="font-size:18px;"><strong>Итого к оплате:</strong> {{ order.total }} руб.</p>
|
||||
<p style="font-size:18px;"><strong>Итого:</strong> {{ order.total }} руб.</p>
|
||||
|
||||
<p><strong>Доставка:</strong> {{ order.get_delivery_method_display }}</p>
|
||||
{% if order.region or order.address %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Здравствуйте, {{ order.customer_name }}!
|
||||
|
||||
Ваш заказ №{{ order.id }} успешно оплачен.
|
||||
Ваш заказ №{{ order.id }} успешно оформлен.
|
||||
|
||||
Состав заказа:
|
||||
{% for item in order.items.all %}
|
||||
@@ -11,7 +11,7 @@
|
||||
Скидка: {{ order.discount_total }} руб.
|
||||
Доставка: {{ order.delivery_total }} руб.
|
||||
Подъём на этаж: {{ order.floor_lift_total }} руб.
|
||||
Итого к оплате: {{ order.total }} руб.
|
||||
Итого: {{ order.total }} руб.
|
||||
|
||||
Доставка: {{ order.get_delivery_method_display }}
|
||||
{% if order.region or order.address %}Адрес: {{ order.region }}{% if order.address %}, {{ order.address }}{% endif %}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ site_name }}: заказ №{{ order.id }} оплачен
|
||||
{{ site_name }}: заказ №{{ order.id }} оформлен
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "_layouts/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Оплата прошла успешно{% endblock %}
|
||||
{% block title %}Заказ оформлен{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static "css/cart.css" %}">
|
||||
@@ -10,11 +10,11 @@
|
||||
{% block content %}
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' url_3=request.path text_3='Заказ оплачен' %}
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' url_3=request.path text_3='Заказ оформлен' %}
|
||||
|
||||
<main class="container cart-page">
|
||||
<section class="order-success">
|
||||
<p class="order-success__eyebrow">Оплата прошла успешно</p>
|
||||
<p class="order-success__eyebrow">Заказ оформлен</p>
|
||||
<h1>Заказ №{{ order.id }} оформлен</h1>
|
||||
<p class="order-success__lead">
|
||||
Мы отправили письмо с деталями заказа на {{ order.email }}. Менеджер свяжется с вами при необходимости.
|
||||
|
||||
@@ -24,7 +24,48 @@
|
||||
<div class="auth-copy-block">
|
||||
<h2 class="auth-panel-title">Данные аккаунта</h2>
|
||||
<p class="auth-panel-text"><strong>Email:</strong> {{ request.user.email }}</p>
|
||||
<p class="auth-panel-text"><strong>Имя:</strong> {{ request.user.get_full_name|default:"Не указано" }}</p>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<p class="auth-panel-text">{{ message }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form class="auth-form" method="post" action="{% url 'users:profile' %}">
|
||||
{% csrf_token %}
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.first_name.id_for_label }}">Имя</label>
|
||||
<input
|
||||
id="{{ form.first_name.id_for_label }}"
|
||||
name="{{ form.first_name.html_name }}"
|
||||
type="text"
|
||||
value="{{ form.first_name.value|default:'' }}"
|
||||
autocomplete="given-name"
|
||||
required
|
||||
>
|
||||
{% for error in form.first_name.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="auth-field">
|
||||
<label for="{{ form.last_name.id_for_label }}">Фамилия</label>
|
||||
<input
|
||||
id="{{ form.last_name.id_for_label }}"
|
||||
name="{{ form.last_name.html_name }}"
|
||||
type="text"
|
||||
value="{{ form.last_name.value|default:'' }}"
|
||||
autocomplete="family-name"
|
||||
required
|
||||
>
|
||||
{% for error in form.last_name.errors %}
|
||||
<p class="auth-field-error">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="auth-actions">
|
||||
<button class="auth-submit" type="submit">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="{% url 'users:logout' %}">
|
||||
{% csrf_token %}
|
||||
<div class="auth-actions">
|
||||
|
||||
@@ -77,6 +77,15 @@ class RegisterForm(UserCreationForm):
|
||||
return user
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
first_name = forms.CharField(label="Имя", max_length=150)
|
||||
last_name = forms.CharField(label="Фамилия", max_length=150)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("first_name", "last_name")
|
||||
|
||||
|
||||
class SantehRayPasswordResetForm(PasswordResetForm):
|
||||
email = forms.EmailField(label="Электронная почта")
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model, login as auth_login, logout as auth_logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
@@ -13,7 +14,7 @@ from django.utils.encoding import force_str
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from users.forms import LoginForm, RegisterForm, SantehRayPasswordResetForm, SantehRaySetPasswordForm
|
||||
from users.forms import LoginForm, ProfileForm, RegisterForm, SantehRayPasswordResetForm, SantehRaySetPasswordForm
|
||||
from users.services import send_activation_email
|
||||
|
||||
|
||||
@@ -71,8 +72,15 @@ def activate(request, uidb64, token):
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def profile(request):
|
||||
return render(request, "users/profile.html", context={})
|
||||
form = ProfileForm(request.POST or None, instance=request.user)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Данные профиля обновлены.")
|
||||
return redirect("users:profile")
|
||||
|
||||
return render(request, "users/profile.html", context={"form": form})
|
||||
|
||||
|
||||
class SantehRayPasswordResetView(PasswordResetView):
|
||||
|
||||
Reference in New Issue
Block a user