Добавил регистрацию и авторизацию пользователей, скрыл меню на мобильном приложении, убрал ТГ канал

This commit is contained in:
NikDizell 2026-02-26 19:16:18 +03:00
parent 64e4037057
commit c65be53032
17 changed files with 526 additions and 203 deletions

View File

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

View File

@ -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'),

View File

@ -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-теги и проверяем, остался ли текст

View File

@ -42,10 +42,19 @@
<!-- Форма добавления комментария -->
<div class="modern-card">
<h4>Добавить комментарий</h4>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
{% if user_is_authenticated %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
{% else %}
<p class="alert alert-info">
Только авторизованные пользователи могут оставлять комментарии.
<a href="{% url 'login' %}?next={{ request.path|urlencode }}">Войдите</a> или
<a href="{% url 'register' %}?next={{ request.path|urlencode }}">зарегистрируйтесь</a>
</p>
{% endif %}
</div>
{% endblock %}

View File

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

View File

@ -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']
@ -31,3 +39,96 @@ class CallbackForm(forms.ModelForm):
'email': 'Электронная почта',
'question': 'Ваш вопрос'
}
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

View File

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

View File

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

View File

@ -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 @@
<nav class="nav">
<a href="{% url 'home' %}" class="logo">
<img src="{% static 'programmer/images/main.ico' %}" alt="Logo" class="logo-img">
<span class="logo-text">Программист 1С</span>
<span class="logo-text">SNA</span>
</a>
<!-- Десктопное меню -->
<ul class="nav-menu">
{% for m in menu %}
{% for item in main_menu %}
<li class="nav-item">
<a href="{% url m.url_name %}" class="nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
{{m.title}}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}">
{{ item.title }}
</a>
</li>
{% endfor %}
</ul>
<div class="nav-actions">
<a href="https://t.me/odinesina_prog" target="_blank" class="telegram-btn">
<span class="telegram-icon">
<img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">
</span>
</a>
<!-- Telegram -->
<!-- <a href="https://t.me/odinesina_prog" target="_blank" class="telegram-btn">-->
<!-- <span class="telegram-icon">-->
<!-- <img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">-->
<!-- </span>-->
<!-- </a>-->
<!-- Theme Toggle Switch -->
<div class="theme-switcher">
@ -453,6 +464,31 @@
</label>
</div>
<div class="user-menu">
{% for item in user_menu %}
{% if item.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link" style="border: none; background: none; padding: 0.5rem 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ item.title }}
</button>
</form>
{% elif item.url_name == 'login' %}
<a href="{% url 'login' %}?next={{ request.path|urlencode }}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
</a>
{% elif item.url_name == 'register' %}
<a href="{% url 'register' %}?next={{ request.path|urlencode }}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
</a>
{% else %}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
</a>
{% endif %}
{% endfor %}
</div>
<!-- Кнопка мобильного меню -->
<button class="mobile-menu-btn" id="mobileMenuBtn">
@ -473,21 +509,45 @@
</div>
<ul class="mobile-nav-menu">
{% for m in menu %}
{% for m in main_menu %}
<li class="mobile-nav-item">
<a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
{{m.title}}
</a>
{% if m.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: block;">
{% csrf_token %}
<button type="submit" class="mobile-nav-link btn btn-link" style="border: none; background: none; width: 100%; text-align: left; padding: 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ m.title }}
</button>
</form>
{% else %}
<a href="{% url m.url_name %}" class="mobile-nav-link {% if request.resolver_match.url_name == m.url_name %}active{% endif %}">
{{ m.title }}
</a>
{% endif %}
</li>
{% endfor %}
{% for item in user_menu %}
{% if item.url_name == 'logout' %}
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="nav-link btn btn-link" style="border: none; background: none; padding: 0.5rem 1rem; font: inherit; cursor: pointer; color: inherit;">
{{ item.title }}
</button>
</form>
{% else %}
<a href="{% url item.url_name %}" class="nav-link {% if request.resolver_match.url_name == item.url_name %}active{% endif %}" style="display: inline-block; padding: 0.5rem 1rem;">
{{ item.title }}
</a>
{% endif %}
{% endfor %}
</ul>
<div class="mobile-nav-actions">
<a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="width: 100%; text-align: center;">
<span class="telegram-icon">
<img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">
</span>
</a>
<!-- <a href="https://t.me/odinesina_prog" target="_blank" class="btn btn-primary" style="width: 100%; text-align: center;">-->
<!-- <span class="telegram-icon">-->
<!-- <img src="{% static 'programmer/images/share_tg.png' %}" alt="Telegram" width="20" height="20">-->
<!-- </span>-->
<!-- </a>-->
<div class="theme-switcher" style="justify-content: center;">
<input type="checkbox" id="mobile-theme-toggle" class="theme-toggle-checkbox" checked>
<label for="mobile-theme-toggle" class="theme-toggle-label">

View File

@ -56,6 +56,13 @@
{{ form.question }}
</div>
{% if form.captcha %}
<div class="form-group">
<label for="id_captcha">Защитный код *</label>
{{ form.captcha }}
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary" style="width: 100%;">
📨 Отправить заявку

View File

@ -0,0 +1,25 @@
{% extends 'programmer/base.html' %}
{% load django_bootstrap5 %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Вход на сайт</h1>
</div>
<div class="content-card" style="max-width: 400px; margin: 0 auto;">
<form method="post">
{% csrf_token %}
{% if request.GET.next %}
<input type="hidden" name="next" value="{{ request.GET.next }}">
{% endif %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary" style="width: 100%;">Войти</button>
</form>
<hr>
<p class="text-center">
Нет аккаунта? <a href="{% url 'register' %}">Зарегистрироваться</a>
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends 'programmer/base.html' %}
{% load django_bootstrap5 %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Личный кабинет</h1>
<p class="page-subtitle">{{ user.username }}</p>
</div>
<div class="content-card">
<div class="row">
<div class="col-md-4">
{% if user.profile.avatar %}
<img src="{{ user.profile.avatar.url }}" class="img-fluid rounded-circle" alt="Аватар">
{% else %}
<div class="text-center p-5 bg-light rounded-circle">
<span style="font-size: 3rem;">👤</span>
</div>
{% endif %}
</div>
<div class="col-md-8">
<h3>Информация</h3>
<p><strong>Имя:</strong> {{ user.first_name|default:"не указано" }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Телефон:</strong> {{ user.profile.phone|default:"не указан" }}</p>
<p><strong>Компания:</strong> {{ user.profile.company|default:"не указана" }}</p>
<p><strong>Специализация:</strong> {{ user.profile.specialization|default:"не указана" }}</p>
<a href="{% url 'profile_edit' %}" class="btn btn-primary">Редактировать профиль</a>
</div>
</div>
</div>
<div class="content-card mt-3">
<h3>Мои действия</h3>
<div class="skills-grid">
<div class="skill-category">
<h4>📋 Мои заявки</h4>
<p>Просмотр истории обращений</p>
</div>
<div class="skill-category">
<h4>💬 Мои комментарии</h4>
<p>Управление комментариями к статьям</p>
</div>
<div class="skill-category">
<h4>🔔 Настройки уведомлений</h4>
<p>Email-рассылка и оповещения</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends 'programmer/base.html' %}
{% load django_bootstrap5 %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Редактирование профиля</h1>
<p class="page-subtitle">{{ user.username }}</p>
</div>
<div class="content-card" style="max-width: 600px; margin: 0 auto;">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<h3>Основная информация</h3>
{% bootstrap_form user_form %}
<h3>Дополнительная информация</h3>
{% bootstrap_form profile_form %}
<div class="form-actions mt-3">
<button type="submit" class="btn btn-primary">Сохранить</button>
<a href="{% url 'profile' %}" class="btn btn-secondary">Отмена</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends 'programmer/base.html' %}
{% load django_bootstrap5 %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Регистрация</h1>
<p class="page-subtitle">Создайте аккаунт для доступа к закрытым материалам</p>
</div>
<div class="content-card" style="max-width: 600px; margin: 0 auto;">
<form method="post">
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}">
{% endif %}
<h3>Учётные данные</h3>
{% bootstrap_field form.username %}
{% bootstrap_field form.password1 %}
{% bootstrap_field form.password2 %}
<h3>Личная информация</h3>
{% bootstrap_field form.first_name %}
{% bootstrap_field form.last_name %}
{% bootstrap_field form.email %}
<h3>Информация о компании (необязательно)</h3>
{% bootstrap_field form.phone %}
{% bootstrap_field form.company %}
{% bootstrap_field form.specialization %}
<button type="submit" class="btn btn-primary" style="width: 100%;">Зарегистрироваться</button>
</form>
<hr>
<p class="text-center">
Уже есть аккаунт?
<a href="{% url 'login' %}{% if next %}?next={{ next|urlencode }}{% endif %}">Войти</a>
</p>
</div>
{% endblock %}

View File

@ -6,6 +6,7 @@ from .views import *
from django.contrib.sitemaps.views import sitemap
from .sitemaps import sitemaps
from blog.views import ArticleListView
from django.contrib.auth import views as auth_views
from . import views
@ -23,6 +24,11 @@ urlpatterns = [
path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
path('robots.txt', robots_txt, name='robots'),
path('login/', auth_views.LoginView.as_view(template_name='programmer/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('register/', views.RegisterView.as_view(), name='register'),
path('profile/', views.ProfileView.as_view(), name='profile'),
path('profile/edit/', views.ProfileEditView.as_view(), name='profile_edit'),
]

View File

@ -1,32 +1,28 @@
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponse, HttpResponseNotFound, HttpRequest, JsonResponse
from django.urls import reverse_lazy
from .models import *
from django.shortcuts import render, redirect
from django.contrib import messages
from .models import Home, Solution, Recall, PageView, Visitor, CallbackRequest
from .forms import CallbackForm
from .forms import CallbackForm, ProfileForm, UserEditForm, RegistrationForm
from django.utils import timezone
from datetime import timedelta
from .models import PageView, Visitor
from django.db.models import Count, QuerySet
from django.contrib.auth.decorators import login_required, user_passes_test
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import TemplateView, ListView
from django.views.generic import TemplateView, ListView, CreateView, UpdateView
from .mixins import PageViewTrackingMixin, MenuContextMixin
from .services import get_published_queryset, track_page_view
from typing import Any, Dict, Type
from django.template.loader import render_to_string
# menu = [
# {'title': "Главная", 'url_name': 'home'},
# {'title': "Проекты", 'url_name': 'solution'},
# # {'title': "Компетенции", 'url_name': 'ability'},
# {'title': "Отзывы", 'url_name': 'recall'},
# {'title': "Статьи", 'url_name': 'blog'},
# {'title': "Обо мне", 'url_name': 'about'}
# ]
class BasePageView(PageViewTrackingMixin, MenuContextMixin, TemplateView):
"""
Базовый класс для всех страниц сайта.
@ -75,13 +71,16 @@ class HomePageView(BasePageView):
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
# Получаем контент для главной страницы
posts = get_published_queryset(Home)
form = CallbackForm()
if self.request.user.is_authenticated:
if 'captcha' in form.fields:
del form.fields['captcha']
context.update({
'posts': posts,
'form': CallbackForm(),
'form': form,
'title': "Программист 1С Николай Сердюк - разработка и сопровождение",
'meta_description': (
@ -176,6 +175,74 @@ class RecallListView(BaseListView):
return context
class ProfileView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, TemplateView):
template_name = 'programmer/profile.html'
class RegisterView(PageViewTrackingMixin, MenuContextMixin, SuccessMessageMixin, CreateView):
"""Регистрация нового пользователя"""
template_name = 'programmer/register.html'
form_class = RegistrationForm
# success_url = reverse_lazy('profile')
success_message = '✅ Регистрация успешна!'
def get_success_url(self):
# Если есть параметр next, используем его
next_url = self.request.POST.get('next') or self.request.GET.get('next')
if next_url:
return next_url
return reverse_lazy('profile')
def form_valid(self, form):
# Сохраняем пользователя
response = super().form_valid(form)
# Автоматически логиним после регистрации
user = self.object
login(self.request, user)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['next'] = self.request.GET.get('next', '')
return context
class ProfileEditView(LoginRequiredMixin, PageViewTrackingMixin, MenuContextMixin, UpdateView):
model = Profile
form_class = ProfileForm
template_name = 'programmer/profile_edit.html'
success_url = reverse_lazy('profile')
def get_object(self, queryset=None):
# Возвращаем профиль текущего пользователя
return self.request.user.profile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Передаём формы в контекст
if 'user_form' not in context:
context['user_form'] = UserEditForm(instance=self.request.user)
if 'profile_form' not in context:
context['profile_form'] = ProfileForm(instance=self.request.user.profile)
return context
def post(self, request, *args, **kwargs):
user_form = UserEditForm(request.POST, instance=request.user)
profile_form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, '✅ Профиль успешно обновлён.')
return redirect('profile')
else:
# Если формы не валидны, показываем ошибки
context = self.get_context_data()
context['user_form'] = user_form
context['profile_form'] = profile_form
return self.render_to_response(context)
@require_POST
def callback_request(request: HttpRequest) -> HttpResponse:
"""
@ -184,6 +251,10 @@ def callback_request(request: HttpRequest) -> HttpResponse:
"""
form = CallbackForm(request.POST)
if request.user.is_authenticated:
if 'captcha' in form.fields:
del form.fields['captcha']
if form.is_valid():
# Сохраняем заявку
callback = form.save()
@ -351,159 +422,5 @@ def track_view(view_func):
return _wrapped_view
# @track_view
# def index(request):
# posts = Home.objects.filter(is_published=True)
# context = {
# 'posts': posts,
# 'menu': menu,
# 'title': "Программист 1С Николай Сердюк - разработка и сопровождение",
# 'meta_description': "Профессиональный программист 1С с более чем 10-летним опытом. Разработка, доработка, обновление и интеграция систем 1С. Сопровождение 1С.",
# 'meta_keywords': "программист 1С, разработка 1С, обновление 1С, сопровождение 1С, интеграция 1С, доработка 1С, 1С предприятие 8.3",
# 'form': CallbackForm()
# }
# return render(request, 'programmer/index.html', context=context)
# @track_view
# def about(request):
# context = {
# 'menu': menu,
# 'title': "Программист 1С Николай Сердюк - 10+ лет опыта | Услуги 1С",
# 'meta_description': "Николай Сердюк - сертифицированный программист 1С с 10+ лет опыта. Специализация: обновление 1С, разработка под ключ, интеграция, миграция с 1С 7.7.",
# 'meta_keywords': "программист 1С Николай Сердюк, обновление 1С, разработка 1С под ключ, интеграция 1С, сертифицированный 1С, миграция 1С 7.7"
# }
# return render(request, 'programmer/about.html', context=context)
# @track_view
# def solution(request):
# posts = Solution.objects.filter(is_published=True)
# context = {
# 'posts': posts,
# 'menu': menu,
# 'meta_description': "Реализованные проекты по автоматизации 1С: складской учет с ТСД, интеграция с оборудованием, миграция с 1С 7.7. Примеры работ и кейсы.",
# 'meta_keywords': "проекты 1С, автоматизация склада 1С, интеграция ТСД 1С, миграция 1С 7.7, кейсы 1С, примеры работ 1С",
# 'title': "Проекты автоматизации 1С | Реализованные кейсы и решения",
# }
# return render(request, 'programmer/solution.html', context=context)
# @track_view
# def ability(request):
# posts = Competence.objects.filter(is_published=True)
# context = {
# 'posts': posts,
# 'menu': menu,
# 'title': "Сертификаты и компетенции 1С | Программист 1С Николай Сердюк",
# 'meta_description': "Сертификаты 1С: Профессионал по платформе 8.3 и БП 3.0. Подтвержденная квалификация программиста 1С с сертификатами фирмы 1С.",
# 'meta_keywords': "сертификаты 1С, 1С профессионал, компетенции 1С, квалификация программиста 1С, сертифицированный специалист 1С"
# }
# return render(request, 'programmer/competence.html', context=context)
# @track_view
# def recall(request):
# posts = Recall.objects.filter(is_published=True)
# context = {
# 'posts': posts,
# 'menu': menu,
# 'title': "Отзывы клиентов о работе программиста 1С | Реальные кейсы",
# 'meta_description': "Реальные отзывы клиентов о работе программиста 1С Николая Сердюка. Отзывы от ООО «РОВЕН-Регионы» и других компаний.",
# 'meta_keywords': "отзывы программист 1С, рекомендации 1С, отзывы клиентов 1С, реальные кейсы 1С, отзыв ООО РОВЕН"
# }
# return render(request, 'programmer/recall.html', context=context)
# def show_post(request, post_id):
# return HttpResponse(f"Отображение № {post_id}")
# def pageNotFound(request, exception):
# return HttpResponseNotFound('<h1>Страница не найдена</h1>')
# def callback_request(request):
# if request.method == 'POST':
# form = CallbackForm(request.POST)
# if form.is_valid():
# # Сохраняем заявку через форму
# form.save()
# messages.success(request, '✅ Ваша заявка успешно отправлена! Я свяжусь с вами в ближайшее время.')
# return redirect('home')
# else:
# # Если форма невалидна, показываем ошибки
# for field, errors in form.errors.items():
# for error in errors:
# messages.error(request, f'❌ Ошибка в поле {form.fields[field].label}: {error}')
# return redirect('home')
#
# # Если GET запрос, просто показываем главную страницу
# return redirect('home')
# def is_admin(user):
# return user.is_staff
# def is_staff(user):
# return user.is_staff
# @login_required
# @user_passes_test(is_staff)
# def statistics_view(request):
# today = timezone.now().date()
# week_ago = today - timedelta(days=7)
#
# # Статистика за сегодня
# today_views = PageView.objects.filter(
# timestamp__date=today
# ).count()
#
# # Статистика за неделю
# weekly_views = PageView.objects.filter(
# timestamp__date__gte=week_ago
# ).count()
#
# # Всего просмотров
# total_views = PageView.objects.count()
#
# # Популярные страницы за неделю
# popular_pages = PageView.objects.filter(
# timestamp__date__gte=week_ago
# ).values('url').annotate(
# views=Count('id')
# ).order_by('-views')[:10]
#
# # Уникальные посетители за неделю
# unique_visitors = Visitor.objects.filter(
# last_visit__date__gte=week_ago
# ).count()
#
# # Последние посещения
# recent_views = PageView.objects.select_related().order_by('-timestamp')[:20]
#
# today = timezone.now().date()
# total_callbacks = CallbackRequest.objects.count()
# today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count()
# unread_callbacks = CallbackRequest.objects.filter(is_read=False).count()
#
# context = {
# 'today_views': today_views,
# 'weekly_views': weekly_views,
# 'total_views': total_views,
# 'unique_visitors': unique_visitors,
# 'popular_pages': popular_pages,
# 'recent_views': recent_views,
# 'total_callbacks': total_callbacks,
# 'today_callbacks': today_callbacks,
# 'unread_callbacks': unread_callbacks,
# }
#
# return render(request, 'admin/statistics.html', context)
# @require_GET
# def robots_txt(request):
# return render(request, 'robots.txt', content_type='text/plain')

View File

@ -8,3 +8,5 @@ django-extensions==3.2.3
python-dotenv>=1.0.0
django-taggit
django_ckeditor_5
django-allauth
django-simple-captcha