mirror of
https://github.com/EDeev/deev.space.git
synced 2026-10-07 20:49:59 +03:00
Исправления безопасности и чистка кода
- render_tech_badge: format_html вместо mark_safe с непроверенной подстановкой (XSS); - превью ссылок статьи: только публичные http(s)-адреса, без запросов во внутреннюю сеть сервера (SSRF); ошибки логируются вместо except: pass; - автопостинг в Telegram переведён на parse_mode=HTML с html.escape (раньше в канал уходили видимые обратные слэши), включается переменной SOCIAL_AUTOPOST; - убран ping Google (эндпоинт отключён в 2023 году); - ошибка отправки письма с формы обратной связи больше не глушится; - whitenoise и DJANGO_SERVE_MEDIA для запуска без nginx; - импорты упорядочены, неиспользуемые удалены (ruff).
This commit is contained in:
parent
312ce5a24a
commit
1320562734
14 changed files with 180 additions and 153 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ INSTALLED_APPS = [
|
|||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
|
@ -102,7 +104,9 @@ if not DEBUG:
|
|||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
MEDIA_ROOT = Path(os.environ.get('DJANGO_MEDIA_ROOT', BASE_DIR / 'media'))
|
||||
# Отдавать media самим Django (для запуска в Docker без nginx); в бою media отдаёт nginx
|
||||
SERVE_MEDIA = os.environ.get('DJANGO_SERVE_MEDIA', 'False') == 'True'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
|
@ -111,6 +115,9 @@ SMARTCAPTCHA_CLIENT_KEY = os.environ.get('SMARTCAPTCHA_CLIENT_KEY', '')
|
|||
SMARTCAPTCHA_SERVER_KEY = os.environ.get('SMARTCAPTCHA_SERVER_KEY', '')
|
||||
|
||||
|
||||
# Автопостинг новых статей в Telegram-канал и группу VK (выключен по умолчанию)
|
||||
SOCIAL_AUTOPOST = os.environ.get('SOCIAL_AUTOPOST', 'False') == 'True'
|
||||
|
||||
# Telegram Bot Settings
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||||
TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
|
||||
|
|
@ -126,7 +133,7 @@ VK_GROUP_ID = os.environ.get('VK_GROUP_ID', '')
|
|||
# Email Settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.yandex.ru')
|
||||
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 465))
|
||||
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', '465'))
|
||||
EMAIL_USE_SSL = True
|
||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
||||
|
|
@ -187,4 +194,4 @@ LOGGING = {
|
|||
}
|
||||
|
||||
# Create logs directory
|
||||
(BASE_DIR / 'logs').mkdir(exist_ok=True)
|
||||
(BASE_DIR / 'logs').mkdir(exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import include, path
|
||||
|
||||
from main.sitemaps import ArticleSitemap, ProjectSitemap, StaticViewSitemap
|
||||
|
||||
sitemaps = {
|
||||
|
|
@ -17,9 +18,10 @@ urlpatterns = [
|
|||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='sitemap'),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
if settings.DEBUG or settings.SERVE_MEDIA:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
||||
|
||||
handler404 = 'main.views.handler404'
|
||||
handler500 = 'main.views.handler500'
|
||||
handler500 = 'main.views.handler500'
|
||||
|
|
|
|||
|
|
@ -4,10 +4,24 @@ import json
|
|||
from django.contrib import admin
|
||||
from django.http import HttpResponse
|
||||
from django.utils.html import format_html
|
||||
|
||||
from .models import (
|
||||
CustomUser, Category, Article, ArticleImage, ArticleFile, ArticleLink,
|
||||
Project, ProjectStatus, Skill, Comment, ArticleLike, CommentLike,
|
||||
ContactMessage, Experience, Education, SiteSettings
|
||||
Article,
|
||||
ArticleFile,
|
||||
ArticleImage,
|
||||
ArticleLike,
|
||||
ArticleLink,
|
||||
Category,
|
||||
Comment,
|
||||
CommentLike,
|
||||
ContactMessage,
|
||||
CustomUser,
|
||||
Education,
|
||||
Experience,
|
||||
Project,
|
||||
ProjectStatus,
|
||||
SiteSettings,
|
||||
Skill,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ def site_settings(request):
|
|||
"""Глобальный контекст с настройками сайта."""
|
||||
return {
|
||||
'global_settings': SiteSettings.load()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.validators import RegexValidator
|
||||
from django.conf import settings
|
||||
import bleach
|
||||
import requests
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
from .models import CustomUser, Comment, ContactMessage
|
||||
from .models import Comment, ContactMessage, CustomUser
|
||||
|
||||
|
||||
class SmartCaptchaField(forms.CharField):
|
||||
|
|
@ -248,4 +248,4 @@ class ContactForm(forms.ModelForm):
|
|||
def clean_message(self):
|
||||
message = self.cleaned_data.get('message')
|
||||
message = bleach.clean(message, tags=[], strip=True)
|
||||
return message
|
||||
return message
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from main.models import (
|
||||
Skill, Experience, Education, Project, Category, SiteSettings, Article
|
||||
)
|
||||
from datetime import date
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from main.models import Article, Category, Education, Experience, Project, SiteSettings, Skill
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Загрузка начальных данных из резюме Деева Е.В.'
|
||||
|
|
@ -363,4 +363,4 @@ class Command(BaseCommand):
|
|||
defaults=ach_data
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f' OK ({len(achievements_data)} достижений)'))
|
||||
self.stdout.write(self.style.SUCCESS(f' OK ({len(achievements_data)} достижений)'))
|
||||
|
|
|
|||
|
|
@ -3,14 +3,20 @@
|
|||
Использование: python manage.py populate_demo
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from main.models import (
|
||||
SiteSettings, Skill, Project, Article, ArticleCategory,
|
||||
Experience, Education, Achievement, Comment
|
||||
Article,
|
||||
ArticleCategory,
|
||||
Education,
|
||||
Experience,
|
||||
Project,
|
||||
SiteSettings,
|
||||
Skill,
|
||||
)
|
||||
from datetime import date, timedelta
|
||||
import random
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
|
@ -147,4 +153,4 @@ class Command(BaseCommand):
|
|||
}
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('Демо-данные успешно созданы!'))
|
||||
self.stdout.write(self.style.SUCCESS('Демо-данные успешно созданы!'))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
from django.db import models
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from unidecode import unidecode
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_public_http_url(url):
|
||||
"""Проверка, что ссылка ведёт на публичный http(s)-адрес, а не во внутреннюю сеть сервера."""
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ('http', 'https') or not parsed.hostname:
|
||||
return False
|
||||
try:
|
||||
infos = socket.getaddrinfo(parsed.hostname, parsed.port or 443)
|
||||
except OSError:
|
||||
return False
|
||||
return all(ipaddress.ip_address(info[4][0]).is_global for info in infos)
|
||||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
|
|
@ -235,6 +253,9 @@ class ArticleLink(models.Model):
|
|||
|
||||
def fetch_preview(self):
|
||||
"""Автоматическое получение метаданных ссылки."""
|
||||
if not is_public_http_url(self.url):
|
||||
logger.warning('Превью не загружено: адрес %s не публичный', self.url)
|
||||
return
|
||||
try:
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; ArticleBot/1.0)'}
|
||||
response = requests.get(self.url, headers=headers, timeout=10)
|
||||
|
|
@ -262,8 +283,8 @@ class ArticleLink(models.Model):
|
|||
if og_image:
|
||||
self.preview_image = og_image.get('content', '')
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
except (requests.RequestException, AttributeError, TypeError) as e:
|
||||
logger.warning('Не удалось получить превью %s: %s', self.url, e)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.title:
|
||||
|
|
@ -653,4 +674,4 @@ class SiteSettings(models.Model):
|
|||
@classmethod
|
||||
def load(cls):
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
return obj
|
||||
|
|
|
|||
139
main/signals.py
139
main/signals.py
|
|
@ -1,79 +1,72 @@
|
|||
import html
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from .models import Article
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SITE_URL = 'https://deev.space'
|
||||
|
||||
|
||||
@receiver(post_save, sender=Article)
|
||||
def publish_to_social_media(sender, instance, created, **kwargs):
|
||||
"""Автоматическая публикация новой статьи в социальные сети."""
|
||||
if not created:
|
||||
if not created or not instance.is_published or instance.is_achievement:
|
||||
return
|
||||
|
||||
if not instance.is_published:
|
||||
return
|
||||
if settings.SOCIAL_AUTOPOST:
|
||||
if settings.TELEGRAM_BOT_TOKEN and settings.TELEGRAM_CHANNEL_ID:
|
||||
try:
|
||||
publish_to_telegram(instance)
|
||||
logger.info('Статья "%s" опубликована в Telegram', instance.title)
|
||||
except requests.RequestException:
|
||||
logger.exception('Ошибка публикации в Telegram')
|
||||
|
||||
if instance.is_achievement:
|
||||
return
|
||||
if settings.VK_ACCESS_TOKEN and settings.VK_GROUP_ID:
|
||||
try:
|
||||
publish_to_vk(instance)
|
||||
logger.info('Статья "%s" опубликована в VK', instance.title)
|
||||
except requests.RequestException:
|
||||
logger.exception('Ошибка публикации в VK')
|
||||
|
||||
'''
|
||||
# Telegram
|
||||
if settings.TELEGRAM_BOT_TOKEN and settings.TELEGRAM_CHANNEL_ID:
|
||||
try:
|
||||
publish_to_telegram(instance)
|
||||
logger.info(f'Статья "{instance.title}" опубликована в Telegram')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка публикации в Telegram: {e}')
|
||||
ping_search_engines()
|
||||
|
||||
# VK
|
||||
if settings.VK_ACCESS_TOKEN and settings.VK_GROUP_ID:
|
||||
try:
|
||||
publish_to_vk(instance)
|
||||
logger.info(f'Статья "{instance.title}" опубликована в VK')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка публикации в VK: {e}')
|
||||
'''
|
||||
|
||||
try:
|
||||
ping_search_engines(instance)
|
||||
logger.info(f'Поисковые системы уведомлены о статье "{instance.title}"')
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка уведомления поисковых систем: {e}')
|
||||
def build_telegram_text(article):
|
||||
"""Текст поста для Telegram в режиме parse_mode=HTML."""
|
||||
article_url = f'{SITE_URL}{article.get_absolute_url()}'
|
||||
text = f'📝 <b>{html.escape(article.title)}</b>\n\n'
|
||||
if article.excerpt:
|
||||
excerpt = article.excerpt[:200] + '...' if len(article.excerpt) > 200 else article.excerpt
|
||||
text += f'{html.escape(excerpt)}\n\n'
|
||||
text += f'<a href="{html.escape(article_url, quote=True)}">Читать полностью</a>'
|
||||
return text
|
||||
|
||||
|
||||
def publish_to_telegram(article):
|
||||
"""Публикация статьи в Telegram-канал."""
|
||||
bot_token = settings.TELEGRAM_BOT_TOKEN
|
||||
channel_id = settings.TELEGRAM_CHANNEL_ID
|
||||
|
||||
article_url = f"https://deev.space{article.get_absolute_url()}"
|
||||
|
||||
text = f"📝 *{escape_markdown(article.title)}*\n\n"
|
||||
if article.excerpt:
|
||||
excerpt = article.excerpt[:200] + '...' if len(article.excerpt) > 200 else article.excerpt
|
||||
text += f"{escape_markdown(excerpt)}\n\n"
|
||||
text += f"[Читать полностью]({article_url})"
|
||||
api = f'https://api.telegram.org/bot{settings.TELEGRAM_BOT_TOKEN}'
|
||||
text = build_telegram_text(article)
|
||||
|
||||
if article.img:
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendPhoto"
|
||||
url = f'{api}/sendPhoto'
|
||||
data = {
|
||||
'chat_id': channel_id,
|
||||
'photo': f"https://deev.space{article.img.url}",
|
||||
'chat_id': settings.TELEGRAM_CHANNEL_ID,
|
||||
'photo': f'{SITE_URL}{article.img.url}',
|
||||
'caption': text,
|
||||
'parse_mode': 'Markdown'
|
||||
'parse_mode': 'HTML',
|
||||
}
|
||||
else:
|
||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
url = f'{api}/sendMessage'
|
||||
data = {
|
||||
'chat_id': channel_id,
|
||||
'chat_id': settings.TELEGRAM_CHANNEL_ID,
|
||||
'text': text,
|
||||
'parse_mode': 'Markdown',
|
||||
'disable_web_page_preview': False
|
||||
'parse_mode': 'HTML',
|
||||
}
|
||||
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
|
|
@ -82,55 +75,31 @@ def publish_to_telegram(article):
|
|||
|
||||
def publish_to_vk(article):
|
||||
"""Публикация статьи в группу VKontakte."""
|
||||
access_token = settings.VK_ACCESS_TOKEN
|
||||
group_id = settings.VK_GROUP_ID
|
||||
article_url = f'{SITE_URL}{article.get_absolute_url()}'
|
||||
|
||||
article_url = f"https://deev.space{article.get_absolute_url()}"
|
||||
|
||||
message = f"📝 {article.title}\n\n"
|
||||
message = f'📝 {article.title}\n\n'
|
||||
if article.excerpt:
|
||||
excerpt = article.excerpt[:300] + '...' if len(article.excerpt) > 300 else article.excerpt
|
||||
message += f"{excerpt}\n\n"
|
||||
message += f"🔗 Читать: {article_url}"
|
||||
message += f'{excerpt}\n\n'
|
||||
message += f'🔗 Читать: {article_url}'
|
||||
|
||||
url = "https://api.vk.com/method/wall.post"
|
||||
params = {
|
||||
'owner_id': f'-{group_id}',
|
||||
'owner_id': f'-{settings.VK_GROUP_ID}',
|
||||
'message': message,
|
||||
'from_group': 1,
|
||||
'access_token': access_token,
|
||||
'v': '5.131'
|
||||
'access_token': settings.VK_ACCESS_TOKEN,
|
||||
'v': '5.131',
|
||||
}
|
||||
|
||||
if article.img:
|
||||
params['attachments'] = f"https://deev.space{article.img.url}"
|
||||
params['attachments'] = f'{SITE_URL}{article.img.url}'
|
||||
|
||||
response = requests.post(url, params=params, timeout=10)
|
||||
response = requests.post('https://api.vk.com/method/wall.post', params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
def escape_markdown(text):
|
||||
"""Экранирование специальных символов Markdown."""
|
||||
escape_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
|
||||
for char in escape_chars:
|
||||
text = text.replace(char, f'\\{char}')
|
||||
return text
|
||||
|
||||
|
||||
def ping_search_engines(article):
|
||||
"""Уведомление поисковых систем о новой статье."""
|
||||
article_url = f"https://deev.space{article.get_absolute_url()}"
|
||||
|
||||
# Google
|
||||
def ping_search_engines():
|
||||
"""Уведомление Яндекса об обновлении sitemap (Google ping-эндпоинт отключил в 2023 году)."""
|
||||
try:
|
||||
google_ping_url = f"https://www.google.com/ping?sitemap=https://deev.space/sitemap.xml"
|
||||
requests.get(google_ping_url, timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка ping Google: {e}')
|
||||
|
||||
# Яндекс
|
||||
try:
|
||||
yandex_ping_url = f"https://webmaster.yandex.ru/ping?sitemap=https://deev.space/sitemap.xml"
|
||||
requests.get(yandex_ping_url, timeout=5)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка ping Яндекс: {e}')
|
||||
requests.get(f'https://webmaster.yandex.ru/ping?sitemap={SITE_URL}/sitemap.xml', timeout=5)
|
||||
except requests.RequestException as e:
|
||||
logger.warning('Ошибка ping Яндекса: %s', e)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import Article, Project
|
||||
|
||||
|
||||
|
|
@ -36,4 +37,4 @@ class StaticViewSitemap(Sitemap):
|
|||
return ['index', 'about', 'projects', 'blog', 'achievements', 'contacts']
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
return reverse(item)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import math
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
import re, math
|
||||
from django.utils.html import format_html
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
|
@ -113,8 +114,7 @@ def status_label(status):
|
|||
def render_tech_badge(tech_name):
|
||||
"""Рендерит HTML для бейджа технологии."""
|
||||
icon_class = tech_icon(tech_name)
|
||||
html = f'<span class="tech-badge"><i class="{icon_class}"></i> {tech_name}</span>'
|
||||
return mark_safe(html)
|
||||
return format_html('<span class="tech-badge"><i class="{}"></i> {}</span>', icon_class, tech_name)
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
|
|||
|
|
@ -1,3 +1,2 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -23,4 +24,4 @@ urlpatterns = [
|
|||
path('api/article/<int:article_id>/comment/', views.add_comment, name='add_comment'),
|
||||
path('api/article/<int:article_id>/like/', views.toggle_article_like, name='toggle_article_like'),
|
||||
path('api/comment/<int:comment_id>/like/', views.toggle_comment_like, name='toggle_comment_like'),
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,30 +1,38 @@
|
|||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.views.generic import ListView, DetailView, TemplateView
|
||||
from django.contrib.auth import login, authenticate, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from django.db.models import Q, Count
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.db.models import Sum
|
||||
from datetime import timedelta
|
||||
from .middleware import client_ip_hash, is_bot
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Count, Q, Sum
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import DetailView, ListView, TemplateView
|
||||
|
||||
from .forms import CommentForm, ContactForm, LoginForm, RegisterForm
|
||||
from .middleware import client_ip_hash, is_bot
|
||||
from .models import (
|
||||
Article, Project, ProjectStatus, Skill, Comment, ArticleLike, CommentLike, ArticleView,
|
||||
ContactMessage, Experience, Education, Category, SiteSettings
|
||||
Article,
|
||||
ArticleLike,
|
||||
ArticleView,
|
||||
Category,
|
||||
Comment,
|
||||
CommentLike,
|
||||
Education,
|
||||
Experience,
|
||||
Project,
|
||||
SiteSettings,
|
||||
Skill,
|
||||
)
|
||||
from .forms import RegisterForm, LoginForm, CommentForm, ContactForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -398,10 +406,9 @@ class ContactsView(TemplateView):
|
|||
message=f'От: {message.name} ({message.email})\n\nТема: {message.subject}\n\n{message.message}',
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
recipient_list=[settings.CONTACT_EMAIL],
|
||||
fail_silently=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка отправки email: {e}')
|
||||
except Exception:
|
||||
logger.exception('Ошибка отправки email')
|
||||
|
||||
try:
|
||||
notify_alertbot_contact_message(message)
|
||||
|
|
@ -595,4 +602,4 @@ def handler404(request, exception):
|
|||
|
||||
def handler500(request):
|
||||
"""Обработчик ошибки 500."""
|
||||
return render(request, 'errors/500.html', status=500)
|
||||
return render(request, 'errors/500.html', status=500)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue