Переделал систему комметариев и сделал профиль более дружелюбным
This commit is contained in:
parent
8bde7db28a
commit
d4967bc4e5
@ -5,9 +5,11 @@ from django.utils.html import strip_tags
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
parent_id = forms.IntegerField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['author_name', 'author_email', 'content']
|
||||
fields = ['author_name', 'author_email', 'content', 'parent_id']
|
||||
widgets = {
|
||||
'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}),
|
||||
'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}),
|
||||
|
||||
@ -81,6 +81,18 @@ class Comment(models.Model):
|
||||
)
|
||||
author_name = models.CharField(max_length=100, verbose_name="Имя")
|
||||
author_email = models.EmailField(verbose_name="Email")
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
verbose_name="Пользователь"
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='replies',
|
||||
verbose_name='Ответ на'
|
||||
)
|
||||
content = models.TextField(verbose_name="Комментарий")
|
||||
is_moderated = models.BooleanField(default=False, verbose_name="Промодерировано")
|
||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
|
||||
@ -2,6 +2,11 @@ from typing import Optional, List
|
||||
from django.db.models import F, QuerySet
|
||||
from django.core.cache import cache
|
||||
from .models import Article, Category, Comment
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_published_articles(category_slug: Optional[str] = None) -> QuerySet[Article]:
|
||||
"""
|
||||
@ -29,13 +34,16 @@ def increment_article_views(article: Article) -> None:
|
||||
"""Увеличивает счётчик просмотров статьи."""
|
||||
Article.objects.filter(pk=article.pk).update(views_count=F('views_count') + 1)
|
||||
|
||||
def add_comment_to_article(article: Article, data: dict) -> Comment:
|
||||
def add_comment_to_article(article: Article, data: dict, user: User = None, parent: Comment = None) -> Comment:
|
||||
"""Создание комментария к статье (без модерации)."""
|
||||
comment = Comment.objects.create(
|
||||
article=article,
|
||||
author_name=data['author_name'],
|
||||
author_email=data['author_email'],
|
||||
content=data['content']
|
||||
content=data['content'],
|
||||
user=user,
|
||||
parent=parent,
|
||||
is_moderated=False
|
||||
)
|
||||
# Здесь можно отправить уведомление администратору
|
||||
return comment
|
||||
|
||||
13
blog/signals.py
Normal file
13
blog/signals.py
Normal file
@ -0,0 +1,13 @@
|
||||
# blog/signals.py
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from .models import Article
|
||||
from programmer.models import Profile
|
||||
from django.core.mail import send_mail
|
||||
|
||||
@receiver(post_save, sender=Article)
|
||||
def notify_about_new_article(sender, instance, created, **kwargs):
|
||||
if created and instance.is_published:
|
||||
profiles = Profile.objects.filter(notify_new_articles=True, user__is_active=True)
|
||||
emails = [p.user.email for p in profiles if p.user.email]
|
||||
# отправка письма (можно асинхронно)
|
||||
@ -88,3 +88,28 @@
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
border-left: 2px solid var(--border-light);
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.badge-warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.comment-content {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.reply-btn {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
{% load static %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load seo_tags %}
|
||||
{% load blog_extras %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link id="theme-css-1c" rel="stylesheet" href="{% static 'programmer/css/1c-light.min.css' %}">
|
||||
@ -29,26 +30,19 @@
|
||||
|
||||
<!-- Комментарии -->
|
||||
<h3>Комментарии</h3>
|
||||
{% for comment in moderated_comments %}
|
||||
<div class="modern-card" style="margin-bottom: 1rem;">
|
||||
<p><strong>{{ comment.author_name }}</strong> ({{ comment.time_create|date:"d.m.Y H:i" }})</p>
|
||||
<p>{{ comment.content|linebreaks }}</p>
|
||||
{% if not comment.is_moderated %}
|
||||
<p><em>Комментарий ожидает модерации</em></p>
|
||||
{% endif %}
|
||||
<div id="comments-section">
|
||||
{% show_comments comments=root_comments %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>Пока нет комментариев. Будьте первым!</p>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Форма добавления комментария -->
|
||||
<div class="modern-card">
|
||||
<div class="modern-card" id="comment-form">
|
||||
<h4>Добавить комментарий</h4>
|
||||
|
||||
{% if user_is_authenticated %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
{{ form.parent_id }}
|
||||
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||
</form>
|
||||
{% else %}
|
||||
@ -73,7 +67,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Скрипт для модального окна -->
|
||||
<script src="{% static 'programmer/js/recall.js' %}"></script>
|
||||
|
||||
{% endblock %}
|
||||
@ -81,18 +74,41 @@
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Находим все изображения внутри .content-card (основной контент статьи)
|
||||
// Клик по изображениям в контенте
|
||||
document.querySelectorAll('.content-card img').forEach(function(img) {
|
||||
// Игнорируем, если изображение уже обёрнуто в ссылку
|
||||
if (img.closest('a')) return;
|
||||
|
||||
// Меняем курсор, чтобы показать, что изображение кликабельно
|
||||
img.style.cursor = 'pointer';
|
||||
img.addEventListener('click', function() {
|
||||
// Вызываем глобальную функцию openModal из recall.js
|
||||
ImageModal.open(this.src, this.alt || 'Изображение из статьи');
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка кнопок "Ответить" (делегирование)
|
||||
document.getElementById('comments-section').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.reply-btn');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
|
||||
const parentId = btn.dataset.commentId;
|
||||
const author = btn.dataset.author;
|
||||
|
||||
// Находим текст комментария
|
||||
const commentItem = btn.closest('.comment-item');
|
||||
const contentDiv = commentItem.querySelector('.comment-content');
|
||||
let quotedText = contentDiv.textContent.trim();
|
||||
|
||||
// Формируем цитату
|
||||
const quote = `> @${author}: ${quotedText}\n\n`;
|
||||
const textarea = document.getElementById('id_content');
|
||||
textarea.value = quote;
|
||||
|
||||
// Устанавливаем parent_id
|
||||
document.getElementById('id_parent_id').value = parentId;
|
||||
|
||||
// Фокус на поле ввода и прокрутка к форме
|
||||
textarea.focus();
|
||||
document.getElementById('comment-form').scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
34
blog/templates/blog/includes/comment_tree.html
Normal file
34
blog/templates/blog/includes/comment_tree.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% load blog_extras %}
|
||||
{% load static %}
|
||||
|
||||
{% for comment in comments %}
|
||||
<div class="comment-item" style="margin-left: {{ depth|add:20 }}px; border-left: 2px solid var(--border-light); padding-left: 15px; margin-bottom: 15px;">
|
||||
<div class="comment-meta">
|
||||
<strong>{{ comment.author_name }}</strong>
|
||||
<span style="color: var(--text-light); font-size: 0.9rem;">#{{ comment.id }}</span>
|
||||
<span style="color: var(--text-light); font-size: 0.9rem;">{{ comment.time_create|date:"d.m.Y H:i" }}</span>
|
||||
{% if not comment.is_moderated %}
|
||||
<span class="badge badge-warning">⏳ На модерации</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="comment-content" style="margin: 5px 0;">
|
||||
{{ comment.content|linebreaks }}
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<button class="btn btn-sm btn-outline-primary reply-btn"
|
||||
data-comment-id="{{ comment.id }}"
|
||||
data-author="{{ comment.author_name }}">
|
||||
Ответить
|
||||
</button>
|
||||
{% endif %}
|
||||
{% with replies=comment.replies.all %}
|
||||
{% if replies %}
|
||||
<div class="replies" style="margin-top: 10px;">
|
||||
{% show_comments replies depth=forloop.parentloop.counter0|add:depth %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>Комментариев пока нет.</p>
|
||||
{% endfor %}
|
||||
@ -1,10 +1,12 @@
|
||||
from django import template
|
||||
from django.utils.html import strip_tags
|
||||
from blog.models import Comment
|
||||
import html
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def clean_html(value):
|
||||
"""Удаляет HTML-теги и заменяет сущности на символы."""
|
||||
@ -12,3 +14,22 @@ def clean_html(value):
|
||||
value = html.unescape(value)
|
||||
# Удаляем все HTML-теги
|
||||
return strip_tags(value)
|
||||
|
||||
|
||||
@register.inclusion_tag('blog/includes/comment_tree.html', takes_context=True)
|
||||
def show_comments(context, comments, depth=0):
|
||||
request = context.get('request')
|
||||
user = request.user if request and request.user.is_authenticated else None
|
||||
|
||||
filtered = []
|
||||
for c in comments:
|
||||
if c.is_moderated:
|
||||
filtered.append(c)
|
||||
elif user and c.user == user:
|
||||
filtered.append(c)
|
||||
|
||||
return {
|
||||
'comments': filtered,
|
||||
'depth': depth,
|
||||
'user': user
|
||||
}
|
||||
@ -4,7 +4,7 @@ from django.urls import reverse_lazy
|
||||
from django.contrib import messages
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from .models import Article, Category
|
||||
from .models import Article, Category, Comment
|
||||
from .services import (
|
||||
get_published_articles, get_article_by_slug,
|
||||
increment_article_views, add_comment_to_article
|
||||
@ -103,9 +103,21 @@ class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
|
||||
|
||||
form = CommentForm(request.POST, request=request)
|
||||
if form.is_valid():
|
||||
|
||||
parent_id = form.cleaned_data.get('parent_id')
|
||||
parent = None
|
||||
|
||||
if parent_id:
|
||||
try:
|
||||
parent = Comment.objects.get(id=parent_id, article=self.object)
|
||||
except Comment.DoesNotExist:
|
||||
pass
|
||||
|
||||
add_comment_to_article(
|
||||
article=self.object,
|
||||
data=form.cleaned_data
|
||||
data=form.cleaned_data,
|
||||
user=request.user,
|
||||
parent=parent
|
||||
)
|
||||
messages.success(request, "✅ Ваш комментарий отправлен на модерацию.")
|
||||
return redirect(self.object.get_absolute_url())
|
||||
@ -121,6 +133,9 @@ class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
|
||||
if 'form' not in kwargs:
|
||||
context['form'] = CommentForm(request=self.request)
|
||||
|
||||
root_comments = self.object.comments.filter(parent__isnull=True, is_moderated=True)
|
||||
context['root_comments'] = root_comments
|
||||
|
||||
context.update({
|
||||
'moderated_comments': self.object.comments.filter(is_moderated=True),
|
||||
'user_is_authenticated': self.request.user.is_authenticated,
|
||||
|
||||
@ -3,7 +3,7 @@ from django.utils.html import format_html
|
||||
from django.shortcuts import render
|
||||
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||
from .models import ProductCategory, Configuration, Product, Order
|
||||
from programmer.utils.email_notifications import send_multiple_order_notifications
|
||||
from programmer.utils.email_notifications import send_multiple_order_notifications, send_multiple_order_notifications_async
|
||||
|
||||
|
||||
@admin.register(ProductCategory)
|
||||
@ -85,7 +85,8 @@ class OrderAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Переотправляет уведомления для выбранных заказов.
|
||||
"""
|
||||
count = send_multiple_order_notifications(queryset)
|
||||
# count = send_multiple_order_notifications(queryset)
|
||||
count = send_multiple_order_notifications_async(queryset)
|
||||
self.message_user(request, f'Уведомления отправлены для {count} заказов.')
|
||||
resend_order_notification.short_description = 'Переотправить email уведомления'
|
||||
|
||||
|
||||
@ -196,6 +196,8 @@ class Profile(models.Model):
|
||||
specialization = models.CharField(max_length=100, blank=True, verbose_name='Специализация')
|
||||
avatar = models.ImageField(upload_to='avatars/%Y/%m/%d/', blank=True, verbose_name='Аватар')
|
||||
email_notifications = models.BooleanField(default=True, verbose_name='Получать уведомления')
|
||||
notify_new_articles = models.BooleanField(default=True, verbose_name="О новых статьях")
|
||||
notify_new_products = models.BooleanField(default=True, verbose_name="О новых продуктах")
|
||||
|
||||
def __str__(self):
|
||||
return f'Профиль {self.user.username}'
|
||||
|
||||
30
programmer/static/programmer/js/profile.js
Normal file
30
programmer/static/programmer/js/profile.js
Normal file
@ -0,0 +1,30 @@
|
||||
// programmer/static/programmer/js/profile.js
|
||||
function openModalContent(el) {
|
||||
const url = el.dataset.url;
|
||||
const title = el.querySelector('h4').innerText;
|
||||
const modal = document.getElementById('contentModal');
|
||||
const body = document.getElementById('contentModalBody');
|
||||
const titleEl = document.getElementById('contentModalTitle');
|
||||
titleEl.textContent = title;
|
||||
body.innerHTML = '<p>Загрузка...</p>';
|
||||
modal.style.display = 'block';
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
body.innerHTML = html;
|
||||
})
|
||||
.catch(() => {
|
||||
body.innerHTML = '<p style="color:red;">Ошибка загрузки. Попробуйте позже.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeContentModal() {
|
||||
document.getElementById('contentModal').style.display = 'none';
|
||||
}
|
||||
// Закрытие по клику на фон и ESC
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target === document.getElementById('contentModal')) closeContentModal();
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeContentModal();
|
||||
});
|
||||
@ -432,6 +432,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Универсальное модальное окно -->
|
||||
<div id="contentModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 700px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="contentModalTitle">Заголовок</h3>
|
||||
<button class="modal-close" onclick="closeContentModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="contentModalBody">
|
||||
<!-- сюда подгружается содержимое -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Плавающая кнопка -->
|
||||
<div id="floatingButton" class="floating-btn" style="position: fixed; bottom: 30px; right: 30px; z-index: 10000; display: none;">
|
||||
<button onclick="CallbackModal.open()" class="btn btn-primary pulse">🎯 Получить консультацию</button>
|
||||
|
||||
80
programmer/templates/programmer/includes/my_callbacks.html
Normal file
80
programmer/templates/programmer/includes/my_callbacks.html
Normal file
@ -0,0 +1,80 @@
|
||||
<style>
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.status-badge.new { background: #dc3545; color: #fff; }
|
||||
.status-badge.read { background: #28a745; color: #fff; }
|
||||
.status-badge.processed { background: #17a2b8; color: #fff; }
|
||||
.status-badge.unprocessed { background: #ffc107; color: #212529; }
|
||||
.status-badge.sent { background: #6c757d; color: #fff; }
|
||||
.list-item {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.list-item:last-child { border-bottom: none; }
|
||||
.list-item .meta { font-size: 0.9rem; color: var(--text-light); }
|
||||
</style>
|
||||
|
||||
<h3>📞 Мои заявки на звонок</h3>
|
||||
{% if callbacks %}
|
||||
{% for cb in callbacks %}
|
||||
<div class="list-item">
|
||||
<div>
|
||||
<strong>{{ cb.name }}</strong>
|
||||
<span class="status-badge {% if cb.is_read %}read{% else %}new{% endif %}">
|
||||
{% if cb.is_read %}✅ Прочитано{% else %}🆕 Новое{% endif %}
|
||||
</span>
|
||||
<span class="status-badge {% if cb.is_processed %}processed{% else %}unprocessed{% endif %}">
|
||||
{% if cb.is_processed %}✅ Обработано{% else %}⏳ В обработке{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
{{ cb.time_create|date:"d.m.Y H:i" }} •
|
||||
📞 {{ cb.phone }} •
|
||||
✉️ {{ cb.email|default:"email не указан" }}
|
||||
{% if cb.question %}
|
||||
<br><span style="font-style:italic;">{{ cb.question|truncatechars:80 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>Заявок пока нет.</p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>🛒 Мои заказы</h3>
|
||||
{% if orders %}
|
||||
{% for order in orders %}
|
||||
<div class="list-item">
|
||||
<div>
|
||||
<strong>{{ order.product.title }}</strong>
|
||||
<span class="status-badge {% if order.is_processed %}processed{% else %}unprocessed{% endif %}">
|
||||
{% if order.is_processed %}✅ Обработан{% else %}⏳ В обработке{% endif %}
|
||||
</span>
|
||||
{% if order.notification_sent %}
|
||||
<span class="status-badge sent">📨 Уведомление отправлено</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
{{ order.created_at|date:"d.m.Y H:i" }} •
|
||||
📞 {{ order.phone }} •
|
||||
✉️ {{ order.email }}
|
||||
{% if order.configuration %}
|
||||
<br>⚙️ Конфигурация: {{ order.configuration.name }}
|
||||
{% endif %}
|
||||
{% if order.comment %}
|
||||
<br><span style="font-style:italic;">{{ order.comment|truncatechars:80 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>Заказов пока нет.</p>
|
||||
{% endif %}
|
||||
59
programmer/templates/programmer/includes/my_comments.html
Normal file
59
programmer/templates/programmer/includes/my_comments.html
Normal file
@ -0,0 +1,59 @@
|
||||
<style>
|
||||
.comment-status {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.comment-status.moderation {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
.comment-status.approved {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
.list-item {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.list-item:last-child { border-bottom: none; }
|
||||
.list-item .meta { font-size: 0.9rem; color: var(--text-light); }
|
||||
.list-item .article-link {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.list-item .article-link:hover {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>💬 Мои комментарии</h3>
|
||||
{% if comments %}
|
||||
{% for comment in comments %}
|
||||
<div class="list-item">
|
||||
<div>
|
||||
<a href="{{ comment.article.get_absolute_url }}" class="article-link">
|
||||
{{ comment.article.title }}
|
||||
</a>
|
||||
{% if not comment.is_moderated %}
|
||||
<span class="comment-status moderation">⏳ На модерации</span>
|
||||
{% else %}
|
||||
<span class="comment-status approved">✅ Одобрен</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
{{ comment.time_create|date:"d.m.Y H:i" }}
|
||||
<br>
|
||||
{{ comment.content|truncatechars:150 }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>Вы ещё не оставили ни одного комментария.</p>
|
||||
{% endif %}
|
||||
@ -0,0 +1,25 @@
|
||||
<h3>🔔 Настройки уведомлений</h3>
|
||||
<form id="notif-form">
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
<input type="checkbox" name="notify_new_articles" {% if profile.notify_new_articles %}checked{% endif %}>
|
||||
Уведомлять о новых статьях
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="checkbox" name="notify_new_products" {% if profile.notify_new_products %}checked{% endif %}>
|
||||
Уведомлять о новых продуктах
|
||||
</label><br>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById('notif-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
fetch('{% url "notification_settings" %}', {
|
||||
method: 'POST',
|
||||
body: new FormData(this),
|
||||
headers: { 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value }
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => { if (data.status === 'ok') alert('Настройки сохранены!'); });
|
||||
});
|
||||
</script>
|
||||
@ -1,5 +1,6 @@
|
||||
{% extends 'programmer/base.html' %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
@ -31,21 +32,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-card mt-3">
|
||||
<h3>Мои действия</h3>
|
||||
<div class="skills-grid">
|
||||
<div class="skill-category">
|
||||
<div class="skill-category" data-url="{% url 'my_callbacks' %}" onclick="openModalContent(this)">
|
||||
<h4>📋 Мои заявки</h4>
|
||||
<p>Просмотр истории обращений</p>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<div class="skill-category" data-url="{% url 'my_comments' %}" onclick="openModalContent(this)">
|
||||
<h4>💬 Мои комментарии</h4>
|
||||
<p>Управление комментариями к статьям</p>
|
||||
</div>
|
||||
<div class="skill-category">
|
||||
<div class="skill-category" data-url="{% url 'notification_settings' %}" onclick="openModalContent(this)">
|
||||
<h4>🔔 Настройки уведомлений</h4>
|
||||
<p>Email-рассылка и оповещения</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'programmer/js/profile.js' %}"></script>
|
||||
{% endblock %}
|
||||
@ -35,6 +35,9 @@ urlpatterns = [
|
||||
path('privacy/', PrivacyPolicyView.as_view(), name='privacy'),
|
||||
path('yandex_cdc16c33291495b9.html/', yandex_html, name='yandex_cdc16c33291495b9'),
|
||||
path('requisites/', RequisitesPageView.as_view(), name='requisites'),
|
||||
path('profile/callbacks/', views.my_callbacks, name='my_callbacks'),
|
||||
path('profile/comments/', views.my_comments, name='my_comments'),
|
||||
path('profile/notifications/', views.notification_settings, name='notification_settings'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ from .services import get_published_queryset, track_page_view
|
||||
from typing import Any, Dict, Type
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from blog.models import Comment
|
||||
from products.models import Order
|
||||
import os
|
||||
|
||||
|
||||
@ -484,3 +486,29 @@ def robots_txt(request):
|
||||
def yandex_html(request: HttpRequest) -> HttpResponse:
|
||||
"""Отдает yandex_cdc16c33291495b9.html."""
|
||||
return render(request, 'programmer/yandex_cdc16c33291495b9.html', content_type='text/html')
|
||||
|
||||
@login_required
|
||||
def my_callbacks(request):
|
||||
callbacks = CallbackRequest.objects.filter(email=request.user.email).order_by('-time_create')
|
||||
# Можно также фильтровать по телефону, если он заполнен в профиле, но email — универсальнее.
|
||||
orders = Order.objects.filter(email=request.user.email).order_by('-created_at') # если поле есть
|
||||
return render(request, 'programmer/includes/my_callbacks.html', {
|
||||
'callbacks': callbacks,
|
||||
'orders': orders,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def my_comments(request):
|
||||
comments = Comment.objects.filter(user=request.user).select_related('article').order_by('-time_create')
|
||||
return render(request, 'programmer/includes/my_comments.html', {'comments': comments})
|
||||
|
||||
@login_required
|
||||
def notification_settings(request):
|
||||
profile = request.user.profile
|
||||
if request.method == 'POST':
|
||||
# обработка формы (можно через AJAX POST)
|
||||
profile.notify_new_articles = request.POST.get('notify_new_articles') == 'on'
|
||||
profile.notify_new_products = request.POST.get('notify_new_products') == 'on'
|
||||
profile.save()
|
||||
return JsonResponse({'status': 'ok'})
|
||||
return render(request, 'programmer/includes/notification_settings.html', {'profile': profile})
|
||||
|
||||
@ -16,9 +16,17 @@ const CallbackModal = {
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
open() {
|
||||
open(productTitle) {
|
||||
const modal = document.getElementById(this.modalId);
|
||||
if (modal) {
|
||||
|
||||
if (productTitle) {
|
||||
const questionField = modal.querySelector('#id_question');
|
||||
if (questionField) {
|
||||
questionField.value = `Запрос цены по ПП "${productTitle}"`;
|
||||
}
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
const floatingBtn = document.getElementById(this.floatingBtnId);
|
||||
if (floatingBtn && this.shown) {
|
||||
|
||||
30
static/programmer/js/profile.js
Normal file
30
static/programmer/js/profile.js
Normal file
@ -0,0 +1,30 @@
|
||||
// programmer/static/programmer/js/profile.js
|
||||
function openModalContent(el) {
|
||||
const url = el.dataset.url;
|
||||
const title = el.querySelector('h4').innerText;
|
||||
const modal = document.getElementById('contentModal');
|
||||
const body = document.getElementById('contentModalBody');
|
||||
const titleEl = document.getElementById('contentModalTitle');
|
||||
titleEl.textContent = title;
|
||||
body.innerHTML = '<p>Загрузка...</p>';
|
||||
modal.style.display = 'block';
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
body.innerHTML = html;
|
||||
})
|
||||
.catch(() => {
|
||||
body.innerHTML = '<p style="color:red;">Ошибка загрузки. Попробуйте позже.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeContentModal() {
|
||||
document.getElementById('contentModal').style.display = 'none';
|
||||
}
|
||||
// Закрытие по клику на фон и ESC
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target === document.getElementById('contentModal')) closeContentModal();
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeContentModal();
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user