From c65be53032413fd467bb958f53cd8be5072d499e Mon Sep 17 00:00:00 2001 From: NikDizell Date: Thu, 26 Feb 2026 19:16:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9,=20=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D0=BB=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D0=B8,=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=A2=D0=93=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OneCprogsite/settings.py | 9 + OneCprogsite/urls.py | 1 + blog/forms.py | 13 + blog/templates/blog/article_detail.html | 19 +- blog/views.py | 16 ++ programmer/forms.py | 105 +++++++- programmer/mixins.py | 32 ++- programmer/models.py | 24 ++ programmer/templates/programmer/base.html | 96 +++++-- programmer/templates/programmer/index.html | 7 + programmer/templates/programmer/login.html | 25 ++ programmer/templates/programmer/profile.html | 51 ++++ .../templates/programmer/profile_edit.html | 26 ++ programmer/templates/programmer/register.html | 40 +++ programmer/urls.py | 6 + programmer/views.py | 255 ++++++------------ requirements.txt | 4 +- 17 files changed, 526 insertions(+), 203 deletions(-) create mode 100644 programmer/templates/programmer/login.html create mode 100644 programmer/templates/programmer/profile.html create mode 100644 programmer/templates/programmer/profile_edit.html create mode 100644 programmer/templates/programmer/register.html 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 @@

Добавить комментарий

-
- {% csrf_token %} - {{ form.as_p }} - -
+ + {% if user_is_authenticated %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% 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 @@