Переделал систему комметариев и сделал профиль более дружелюбным

This commit is contained in:
NikDizell 2026-06-19 16:27:14 +03:00
parent 8bde7db28a
commit d4967bc4e5
21 changed files with 470 additions and 43 deletions

View File

@ -5,9 +5,11 @@ from django.utils.html import strip_tags
class CommentForm(forms.ModelForm): class CommentForm(forms.ModelForm):
parent_id = forms.IntegerField(widget=forms.HiddenInput, required=False)
class Meta: class Meta:
model = Comment model = Comment
fields = ['author_name', 'author_email', 'content'] fields = ['author_name', 'author_email', 'content', 'parent_id']
widgets = { widgets = {
'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}), 'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}),
'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}), 'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}),

View File

@ -81,6 +81,18 @@ class Comment(models.Model):
) )
author_name = models.CharField(max_length=100, verbose_name="Имя") author_name = models.CharField(max_length=100, verbose_name="Имя")
author_email = models.EmailField(verbose_name="Email") 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="Комментарий") content = models.TextField(verbose_name="Комментарий")
is_moderated = models.BooleanField(default=False, verbose_name="Промодерировано") is_moderated = models.BooleanField(default=False, verbose_name="Промодерировано")
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")

View File

@ -2,6 +2,11 @@ from typing import Optional, List
from django.db.models import F, QuerySet from django.db.models import F, QuerySet
from django.core.cache import cache from django.core.cache import cache
from .models import Article, Category, Comment 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]: 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) 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( comment = Comment.objects.create(
article=article, article=article,
author_name=data['author_name'], author_name=data['author_name'],
author_email=data['author_email'], author_email=data['author_email'],
content=data['content'] content=data['content'],
user=user,
parent=parent,
is_moderated=False
) )
# Здесь можно отправить уведомление администратору # Здесь можно отправить уведомление администратору
return comment return comment

13
blog/signals.py Normal file
View 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]
# отправка письма (можно асинхронно)

View File

@ -87,4 +87,29 @@
padding: 0; padding: 0;
background: none; background: none;
color: inherit; 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;
} }

View File

@ -2,6 +2,7 @@
{% load static %} {% load static %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% load seo_tags %} {% load seo_tags %}
{% load blog_extras %}
{% block extra_css %} {% block extra_css %}
<link id="theme-css-1c" rel="stylesheet" href="{% static 'programmer/css/1c-light.min.css' %}"> <link id="theme-css-1c" rel="stylesheet" href="{% static 'programmer/css/1c-light.min.css' %}">
@ -29,26 +30,19 @@
<!-- Комментарии --> <!-- Комментарии -->
<h3>Комментарии</h3> <h3>Комментарии</h3>
{% for comment in moderated_comments %} <div id="comments-section">
<div class="modern-card" style="margin-bottom: 1rem;"> {% show_comments comments=root_comments %}
<p><strong>{{ comment.author_name }}</strong> ({{ comment.time_create|date:"d.m.Y H:i" }})</p> </div>
<p>{{ comment.content|linebreaks }}</p>
{% if not comment.is_moderated %}
<p><em>Комментарий ожидает модерации</em></p>
{% endif %}
</div>
{% empty %}
<p>Пока нет комментариев. Будьте первым!</p>
{% endfor %}
<!-- Форма добавления комментария --> <!-- Форма добавления комментария -->
<div class="modern-card"> <div class="modern-card" id="comment-form">
<h4>Добавить комментарий</h4> <h4>Добавить комментарий</h4>
{% if user_is_authenticated %} {% if user_is_authenticated %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
{{ form.parent_id }}
<button type="submit" class="btn btn-primary">Отправить</button> <button type="submit" class="btn btn-primary">Отправить</button>
</form> </form>
{% else %} {% else %}
@ -73,7 +67,6 @@
</div> </div>
</div> </div>
<!-- Скрипт для модального окна -->
<script src="{% static 'programmer/js/recall.js' %}"></script> <script src="{% static 'programmer/js/recall.js' %}"></script>
{% endblock %} {% endblock %}
@ -81,18 +74,41 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Находим все изображения внутри .content-card (основной контент статьи) // Клик по изображениям в контенте
document.querySelectorAll('.content-card img').forEach(function(img) { document.querySelectorAll('.content-card img').forEach(function(img) {
// Игнорируем, если изображение уже обёрнуто в ссылку
if (img.closest('a')) return; if (img.closest('a')) return;
// Меняем курсор, чтобы показать, что изображение кликабельно
img.style.cursor = 'pointer'; img.style.cursor = 'pointer';
img.addEventListener('click', function() { img.addEventListener('click', function() {
// Вызываем глобальную функцию openModal из recall.js
ImageModal.open(this.src, this.alt || 'Изображение из статьи'); 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> </script>
{% endblock %} {% endblock %}

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

View File

@ -1,10 +1,12 @@
from django import template from django import template
from django.utils.html import strip_tags from django.utils.html import strip_tags
from blog.models import Comment
import html import html
register = template.Library() register = template.Library()
@register.filter @register.filter
def clean_html(value): def clean_html(value):
"""Удаляет HTML-теги и заменяет сущности на символы.""" """Удаляет HTML-теги и заменяет сущности на символы."""
@ -12,3 +14,22 @@ def clean_html(value):
value = html.unescape(value) value = html.unescape(value)
# Удаляем все HTML-теги # Удаляем все HTML-теги
return strip_tags(value) 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
}

View File

@ -4,7 +4,7 @@ from django.urls import reverse_lazy
from django.contrib import messages from django.contrib import messages
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from .models import Article, Category from .models import Article, Category, Comment
from .services import ( from .services import (
get_published_articles, get_article_by_slug, get_published_articles, get_article_by_slug,
increment_article_views, add_comment_to_article increment_article_views, add_comment_to_article
@ -103,9 +103,21 @@ class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
form = CommentForm(request.POST, request=request) form = CommentForm(request.POST, request=request)
if form.is_valid(): 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( add_comment_to_article(
article=self.object, article=self.object,
data=form.cleaned_data data=form.cleaned_data,
user=request.user,
parent=parent
) )
messages.success(request, "✅ Ваш комментарий отправлен на модерацию.") messages.success(request, "✅ Ваш комментарий отправлен на модерацию.")
return redirect(self.object.get_absolute_url()) return redirect(self.object.get_absolute_url())
@ -121,6 +133,9 @@ class ArticleDetailView(MenuContextMixin, BreadcrumbMixin, DetailView):
if 'form' not in kwargs: if 'form' not in kwargs:
context['form'] = CommentForm(request=self.request) 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({ context.update({
'moderated_comments': self.object.comments.filter(is_moderated=True), 'moderated_comments': self.object.comments.filter(is_moderated=True),
'user_is_authenticated': self.request.user.is_authenticated, 'user_is_authenticated': self.request.user.is_authenticated,

View File

@ -3,7 +3,7 @@ from django.utils.html import format_html
from django.shortcuts import render from django.shortcuts import render
from django_ckeditor_5.widgets import CKEditor5Widget from django_ckeditor_5.widgets import CKEditor5Widget
from .models import ProductCategory, Configuration, Product, Order 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) @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} заказов.') self.message_user(request, f'Уведомления отправлены для {count} заказов.')
resend_order_notification.short_description = 'Переотправить email уведомления' resend_order_notification.short_description = 'Переотправить email уведомления'

View File

@ -196,6 +196,8 @@ class Profile(models.Model):
specialization = models.CharField(max_length=100, blank=True, verbose_name='Специализация') specialization = models.CharField(max_length=100, blank=True, verbose_name='Специализация')
avatar = models.ImageField(upload_to='avatars/%Y/%m/%d/', 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='Получать уведомления') 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): def __str__(self):
return f'Профиль {self.user.username}' return f'Профиль {self.user.username}'

View 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();
});

View File

@ -432,6 +432,19 @@
</div> </div>
</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()">&times;</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;"> <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> <button onclick="CallbackModal.open()" class="btn btn-primary pulse">🎯 Получить консультацию</button>

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

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

View File

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

View File

@ -1,5 +1,6 @@
{% extends 'programmer/base.html' %} {% extends 'programmer/base.html' %}
{% load django_bootstrap5 %} {% load django_bootstrap5 %}
{% load static %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
@ -31,21 +32,22 @@
</div> </div>
</div> </div>
<div class="content-card mt-3"> <div class="skills-grid">
<h3>Мои действия</h3> <div class="skill-category" data-url="{% url 'my_callbacks' %}" onclick="openModalContent(this)">
<div class="skills-grid"> <h4>📋 Мои заявки</h4>
<div class="skill-category"> <p>Просмотр истории обращений</p>
<h4>📋 Мои заявки</h4> </div>
<p>Просмотр истории обращений</p> <div class="skill-category" data-url="{% url 'my_comments' %}" onclick="openModalContent(this)">
</div> <h4>💬 Мои комментарии</h4>
<div class="skill-category"> <p>Управление комментариями к статьям</p>
<h4>💬 Мои комментарии</h4> </div>
<p>Управление комментариями к статьям</p> <div class="skill-category" data-url="{% url 'notification_settings' %}" onclick="openModalContent(this)">
</div> <h4>🔔 Настройки уведомлений</h4>
<div class="skill-category"> <p>Email-рассылка и оповещения</p>
<h4>🔔 Настройки уведомлений</h4>
<p>Email-рассылка и оповещения</p>
</div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'programmer/js/profile.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -35,6 +35,9 @@ urlpatterns = [
path('privacy/', PrivacyPolicyView.as_view(), name='privacy'), path('privacy/', PrivacyPolicyView.as_view(), name='privacy'),
path('yandex_cdc16c33291495b9.html/', yandex_html, name='yandex_cdc16c33291495b9'), path('yandex_cdc16c33291495b9.html/', yandex_html, name='yandex_cdc16c33291495b9'),
path('requisites/', RequisitesPageView.as_view(), name='requisites'), 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'),
] ]

View File

@ -21,6 +21,8 @@ from .services import get_published_queryset, track_page_view
from typing import Any, Dict, Type from typing import Any, Dict, Type
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
from blog.models import Comment
from products.models import Order
import os import os
@ -484,3 +486,29 @@ def robots_txt(request):
def yandex_html(request: HttpRequest) -> HttpResponse: def yandex_html(request: HttpRequest) -> HttpResponse:
"""Отдает yandex_cdc16c33291495b9.html.""" """Отдает yandex_cdc16c33291495b9.html."""
return render(request, 'programmer/yandex_cdc16c33291495b9.html', content_type='text/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})

View File

@ -16,9 +16,17 @@ const CallbackModal = {
}, 5000); }, 5000);
}, },
open() { open(productTitle) {
const modal = document.getElementById(this.modalId); const modal = document.getElementById(this.modalId);
if (modal) { if (modal) {
if (productTitle) {
const questionField = modal.querySelector('#id_question');
if (questionField) {
questionField.value = `Запрос цены по ПП "${productTitle}"`;
}
}
modal.style.display = 'block'; modal.style.display = 'block';
const floatingBtn = document.getElementById(this.floatingBtnId); const floatingBtn = document.getElementById(this.floatingBtnId);
if (floatingBtn && this.shown) { if (floatingBtn && this.shown) {

View 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();
});