Добавил регистрацию и авторизацию пользователей, скрыл меню на мобильном приложении, убрал ТГ канал
This commit is contained in:
parent
64e4037057
commit
c65be53032
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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-теги и проверяем, остался ли текст
|
||||
|
||||
@ -42,10 +42,19 @@
|
||||
<!-- Форма добавления комментария -->
|
||||
<div class="modern-card">
|
||||
<h4>Добавить комментарий</h4>
|
||||
|
||||
{% 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 %}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -18,17 +18,33 @@ class MenuContextMixin(ContextMixin):
|
||||
"""
|
||||
Миксин для добавления меню в контекст.
|
||||
"""
|
||||
menu = [
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Основное меню (всегда)
|
||||
main_menu = [
|
||||
{'title': "Главная", 'url_name': 'home'},
|
||||
{'title': "Проекты", 'url_name': 'solution'},
|
||||
{'title': "Статьи", 'url_name': 'blog'},
|
||||
{'title': "Отзывы", 'url_name': 'recall'},
|
||||
{'title': "Обо мне", 'url_name': 'about'}
|
||||
{'title': "Обо мне", 'url_name': 'about'},
|
||||
]
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['menu'] = self.menu
|
||||
# Пользовательское меню (зависит от статуса)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
{% 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}}
|
||||
{{ 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">
|
||||
|
||||
@ -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%;">
|
||||
📨 Отправить заявку
|
||||
|
||||
25
programmer/templates/programmer/login.html
Normal file
25
programmer/templates/programmer/login.html
Normal 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 %}
|
||||
51
programmer/templates/programmer/profile.html
Normal file
51
programmer/templates/programmer/profile.html
Normal 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 %}
|
||||
26
programmer/templates/programmer/profile_edit.html
Normal file
26
programmer/templates/programmer/profile_edit.html
Normal 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 %}
|
||||
40
programmer/templates/programmer/register.html
Normal file
40
programmer/templates/programmer/register.html
Normal 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 %}
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -8,3 +8,5 @@ django-extensions==3.2.3
|
||||
python-dotenv>=1.0.0
|
||||
django-taggit
|
||||
django_ckeditor_5
|
||||
django-allauth
|
||||
django-simple-captcha
|
||||
Loading…
x
Reference in New Issue
Block a user