1
0
Fork 0
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:
Деев Егор Викторович 2026-10-02 11:59:07 +00:00
parent 312ce5a24a
commit 1320562734
14 changed files with 180 additions and 153 deletions

View file

@ -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', '')
@ -187,4 +194,4 @@ LOGGING = {
} }
# Create logs directory # Create logs directory
(BASE_DIR / 'logs').mkdir(exist_ok=True) (BASE_DIR / 'logs').mkdir(exist_ok=True)

View file

@ -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,9 +18,10 @@ 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'
handler500 = 'main.views.handler500' handler500 = 'main.views.handler500'

View file

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

View file

@ -5,4 +5,4 @@ def site_settings(request):
"""Глобальный контекст с настройками сайта.""" """Глобальный контекст с настройками сайта."""
return { return {
'global_settings': SiteSettings.load() 'global_settings': SiteSettings.load()
} }

View file

@ -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):
@ -248,4 +248,4 @@ class ContactForm(forms.ModelForm):
def clean_message(self): def clean_message(self):
message = self.cleaned_data.get('message') message = self.cleaned_data.get('message')
message = bleach.clean(message, tags=[], strip=True) message = bleach.clean(message, tags=[], strip=True)
return message return message

View file

@ -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 = 'Загрузка начальных данных из резюме Деева Е.В.'
@ -363,4 +363,4 @@ class Command(BaseCommand):
defaults=ach_data 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)} достижений)'))

View file

@ -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()
@ -147,4 +153,4 @@ class Command(BaseCommand):
} }
) )
self.stdout.write(self.style.SUCCESS('Демо-данные успешно созданы!')) self.stdout.write(self.style.SUCCESS('Демо-данные успешно созданы!'))

View file

@ -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:
@ -653,4 +674,4 @@ class SiteSettings(models.Model):
@classmethod @classmethod
def load(cls): def load(cls):
obj, created = cls.objects.get_or_create(pk=1) obj, created = cls.objects.get_or_create(pk=1)
return obj return obj

View file

@ -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 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: if settings.VK_ACCESS_TOKEN and settings.VK_GROUP_ID:
return try:
publish_to_vk(instance)
logger.info('Статья "%s" опубликована в VK', instance.title)
except requests.RequestException:
logger.exception('Ошибка публикации в VK')
''' ping_search_engines()
# 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}')
# 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: def build_telegram_text(article):
ping_search_engines(instance) """Текст поста для Telegram в режиме parse_mode=HTML."""
logger.info(f'Поисковые системы уведомлены о статье "{instance.title}"') article_url = f'{SITE_URL}{article.get_absolute_url()}'
except Exception as e: text = f'📝 <b>{html.escape(article.title)}</b>\n\n'
logger.error(f'Ошибка уведомления поисковых систем: {e}') 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}')

View file

@ -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
@ -36,4 +37,4 @@ class StaticViewSitemap(Sitemap):
return ['index', 'about', 'projects', 'blog', 'achievements', 'contacts'] return ['index', 'about', 'projects', 'blog', 'achievements', 'contacts']
def location(self, item): def location(self, item):
return reverse(item) return reverse(item)

View file

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

View file

@ -1,3 +1,2 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View file

@ -1,4 +1,5 @@
from django.urls import path from django.urls import path
from . import views from . import views
urlpatterns = [ 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>/comment/', views.add_comment, name='add_comment'),
path('api/article/<int:article_id>/like/', views.toggle_article_like, name='toggle_article_like'), 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'), path('api/comment/<int:comment_id>/like/', views.toggle_comment_like, name='toggle_comment_like'),
] ]

View file

@ -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)
@ -595,4 +602,4 @@ def handler404(request, exception):
def handler500(request): def handler500(request):
"""Обработчик ошибки 500.""" """Обработчик ошибки 500."""
return render(request, 'errors/500.html', status=500) return render(request, 'errors/500.html', status=500)