diff --git a/dspace/settings.py b/dspace/settings.py index 71dc5ef..124700c 100644 --- a/dspace/settings.py +++ b/dspace/settings.py @@ -1,156 +1,177 @@ -import os -from pathlib import Path - -BASE_DIR = Path(__file__).resolve().parent.parent - -SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '*****') - -DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' - -ALLOWED_HOSTS = ['deev.space', 'deev.su', 'www.deev.space', 'www.deev.su', 'localhost', '127.0.0.1'] - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sitemaps', - 'main.apps.MainConfig', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'dspace.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'main.context_processors.site_settings', - ], - }, - }, -] - -WSGI_APPLICATION = 'dspace.wsgi.application' - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - -AUTH_USER_MODEL = 'main.CustomUser' - -AUTH_PASSWORD_VALIDATORS = [] - -LANGUAGE_CODE = 'ru-ru' -TIME_ZONE = 'Europe/Moscow' -USE_I18N = True -USE_TZ = True - -STATIC_URL = '/static/' -STATICFILES_DIRS = [BASE_DIR / 'static'] -STATIC_ROOT = BASE_DIR / 'staticfiles' - -MEDIA_URL = '/media/' -MEDIA_ROOT = BASE_DIR / 'media' - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -# Yandex SmartCaptcha Settings -SMARTCAPTCHA_CLIENT_KEY = os.environ.get('SMARTCAPTCHA_CLIENT_KEY', '*****') -SMARTCAPTCHA_SERVER_KEY = os.environ.get('SMARTCAPTCHA_SERVER_KEY', '*****') - -# Telegram Bot Settings -TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '') -TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '') - -# VK API Settings -VK_ACCESS_TOKEN = os.environ.get('VK_ACCESS_TOKEN', '') -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_USE_SSL = True -EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') -EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '') -DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'egor@deev.space') -CONTACT_EMAIL = os.environ.get('CONTACT_EMAIL', 'egor@deev.space') - -# Site Settings -SITE_NAME = 'deev.space' -SITE_AUTHOR = 'Деев Егор Викторович' -SITE_DESCRIPTION = 'Персональный сайт backend-разработчика' - -# Session Settings -SESSION_COOKIE_AGE = 31536000 # 1 year -SESSION_COOKIE_SECURE = not DEBUG -CSRF_COOKIE_SECURE = not DEBUG - -# Security Settings (Production) -if not DEBUG: - SECURE_BROWSER_XSS_FILTER = True - SECURE_CONTENT_TYPE_NOSNIFF = True - X_FRAME_OPTIONS = 'DENY' - SECURE_SSL_REDIRECT = True - SECURE_HSTS_SECONDS = 31536000 - SECURE_HSTS_INCLUDE_SUBDOMAINS = True - SECURE_HSTS_PRELOAD = True - -# Logging -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', - }, - }, - 'handlers': { - 'file': { - 'level': 'ERROR', - 'class': 'logging.FileHandler', - 'filename': BASE_DIR / 'logs' / 'django.log', - 'formatter': 'verbose', - }, - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['console'] if DEBUG else ['file', 'console'], - 'level': 'INFO', - }, - 'main': { - 'handlers': ['console'] if DEBUG else ['file', 'console'], - 'level': 'DEBUG' if DEBUG else 'INFO', - }, - }, -} - -# Create logs directory +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '*****') + +DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' + +ALLOWED_HOSTS = ['deev.space', 'deev.su', 'www.deev.space', 'www.deev.su', 'localhost', '127.0.0.1'] + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sitemaps', + 'main.apps.MainConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'dspace.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'main.context_processors.site_settings', + ], + }, + }, +] + +WSGI_APPLICATION = 'dspace.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +AUTH_USER_MODEL = 'main.CustomUser' + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'Europe/Moscow' +USE_I18N = True +USE_TZ = True + +STATIC_URL = '/static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Хеширование статических файлов в production +if not DEBUG: + STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' + +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Yandex SmartCaptcha Settings +SMARTCAPTCHA_CLIENT_KEY = os.environ.get('SMARTCAPTCHA_CLIENT_KEY', '*****') +SMARTCAPTCHA_SERVER_KEY = os.environ.get('SMARTCAPTCHA_SERVER_KEY', '*****') + +# Telegram Bot Settings +TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '') +TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '') + +# VK API Settings +VK_ACCESS_TOKEN = os.environ.get('VK_ACCESS_TOKEN', '') +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_USE_SSL = True +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '') +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'egor@deev.space') +CONTACT_EMAIL = os.environ.get('CONTACT_EMAIL', 'egor@deev.space') + +# Site Settings +SITE_NAME = 'deev.space' +SITE_AUTHOR = 'Деев Егор Викторович' +SITE_DESCRIPTION = 'Персональный сайт backend-разработчика' + +# Session Settings +SESSION_COOKIE_AGE = 31536000 # 1 year +SESSION_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SECURE = not DEBUG + +# Security Settings (Production) +if not DEBUG: + SECURE_BROWSER_XSS_FILTER = True + SECURE_CONTENT_TYPE_NOSNIFF = True + X_FRAME_OPTIONS = 'DENY' + SECURE_SSL_REDIRECT = True + SECURE_HSTS_SECONDS = 31536000 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'level': 'ERROR', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs' / 'django.log', + 'formatter': 'verbose', + }, + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['console'] if DEBUG else ['file', 'console'], + 'level': 'INFO', + }, + 'main': { + 'handlers': ['console'] if DEBUG else ['file', 'console'], + 'level': 'DEBUG' if DEBUG else 'INFO', + }, + }, +} + +# Create logs directory (BASE_DIR / 'logs').mkdir(exist_ok=True) \ No newline at end of file diff --git a/static/css/blog.css b/static/css/blog.css index 62a1a23..2388e48 100644 --- a/static/css/blog.css +++ b/static/css/blog.css @@ -399,46 +399,273 @@ } .article-body { - font-size: 1.1rem; - line-height: 1.8; + font-size: 1.05rem; + line-height: 1.7; color: var(--text-secondary); + text-align: justify; + text-justify: inter-word; + hyphens: auto; + -webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; } .article-body p { - margin-bottom: var(--space-lg); + margin-bottom: 1rem; + margin-top: 0; + text-align: justify; } .article-body h2, .article-body h3, +.article-body h4, +.article-body h5, +.article-body h6 { + text-align: left; +} + +.article-body h2 { + color: var(--text-primary); + margin-top: 2.5rem; + margin-bottom: 1rem; + font-size: 1.75rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border-color); +} + +.article-body h3 { + color: var(--text-primary); + margin-top: 2rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; +} + .article-body h4 { color: var(--text-primary); - margin-top: var(--space-2xl); - margin-bottom: var(--space-md); + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-size: 1.15rem; } .article-body ul, .article-body ol { - margin-bottom: var(--space-lg); - padding-left: var(--space-xl); + margin-bottom: 1.25rem; + margin-top: 0.75rem; + padding-left: 2rem; + text-align: justify; } .article-body li { - margin-bottom: var(--space-sm); + margin-bottom: 0.5rem; + line-height: 1.6; + text-align: justify; +} + +.article-body li p { + margin-bottom: 0.5rem; } .article-body img { max-width: 100%; height: auto; border-radius: var(--radius-md); - margin: var(--space-lg) 0; + margin: 1.5rem 0; + box-shadow: var(--shadow-md); } .article-body blockquote { border-left: 4px solid var(--primary-color); - padding-left: var(--space-lg); - margin: var(--space-lg) 0; - font-style: italic; + padding: 1rem 1.5rem; + margin: 1.5rem 0; + background: var(--bg-card); + border-radius: var(--radius-sm); + font-style: normal; + color: var(--text-secondary); + text-align: justify; +} + +.article-body blockquote p { + text-align: justify; +} + +.article-body blockquote p:last-child { + margin-bottom: 0; +} + +.article-body hr { + border: none; + height: 1px; + background: var(--border-color); + margin: 2rem 0; +} + +.article-body table { + width: 100%; + border-collapse: collapse; + margin: 1.5rem 0; + background: var(--bg-card); + border-radius: var(--radius-md); + overflow: hidden; +} + +.article-body th, +.article-body td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.article-body th { + background: var(--bg-elevated); + color: var(--text-primary); + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.article-body tr:last-child td { + border-bottom: none; +} + +.article-body tr:hover { + background: var(--bg-elevated); +} + +.article-body code { + font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; + background: var(--bg-elevated); + padding: 0.2em 0.5em; + border-radius: var(--radius-xs); + font-size: 0.9em; + color: var(--primary-light); + border: 1px solid var(--border-color); +} + +.article-body pre { + position: relative; + background: var(--bg-darker); + padding: 1.25rem; + border-radius: var(--radius-md); + overflow-x: auto; + margin: 1.5rem 0; + border: 1px solid var(--border-color); + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.article-body pre code { + background: none; + padding: 0; + border: none; + color: #e0e0e0; + font-size: 0.9rem; + line-height: 1.6; + display: block; +} + +.article-body pre:hover .code-copy-btn { + opacity: 1; +} + +.code-copy-btn { + position: absolute; + top: 0.75rem; + right: 0.75rem; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); + opacity: 0; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.code-copy-btn:hover { + background: var(--primary-muted); + border-color: var(--primary-color); + color: var(--primary-color); +} + +.code-copy-btn.copied { + background: rgba(34, 197, 94, 0.15); + border-color: var(--success-color); + color: var(--success-color); +} + +.code-copy-btn i { + font-size: 0.85rem; +} + +.code-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + background: var(--bg-elevated); + border-bottom: 1px solid var(--border-color); + border-radius: var(--radius-md) var(--radius-md) 0 0; + margin-bottom: -1px; +} + +.code-language { + font-size: 0.75rem; color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 600; +} + +.article-body pre.has-header { + border-radius: 0 0 var(--radius-md) var(--radius-md); + margin-top: 0; +} + +.article-body h2, +.article-body h3 { + position: relative; +} + +.header-anchor { + position: absolute; + left: -1.5rem; + opacity: 0; + transition: opacity 0.2s; + color: var(--text-muted); + text-decoration: none; + font-size: 0.8em; +} + +.article-body h2:hover .header-anchor, +.article-body h3:hover .header-anchor { + opacity: 1; +} + +.header-anchor:hover { + color: var(--primary-color); +} + +.article-body pre, +.article-body pre code { + text-align: left; + hyphens: none; + -webkit-hyphens: none; +} + +.article-body table, +.article-body th, +.article-body td { + text-align: left; +} + +.article-body figcaption, +.carousel-caption, +.collage-caption { + text-align: center; } /* Article Footer */ @@ -689,7 +916,7 @@ gap: var(--space-md); } -.comment { +div.comment { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-xl); diff --git a/static/js/main.js b/static/js/main.js index 5e72da0..36cfd1d 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -13,6 +13,8 @@ initBackToTop(); initMessages(); initContactForm(); + initCodeBlocks(); + addHeaderAnchors(); }); // ===== AOS Animation ===== @@ -453,4 +455,98 @@ }); }; + // ===== Code Copy Functionality ===== + function initCodeBlocks() { + const codeBlocks = document.querySelectorAll('.article-body pre'); + + codeBlocks.forEach((pre) => { + // Проверяем, не добавлена ли уже кнопка + if (pre.querySelector('.code-copy-btn')) { + return; + } + + const codeElement = pre.querySelector('code'); + if (!codeElement) return; + + // Создаём кнопку копирования + const copyButton = document.createElement('button'); + copyButton.className = 'code-copy-btn'; + copyButton.type = 'button'; + copyButton.setAttribute('aria-label', 'Копировать код'); + copyButton.innerHTML = 'Копировать'; + + // Обработчик клика + copyButton.addEventListener('click', async function(e) { + e.preventDefault(); + + // Получаем текст кода без HTML-тегов + const codeText = codeElement.textContent || codeElement.innerText; + + try { + await navigator.clipboard.writeText(codeText); + + // Изменяем визуальное состояние кнопки + copyButton.innerHTML = 'Сохранено!'; + copyButton.classList.add('copied'); + + // Возвращаем исходное состояние через 2 секунды + setTimeout(() => { + copyButton.innerHTML = 'Копировать'; + copyButton.classList.remove('copied'); + }, 2000); + + } catch (err) { + console.error('Ошибка при копировании:', err); + + // Fallback для старых браузеров + const textArea = document.createElement('textarea'); + textArea.value = codeText; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + + try { + document.execCommand('copy'); + copyButton.innerHTML = 'Сохранено!'; + copyButton.classList.add('copied'); + + setTimeout(() => { + copyButton.innerHTML = 'Копировать'; + copyButton.classList.remove('copied'); + }, 2000); + } catch (err2) { + showNotification('Не удалось скопировать код', 'error'); + } + + document.body.removeChild(textArea); + } + }); + + // Добавляем кнопку в блок кода + pre.style.position = 'relative'; + pre.appendChild(copyButton); + }); + } + + function addHeaderAnchors() { + const headers = document.querySelectorAll('.article-body h2, .article-body h3'); + headers.forEach(header => { + const id = header.textContent + .toLowerCase() + .replace(/[^a-zа-яё0-9]+/g, '-') + .replace(/^-|-$/g, ''); + + header.id = id; + + const anchor = document.createElement('a'); + anchor.className = 'header-anchor'; + anchor.href = `#${id}`; + anchor.innerHTML = ''; + anchor.setAttribute('aria-label', 'Ссылка на раздел'); + + header.appendChild(anchor); + }); + } + })(); \ No newline at end of file diff --git a/templates/auth/login.html b/templates/auth/login.html index e65ca2c..9620cfb 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -6,9 +6,7 @@
Войдите, чтобы оставлять комментарии и оценивать статьи
diff --git a/templates/auth/register.html b/templates/auth/register.html index 2f602cc..da4327b 100644 --- a/templates/auth/register.html +++ b/templates/auth/register.html @@ -6,9 +6,7 @@
Создайте аккаунт для участия в обсуждениях
diff --git a/templates/blog/article.html b/templates/blog/article.html index d01be55..cb26dcf 100644 --- a/templates/blog/article.html +++ b/templates/blog/article.html @@ -91,7 +91,7 @@