mirror of
https://github.com/EDeev/deev.space.git
synced 2026-10-08 04:59:31 +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
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
@ -34,6 +35,7 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
|
@ -102,7 +104,9 @@ if not DEBUG:
|
||||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
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'
|
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', '')
|
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 Settings
|
||||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||||||
TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
|
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 Settings
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.yandex.ru')
|
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_USE_SSL = True
|
||||||
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '')
|
||||||
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, include
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
from django.contrib.sitemaps.views import sitemap
|
from django.contrib.sitemaps.views import sitemap
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
from main.sitemaps import ArticleSitemap, ProjectSitemap, StaticViewSitemap
|
from main.sitemaps import ArticleSitemap, ProjectSitemap, StaticViewSitemap
|
||||||
|
|
||||||
sitemaps = {
|
sitemaps = {
|
||||||
|
|
@ -17,8 +18,9 @@ urlpatterns = [
|
||||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='sitemap'),
|
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)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
||||||
|
|
||||||
handler404 = 'main.views.handler404'
|
handler404 = 'main.views.handler404'
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,24 @@ import json
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
CustomUser, Category, Article, ArticleImage, ArticleFile, ArticleLink,
|
Article,
|
||||||
Project, ProjectStatus, Skill, Comment, ArticleLike, CommentLike,
|
ArticleFile,
|
||||||
ContactMessage, Experience, Education, SiteSettings
|
ArticleImage,
|
||||||
|
ArticleLike,
|
||||||
|
ArticleLink,
|
||||||
|
Category,
|
||||||
|
Comment,
|
||||||
|
CommentLike,
|
||||||
|
ContactMessage,
|
||||||
|
CustomUser,
|
||||||
|
Education,
|
||||||
|
Experience,
|
||||||
|
Project,
|
||||||
|
ProjectStatus,
|
||||||
|
SiteSettings,
|
||||||
|
Skill,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 bleach
|
||||||
import requests
|
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):
|
class SmartCaptchaField(forms.CharField):
|
||||||
|
|
|
||||||
|
|
@ -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 datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from main.models import Article, Category, Education, Experience, Project, SiteSettings, Skill
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Загрузка начальных данных из резюме Деева Е.В.'
|
help = 'Загрузка начальных данных из резюме Деева Е.В.'
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,20 @@
|
||||||
Использование: python manage.py populate_demo
|
Использование: 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.contrib.auth import get_user_model
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from main.models import (
|
from main.models import (
|
||||||
SiteSettings, Skill, Project, Article, ArticleCategory,
|
Article,
|
||||||
Experience, Education, Achievement, Comment
|
ArticleCategory,
|
||||||
|
Education,
|
||||||
|
Experience,
|
||||||
|
Project,
|
||||||
|
SiteSettings,
|
||||||
|
Skill,
|
||||||
)
|
)
|
||||||
from datetime import date, timedelta
|
|
||||||
import random
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from unidecode import unidecode
|
from unidecode import unidecode
|
||||||
import requests
|
|
||||||
from bs4 import BeautifulSoup
|
logger = logging.getLogger(__name__)
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
|
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):
|
class CustomUser(AbstractUser):
|
||||||
|
|
@ -235,6 +253,9 @@ class ArticleLink(models.Model):
|
||||||
|
|
||||||
def fetch_preview(self):
|
def fetch_preview(self):
|
||||||
"""Автоматическое получение метаданных ссылки."""
|
"""Автоматическое получение метаданных ссылки."""
|
||||||
|
if not is_public_http_url(self.url):
|
||||||
|
logger.warning('Превью не загружено: адрес %s не публичный', self.url)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
headers = {'User-Agent': 'Mozilla/5.0 (compatible; ArticleBot/1.0)'}
|
headers = {'User-Agent': 'Mozilla/5.0 (compatible; ArticleBot/1.0)'}
|
||||||
response = requests.get(self.url, headers=headers, timeout=10)
|
response = requests.get(self.url, headers=headers, timeout=10)
|
||||||
|
|
@ -262,8 +283,8 @@ class ArticleLink(models.Model):
|
||||||
if og_image:
|
if og_image:
|
||||||
self.preview_image = og_image.get('content', '')
|
self.preview_image = og_image.get('content', '')
|
||||||
|
|
||||||
except Exception:
|
except (requests.RequestException, AttributeError, TypeError) as e:
|
||||||
pass
|
logger.warning('Не удалось получить превью %s: %s', self.url, e)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.title:
|
if not self.title:
|
||||||
|
|
|
||||||
131
main/signals.py
131
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.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.conf import settings
|
|
||||||
import requests
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from .models import Article
|
from .models import Article
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SITE_URL = 'https://deev.space'
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Article)
|
@receiver(post_save, sender=Article)
|
||||||
def publish_to_social_media(sender, instance, created, **kwargs):
|
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
|
return
|
||||||
|
|
||||||
if not instance.is_published:
|
if settings.SOCIAL_AUTOPOST:
|
||||||
return
|
|
||||||
|
|
||||||
if instance.is_achievement:
|
|
||||||
return
|
|
||||||
|
|
||||||
'''
|
|
||||||
# Telegram
|
|
||||||
if settings.TELEGRAM_BOT_TOKEN and settings.TELEGRAM_CHANNEL_ID:
|
if settings.TELEGRAM_BOT_TOKEN and settings.TELEGRAM_CHANNEL_ID:
|
||||||
try:
|
try:
|
||||||
publish_to_telegram(instance)
|
publish_to_telegram(instance)
|
||||||
logger.info(f'Статья "{instance.title}" опубликована в Telegram')
|
logger.info('Статья "%s" опубликована в Telegram', instance.title)
|
||||||
except Exception as e:
|
except requests.RequestException:
|
||||||
logger.error(f'Ошибка публикации в Telegram: {e}')
|
logger.exception('Ошибка публикации в Telegram')
|
||||||
|
|
||||||
# VK
|
|
||||||
if settings.VK_ACCESS_TOKEN and settings.VK_GROUP_ID:
|
if settings.VK_ACCESS_TOKEN and settings.VK_GROUP_ID:
|
||||||
try:
|
try:
|
||||||
publish_to_vk(instance)
|
publish_to_vk(instance)
|
||||||
logger.info(f'Статья "{instance.title}" опубликована в VK')
|
logger.info('Статья "%s" опубликована в VK', instance.title)
|
||||||
except Exception as e:
|
except requests.RequestException:
|
||||||
logger.error(f'Ошибка публикации в VK: {e}')
|
logger.exception('Ошибка публикации в VK')
|
||||||
'''
|
|
||||||
|
|
||||||
try:
|
ping_search_engines()
|
||||||
ping_search_engines(instance)
|
|
||||||
logger.info(f'Поисковые системы уведомлены о статье "{instance.title}"')
|
|
||||||
except Exception as e:
|
def build_telegram_text(article):
|
||||||
logger.error(f'Ошибка уведомления поисковых систем: {e}')
|
"""Текст поста для 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):
|
def publish_to_telegram(article):
|
||||||
"""Публикация статьи в Telegram-канал."""
|
"""Публикация статьи в Telegram-канал."""
|
||||||
bot_token = settings.TELEGRAM_BOT_TOKEN
|
api = f'https://api.telegram.org/bot{settings.TELEGRAM_BOT_TOKEN}'
|
||||||
channel_id = settings.TELEGRAM_CHANNEL_ID
|
text = build_telegram_text(article)
|
||||||
|
|
||||||
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})"
|
|
||||||
|
|
||||||
if article.img:
|
if article.img:
|
||||||
url = f"https://api.telegram.org/bot{bot_token}/sendPhoto"
|
url = f'{api}/sendPhoto'
|
||||||
data = {
|
data = {
|
||||||
'chat_id': channel_id,
|
'chat_id': settings.TELEGRAM_CHANNEL_ID,
|
||||||
'photo': f"https://deev.space{article.img.url}",
|
'photo': f'{SITE_URL}{article.img.url}',
|
||||||
'caption': text,
|
'caption': text,
|
||||||
'parse_mode': 'Markdown'
|
'parse_mode': 'HTML',
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
url = f'{api}/sendMessage'
|
||||||
data = {
|
data = {
|
||||||
'chat_id': channel_id,
|
'chat_id': settings.TELEGRAM_CHANNEL_ID,
|
||||||
'text': text,
|
'text': text,
|
||||||
'parse_mode': 'Markdown',
|
'parse_mode': 'HTML',
|
||||||
'disable_web_page_preview': False
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(url, data=data, timeout=10)
|
response = requests.post(url, data=data, timeout=10)
|
||||||
|
|
@ -82,55 +75,31 @@ def publish_to_telegram(article):
|
||||||
|
|
||||||
def publish_to_vk(article):
|
def publish_to_vk(article):
|
||||||
"""Публикация статьи в группу VKontakte."""
|
"""Публикация статьи в группу VKontakte."""
|
||||||
access_token = settings.VK_ACCESS_TOKEN
|
article_url = f'{SITE_URL}{article.get_absolute_url()}'
|
||||||
group_id = settings.VK_GROUP_ID
|
|
||||||
|
|
||||||
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:
|
if article.excerpt:
|
||||||
excerpt = article.excerpt[:300] + '...' if len(article.excerpt) > 300 else article.excerpt
|
excerpt = article.excerpt[:300] + '...' if len(article.excerpt) > 300 else article.excerpt
|
||||||
message += f"{excerpt}\n\n"
|
message += f'{excerpt}\n\n'
|
||||||
message += f"🔗 Читать: {article_url}"
|
message += f'🔗 Читать: {article_url}'
|
||||||
|
|
||||||
url = "https://api.vk.com/method/wall.post"
|
|
||||||
params = {
|
params = {
|
||||||
'owner_id': f'-{group_id}',
|
'owner_id': f'-{settings.VK_GROUP_ID}',
|
||||||
'message': message,
|
'message': message,
|
||||||
'from_group': 1,
|
'from_group': 1,
|
||||||
'access_token': access_token,
|
'access_token': settings.VK_ACCESS_TOKEN,
|
||||||
'v': '5.131'
|
'v': '5.131',
|
||||||
}
|
}
|
||||||
|
|
||||||
if article.img:
|
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()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
def escape_markdown(text):
|
def ping_search_engines():
|
||||||
"""Экранирование специальных символов Markdown."""
|
"""Уведомление Яндекса об обновлении sitemap (Google ping-эндпоинт отключил в 2023 году)."""
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
google_ping_url = f"https://www.google.com/ping?sitemap=https://deev.space/sitemap.xml"
|
requests.get(f'https://webmaster.yandex.ru/ping?sitemap={SITE_URL}/sitemap.xml', timeout=5)
|
||||||
requests.get(google_ping_url, timeout=5)
|
except requests.RequestException as e:
|
||||||
except Exception as e:
|
logger.warning('Ошибка ping Яндекса: %s', 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}')
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib.sitemaps import Sitemap
|
from django.contrib.sitemaps import Sitemap
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import Article, Project
|
from .models import Article, Project
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import math
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.html import format_html
|
||||||
import re, math
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
@ -113,8 +114,7 @@ def status_label(status):
|
||||||
def render_tech_badge(tech_name):
|
def render_tech_badge(tech_name):
|
||||||
"""Рендерит HTML для бейджа технологии."""
|
"""Рендерит HTML для бейджа технологии."""
|
||||||
icon_class = tech_icon(tech_name)
|
icon_class = tech_icon(tech_name)
|
||||||
html = f'<span class="tech-badge"><i class="{icon_class}"></i> {tech_name}</span>'
|
return format_html('<span class="tech-badge"><i class="{}"></i> {}</span>', icon_class, tech_name)
|
||||||
return mark_safe(html)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
||||||
|
|
@ -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 json
|
||||||
import logging
|
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 (
|
from .models import (
|
||||||
Article, Project, ProjectStatus, Skill, Comment, ArticleLike, CommentLike, ArticleView,
|
Article,
|
||||||
ContactMessage, Experience, Education, Category, SiteSettings
|
ArticleLike,
|
||||||
|
ArticleView,
|
||||||
|
Category,
|
||||||
|
Comment,
|
||||||
|
CommentLike,
|
||||||
|
Education,
|
||||||
|
Experience,
|
||||||
|
Project,
|
||||||
|
SiteSettings,
|
||||||
|
Skill,
|
||||||
)
|
)
|
||||||
from .forms import RegisterForm, LoginForm, CommentForm, ContactForm
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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}',
|
message=f'От: {message.name} ({message.email})\n\nТема: {message.subject}\n\n{message.message}',
|
||||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
recipient_list=[settings.CONTACT_EMAIL],
|
recipient_list=[settings.CONTACT_EMAIL],
|
||||||
fail_silently=True,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f'Ошибка отправки email: {e}')
|
logger.exception('Ошибка отправки email')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
notify_alertbot_contact_message(message)
|
notify_alertbot_contact_message(message)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue