diff --git a/OneCprogsite/settings.py b/OneCprogsite/settings.py
index 4a354f8..5671409 100644
--- a/OneCprogsite/settings.py
+++ b/OneCprogsite/settings.py
@@ -92,6 +92,7 @@ INSTALLED_APPS = [
'blog.apps.BlogConfig',
'taggit',
'django_ckeditor_5',
+ 'captcha',
]
MIDDLEWARE = [
@@ -256,4 +257,12 @@ CKEDITOR_5_CONFIGS = {
}
}
+LOGIN_REDIRECT_URL = 'profile'
+LOGOUT_REDIRECT_URL = 'home'
+LOGIN_URL = 'login'
+
+CAPTCHA_LENGTH = 6
+CAPTCHA_FONT_SIZE = 30
+CAPTCHA_IMAGE_SIZE = (150, 50)
+
diff --git a/OneCprogsite/urls.py b/OneCprogsite/urls.py
index e5bf171..67985f1 100644
--- a/OneCprogsite/urls.py
+++ b/OneCprogsite/urls.py
@@ -25,6 +25,7 @@ urlpatterns = [
path('', include('programmer.urls')),
path('blog/', include('blog.urls')),
path('ckeditor5/', include('django_ckeditor_5.urls')),
+ path('captcha/', include('captcha.urls')),
# path('', index, name='home'),
# path('about/', about, name='about'),
# path('solution/', solution, name='solution'),
diff --git a/blog/forms.py b/blog/forms.py
index f6b87b0..620bf01 100644
--- a/blog/forms.py
+++ b/blog/forms.py
@@ -15,6 +15,19 @@ class CommentForm(forms.ModelForm):
'autocomplete': 'off'}),
}
+ def __init__(self, *args, **kwargs):
+ self.request = kwargs.pop('request', None)
+ super().__init__(*args, **kwargs)
+
+ if self.request and self.request.user.is_authenticated:
+ # Скрываем поля имени и email
+ self.fields['author_name'].widget = forms.HiddenInput()
+ self.fields['author_email'].widget = forms.HiddenInput()
+ # Заполняем начальные значения из профиля
+ self.initial['author_name'] = self.request.user.get_full_name() or self.request.user.username
+ self.initial['author_email'] = self.request.user.email
+
+
def clean_content(self):
content = self.cleaned_data['content']
# Удаляем HTML-теги и проверяем, остался ли текст
diff --git a/blog/templates/blog/article_detail.html b/blog/templates/blog/article_detail.html
index 6b7aa97..c0e224f 100644
--- a/blog/templates/blog/article_detail.html
+++ b/blog/templates/blog/article_detail.html
@@ -42,10 +42,19 @@
Добавить комментарий
-
+
+ {% if user_is_authenticated %}
+
+ {% else %}
+
+ Только авторизованные пользователи могут оставлять комментарии.
+ Войдите или
+ зарегистрируйтесь
+
+ {% endif %}
{% endblock %}
\ No newline at end of file
diff --git a/blog/views.py b/blog/views.py
index 01f8396..4c7bfbb 100644
--- a/blog/views.py
+++ b/blog/views.py
@@ -71,6 +71,11 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
+ # Проверяем, авторизован ли пользователь
+ if not request.user.is_authenticated:
+ messages.error(request, "❌ Только авторизованные пользователи могут оставлять комментарии.")
+ return redirect(request.path) # redirect('login') или redirect(request.path) чтобы остаться на странице
+
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
@@ -78,6 +83,11 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
else:
return self.form_invalid(form)
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs['request'] = self.request
+ return kwargs
+
def get_initial(self):
"""Возвращает пустые начальные данные для формы комментария."""
initial = super().get_initial()
@@ -85,6 +95,11 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
return initial
def form_valid(self, form):
+ # Для авторизованных пользователей гарантируем наличие имени и email
+ if self.request.user.is_authenticated:
+ form.cleaned_data['author_name'] = self.request.user.get_full_name() or self.request.user.username
+ form.cleaned_data['author_email'] = self.request.user.email
+
comment = add_comment_to_article(
article=self.object,
data=form.cleaned_data
@@ -104,6 +119,7 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
"проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, "
"миграция 1С 7.7, кейсы 1С, примеры работ 1С"
),
+ 'user_is_authenticated': self.request.user.is_authenticated,
})
# Добавляем только одобренные комментарии
context['moderated_comments'] = self.object.comments.filter(is_moderated=True)
diff --git a/programmer/forms.py b/programmer/forms.py
index 9726b2c..d0d66c3 100644
--- a/programmer/forms.py
+++ b/programmer/forms.py
@@ -1,8 +1,16 @@
+from captcha.fields import CaptchaField
from django import forms
-from .models import CallbackRequest
+from django.contrib.auth import get_user_model
+from django.contrib.auth.forms import UserCreationForm
+from .models import CallbackRequest, Profile
+
+
+User = get_user_model()
class CallbackForm(forms.ModelForm):
+ captcha = CaptchaField(label='Введите текст с картинки', required=True)
+
class Meta:
model = CallbackRequest
fields = ['name', 'phone', 'email', 'question']
@@ -30,4 +38,97 @@ class CallbackForm(forms.ModelForm):
'phone': 'Телефон',
'email': 'Электронная почта',
'question': 'Ваш вопрос'
- }
\ No newline at end of file
+ }
+
+ def __init__(self, *args, **kwargs):
+ # Извлекаем request из kwargs, если он передан
+ self.request = kwargs.pop('request', None)
+ super().__init__(*args, **kwargs)
+
+ # Если пользователь авторизован — удаляем поле капчи
+ if self.request and self.request.user.is_authenticated:
+ del self.fields['captcha']
+
+
+class UserEditForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ['first_name', 'last_name', 'email']
+ widgets = {
+ 'first_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Имя'}),
+ 'last_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Фамилия'}),
+ 'email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Email'}),
+ }
+
+
+class ProfileForm(forms.ModelForm):
+ class Meta:
+ model = Profile
+ fields = ['phone', 'company', 'specialization', 'avatar', 'email_notifications']
+ widgets = {
+ 'phone': forms.TextInput(attrs={'class': 'form-input', 'placeholder': '+7 (___) ___-__-__'}),
+ 'email_notifications': forms.CheckboxInput(attrs={'class': 'form-checkbox'}),
+ 'company': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Название компании'}),
+ 'specialization': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Специализация'}),
+ 'avatar': forms.FileInput(attrs={'class': 'form-file'}),
+ 'email_notifications': forms.CheckboxInput(attrs={'class': 'form-checkbox'}),
+ }
+
+
+class RegistrationForm(UserCreationForm):
+ # Поля пользователя
+ first_name = forms.CharField(
+ max_length=30,
+ required=True,
+ label='Имя',
+ widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Имя'})
+ )
+ last_name = forms.CharField(
+ max_length=30,
+ required=False,
+ label='Фамилия',
+ widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Фамилия'})
+ )
+ email = forms.EmailField(
+ required=True,
+ widget=forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Email'})
+ )
+
+ # Поля профиля
+ phone = forms.CharField(
+ max_length=20,
+ required=False,
+ label='Телефон',
+ widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': '+7 (___) ___-__-__'})
+ )
+ company = forms.CharField(
+ max_length=255,
+ required=False,
+ label='Компания',
+ widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Компания'})
+ )
+ specialization = forms.CharField(
+ max_length=100,
+ required=False,
+ label='Специализация',
+ widget=forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Специализация'})
+ )
+
+ class Meta:
+ model = User
+ fields = ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.first_name = self.cleaned_data['first_name']
+ user.last_name = self.cleaned_data['last_name']
+ user.email = self.cleaned_data['email']
+ if commit:
+ user.save()
+ # Создаём или обновляем профиль
+ profile, created = Profile.objects.get_or_create(user=user)
+ profile.phone = self.cleaned_data['phone']
+ profile.company = self.cleaned_data['company']
+ profile.specialization = self.cleaned_data['specialization']
+ profile.save()
+ return user
\ No newline at end of file
diff --git a/programmer/mixins.py b/programmer/mixins.py
index 389a761..f4743ee 100644
--- a/programmer/mixins.py
+++ b/programmer/mixins.py
@@ -18,17 +18,33 @@ class MenuContextMixin(ContextMixin):
"""
Миксин для добавления меню в контекст.
"""
- menu = [
- {'title': "Главная", 'url_name': 'home'},
- {'title': "Проекты", 'url_name': 'solution'},
- {'title': "Статьи", 'url_name': 'blog'},
- {'title': "Отзывы", 'url_name': 'recall'},
- {'title': "Обо мне", 'url_name': 'about'}
- ]
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
- context['menu'] = self.menu
+
+ # Основное меню (всегда)
+ main_menu = [
+ {'title': "Главная", 'url_name': 'home'},
+ {'title': "Проекты", 'url_name': 'solution'},
+ {'title': "Статьи", 'url_name': 'blog'},
+ {'title': "Отзывы", 'url_name': 'recall'},
+ {'title': "Обо мне", 'url_name': 'about'},
+ ]
+
+ # Пользовательское меню (зависит от статуса)
+ if self.request.user.is_authenticated:
+ user_menu = [
+ {'title': "Профиль", 'url_name': 'profile'},
+ {'title': "Выйти", 'url_name': 'logout'}, # будет обработан как форма
+ ]
+ else:
+ user_menu = [
+ {'title': "Войти", 'url_name': 'login'},
+ {'title': "Регистрация", 'url_name': 'register'},
+ ]
+
+ context['main_menu'] = main_menu
+ context['user_menu'] = user_menu
return context
diff --git a/programmer/models.py b/programmer/models.py
index b62f79b..9643587 100644
--- a/programmer/models.py
+++ b/programmer/models.py
@@ -1,3 +1,4 @@
+from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from django.utils import timezone
@@ -171,6 +172,29 @@ class Visitor(models.Model):
]
+class Profile(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name='Пользователь')
+ phone = models.CharField(max_length=20, blank=True, verbose_name='Телефон')
+ company = models.CharField(max_length=255, 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='Аватар')
+ email_notifications = models.BooleanField(default=True, verbose_name='Получать уведомления')
+
+ def __str__(self):
+ return f'Профиль {self.user.username}'
+
+ class Meta:
+ verbose_name = 'Профиль'
+ verbose_name_plural = 'Профили'
+
+
+@receiver(post_save, sender=User)
+def create_or_save_user_profile(sender, instance, **kwargs):
+ # Получаем или создаём профиль, затем сохраняем
+ profile, created = Profile.objects.get_or_create(user=instance)
+ profile.save()
+
+
@receiver([post_save, post_delete], sender=Home)
@receiver([post_save, post_delete], sender=Solution)
@receiver([post_save, post_delete], sender=Competence)
diff --git a/programmer/templates/programmer/base.html b/programmer/templates/programmer/base.html
index ff10cda..50d695b 100644
--- a/programmer/templates/programmer/base.html
+++ b/programmer/templates/programmer/base.html
@@ -214,6 +214,15 @@
/* ===== MOBILE RESPONSIVE STYLES ===== */
@media (max-width: 768px) {
+
+ .nav-actions .theme-switcher {
+ display: none;
+ }
+
+ .user-menu {
+ display: none;
+ }
+
.hero-title {
font-size: 2.5rem;
}
@@ -422,26 +431,28 @@