Add basic admin, db models, alchemy and first migration
parent
8c287f4a67
commit
5852352286
|
@ -0,0 +1 @@
|
|||
Single-database configuration for Flask.
|
|
@ -0,0 +1,50 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,113 @@
|
|||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,131 @@
|
|||
"""initial_migration
|
||||
|
||||
Revision ID: 70be0dbb9fbb
|
||||
Revises:
|
||||
Create Date: 2023-09-18 21:01:48.648924
|
||||
|
||||
"""
|
||||
import flask_security
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "70be0dbb9fbb"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"role",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(length=80), 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(), 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(), nullable=False),
|
||||
sa.Column("author", sa.Integer(), nullable=True),
|
||||
sa.Column("slug", sa.String(length=30), nullable=True),
|
||||
sa.Column("title", sa.String(length=50), 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", "slug"),
|
||||
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 ###
|
|
@ -1,4 +1,23 @@
|
|||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.12.0"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "alembic-1.12.0-py3-none-any.whl", hash = "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f"},
|
||||
{file = "alembic-1.12.0.tar.gz", hash = "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Mako = "*"
|
||||
SQLAlchemy = ">=1.3.0"
|
||||
typing-extensions = ">=4"
|
||||
|
||||
[package.extras]
|
||||
tz = ["python-dateutil"]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
|
@ -92,6 +111,25 @@ Werkzeug = ">=2.3.7"
|
|||
async = ["asgiref (>=3.2)"]
|
||||
dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-admin"
|
||||
version = "1.6.1"
|
||||
description = "Simple and extensible admin interface framework for Flask"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "Flask-Admin-1.6.1.tar.gz", hash = "sha256:24cae2af832b6a611a01d7dc35f42d266c1d6c75a426b869d8cb241b78233369"},
|
||||
{file = "Flask_Admin-1.6.1-py3-none-any.whl", hash = "sha256:fd8190f1ec3355913a22739c46ed3623f1d82b8112cde324c60a6fc9b21c9406"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Flask = ">=0.7"
|
||||
wtforms = "*"
|
||||
|
||||
[package.extras]
|
||||
aws = ["boto"]
|
||||
azure = ["azure-storage-blob"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-login"
|
||||
version = "0.6.2"
|
||||
|
@ -107,6 +145,22 @@ files = [
|
|||
Flask = ">=1.0.4"
|
||||
Werkzeug = ">=1.0.1"
|
||||
|
||||
[[package]]
|
||||
name = "flask-migrate"
|
||||
version = "4.0.5"
|
||||
description = "SQLAlchemy database migrations for Flask applications using Alembic."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "Flask-Migrate-4.0.5.tar.gz", hash = "sha256:d3f437a8b5f3849d1bb1b60e1b818efc564c66e3fefe90b62e5db08db295e1b1"},
|
||||
{file = "Flask_Migrate-4.0.5-py3-none-any.whl", hash = "sha256:613a2df703998e78716cace68cd83972960834424457f5b67f56e74fff950aef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alembic = ">=1.9.0"
|
||||
Flask = ">=0.9"
|
||||
Flask-SQLAlchemy = ">=1.0"
|
||||
|
||||
[[package]]
|
||||
name = "flask-principal"
|
||||
version = "0.4.0"
|
||||
|
@ -195,7 +249,6 @@ files = [
|
|||
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
|
||||
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
|
||||
|
@ -204,7 +257,6 @@ files = [
|
|||
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
|
||||
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
|
||||
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
|
||||
|
@ -234,7 +286,6 @@ files = [
|
|||
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
|
||||
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
|
||||
|
@ -243,7 +294,6 @@ files = [
|
|||
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
|
||||
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
|
||||
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
|
||||
|
@ -334,6 +384,25 @@ MarkupSafe = ">=2.0"
|
|||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.2.4"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
|
||||
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.9.2"
|
||||
|
||||
[package.extras]
|
||||
babel = ["Babel"]
|
||||
lingua = ["lingua"]
|
||||
testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.3"
|
||||
|
@ -361,16 +430,6 @@ files = [
|
|||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
|
@ -431,6 +490,26 @@ bcrypt = ["bcrypt (>=3.1.0)"]
|
|||
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||
totp = ["cryptography"]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.9.7"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "psycopg2-2.9.7-cp310-cp310-win32.whl", hash = "sha256:1a6a2d609bce44f78af4556bea0c62a5e7f05c23e5ea9c599e07678995609084"},
|
||||
{file = "psycopg2-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:b22ed9c66da2589a664e0f1ca2465c29b75aaab36fa209d4fb916025fb9119e5"},
|
||||
{file = "psycopg2-2.9.7-cp311-cp311-win32.whl", hash = "sha256:44d93a0109dfdf22fe399b419bcd7fa589d86895d3931b01fb321d74dadc68f1"},
|
||||
{file = "psycopg2-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:91e81a8333a0037babfc9fe6d11e997a9d4dac0f38c43074886b0d9dead94fe9"},
|
||||
{file = "psycopg2-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:d1210fcf99aae6f728812d1d2240afc1dc44b9e6cba526a06fb8134f969957c2"},
|
||||
{file = "psycopg2-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:e9b04cbef584310a1ac0f0d55bb623ca3244c87c51187645432e342de9ae81a8"},
|
||||
{file = "psycopg2-2.9.7-cp38-cp38-win32.whl", hash = "sha256:d5c5297e2fbc8068d4255f1e606bfc9291f06f91ec31b2a0d4c536210ac5c0a2"},
|
||||
{file = "psycopg2-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:8275abf628c6dc7ec834ea63f6f3846bf33518907a2b9b693d41fd063767a866"},
|
||||
{file = "psycopg2-2.9.7-cp39-cp39-win32.whl", hash = "sha256:c7949770cafbd2f12cecc97dea410c514368908a103acf519f2a346134caa4d5"},
|
||||
{file = "psycopg2-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:b6bd7d9d3a7a63faae6edf365f0ed0e9b0a1aaf1da3ca146e6b043fb3eb5d723"},
|
||||
{file = "psycopg2-2.9.7.tar.gz", hash = "sha256:f00cc35bd7119f1fed17b85bd1007855194dde2cbd8de01ab8ebb17487440ad8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.20"
|
||||
|
@ -482,7 +561,7 @@ files = [
|
|||
]
|
||||
|
||||
[package.dependencies]
|
||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
||||
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""}
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
|
@ -557,4 +636,4 @@ email = ["email-validator"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "3b8dfa8cb4d25075ed8465782a8fdbcbf9004ea82ea533898f9bb797f55025a3"
|
||||
content-hash = "0b3e5f508d905b937d323c2545bbba6bcf2ee7a5f1227c549cf0d12e49ffac99"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
from . import views
|
||||
from .admin import admin
|
|
@ -0,0 +1,8 @@
|
|||
from flask_admin import Admin
|
||||
|
||||
admin = Admin(
|
||||
name="Админ панель",
|
||||
url="/admin",
|
||||
base_template="my_master.html",
|
||||
template_mode="bootstrap4",
|
||||
)
|
|
@ -0,0 +1,61 @@
|
|||
from flask import abort, redirect, request, url_for
|
||||
from flask_admin.contrib import sqla
|
||||
from flask_security import current_user
|
||||
|
||||
|
||||
class MyAdminView(sqla.ModelView):
|
||||
def is_accessible(self):
|
||||
return (
|
||||
current_user.is_active
|
||||
and current_user.is_authenticated
|
||||
and current_user.has_role("superuser")
|
||||
)
|
||||
|
||||
def _handle_view(self, name, **kwargs):
|
||||
"""
|
||||
Override builtin _handle_view in
|
||||
order to redirect users when a
|
||||
view is not accessible.
|
||||
"""
|
||||
if not self.is_accessible():
|
||||
if current_user.is_authenticated:
|
||||
# permission denied
|
||||
abort(403)
|
||||
else:
|
||||
# login
|
||||
return redirect(url_for("security.login", next=request.url))
|
||||
|
||||
|
||||
class UserView(MyAdminView):
|
||||
column_hide_backrefs = False
|
||||
column_list = (
|
||||
"email",
|
||||
"active",
|
||||
"roles",
|
||||
)
|
||||
|
||||
|
||||
class RoleView(MyAdminView):
|
||||
column_list = (
|
||||
"name",
|
||||
"description",
|
||||
)
|
||||
|
||||
|
||||
class TagView(MyAdminView):
|
||||
pass
|
||||
|
||||
|
||||
class PostView(MyAdminView):
|
||||
# form_excluded_columns = ("author", "create_datetime", "update_datetime")
|
||||
column_list = (
|
||||
"title",
|
||||
"published",
|
||||
)
|
||||
column_labels = dict(
|
||||
tags="Tags",
|
||||
title="Title",
|
||||
author="Author",
|
||||
published="Published",
|
||||
published_datetime="Pubdate",
|
||||
)
|
|
@ -1,12 +1,58 @@
|
|||
from flask import Flask, render_template_string
|
||||
import os
|
||||
|
||||
from flask import Flask, render_template_string, request, url_for
|
||||
from flask_admin import helpers
|
||||
from flask_migrate import Migrate
|
||||
from flask_security.core import Security
|
||||
|
||||
from pyproger.dbase import Role, User, db, user_datastore
|
||||
from pyproger.dbase.models import Post, Tag
|
||||
|
||||
|
||||
def create_app():
|
||||
def create_app(test_config=None):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/index")
|
||||
@app.route("/")
|
||||
def index() -> str:
|
||||
return render_template_string("pyproger temporary page")
|
||||
if test_config is None:
|
||||
app.config.from_pyfile("config.py", silent=True)
|
||||
else:
|
||||
app.config.from_mapping(test_config)
|
||||
|
||||
# Проверям/создаем папку instanse
|
||||
try:
|
||||
os.makedirs(app.instance_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
security = Security(app, user_datastore)
|
||||
|
||||
migrate = Migrate(db=db)
|
||||
migrate.init_app(app)
|
||||
|
||||
from .admin import admin
|
||||
|
||||
admin.init_app(app)
|
||||
|
||||
from pyproger.admin.views import PostView, RoleView, TagView, UserView
|
||||
|
||||
admin.add_view(RoleView(Role, db.session, category="admin"))
|
||||
admin.add_view(UserView(User, db.session, category="admin"))
|
||||
admin.add_view(TagView(Tag, db.session, category="posts"))
|
||||
admin.add_view(PostView(Post, db.session, category="posts"))
|
||||
|
||||
@security.context_processor
|
||||
def security_context_processor():
|
||||
return dict(
|
||||
admin_base_template=admin.base_template,
|
||||
admin_view=admin.index_view,
|
||||
h=helpers,
|
||||
get_url=url_for,
|
||||
)
|
||||
|
||||
@app.route("/ping")
|
||||
def hello():
|
||||
logging.info("Проверка ping-pong")
|
||||
return render_template_string("pong")
|
||||
|
||||
return app
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
FLASK_ADMIN_SWATCH = "slate"
|
||||
# Create secret key so we can use sessions
|
||||
# python3: secrets.token_urlsafe()
|
||||
SECRET_KEY = "hxfjbcfry52"
|
||||
|
||||
# Create in-memory database
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://pi3c:@localhost/pyproger"
|
||||
# For debug - show every DB query
|
||||
SQLALCHEMY_ECHO = False
|
||||
|
||||
# Flask-Security config
|
||||
SECURITY_URL_PREFIX = "/admin"
|
||||
SECURITY_PASSWORD_HASH = "pbkdf2_sha512"
|
||||
SECURITY_PASSWORD_SALT = "ATGUOHAELKiubahiughaerGOJAEGj"
|
||||
SECURITY_TRACKABLE = True
|
||||
|
||||
# Flask-Security URLs, overridden because they don't put a / at the end
|
||||
SECURITY_LOGIN_URL = "/login/"
|
||||
SECURITY_LOGOUT_URL = "/logout/"
|
||||
SECURITY_REGISTER_URL = "/register/"
|
||||
|
||||
SECURITY_POST_LOGIN_VIEW = "/admin/"
|
||||
SECURITY_POST_LOGOUT_VIEW = "/admin/"
|
||||
SECURITY_POST_REGISTER_VIEW = "/admin/"
|
||||
SECURITY_POST_RESET_VIEW = "/admin/"
|
||||
|
||||
# Flask-Security features
|
||||
SECURITY_REGISTERABLE = False
|
||||
SECURITY_CHANGEABLE = True
|
||||
SECURITY_RECOVERABLE = False
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# For demo - no email
|
||||
SECURITY_SEND_REGISTER_EMAIL = False
|
||||
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False
|
||||
SECURITY_SEND_PASSWORD_RESET_EMAIL = False
|
||||
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False
|
|
@ -0,0 +1,10 @@
|
|||
from flask_security.datastore import SQLAlchemyUserDatastore
|
||||
from flask_security.models import fsqla
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
fsqla.FsModels.set_db_info(db)
|
||||
|
||||
from .models import Role, User
|
||||
|
||||
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
|
@ -0,0 +1,57 @@
|
|||
import datetime
|
||||
|
||||
from flask_security.models import fsqla
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text
|
||||
from sqlalchemy.util import unique_list
|
||||
|
||||
from . import db
|
||||
|
||||
|
||||
class Role(db.Model, fsqla.FsRoleMixin):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class User(db.Model, fsqla.FsUserMixin):
|
||||
first_name = Column(String(255))
|
||||
last_name = Column(String(255))
|
||||
posts = db.relationship("Post", backref="user", lazy="dynamic")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.email
|
||||
|
||||
|
||||
tag_post = db.Table(
|
||||
"tag_post",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id")),
|
||||
db.Column("post_id", db.Integer, db.ForeignKey("post.id")),
|
||||
)
|
||||
|
||||
|
||||
class Tag(db.Model):
|
||||
__tablename__ = "tag"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
tag = Column(String(20))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.tag
|
||||
|
||||
|
||||
class Post(db.Model):
|
||||
__tablename__ = "post"
|
||||
id = Column(Integer, primary_key=True, nullable=False, unique=True)
|
||||
author = Column(Integer, db.ForeignKey("user.id"))
|
||||
slug = Column(String(30), primary_key=True, nullable=True)
|
||||
title = Column(String(50), nullable=False)
|
||||
published = Column(Boolean, default=False)
|
||||
tags = db.relationship("Tag", secondary=tag_post)
|
||||
|
||||
create_datetime = Column(
|
||||
DateTime(), nullable=True, default=datetime.datetime.utcnow()
|
||||
)
|
||||
update_datetime = Column(
|
||||
DateTime(),
|
||||
nullable=True,
|
||||
)
|
||||
text = Column(Text)
|
|
@ -0,0 +1,39 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-10 col-sm-offset-1">
|
||||
<h1>Админ панель</h1>
|
||||
<p class="lead">
|
||||
Авторизация
|
||||
</p>
|
||||
<p>
|
||||
Админ панель блога <a href="https://pyproger.ru"
|
||||
target="_blank">pyproger.ru</a>
|
||||
</p>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p>Войдите используя Ваш логин и пароль.
|
||||
<br><br>
|
||||
По всем вопросам обращайтесь к администратору:
|
||||
<ul>
|
||||
<li>Сергей Ванюшкин: <b>pi3c@yandex.ru</b></li>
|
||||
</ul>
|
||||
<p>
|
||||
<a class="btn btn-primary" href="{{ url_for('security.login') }}">Войти</a>
|
||||
</p>
|
||||
<p>
|
||||
{% else %}
|
||||
{% if current_user.first_name %}
|
||||
{{ current_user.first_name }}
|
||||
{% else %}
|
||||
{{ current_user.email }}
|
||||
{% endif %}, добро пожаловать.
|
||||
{% endif %}
|
||||
<p>
|
||||
<a class="btn btn-primary" href="/"><i class="glyphicon glyphicon-chevron-left"></i>На главную</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
|
@ -0,0 +1,19 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
|
||||
{% block access_control %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="navbar-text btn-group pull-right">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
||||
<i class="glyphicon glyphicon-user"></i>
|
||||
{% if current_user.first_name -%}
|
||||
{{ current_user.first_name }}
|
||||
{% else -%}
|
||||
{{ current_user.email }}
|
||||
{%- endif %}<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="{{ url_for('security.logout') }}">Выйти</a></li>
|
||||
<li><a href="{{ url_for('security.change_password') }}">Сменить пароль</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,27 @@
|
|||
{% macro render_field_with_errors(field) %}
|
||||
|
||||
<div class="form-group">
|
||||
{{ field.label }} {{ field(class_='form-control', **kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul>
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_field(field) %}
|
||||
<p>{{ field(class_='form-control', **kwargs)|safe }}</p>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_checkbox_field(field) -%}
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
{{ field(type='checkbox', **kwargs) }} {{ field.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/master.html" %}
|
||||
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<h1>{{ _fsdomain('Change password') }}</h1>
|
||||
<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
|
||||
{{ change_password_form.hidden_tag() }}
|
||||
{{ render_field_with_errors(change_password_form.password) }}
|
||||
{{ render_field_with_errors(change_password_form.new_password) }}
|
||||
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
|
||||
{{ render_field(change_password_form.submit, class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,16 @@
|
|||
{% extends "admin/master.html" %}
|
||||
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<h1>{{ _fsdomain('Send password reset instructions') }}</h1>
|
||||
<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
|
||||
{{ forgot_password_form.hidden_tag() }}
|
||||
{{ render_field_with_errors(forgot_password_form.email) }}
|
||||
{{ render_field(forgot_password_form.submit, class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,20 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% from "security/_macros.html" import render_field, render_field_with_errors, render_checkbox_field %}
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<h1>Авторизация</h1>
|
||||
<div class="well">
|
||||
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
|
||||
{{ login_user_form.hidden_tag() }}
|
||||
{{ render_field_with_errors(login_user_form.email) }}
|
||||
{{ render_field_with_errors(login_user_form.password) }}
|
||||
{{ render_checkbox_field(login_user_form.remember) }}
|
||||
{{ render_field(login_user_form.next) }}
|
||||
{{ render_field(login_user_form.submit, class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
|
@ -0,0 +1,22 @@
|
|||
{% extends 'admin/master.html' %}
|
||||
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<h1>{{ _fsdomain("Register") }}</h1>
|
||||
<div class="well">
|
||||
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
|
||||
{{ register_user_form.hidden_tag() }}
|
||||
{{ render_field_with_errors(register_user_form.email) }}
|
||||
{{ render_field_with_errors(register_user_form.password) }}
|
||||
{% if register_user_form.password_confirm %}
|
||||
{{ render_field_with_errors(register_user_form.password_confirm) }}
|
||||
{% endif %}
|
||||
{{ render_field(register_user_form.submit, class="btn btn-primary") }}
|
||||
</form>
|
||||
<p>Already signed up? Please <a href="{{ url_for_security('login') }}">log in</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "admin/master.html" %}
|
||||
{% from "security/_macros.html" import render_field_with_errors, render_field %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-8 col-sm-offset-2">
|
||||
<h1>{{ _fsdomain('Reset password') }}</h1>
|
||||
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST"
|
||||
name="reset_password_form">
|
||||
{{ reset_password_form.hidden_tag() }}
|
||||
{{ render_field_with_errors(reset_password_form.password) }}
|
||||
{{ render_field_with_errors(reset_password_form.password_confirm) }}
|
||||
{{ render_field(reset_password_form.submit, class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -12,6 +12,9 @@ gunicorn = "^21.2.0"
|
|||
sqlalchemy = "^2.0.20"
|
||||
flask-sqlalchemy = "^3.1.1"
|
||||
flask-security-too = "^5.3.0"
|
||||
flask-migrate = "^4.0.5"
|
||||
flask-admin = "^1.6.1"
|
||||
psycopg2 = "^2.9.7"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue