Compare commits

...

28 Commits

Author SHA1 Message Date
Сергей Ванюшкин 4d3a309940 favicon url 2023-10-30 23:46:00 +03:00
Сергей Ванюшкин c2e68881d9 удалил лишние импорты 2023-10-25 08:54:57 +03:00
Сергей Ванюшкин cdf9bfe072 favicon 2023-10-23 10:36:27 +03:00
Сергей Ванюшкин 50e74ccf38 добавил tag description на страницы 2023-10-23 10:03:46 +03:00
Сергей Ванюшкин 52d1b84658 Todo в ридми2 2023-10-21 10:19:46 +03:00
Сергей Ванюшкин 9cecc960db Todo в ридми 2023-10-21 10:16:24 +03:00
Сергей Ванюшкин 985301d27f увеличил поле ввода в редакторе статьи 2023-10-18 08:57:52 +03:00
Сергей Ванюшкин 30f2dbde98 robots 2023-10-16 23:22:21 +03:00
Сергей Ванюшкин 54c40985cb локаль в штампе времени 2023-10-16 23:06:43 +03:00
Сергей Ванюшкин d4722945e9 фиксация 2023-10-16 22:02:29 +03:00
Сергей Ванюшкин e2743db969 поправил сортировку по обновлению 2023-10-16 19:36:02 +03:00
Сергей Ванюшкин 3bc178f2cc убрал навиг гет запросы, зафиксил навигацию пашинации 2023-10-16 19:24:14 +03:00
Сергей Ванюшкин 18724c455a убрал сессион 2023-10-16 17:01:15 +03:00
Сергей Ванюшкин 2c6c2ebbe9 fix 2023-10-16 15:41:31 +03:00
Сергей Ванюшкин 6af1d3c7cb исправил пагинацию на главной, центрирование футера, ссылки со взаимодействием 2023-10-15 23:22:15 +03:00
Сергей Ванюшкин 21a818d71e цвета в консоли 2023-10-12 15:05:34 +03:00
Сергей Ванюшкин c126531e49 скрыл 2023-10-11 21:42:54 +03:00
Сергей Ванюшкин 3b63421f5f скрыл отображение неопубликованных 2023-10-11 21:34:45 +03:00
Сергей Ванюшкин d8f8b36469 ошибки 2023-10-11 19:37:04 +03:00
Сергей Ванюшкин 009415d261 ошибки 2023-10-11 19:35:18 +03:00
Сергей Ванюшкин 9301f984c3 ошибку не заметил 2023-10-11 19:25:48 +03:00
Сергей Ванюшкин 40274136fe ошибку не заметил 2023-10-11 19:16:37 +03:00
Сергей Ванюшкин eb5eb79383 gitignore 2023-10-11 17:50:41 +03:00
Сергей Ванюшкин 48be7e96d9 gitignore 2023-10-11 17:16:29 +03:00
Сергей Ванюшкин 911d3ef845 загрузка файлов при создании поста и вставка картинок динамически с загрузкой в папку uploads 2023-10-11 14:43:58 +03:00
Сергей Ванюшкин 3785ab37e6 fix 404#2 2023-10-11 09:55:26 +03:00
Сергей Ванюшкин 6103b1fd22 fix 404 2023-10-11 09:49:40 +03:00
Сергей Ванюшкин 199eb0f3ae refactoring 2023-10-10 22:44:32 +03:00
32 changed files with 544 additions and 359 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
pyproger/uploads/*
!pyproger/uploads/.gitkeep
config.ini
data/

View File

@ -55,6 +55,11 @@
Перейдите в браузере по адресу 127.0.0.1:5000 для доступа к блогу или 127.0.0.1:5000/admin в админ панель
### TODO
- Добавить на страницы постов "Оглавление" со ссылками на статьи
- Добавить подписку на статьи
- Добавить комментарии к постам
### Авторы
- Сергей Ванюшкин <pi3c@yandex.ru>

View File

@ -1,5 +1,6 @@
import os
import uuid
from datetime import datetime as dt
from dotenv import load_dotenv
@ -8,15 +9,54 @@ if os.path.exists(dotenv_path):
load_dotenv(dotenv_path)
def cls():
os.system("cls" if os.name == "nt" else "clear")
with open(dotenv_path, "a") as f:
print("Генерирую SECRET_KEY...")
if os.getenv("BRAND") is None:
cls()
print("\033[32m{}\033[0m ".format("Введите название проекта."))
print(
"Это название будет отображаться в строке",
"меню и футере на страницах сайта",
sep="\n",
)
br = input("-> ")
f.writelines(f"BRAND={br}\n")
print("\033[32m{}\033[0m ".format("Настройка блока copyright в футере сайта"))
if os.getenv("COPYRIGHT_YEAR") is None:
start_date = dt.utcnow().strftime("%Y")
f.writelines(f"COPYRIGHT_YEAR={start_date}\n")
if os.getenv("COPYRIGHT_NAME") is None:
name = input("Введите имя для для футера:")
f.writelines(f"COPYRIGHT_NAME={name}\n")
if os.getenv("COPYRIGHT_LINK") is None:
print("Введите ссылку для футера:")
print(
"email('something@somthing.else) или веб адрес полностью('http://anysite.any')"
)
link = input(">:")
if link.startswith("http"):
f.writelines(f"COPYRIGHT_LINK={link}\n")
else:
f.writelines(f"COPYRIGHT_LINK=mailto:{link}\n")
if os.getenv("COPYRIGHT_CITY") is None:
name = input("Введите свой город для для футера:")
f.writelines(f"COPYRIGHT_CITY={name}\n")
print("\033[32m{}\033[0m ".format("Генерирую SECRET_KEY..."))
if os.getenv("SECRET_KEY") is None:
f.writelines(f"SECRET_KEY={uuid.uuid4().hex}\n")
print("_Ok_")
else:
print("SECRET_KEY уже установлен, прорускаю")
print("SECRET_KEY уже установлен, пропускаю")
print('Генерирую "Соль"...')
print("\033[32m{}\033[0m ".format('Генерирую "Соль"...'))
if os.getenv("SECURITY_PASSWORD_SALT") is None:
f.writelines(f"SECURITY_PASSWORD_SALT={uuid.uuid4().hex}\n")
print("_Ok_")
@ -24,12 +64,14 @@ with open(dotenv_path, "a") as f:
print("SECURITY_PASSWORD_SALT уже установлен, пропускаю")
if os.getenv("SQLALCHEMY_DATABASE_URI") is None:
print("Настроки подключения к базе данных Posgresql:")
print(
"\033[32m{}\033[0m ".format("Настроки подключения к базе данных Posgresql:")
)
login = input("Введите логин пользователя бд: ")
passwd = input("Пароль: ")
db = input("Название бд (по умолчанию pyproger):") or "pyproger"
db = input("Название бд (по умолчанию pyprogerdb):") or "pyprogerdb"
ip = input("Адрес бд (по умолчанию localhost)") or "localhost"
port = input("Порт подключения: (по умолчанию 5432)") or "5432"
f.writelines(
f"SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://{login}:{passwd}@{ip}:{port}/{db}"
f"SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://{login}:{passwd}@{ip}:{port}/{db}\n"
)

View File

@ -7,6 +7,7 @@ Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
import flask_security
${imports if imports else ""}
# revision identifiers, used by Alembic.

View File

@ -1,17 +1,18 @@
"""empty message
Revision ID: 5e6d181b4b74
Revises: e24055e16b26
Create Date: 2023-10-04 08:46:50.473590
Revision ID: 055327bef08e
Revises: 849ff22feb97
Create Date: 2023-10-23 09:29:00.193470
"""
from alembic import op
import sqlalchemy as sa
import flask_security
# revision identifiers, used by Alembic.
revision = '5e6d181b4b74'
down_revision = 'e24055e16b26'
revision = '055327bef08e'
down_revision = '849ff22feb97'
branch_labels = None
depends_on = None
@ -19,8 +20,7 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('page', schema=None) as batch_op:
batch_op.add_column(sa.Column('update_datetime', sa.DateTime(), nullable=True))
batch_op.create_unique_constraint(None, ['id'])
batch_op.add_column(sa.Column('header_description', sa.Text(), nullable=True))
# ### end Alembic commands ###
@ -28,7 +28,6 @@ def upgrade():
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('page', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('update_datetime')
batch_op.drop_column('header_description')
# ### end Alembic commands ###

View File

@ -0,0 +1,126 @@
"""initial_migration
Revision ID: 68537cc1688c
Revises:
Create Date: 2023-10-10 22:17:40.476992
"""
from alembic import op
import sqlalchemy as sa
import flask_security
# revision identifiers, used by Alembic.
revision = '68537cc1688c'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('footer_icons',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=30), nullable=True),
sa.Column('bootstrap_ico', sa.String(length=20), nullable=True),
sa.Column('link', sa.String(length=100), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
op.create_table('page',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=20), nullable=True),
sa.Column('slug', sa.String(length=50), nullable=False),
sa.Column('text', sa.Text(), nullable=True),
sa.Column('update_datetime', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
op.create_table('role',
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('permissions', flask_security.datastore.AsaList(), nullable=True),
sa.Column('update_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('site_headers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=20), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
op.create_table('tag',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('tag', sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('user',
sa.Column('first_name', sa.String(length=255), nullable=True),
sa.Column('last_name', sa.String(length=255), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('username', sa.String(length=255), nullable=True),
sa.Column('password', sa.String(length=255), nullable=False),
sa.Column('active', sa.Boolean(), nullable=False),
sa.Column('fs_uniquifier', sa.String(length=64), nullable=False),
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
sa.Column('last_login_at', sa.DateTime(), nullable=True),
sa.Column('current_login_at', sa.DateTime(), nullable=True),
sa.Column('last_login_ip', sa.String(length=64), nullable=True),
sa.Column('current_login_ip', sa.String(length=64), nullable=True),
sa.Column('login_count', sa.Integer(), nullable=True),
sa.Column('tf_primary_method', sa.String(length=64), nullable=True),
sa.Column('tf_totp_secret', sa.String(length=255), nullable=True),
sa.Column('tf_phone_number', sa.String(length=128), nullable=True),
sa.Column('create_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('update_datetime', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('fs_uniquifier')
)
op.create_table('post',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('author', sa.Integer(), nullable=True),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('title', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('published', sa.Boolean(), nullable=True),
sa.Column('create_datetime', sa.DateTime(), nullable=True),
sa.Column('update_datetime', sa.DateTime(), nullable=True),
sa.Column('text', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['author'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
op.create_table('roles_users',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
)
op.create_table('tag_post',
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.Column('post_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], )
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('tag_post')
op.drop_table('roles_users')
op.drop_table('post')
op.drop_table('user')
op.drop_table('tag')
op.drop_table('site_headers')
op.drop_table('role')
op.drop_table('page')
op.drop_table('footer_icons')
# ### end Alembic commands ###

View File

@ -0,0 +1,53 @@
"""empty message
Revision ID: 849ff22feb97
Revises: 68537cc1688c
Create Date: 2023-10-23 08:34:08.251453
"""
from alembic import op
import sqlalchemy as sa
import flask_security
# revision identifiers, used by Alembic.
revision = '849ff22feb97'
down_revision = '68537cc1688c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('footer_icons', schema=None) as batch_op:
batch_op.create_unique_constraint(None, ['id'])
with op.batch_alter_table('page', schema=None) as batch_op:
batch_op.create_unique_constraint(None, ['id'])
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.add_column(sa.Column('header_description', sa.Text(), nullable=True))
batch_op.create_unique_constraint(None, ['id'])
with op.batch_alter_table('site_headers', schema=None) as batch_op:
batch_op.create_unique_constraint(None, ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('site_headers', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('header_description')
with op.batch_alter_table('page', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
with op.batch_alter_table('footer_icons', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
# ### end Alembic commands ###

View File

@ -1,132 +0,0 @@
"""initial_migration
Revision ID: 8c415e462cb3
Revises:
Create Date: 2023-09-22 12:19:22.923813
"""
import flask_security
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "8c415e462cb3"
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"role",
sa.Column("name", sa.String(length=80), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("description", sa.String(length=255), nullable=True),
sa.Column("permissions", flask_security.datastore.AsaList(), nullable=True),
sa.Column(
"update_datetime",
sa.DateTime(),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"tag",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("tag", sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"user",
sa.Column("first_name", sa.String(length=255), nullable=True),
sa.Column("last_name", sa.String(length=255), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(length=255), nullable=False),
sa.Column("username", sa.String(length=255), nullable=True),
sa.Column("password", sa.String(length=255), nullable=False),
sa.Column("active", sa.Boolean(), nullable=False),
sa.Column("fs_uniquifier", sa.String(length=64), nullable=False),
sa.Column("confirmed_at", sa.DateTime(), nullable=True),
sa.Column("last_login_at", sa.DateTime(), nullable=True),
sa.Column("current_login_at", sa.DateTime(), nullable=True),
sa.Column("last_login_ip", sa.String(length=64), nullable=True),
sa.Column("current_login_ip", sa.String(length=64), nullable=True),
sa.Column("login_count", sa.Integer(), nullable=True),
sa.Column("tf_primary_method", sa.String(length=64), nullable=True),
sa.Column("tf_totp_secret", sa.String(length=255), nullable=True),
sa.Column("tf_phone_number", sa.String(length=128), nullable=True),
sa.Column(
"create_datetime",
sa.DateTime(),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"update_datetime",
sa.DateTime(),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
sa.UniqueConstraint("fs_uniquifier"),
)
op.create_table(
"post",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("author", sa.Integer(), nullable=True),
sa.Column("slug", sa.String(length=100), nullable=False),
sa.Column("title", sa.Text(), nullable=False),
sa.Column("description", sa.Text(), nullable=False),
sa.Column("published", sa.Boolean(), nullable=True),
sa.Column("create_datetime", sa.DateTime(), nullable=True),
sa.Column("update_datetime", sa.DateTime(), nullable=True),
sa.Column("text", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(
["author"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
op.create_table(
"roles_users",
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("role_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["role_id"],
["role.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
)
op.create_table(
"tag_post",
sa.Column("tag_id", sa.Integer(), nullable=True),
sa.Column("post_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["post_id"],
["post.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tag.id"],
),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("tag_post")
op.drop_table("roles_users")
op.drop_table("post")
op.drop_table("user")
op.drop_table("tag")
op.drop_table("role")
# ### end Alembic commands ###

View File

@ -1,42 +0,0 @@
"""empty message
Revision ID: e24055e16b26
Revises: 8c415e462cb3
Create Date: 2023-10-02 09:30:10.566908
"""
import flask_security
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "e24055e16b26"
down_revision = "8c415e462cb3"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"page",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("name", sa.String(length=20), nullable=True),
sa.Column("slug", sa.String(length=50), nullable=False),
sa.Column("text", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
)
with op.batch_alter_table("post", schema=None) as batch_op:
batch_op.create_unique_constraint(None, ["id"])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("post", schema=None) as batch_op:
batch_op.drop_constraint(None, type_="unique")
op.drop_table("page")
# ### end Alembic commands ###

View File

@ -1,36 +0,0 @@
"""empty message
Revision ID: ea0fde3014d7
Revises: 5e6d181b4b74
Create Date: 2023-10-07 14:32:08.410786
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ea0fde3014d7'
down_revision = '5e6d181b4b74'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('site_headers',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=20), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('enabled', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('site_headers')
# ### end Alembic commands ###

123
poetry.lock generated
View File

@ -35,13 +35,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "blinker"
version = "1.6.2"
version = "1.6.3"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.7"
files = [
{file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
{file = "blinker-1.6.3-py3-none-any.whl", hash = "sha256:296320d6c28b006eb5e32d4712202dbcdcbf5dc482da298c2f44881c43884aaa"},
{file = "blinker-1.6.3.tar.gz", hash = "sha256:152090d27c1c5c722ee7e48504b02d76502811ce02e1523553b4cf8c8b3d3a8d"},
]
[[package]]
@ -222,22 +222,23 @@ Flask = "*"
[[package]]
name = "flask-security-too"
version = "5.3.0"
version = "5.3.1"
description = "Quickly add security features to your Flask application."
optional = false
python-versions = ">=3.8"
files = [
{file = "Flask-Security-Too-5.3.0.tar.gz", hash = "sha256:9f5d830913eac66f18845795ae5f7d044bdd0d836aeabccfebadab6a29f79354"},
{file = "Flask_Security_Too-5.3.0-py3-none-any.whl", hash = "sha256:c358893d9de3332e523288e6751b51d354973e25e70b0b4a7ef229eca9f910db"},
{file = "Flask-Security-Too-5.3.1.tar.gz", hash = "sha256:1dafe00c611ce3811e7fe1686ecd7750938806a1ec3c5a278185f31958895d3c"},
{file = "Flask_Security_Too-5.3.1-py3-none-any.whl", hash = "sha256:159ed080dce4a717c2852eac50443221f50b391f5af6f03c82febc3740d572d1"},
]
[package.dependencies]
email-validator = ">=1.1.1"
Flask = ">=2.3.0"
Flask = ">=2.3.2"
Flask-Login = ">=0.6.2"
Flask-Principal = ">=0.4.0"
Flask-WTF = ">=1.1.1"
Flask-WTF = ">=1.1.2"
importlib-resources = ">=5.10.0"
markupsafe = ">=2.1.0"
passlib = ">=1.7.4"
wtforms = ">=3.0.0"
@ -245,8 +246,8 @@ wtforms = ">=3.0.0"
babel = ["babel (>=2.12.1)", "flask-babel (>=3.1.0)"]
common = ["bcrypt (>=4.0.1)", "bleach (>=6.0.0)", "flask-mailman (>=0.3.0)"]
fsqla = ["flask-sqlalchemy (>=3.0.3)", "sqlalchemy (>=2.0.12)", "sqlalchemy-utils (>=0.41.1)"]
low = ["Flask (==2.3.2)", "Flask-Babel (==3.1.0)", "Flask-Login (==0.6.2)", "Flask-Mailman (==0.3.0)", "Flask-SQLAlchemy (==3.0.3)", "Flask-WTF (==1.1.1)", "argon2-cffi (==21.3.0)", "authlib (==1.2.0)", "babel (==2.12.1)", "bcrypt (==4.0.1)", "bleach (==6.0.0)", "itsdangerous (==2.1.2)", "jinja2 (==3.1.2)", "markupsafe (==2.1.2)", "mongoengine (==0.27.0)", "mongomock (==4.1.2)", "peewee (==3.16.2)", "phonenumberslite (==8.13.11)", "pony (==0.7.16)", "python-dateutil (==2.8.2)", "qrcode (==7.4.2)", "requests", "sqlalchemy (==2.0.12)", "sqlalchemy-utils (==0.41.1)", "webauthn (==1.9.0)", "werkzeug (==2.3.3)", "zxcvbn (==4.4.28)"]
mfa = ["cryptography (>=40.0.2)", "phonenumberslite (>=8.13.11)", "qrcode (>=7.4.2)", "webauthn (>=1.9.0)"]
low = ["Flask (==2.3.2)", "Flask-Babel (==3.1.0)", "Flask-Login (==0.6.2)", "Flask-Mailman (==0.3.0)", "Flask-SQLAlchemy (==3.0.3)", "Flask-WTF (==1.1.2)", "argon2-cffi (==21.3.0)", "authlib (==1.2.0)", "babel (==2.12.1)", "bcrypt (==4.0.1)", "bleach (==6.0.0)", "itsdangerous (==2.1.2)", "jinja2 (==3.1.2)", "markupsafe (==2.1.2)", "mongoengine (==0.27.0)", "mongomock (==4.1.2)", "peewee (==3.16.2)", "phonenumberslite (==8.13.11)", "pony (==0.7.16)", "pydantic (<2.0)", "python-dateutil (==2.8.2)", "qrcode (==7.4.2)", "requests", "sqlalchemy (==2.0.12)", "sqlalchemy-utils (==0.41.1)", "webauthn (==1.11.0)", "werkzeug (==2.3.3)", "zxcvbn (==4.4.28)"]
mfa = ["cryptography (>=40.0.2)", "phonenumberslite (>=8.13.11)", "qrcode (>=7.4.2)", "webauthn (>=1.11.0)"]
[[package]]
name = "flask-sqlalchemy"
@ -594,52 +595,60 @@ files = [
[[package]]
name = "sqlalchemy"
version = "2.0.21"
version = "2.0.22"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-win32.whl", hash = "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4"},
{file = "SQLAlchemy-2.0.21-cp310-cp310-win_amd64.whl", hash = "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"},
{file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-win32.whl", hash = "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9"},
{file = "SQLAlchemy-2.0.21-cp37-cp37m-win_amd64.whl", hash = "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-win32.whl", hash = "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6"},
{file = "SQLAlchemy-2.0.21-cp38-cp38-win_amd64.whl", hash = "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-win32.whl", hash = "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9"},
{file = "SQLAlchemy-2.0.21-cp39-cp39-win_amd64.whl", hash = "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce"},
{file = "SQLAlchemy-2.0.21-py3-none-any.whl", hash = "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce"},
{file = "SQLAlchemy-2.0.21.tar.gz", hash = "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f146c61ae128ab43ea3a0955de1af7e1633942c2b2b4985ac51cc292daf33222"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:875de9414393e778b655a3d97d60465eb3fae7c919e88b70cc10b40b9f56042d"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13790cb42f917c45c9c850b39b9941539ca8ee7917dacf099cc0b569f3d40da7"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e04ab55cf49daf1aeb8c622c54d23fa4bec91cb051a43cc24351ba97e1dd09f5"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a42c9fa3abcda0dcfad053e49c4f752eef71ecd8c155221e18b99d4224621176"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:14cd3bcbb853379fef2cd01e7c64a5d6f1d005406d877ed9509afb7a05ff40a5"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-win32.whl", hash = "sha256:d143c5a9dada696bcfdb96ba2de4a47d5a89168e71d05a076e88a01386872f97"},
{file = "SQLAlchemy-2.0.22-cp310-cp310-win_amd64.whl", hash = "sha256:ccd87c25e4c8559e1b918d46b4fa90b37f459c9b4566f1dfbce0eb8122571547"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f6ff392b27a743c1ad346d215655503cec64405d3b694228b3454878bf21590"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f776c2c30f0e5f4db45c3ee11a5f2a8d9de68e81eb73ec4237de1e32e04ae81c"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8f1792d20d2f4e875ce7a113f43c3561ad12b34ff796b84002a256f37ce9437"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80eeb5189d7d4b1af519fc3f148fe7521b9dfce8f4d6a0820e8f5769b005051"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:69fd9e41cf9368afa034e1c81f3570afb96f30fcd2eb1ef29cb4d9371c6eece2"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54bcceaf4eebef07dadfde424f5c26b491e4a64e61761dea9459103ecd6ccc95"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-win32.whl", hash = "sha256:7ee7ccf47aa503033b6afd57efbac6b9e05180f492aeed9fcf70752556f95624"},
{file = "SQLAlchemy-2.0.22-cp311-cp311-win_amd64.whl", hash = "sha256:b560f075c151900587ade06706b0c51d04b3277c111151997ea0813455378ae0"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2c9bac865ee06d27a1533471405ad240a6f5d83195eca481f9fc4a71d8b87df8"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:625b72d77ac8ac23da3b1622e2da88c4aedaee14df47c8432bf8f6495e655de2"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39a6e21110204a8c08d40ff56a73ba542ec60bab701c36ce721e7990df49fb9"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a766cb0b468223cafdf63e2d37f14a4757476157927b09300c8c5832d88560"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0e1ce8ebd2e040357dde01a3fb7d30d9b5736b3e54a94002641dfd0aa12ae6ce"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:505f503763a767556fa4deae5194b2be056b64ecca72ac65224381a0acab7ebe"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-win32.whl", hash = "sha256:154a32f3c7b00de3d090bc60ec8006a78149e221f1182e3edcf0376016be9396"},
{file = "SQLAlchemy-2.0.22-cp312-cp312-win_amd64.whl", hash = "sha256:129415f89744b05741c6f0b04a84525f37fbabe5dc3774f7edf100e7458c48cd"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3940677d341f2b685a999bffe7078697b5848a40b5f6952794ffcf3af150c301"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55914d45a631b81a8a2cb1a54f03eea265cf1783241ac55396ec6d735be14883"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2096d6b018d242a2bcc9e451618166f860bb0304f590d205173d317b69986c95"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:19c6986cf2fb4bc8e0e846f97f4135a8e753b57d2aaaa87c50f9acbe606bd1db"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ac28bd6888fe3c81fbe97584eb0b96804bd7032d6100b9701255d9441373ec1"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-win32.whl", hash = "sha256:cb9a758ad973e795267da334a92dd82bb7555cb36a0960dcabcf724d26299db8"},
{file = "SQLAlchemy-2.0.22-cp37-cp37m-win_amd64.whl", hash = "sha256:40b1206a0d923e73aa54f0a6bd61419a96b914f1cd19900b6c8226899d9742ad"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3aa1472bf44f61dd27987cd051f1c893b7d3b17238bff8c23fceaef4f1133868"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:56a7e2bb639df9263bf6418231bc2a92a773f57886d371ddb7a869a24919face"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccca778c0737a773a1ad86b68bda52a71ad5950b25e120b6eb1330f0df54c3d0"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6c3e9350f9fb16de5b5e5fbf17b578811a52d71bb784cc5ff71acb7de2a7f9"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:564e9f9e4e6466273dbfab0e0a2e5fe819eec480c57b53a2cdee8e4fdae3ad5f"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af66001d7b76a3fab0d5e4c1ec9339ac45748bc4a399cbc2baa48c1980d3c1f4"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-win32.whl", hash = "sha256:9e55dff5ec115316dd7a083cdc1a52de63693695aecf72bc53a8e1468ce429e5"},
{file = "SQLAlchemy-2.0.22-cp38-cp38-win_amd64.whl", hash = "sha256:4e869a8ff7ee7a833b74868a0887e8462445ec462432d8cbeff5e85f475186da"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9886a72c8e6371280cb247c5d32c9c8fa141dc560124348762db8a8b236f8692"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a571bc8ac092a3175a1d994794a8e7a1f2f651e7c744de24a19b4f740fe95034"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db5ba8b7da759b727faebc4289a9e6a51edadc7fc32207a30f7c6203a181592"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b0b3f2686c3f162123adba3cb8b626ed7e9b8433ab528e36ed270b4f70d1cdb"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c1fea8c0abcb070ffe15311853abfda4e55bf7dc1d4889497b3403629f3bf00"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4bb062784f37b2d75fd9b074c8ec360ad5df71f933f927e9e95c50eb8e05323c"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-win32.whl", hash = "sha256:58a3aba1bfb32ae7af68da3f277ed91d9f57620cf7ce651db96636790a78b736"},
{file = "SQLAlchemy-2.0.22-cp39-cp39-win_amd64.whl", hash = "sha256:92e512a6af769e4725fa5b25981ba790335d42c5977e94ded07db7d641490a85"},
{file = "SQLAlchemy-2.0.22-py3-none-any.whl", hash = "sha256:3076740335e4aaadd7deb3fe6dcb96b3015f1613bd190a4e1634e1b99b02ec86"},
{file = "SQLAlchemy-2.0.22.tar.gz", hash = "sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b"},
]
[package.dependencies]
@ -700,17 +709,17 @@ watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "wtforms"
version = "3.0.1"
version = "3.1.0"
description = "Form validation and rendering for Python web development."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "WTForms-3.0.1-py3-none-any.whl", hash = "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"},
{file = "WTForms-3.0.1.tar.gz", hash = "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc"},
{file = "wtforms-3.1.0-py3-none-any.whl", hash = "sha256:addd7899004fdf9318eb711d33aae9c1973fe80378257b7383e06de2eff7c559"},
{file = "wtforms-3.1.0.tar.gz", hash = "sha256:4edd15771630289a5fa343d58822f72749822ca5a39dd33f92ee917cf72b954b"},
]
[package.dependencies]
MarkupSafe = "*"
markupsafe = "*"
[package.extras]
email = ["email-validator"]

View File

@ -1,5 +1,4 @@
from flask_admin import Admin
from flask_ckeditor import CKEditor
admin = Admin(
name="Админ панель",

View File

@ -72,6 +72,7 @@ class PostView(MyAdminView):
tags="Тэги",
slug="Слаг",
title="Заголовок",
header_description="description заголовок в head страницы",
description="Краткое описание статьи",
author="Автор",
published="Опубликовано",
@ -92,6 +93,7 @@ class PostView(MyAdminView):
class PageView(MyAdminView):
column_labels = dict(
name="Название страницы",
header_description="description заголовок в head страницы",
slug="URL страницы",
text="Содержимое страницы",
)
@ -112,3 +114,12 @@ class HeadersView(MyAdminView):
"name",
"description",
)
class FooterLinksView(MyAdminView):
column_labels = dict(
name="Название",
bootstrap_ico="Bootstrap код иконки",
link="Ссылка",
)
column_list = ("name",)

View File

@ -1,15 +1,19 @@
import os
from dotenv import load_dotenv
from flask import Flask, render_template_string, url_for
from flask import (Flask, render_template_string, request, send_from_directory,
url_for)
from flask_admin import helpers
from flask_ckeditor import CKEditor
from flask_ckeditor import CKEditor, upload_fail, upload_success
from flask_migrate import Migrate
from flask_security.core import Security
from pyproger.admin.views import FooterLinksView
from pyproger.dbase import Role, User, db, user_datastore
from pyproger.dbase.database import get_headers, get_menu_items
from pyproger.dbase.models import Page, Post, SiteHeaders, Tag
from pyproger.dbase.database import (get_footer_links, get_headers,
get_menu_items)
from pyproger.dbase.models import (FooterContactLinks, Page, Post, SiteHeaders,
Tag)
def create_app(test_config=None):
@ -23,6 +27,13 @@ def create_app(test_config=None):
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
app.config["SECURITY_PASSWORD_SALT"] = os.getenv("SECURITY_PASSWORD_SALT")
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("SQLALCHEMY_DATABASE_URI")
app.config["MYCOPYRIGHT"] = {
"year": os.getenv("COPYRIGHT_YEAR"),
"name": os.getenv("COPYRIGHT_NAME"),
"link": os.getenv("COPYRIGHT_LINK"),
"city": os.getenv("COPYRIGHT_CITY"),
}
app.config["BRAND"] = os.getenv("BRAND")
else:
app.config.from_mapping(test_config)
@ -98,6 +109,14 @@ def create_app(test_config=None):
name="Включаемые в html заголовки",
)
)
admin.add_view(
FooterLinksView(
FooterContactLinks,
db.session,
category="Страницы",
name="Иконки-ссылки футера",
)
)
from pyproger.blog.blog import bp as bp_blog
from pyproger.cli.commands import bp_cli
@ -110,11 +129,27 @@ def create_app(test_config=None):
app.register_blueprint(bp_robots)
with app.app_context():
site_headers = get_headers()
try:
site_headers = get_headers()
except Exception as e:
print(e)
site_headers = None
app.config["SITE_HEADERS"] = site_headers
menu_items = get_menu_items()
try:
menu_items = get_menu_items()
except Exception as e:
print(e)
menu_items = None
app.config["MENU_ITEMS"] = menu_items
try:
links = get_footer_links()
except Exception as e:
print(e)
links = None
app.config["MYLINKS"] = links
@security.context_processor
def security_context_processor():
return dict(
@ -134,4 +169,27 @@ def create_app(test_config=None):
def hello():
return render_template_string("pong")
@app.route("/files/<filename>")
def uploaded_files(filename):
path = app.config["UPLOADED_PATH"]
return send_from_directory(path, filename)
@app.route("/upload", methods=["POST"])
def upload():
f = request.files.get("upload")
extension = f.filename.split(".")[-1].lower()
if extension not in ["jpg", "gif", "png", "jpeg"]:
return upload_fail(message="Image only!")
f.save(os.path.join(app.config["UPLOADED_PATH"], f.filename))
url = url_for("uploaded_files", filename=f.filename)
return upload_success(url=url)
@app.route("/favicon.ico")
def favicon():
return send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
return app

View File

@ -1,29 +1,35 @@
import locale
import re
from flask import (abort, current_app, redirect, render_template, request,
session, url_for)
from flask import abort, current_app, render_template
from ..dbase.database import (get_all_posts_by_tag, get_page,
get_paginated_posts, get_post, get_tags)
from ..dbase.database import (
get_all_posts_by_tag,
get_page,
get_paginated_posts,
get_post,
get_tags,
)
from .blog import bp
locale.setlocale(locale.LC_ALL, "")
locale.setlocale(locale.LC_ALL, ("ru", "utf-8"))
@bp.route("/", methods=["GET"], defaults={"page": 1})
@bp.route("/")
@bp.route("/<int:page>")
def index(page=1):
session["back_url"] = request.url
per_page = current_app.config.get("POSTS_ON_PAGE")
posts, total_pages = get_paginated_posts(page, per_page)
posts, total = get_paginated_posts(page, per_page)
total_pages = total // per_page + [0, 1][total % per_page != 0]
list_pages = [
x for x in range(1, total_pages + 1) if x >= page - 2 and x <= page + 2
]
return render_template(
"blog/index.html",
title=f'{current_app.config.get("BRAND")} - разговоры про питон',
header_description="Про изучение python, веб разработку и прочие вещи",
headers=current_app.config.get("SITE_HEADERS"),
menu_title=current_app.config.get("BRAND"),
menu_items=current_app.config.get("MENU_ITEMS"),
@ -72,6 +78,7 @@ def get_all_tags():
return render_template(
"blog/tags.html",
title=f'{current_app.config.get("BRAND")} - поиск по тэгу',
header_description="Выбор статей по тематике",
headers=current_app.config.get("SITE_HEADERS"),
menu_title=current_app.config.get("BRAND"),
tags=tags,
@ -81,32 +88,30 @@ def get_all_tags():
)
@bp.route("/tag/", methods=["GET"], defaults={"page": 1})
@bp.route("/tag/<path:tag>")
@bp.route("/tag/")
@bp.route("/tag/<path:tag>/<int:page>")
def get_posts_by_tag(page=1, tag=None):
if tag is None:
tag = request.args.get("tag")
if tag is None:
return redirect(url_for(".get_all_tags"))
per_page = current_app.config.get("POSTS_ON_PAGE")
posts, total = get_all_posts_by_tag(tag, page, per_page)
total_pages = total // per_page + [0, 1][total % per_page != 0]
if posts is None:
abort(404)
total_pages = total // per_page + [0, 1][total % per_page != 0]
list_pages = [
x for x in range(1, total_pages + 1) if x >= page - 2 and x <= page + 2
]
return render_template(
"blog/index.html",
"blog/tagget_posts.html",
title=f'{current_app.config.get("BRAND")} - посты по {tag}',
header_description=f"Статьи по теме {tag}",
headers=current_app.config.get("SITE_HEADERS"),
menu_title=current_app.config.get("BRAND"),
menu_items=current_app.config.get("MENU_ITEMS"),
posts=posts,
page=page,
tag=tag,
total_pages=total_pages,
list_pages=list_pages,
mylinks=current_app.config.get("MYLINKS"),
@ -122,6 +127,7 @@ def page(slug=None):
return render_template(
"blog/page.html",
title=f'{current_app.config.get("BRAND")} - {page.name}',
header_description=page.header_description,
headers=current_app.config.get("SITE_HEADERS"),
menu_title=current_app.config.get("BRAND"),
menu_items=current_app.config.get("MENU_ITEMS"),

View File

@ -1,19 +1,6 @@
import os
# Настройки блога
BRAND = "блог"
MYCOPYRIGHT = {
"year": "2023",
"name": "Иванов Иван",
"link": "http://yandex.ru",
"city": "г.Москва",
}
MYLINKS = (
{"icon": "fab fa-telegram", "link": "https://t.me"},
{"icon": "fab fa-vk", "link": "https://m.vk.com"},
{"icon": "fab fa-yandex", "link": "mailto:user@yandex.ru"},
{"icon": "fab fa-github", "link": "https://github.com"},
)
POSTS_ON_PAGE = 6
# Тема оформления админ панели
@ -53,7 +40,11 @@ SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False
LANGUAGES = ["ru"]
BABEL_TRANSLATION_DIRECTORIES = os.path.join(os.path.curdir, "translations")
basedir = os.path.abspath(os.path.dirname(__file__))
CKEDITOR_PKG_TYPE = "full"
CKEDITOR_SERVE_LOCAL = True
CKEDITOR_ENABLE_CODESNIPPET = True
CKEDITOR_CODE_THEME = "monokai_sublime"
CKEDITOR_FILE_UPLOADER = "upload"
# app.config['CKEDITOR_ENABLE_CSRF'] = True # if you want to enable CSRF protect, uncomment this line
UPLOADED_PATH = os.path.join(basedir, "uploads")

View File

@ -1,12 +1,13 @@
from . import db
from .models import Page, Post, SiteHeaders, Tag, User
from .models import FooterContactLinks, Page, Post, SiteHeaders, Tag, User
def get_paginated_posts(page, per_page):
all_post_query = (
db.session.query(Post, User)
.join(User, Post.author == User.id)
.order_by(Post.create_datetime.desc())
.filter(Post.published.is_(True))
.order_by(Post.update_datetime.desc())
.paginate(page=page, per_page=per_page, error_out=True)
)
return all_post_query, all_post_query.total
@ -16,6 +17,7 @@ def get_post(slug):
post_query = (
db.session.query(Post, User)
.join(User, Post.author == User.id)
.filter(Post.published.is_(True))
.filter(Post.slug == slug)
.first()
)
@ -31,16 +33,14 @@ def get_all_posts_by_tag(tag, page, per_page):
posts_query = (
db.session.query(Post, User)
.join(User, Post.author == User.id)
.filter(Post.published.is_(True))
.filter(Post.tags.any(Tag.tag == tag))
.order_by(Post.create_datetime.desc())
.order_by(Post.update_datetime.desc())
.paginate(page=page, per_page=per_page, error_out=True)
)
total_pages = (
posts_query.total // per_page + [0, 1][posts_query.total % per_page != 0]
)
if posts_query.total == 0:
return None, None
return posts_query, total_pages
return None, False
return posts_query, posts_query.total
def get_page(slug):
@ -72,3 +72,11 @@ def get_headers():
.all()
)
return headers
def get_footer_links():
links = db.session.query(
FooterContactLinks.link,
FooterContactLinks.bootstrap_ico,
).all()
return links

View File

@ -1,5 +1,3 @@
import datetime
from flask_security.models import fsqla
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text
from sqlalchemy.sql import func
@ -72,6 +70,7 @@ class Post(db.Model):
author = Column(Integer, db.ForeignKey("user.id"))
slug = Column(String(100), nullable=False)
title = Column(Text, nullable=False)
header_description = Column(Text)
description = Column(Text, nullable=False)
published = Column(Boolean, default=False)
tags = db.relationship("Tag", secondary=tag_post)
@ -99,6 +98,7 @@ class Page(db.Model):
unique=True,
autoincrement=True,
)
header_description = Column(Text)
name = Column(String(20))
slug = Column(String(50), nullable=False)
text = Column(Text)
@ -122,3 +122,17 @@ class SiteHeaders(db.Model):
description = Column(Text)
content = Column(Text)
enabled = Column(Boolean, default=False)
class FooterContactLinks(db.Model):
__tablename__ = "footer_icons"
id = Column(
Integer,
primary_key=True,
nullable=False,
unique=True,
autoincrement=True,
)
name = Column(String(30))
bootstrap_ico = Column(String(20))
link = Column(String(100))

View File

@ -1,4 +1,4 @@
from flask import render_template, url_for
from flask import current_app, render_template
from . import bp
@ -8,8 +8,12 @@ def handle_404(err):
return (
render_template(
"errors/404.html",
title="pyproger - Страница не найдена",
menu_title="pyproger",
title=f'{current_app.config.get("BRAND")} - поиск по тэгу',
headers=current_app.config.get("SITE_HEADERS"),
menu_title=current_app.config.get("BRAND"),
menu_items=current_app.config.get("MENU_ITEMS"),
mylinks=current_app.config.get("MYLINKS"),
copyright=current_app.config.get("MYCOPYRIGHT"),
),
404,
)

View File

@ -1,5 +1,4 @@
from flask import make_response, render_template
from sqlalchemy.sql import func
from pyproger.dbase.database import get_posts_for_sitemap

BIN
pyproger/static/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,6 +1,14 @@
{% extends 'admin/model/edit.html' %}
{% block head %}
{{ super()}}
{{ ckeditor.load_code_theme() }}
{% endblock head %}
{% block tail %}
{{ super() }}
{{ ckeditor.load() }}
{% endblock %}
{{ ckeditor.config(name='title')}}
{{ ckeditor.config(name='description')}}
{{ ckeditor.config(name='text', height=700)}}
{% endblock tail %}

View File

@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<title>{% block title %}{% endblock title %}</title>
<meta name="description" content="{% block header_description %}{% endblock header_description %}" />
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% for h in headers %}
{{h.content | safe}}
{% endfor %}
@ -36,17 +38,17 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="text-body navbar-nav me-auto mb-2 mb-lg-0 p-3">
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white p-2 link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="{{ url_for('bp_blog.index')}}">Главная
</a>
</li>
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white p-2 link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="{{ url_for('bp_blog.get_all_tags')}}">Статьи по темам</a>
</li>
{% for m in menu_items %}
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white p-2 link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="/{{m.slug}}">{{m.name}}</a>
</li>
{% endfor %}
@ -71,34 +73,34 @@
<a class="btn btn-outline-light btn-floating m-2"
href="{{ l.link }}"
role="button"
><i class="{{ l.icon }}"></i
><i class="{{ l.bootstrap_ico }}"></i
></a>
{% endfor %}
</section>
<!-- Section: Social media -->
<!-- Section: Links -->
<section class="">
<section class="container-fluid justify-content-center">
<!--Grid row-->
<div class="row">
<!--Grid column-->
<div class="col-lg-3 col-md-6 mb-4 mb-md-0">
<div class="col-lg-6 col-md-6 mb-4 mb-md-0">
<h5 class="text-white">{{ menu_title }}.ru</h5>
<ul class="list-unstyled mb-0">
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="{{ url_for('bp_blog.index')}}">Главная
</a>
</li>
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="{{ url_for('bp_blog.get_all_tags')}}">Статьи по темам</a>
</li>
{% for m in menu_items %}
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="/{{m.slug}}">{{m.name}}</a>
</li>
{% endfor %}
@ -107,24 +109,24 @@
<!--Grid column-->
<!--Grid column-->
<div class="col-lg-3 col-md-6 mb-4 mb-md-0">
<div class="col-lg-6 col-md-6 mb-4 mb-md-0">
<h5 class="text-white">Вебхостинг</h5>
<ul class="list-unstyled mb-0">
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="https://www.reg.ru/?rlink=reflink-11750021">Российский регистратор reg.ru </a>
</li>
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="https://www.reg.ru/domain/new/?rlink=reflink-11750021">Регистрация доменов</a>
</li>
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="https://www.reg.ru/hosting/?rlink=reflink-11750021">Веб хостинг сайтов</a>
</li>
<li>
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white"
<a class="link-offset-2 link-underline link-underline-opacity-0 text-white link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-75-hover"
href="https://www.reg.ru/vps/cloud/?rlink=reflink-11750021">Облачные серверы</a>
</li>
</ul>

View File

@ -4,6 +4,10 @@
{{title}}
{% endblock %}
{% block header_description %}
{{ header_description }}
{% endblock %}
{% block menu_title %}
{{ menu_title }}
{% endblock %}
@ -18,7 +22,7 @@
</a>
<small>
{% for t in p.Post.tags %}
<a class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover" href="{{url_for(".get_posts_by_tag", tag=t.tag )}}">
<a class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover" href="/tag/{{t.tag}}/1">
#{{t.tag}}
</a>
{% endfor %}<br>
@ -33,25 +37,27 @@
<nav class="container p-3">
<ul class="pagination justify-content-center"
{% block nav_pages %}
{% if posts.has_prev %}
<li class="page-item"><a class="page-link" href="{{ url_for(".index", page=posts.prev_num)}}">&lt;&lt;</a></li>
<li class="page-item"><a class="page-link" href="/1">&lt;&lt;</a></li>
{% else %}
<li class="page-item disabled"><a class="page-link" >&lt;&lt;</a></li>
{% endif %}
{% for i in list_pages %}
{% if i == page %}
<li class="page-item active"><a class="page-link" href="{{ url_for(".index", page=i)}}">{{ i }}</a></li>
<li class="page-item active"><a class="page-link" href="/{{i}}">{{ i }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="{{ url_for(".index", page=i)}}">{{ i }}</a></li>
<li class="page-item"><a class="page-link" href="/{{i}}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item"><a class="page-link" href="{{ url_for(".index", page=posts.next_num)}}">&gt;&gt;</a></li>
<li class="page-item"><a class="page-link" href="/{{total_pages}}">&gt;&gt;</a></li>
{% else %}
<li class="page-item disabled"><a class="page-link" >&gt;&gt;</a></li>
{% endif %}
{% endif %}
{% endblock nav_pages %}
</ul>
</nav>
{% endblock %}

View File

@ -8,6 +8,10 @@
{{ menu_title }}
{% endblock %}
{% block header_description %}
{{ header_description }}
{% endblock %}
{% block content %}
<!-- Main Content-->
<div class="conteiner ">

View File

@ -4,12 +4,16 @@
{{ title }}
{% endblock %}
{% block header_description%}
{{ post.Post.header_description }}
{% endblock%}
{% block menu_title %}
{{ menu_title }}
{% endblock %}
{% block content %}
<div class="conteiner p-4 px-lg-5">
<div class="container p-4 px-lg-5">
<h4>{{ post.Post.title | safe }}</h4>
{{ post.Post.text | safe }}
</div>

View File

@ -0,0 +1,23 @@
{% extends 'blog/index.html' %}
{% block nav_pages %}
{% if posts.has_prev %}
<li class="page-item"><a class="page-link" href="/tag/{{tag}}/1">&lt;&lt;</a></li>
{% else %}
<li class="page-item disabled"><a class="page-link">&lt;&lt;</a></li>
{% endif %}
{% for i in list_pages %}
{% if i == page %}
<li class="page-item active"><a class="page-link" href="/tag/{{tag}}/{{i}}">{{ i }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="/tag/{{tag}}/{{i}}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if posts.has_next %}
<li class="page-item"><a class="page-link" href="/tag/{{tag}}/{{total_pages}}">&gt;&gt;</a></li>
{% else %}
<li class="page-item disabled"><a class="page-link">&gt;&gt;</a></li>
{% endif %}
{% endblock nav_pages %}

View File

@ -4,6 +4,10 @@
{{title}}
{% endblock %}
{% block header_description %}
{{ header_description }}
{% endblock %}
{% block menu_title %}
{{ menu_title }}
{% endblock %}
@ -15,7 +19,7 @@
<h4>Тэги статей:</h4>
{% for t in tags %}
<li>
<a class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover" href="{{url_for(".get_posts_by_tag", tag=t.tag )}}">
<a class="link-offset-2 link-offset-3-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover" href="/tag/{{t.tag}}/1">
#{{t.tag}}
</a>
</li>

View File

@ -26,3 +26,18 @@
</div>
{% endblock content%}
{% block copyright_year %}
{{ copyright.year }}
{% endblock %}
{% block copyright_name %}
{{ copyright.name }}
{% endblock %}
{% block copyright_link %}
{{ copyright.link }}
{% endblock %}
{% block copyright_city %}
{{ copyright.city }}
{% endblock %}

View File

@ -5,6 +5,7 @@ Disallow: /static/
Disallow: /post/$
Disallow: /tag/
Disallow: /tags/
Allow: /post/*
Allow: /static/*.css
Allow: /static/*.js
Allow: /static/*.png
@ -18,6 +19,7 @@ Disallow: /static/
Disallow: /post/$
Disallow: /tag/
Disallow: /tags/
Allow: /post/*
Allow: /static/*.css
Allow: /static/*.js
Allow: /static/*.png
@ -31,6 +33,7 @@ Disallow: /static/
Disallow: /post/$
Disallow: /tag/
Disallow: /tags/
Allow: /post/*
Crawl-Delay: 5
Sitemap: https://pyproger.ru/sitemap.xml

View File

@ -1,5 +1,3 @@
import os
from flask import Blueprint, current_app, request
from flask_babel import Babel

View File