Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
23 changed files with 334 additions and 664 deletions
|
|
@ -1,12 +1,5 @@
|
|||
# What’s New
|
||||
|
||||
## 1.1.0
|
||||
|
||||
+ **Deprecated** several configuration values ~
|
||||
+ **Schema changes**: several columns changed format. Update your schema.
|
||||
+ Removed permanently the remains of extensions.
|
||||
+ I18n improvements.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
+ **BREAKING CHANGES AHEAD**!
|
||||
|
|
@ -18,7 +11,7 @@
|
|||
+ Switched to `pyproject.toml`. `requirements.txt` has been sunset.
|
||||
+ Switched to the Apache License; the old license text is moved to `LICENSE.0_9`
|
||||
+ Added color themes! This is a breaking (but trivial) aesthetic change. Default theme is 'Miku' (aquamarine green).
|
||||
+ Extensions **have been removed**. They never had a clear, usable, public API in the first place.
|
||||
+ Extensions have been removed. They never had a clear, usable, public API in the first place.
|
||||
|
||||
## 0.9
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Generic single-database configuration.
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
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.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from salvi.models import Base
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# 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 run_migrations_offline() -> None:
|
||||
"""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=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
"""empty message
|
||||
|
||||
Sorry, due to unattended changes, upgrade from 1.0.0 is not reliable and untested. Sorry.
|
||||
|
||||
Revision ID: ae0587e14725
|
||||
Revises:
|
||||
Create Date: 2025-10-04 09:43:41.158057
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'ae0587e14725'
|
||||
down_revision: Union[str, None] = 'ebde30d24167'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
#op.drop_index('pagepolicykey_passphrase_sec_code', table_name='pagepolicykey')
|
||||
#op.drop_index('page_owner', table_name='page')
|
||||
op.create_index(op.f('ix_page_calendar'), 'page', ['calendar'], unique=False)
|
||||
op.create_index('page_calendar', 'page', ['calendar'], unique=False)
|
||||
op.drop_index('user_id', table_name='usergroupmembership')
|
||||
op.drop_index('user_id_2', table_name='usergroupmembership')
|
||||
#op.drop_index('usergroupmembership_group_id', table_name='usergroupmembership')
|
||||
#op.drop_index('usergroupmembership_user_id', table_name='usergroupmembership')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index('usergroupmembership_user_id', 'usergroupmembership', ['user_id'], unique=False)
|
||||
op.create_index('usergroupmembership_group_id', 'usergroupmembership', ['group_id'], unique=False)
|
||||
op.create_index('user_id_2', 'usergroupmembership', ['user_id', 'group_id'], unique=True)
|
||||
op.create_index('user_id', 'usergroupmembership', ['user_id', 'group_id'], unique=True)
|
||||
op.drop_index('page_calendar', table_name='page')
|
||||
op.drop_index(op.f('ix_page_calendar'), table_name='page')
|
||||
op.create_index('page_owner', 'page', ['owner_id'], unique=False)
|
||||
op.create_table('upload',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('name', mysql.VARCHAR(length=256), nullable=False),
|
||||
sa.Column('url_name', mysql.VARCHAR(length=256), nullable=True),
|
||||
sa.Column('filetype', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('filesize', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('upload_date', mysql.DATETIME(), nullable=False),
|
||||
sa.Column('md5', mysql.VARCHAR(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.create_index('upload_upload_date', 'upload', ['upload_date'], unique=False)
|
||||
op.create_index('upload_md5', 'upload', ['md5'], unique=False)
|
||||
op.create_table('pagepolicy',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('page_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
|
||||
sa.Column('type', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('key_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('sitewide', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(['key_id'], ['pagepolicykey.id'], name='pagepolicy_FK_0_0'),
|
||||
sa.ForeignKeyConstraint(['page_id'], ['page.id'], name='pagepolicy_FK_1_0'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.create_index('pagepolicy_page_id_key_id', 'pagepolicy', ['page_id', 'key_id'], unique=True)
|
||||
op.create_index('pagepolicy_page_id', 'pagepolicy', ['page_id'], unique=False)
|
||||
op.create_index('pagepolicy_key_id', 'pagepolicy', ['key_id'], unique=False)
|
||||
op.create_table('pagepolicykey',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('passphrase', mysql.VARCHAR(length=255), nullable=False),
|
||||
sa.Column('sec_code', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.create_index('pagepolicykey_passphrase_sec_code', 'pagepolicykey', ['passphrase', 'sec_code'], unique=True)
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -1,312 +0,0 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: ebde30d24167
|
||||
Revises: ae0587e14725
|
||||
Create Date: 2025-10-10 19:01:04.309127
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'ebde30d24167'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('pagepolicy',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('page_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
|
||||
sa.Column('type', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('key_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('sitewide', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(['key_id'], ['pagepolicykey.id'], name='pagepolicy_FK_0_0'),
|
||||
sa.ForeignKeyConstraint(['page_id'], ['page.id'], name='pagepolicy_FK_1_0'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.create_index('pagepolicy_page_id_key_id', 'pagepolicy', ['page_id', 'key_id'], unique=True)
|
||||
op.create_index('pagepolicy_page_id', 'pagepolicy', ['page_id'], unique=False)
|
||||
op.create_index('pagepolicy_key_id', 'pagepolicy', ['key_id'], unique=False)
|
||||
op.create_table('pagepolicykey',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('passphrase', mysql.VARCHAR(length=255), nullable=False),
|
||||
sa.Column('sec_code', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.alter_column('page', 'title',
|
||||
existing_type=mysql.VARCHAR(length=256),
|
||||
nullable=True)
|
||||
op.alter_column('page', 'touched',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=True)
|
||||
op.alter_column('page', 'flags',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=True)
|
||||
op.drop_index('page_calendar', table_name='page')
|
||||
op.drop_index('page_owner', table_name='page')
|
||||
op.drop_index('page_title', table_name='page')
|
||||
op.drop_index('page_touched', table_name='page')
|
||||
op.create_index(op.f('ix_page_title'), 'page', ['title'], unique=False)
|
||||
op.create_index(op.f('ix_page_touched'), 'page', ['touched'], unique=False)
|
||||
op.alter_column('pagelink', 'from_page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagelink', 'to_page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.drop_constraint('pagelink_ibfk_2', 'pagelink', type_='foreignkey')
|
||||
op.drop_constraint('pagelink_ibfk_1', 'pagelink', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'pagelink', 'page', ['from_page_id'], ['id'])
|
||||
op.create_foreign_key(None, 'pagelink', 'page', ['to_page_id'], ['id'])
|
||||
op.alter_column('pagepermission', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagepermission', 'group_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagepermission', 'permissions',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=True)
|
||||
op.drop_constraint('pagepermission_ibfk_4', 'pagepermission', type_='foreignkey')
|
||||
op.drop_constraint('pagepermission_ibfk_5', 'pagepermission', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'pagepermission', 'usergroup', ['group_id'], ['id'])
|
||||
op.create_foreign_key(None, 'pagepermission', 'page', ['page_id'], ['id'])
|
||||
op.alter_column('pageproperty', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pageproperty', 'key',
|
||||
existing_type=mysql.VARCHAR(length=64),
|
||||
nullable=True)
|
||||
op.alter_column('pageproperty', 'value',
|
||||
existing_type=mysql.VARCHAR(length=8000),
|
||||
nullable=True)
|
||||
op.alter_column('pagerevision', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagerevision', 'comment',
|
||||
existing_type=mysql.VARCHAR(length=1024),
|
||||
nullable=True)
|
||||
op.alter_column('pagerevision', 'textref_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagerevision', 'pub_date',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=True)
|
||||
op.alter_column('pagerevision', 'length',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.drop_constraint('pagerevision_ibfk_1', 'pagerevision', type_='foreignkey')
|
||||
op.drop_constraint('pagerevision_ibfk_2', 'pagerevision', type_='foreignkey')
|
||||
op.drop_constraint('pagerevision_ibfk_3', 'pagerevision', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'pagerevision', 'pagetext', ['textref_id'], ['id'])
|
||||
op.create_foreign_key(None, 'pagerevision', 'page', ['page_id'], ['id'])
|
||||
op.create_foreign_key(None, 'pagerevision', 'user', ['user_id'], ['id'])
|
||||
op.alter_column('pagetag', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('pagetag', 'name',
|
||||
existing_type=mysql.VARCHAR(length=64),
|
||||
nullable=True)
|
||||
op.drop_constraint('pagetag_ibfk_1', 'pagetag', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'pagetag', 'page', ['page_id'], ['id'])
|
||||
op.alter_column('pagetext', 'content',
|
||||
existing_type=sa.BLOB(),
|
||||
nullable=True)
|
||||
op.alter_column('pagetext', 'flags',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=True)
|
||||
op.add_column('user', sa.Column('privileges', sa.BigInteger(), nullable=True))
|
||||
op.add_column('user', sa.Column('restrictions', sa.BigInteger(), nullable=True))
|
||||
op.alter_column('user', 'username',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=True)
|
||||
op.alter_column('user', 'password',
|
||||
existing_type=mysql.VARCHAR(length=255),
|
||||
nullable=True)
|
||||
op.alter_column('user', 'join_date',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=True)
|
||||
op.alter_column('user', 'karma',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.drop_column('user', 'is_disabled')
|
||||
op.drop_column('user', 'is_admin')
|
||||
op.drop_column('user', 'color_theme')
|
||||
op.alter_column('usergroup', 'name',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=True)
|
||||
op.alter_column('usergroup', 'permissions',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=True)
|
||||
op.alter_column('usergroupmembership', 'user_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('usergroupmembership', 'group_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
op.alter_column('usergroupmembership', 'since',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=True)
|
||||
op.drop_index('usergroupmembership_group_id', table_name='usergroupmembership')
|
||||
op.drop_index('usergroupmembership_user_id', table_name='usergroupmembership')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index('usergroupmembership_user_id', 'usergroupmembership', ['user_id'], unique=False)
|
||||
op.create_index('usergroupmembership_group_id', 'usergroupmembership', ['group_id'], unique=False)
|
||||
op.alter_column('usergroupmembership', 'since',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=False)
|
||||
op.alter_column('usergroupmembership', 'group_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('usergroupmembership', 'user_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('usergroup', 'permissions',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=False)
|
||||
op.alter_column('usergroup', 'name',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=False)
|
||||
op.add_column('user', sa.Column('color_theme', mysql.SMALLINT(display_width=6), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
op.add_column('user', sa.Column('is_admin', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
op.add_column('user', sa.Column('is_disabled', mysql.TINYINT(display_width=1), server_default=sa.text('0'), autoincrement=False, nullable=False))
|
||||
op.alter_column('user', 'karma',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('user', 'join_date',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=False)
|
||||
op.alter_column('user', 'password',
|
||||
existing_type=mysql.VARCHAR(length=255),
|
||||
nullable=False)
|
||||
op.alter_column('user', 'username',
|
||||
existing_type=mysql.VARCHAR(length=32),
|
||||
nullable=False)
|
||||
op.drop_column('user', 'restrictions')
|
||||
op.drop_column('user', 'privileges')
|
||||
op.alter_column('pagetext', 'flags',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=False)
|
||||
op.alter_column('pagetext', 'content',
|
||||
existing_type=sa.BLOB(),
|
||||
nullable=False)
|
||||
op.drop_constraint(None, 'pagetag', type_='foreignkey')
|
||||
op.create_foreign_key('pagetag_ibfk_1', 'pagetag', 'page', ['page_id'], ['id'], ondelete='CASCADE')
|
||||
op.alter_column('pagetag', 'name',
|
||||
existing_type=mysql.VARCHAR(length=64),
|
||||
nullable=False)
|
||||
op.alter_column('pagetag', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.drop_constraint(None, 'pagerevision', type_='foreignkey')
|
||||
op.drop_constraint(None, 'pagerevision', type_='foreignkey')
|
||||
op.drop_constraint(None, 'pagerevision', type_='foreignkey')
|
||||
op.create_foreign_key('pagerevision_ibfk_3', 'pagerevision', 'user', ['user_id'], ['id'], ondelete='SET NULL')
|
||||
op.create_foreign_key('pagerevision_ibfk_2', 'pagerevision', 'page', ['page_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key('pagerevision_ibfk_1', 'pagerevision', 'pagetext', ['textref_id'], ['id'], ondelete='CASCADE')
|
||||
op.alter_column('pagerevision', 'length',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('pagerevision', 'pub_date',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=False)
|
||||
op.alter_column('pagerevision', 'textref_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('pagerevision', 'comment',
|
||||
existing_type=mysql.VARCHAR(length=1024),
|
||||
nullable=False)
|
||||
op.alter_column('pagerevision', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('pageproperty', 'value',
|
||||
existing_type=mysql.VARCHAR(length=8000),
|
||||
nullable=False)
|
||||
op.alter_column('pageproperty', 'key',
|
||||
existing_type=mysql.VARCHAR(length=64),
|
||||
nullable=False)
|
||||
op.alter_column('pageproperty', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.drop_constraint(None, 'pagepermission', type_='foreignkey')
|
||||
op.drop_constraint(None, 'pagepermission', type_='foreignkey')
|
||||
op.create_foreign_key('pagepermission_ibfk_5', 'pagepermission', 'usergroup', ['group_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key('pagepermission_ibfk_4', 'pagepermission', 'page', ['page_id'], ['id'], ondelete='CASCADE')
|
||||
op.alter_column('pagepermission', 'permissions',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=False)
|
||||
op.alter_column('pagepermission', 'group_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('pagepermission', 'page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.drop_constraint(None, 'pagelink', type_='foreignkey')
|
||||
op.drop_constraint(None, 'pagelink', type_='foreignkey')
|
||||
op.create_foreign_key('pagelink_ibfk_1', 'pagelink', 'page', ['to_page_id'], ['id'], ondelete='CASCADE')
|
||||
op.create_foreign_key('pagelink_ibfk_2', 'pagelink', 'page', ['from_page_id'], ['id'], ondelete='CASCADE')
|
||||
op.alter_column('pagelink', 'to_page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('pagelink', 'from_page_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.drop_index(op.f('ix_page_touched'), table_name='page')
|
||||
op.drop_index(op.f('ix_page_title'), table_name='page')
|
||||
op.create_index('page_touched', 'page', ['touched'], unique=False)
|
||||
op.create_index('page_title', 'page', ['title'], unique=False)
|
||||
op.create_index('page_owner', 'page', ['owner_id'], unique=False)
|
||||
op.create_index('page_calendar', 'page', ['calendar'], unique=False)
|
||||
op.alter_column('page', 'flags',
|
||||
existing_type=mysql.BIGINT(display_width=20),
|
||||
nullable=False)
|
||||
op.alter_column('page', 'touched',
|
||||
existing_type=mysql.DATETIME(),
|
||||
nullable=False)
|
||||
op.alter_column('page', 'title',
|
||||
existing_type=mysql.VARCHAR(length=256),
|
||||
nullable=False)
|
||||
op.create_table('upload',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
|
||||
sa.Column('name', mysql.VARCHAR(length=256), nullable=False),
|
||||
sa.Column('url_name', mysql.VARCHAR(length=256), nullable=True),
|
||||
sa.Column('filetype', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('filesize', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
|
||||
sa.Column('upload_date', mysql.DATETIME(), nullable=False),
|
||||
sa.Column('md5', mysql.VARCHAR(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mariadb_collate='utf8mb4_general_ci',
|
||||
mariadb_default_charset='utf8mb4',
|
||||
mariadb_engine='InnoDB'
|
||||
)
|
||||
op.create_index('upload_upload_date', 'upload', ['upload_date'], unique=False)
|
||||
op.create_index('upload_md5', 'upload', ['md5'], unique=False)
|
||||
try:
|
||||
## drop tables removed in 0.x
|
||||
op.drop_table('pagepolicykey')
|
||||
op.drop_index('pagepolicy_key_id', table_name='pagepolicy')
|
||||
op.drop_index('pagepolicy_page_id', table_name='pagepolicy')
|
||||
op.drop_index('pagepolicy_page_id_key_id', table_name='pagepolicy')
|
||||
op.drop_table('pagepolicy')
|
||||
op.drop_index('upload_md5', table_name='upload')
|
||||
op.drop_index('upload_upload_date', table_name='upload')
|
||||
op.drop_table('upload')
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
"back-to": "Back to",
|
||||
"backlinks": "Backlinks",
|
||||
"backlinks-empty": "No other pages linking here. Is this page orphan?",
|
||||
"bad-request": "Bad request",
|
||||
"calculate": "Calculate",
|
||||
"calendar": "Calendar",
|
||||
"confirm-password": "Confirm password",
|
||||
|
|
@ -19,8 +18,6 @@
|
|||
"groups-count": "User group count",
|
||||
"have-read-terms": "I have read {0} and {1}",
|
||||
"homepage": "Homepage",
|
||||
"in-the-future": "in the future",
|
||||
"in-the-past": "in the past",
|
||||
"include-tags": "Include tags",
|
||||
"input-tags": "Tags (comma separated)",
|
||||
"jump-to-actions": "Jump to actions",
|
||||
|
|
@ -30,7 +27,6 @@
|
|||
"latest-uploads": "Latest uploads",
|
||||
"logged-in-as": "Logged in as",
|
||||
"login": "Log in",
|
||||
"manage-accounts": "Manage accounts",
|
||||
"month": "Month",
|
||||
"n-days-ago": "{0} days ago",
|
||||
"n-hours-ago": "{0} hours ago",
|
||||
|
|
@ -44,7 +40,6 @@
|
|||
"not-found-text-2": "does not exist.",
|
||||
"not-logged-in": "Not logged in",
|
||||
"note-history": "Page history",
|
||||
"notes-by-date": "Pages by date",
|
||||
"notes-count": "Number of pages",
|
||||
"notes-count-with-url": "Number of pages with URL set",
|
||||
"notes-month-empty": "None found :(",
|
||||
|
|
@ -67,13 +62,10 @@
|
|||
"search-results": "Search results for",
|
||||
"search-no-results": "No results for",
|
||||
"show-all": "Show all",
|
||||
"show-more-years": "Show more years",
|
||||
"sign-up": "Sign up",
|
||||
"tags": "Tags",
|
||||
"terms-of-service": "Terms of Service",
|
||||
"upload-file": "Upload file",
|
||||
"user-generated-warning": "Any content in this site is user-generated and",
|
||||
"user-liability-warning": "each user is responsible for what they publish",
|
||||
"username": "Username",
|
||||
"users-count": "Number of users",
|
||||
"welcome": "Welcome to {0}!",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
"back-to": "Torna a",
|
||||
"backlinks": "Collegamenti in entrata",
|
||||
"backlinks-empty": "Nessuna altra pagina punta qui. Questa pagina è orfana?",
|
||||
"bad-request": "Richiesta non conforme",
|
||||
"calculate": "Calcola",
|
||||
"calendar": "Calendario",
|
||||
"confirm-password": "Conferma password",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ dependencies = [
|
|||
"Flask-WTF",
|
||||
"python-dotenv>=1.0.0",
|
||||
"pymysql",
|
||||
"sakuragasaki46-suou>=0.7.0"
|
||||
"sakuragasaki46-suou>=0.6.0"
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
|
|
|
|||
|
|
@ -8,21 +8,27 @@ Pages are stored in SQLite/MySQL databases.
|
|||
Markdown is used for text formatting.
|
||||
'''
|
||||
|
||||
__version__ = '1.1.0-dev40'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
from flask import (
|
||||
Flask, g, make_response, redirect,
|
||||
Flask, abort, flash, g, jsonify, make_response, redirect,
|
||||
request, render_template, send_from_directory
|
||||
)
|
||||
from markupsafe import Markup
|
||||
from flask_login import LoginManager
|
||||
from flask_login import LoginManager, login_user, logout_user, current_user, login_required
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import select
|
||||
from suou.configparse import ConfigOptions, ConfigParserConfigSource, ConfigValue
|
||||
from suou.flask import add_context_from_config
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.routing import BaseConverter
|
||||
import html, os
|
||||
from functools import partial
|
||||
import datetime, hashlib, html, importlib, json, markdown, os, random, \
|
||||
re, sys, warnings
|
||||
from functools import lru_cache, partial
|
||||
from urllib.parse import quote
|
||||
import gzip
|
||||
from getpass import getpass
|
||||
from configparser import ConfigParser
|
||||
import dotenv
|
||||
|
||||
|
|
@ -34,13 +40,11 @@ dotenv.load_dotenv(os.path.join(APP_BASE_DIR, '.env'))
|
|||
|
||||
class SiteConfig(ConfigOptions):
|
||||
app_name = ConfigValue(default='Salvi', legacy_src='site.title', public=True)
|
||||
## v--- will change to server_name in 2.0.0
|
||||
domain_name = ConfigValue(public=True)
|
||||
database_url = ConfigValue(legacy_src='database.url', required=True)
|
||||
secret_key = ConfigValue(required=True)
|
||||
default_group = ConfigValue(prefix='salvi_', cast=int, default=1)
|
||||
material_icons_url = ConfigValue(default='https://fonts.googleapis.com/icon?family=Material+Icons', public=True)
|
||||
# v--- pending deprecation
|
||||
default_items_per_page = ConfigValue(prefix='salvi_', legacy_src='appearance.items_per_page', default=20, cast=int)
|
||||
...
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ PING_RE = r'(?<!\w)@(' + USERNAME_RE + r')'
|
|||
|
||||
FORBIDDEN_URLS = [
|
||||
'about', 'accounts', 'ajax', 'backlinks', 'calendar', 'circles', 'create',
|
||||
'easter', 'edit', 'embed', 'group', 'help', 'history', 'init-config',
|
||||
'easter', 'edit', 'embed', 'group', 'help', 'history', 'init-config', 'kt',
|
||||
'manage', 'media', 'p', 'privacy', 'protect', 'rules', 'search', 'static',
|
||||
'stats', 'tags', 'terms', 'theme-switch', 'timeline', 'u', 'upload', 'upload-info', 'v1'
|
||||
'stats', 'tags', 'terms', 'theme-switch', 'u', 'upload', 'upload-info', 'v1'
|
||||
]
|
||||
266
salvi/models.py
266
salvi/models.py
|
|
@ -6,20 +6,22 @@ from __future__ import annotations
|
|||
|
||||
from functools import partial, wraps
|
||||
from getpass import getpass
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, Callable, Iterable, List, Mapping
|
||||
import datetime
|
||||
import gzip
|
||||
import warnings
|
||||
|
||||
# sqlalchemy imports
|
||||
from flask import abort, flash
|
||||
from flask_login import AnonymousUserMixin, current_user, login_required
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from markupsafe import Markup
|
||||
|
||||
# sqlalchemy imports
|
||||
from sqlalchemy.orm import Mapped, Relationship, declarative_base, mapped_column, relationship
|
||||
from sqlalchemy import Index, Integer, SmallInteger, String, DateTime, BigInteger, ForeignKey, UniqueConstraint, BLOB as Blob, delete, func, insert, or_, select, text, update
|
||||
from sqlalchemy.orm import Relationship, declarative_base, deferred, relationship
|
||||
from sqlalchemy import Column, Integer, String, DateTime, BigInteger, ForeignKey, UniqueConstraint, create_engine, Index, BLOB as Blob, delete, func, insert, or_, select, update
|
||||
import datetime
|
||||
import os
|
||||
import gzip
|
||||
|
||||
from suou.functools import deprecated, deprecated_alias, not_implemented
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
|
@ -31,8 +33,59 @@ from .i18n import human_elapsed_time
|
|||
|
||||
current_user: User
|
||||
|
||||
## TODO consider moving it to suou.
|
||||
## TODO consider sqlalchemy.ext.hybrid.hybrid_property works
|
||||
from sqlalchemy.ext.hybrid import Comparator
|
||||
|
||||
class _BitComparator(Comparator):
|
||||
_column: Column
|
||||
_flag: int
|
||||
def __init__(self, col, flag):
|
||||
self._column = col
|
||||
self._flag = flag
|
||||
def _bulk_update_tuples(self, value):
|
||||
return [ (self._column, self._upd_exp(value)) ]
|
||||
def operate(self, op, other, **kwargs):
|
||||
return op(self._sel_exp(), self._flag if other else 0, **kwargs)
|
||||
def __clause_element__(self):
|
||||
return self._column
|
||||
def __str__(self):
|
||||
return self._column
|
||||
def _sel_exp(self):
|
||||
return self._column.op('&')(self._flag)
|
||||
def _upd_exp(self, value):
|
||||
return self._column.op('|')(self._flag) if value else self._column.op('&')(~self._flag)
|
||||
|
||||
class BitSelector:
|
||||
_column: Column
|
||||
_flag: int
|
||||
_name: str
|
||||
def __init__(self, column, flag: int):
|
||||
if bin(flag := int(flag))[2:].rstrip('0') != '1':
|
||||
warnings.warn('using non-powers of 2 as flags may cause errors or undefined behavior', FutureWarning)
|
||||
self._column = column
|
||||
self._flag = flag
|
||||
def __set_name__(self, name, owner=None):
|
||||
self._name = name
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj:
|
||||
return getattr(obj, self._column.name) & self._flag > 0
|
||||
else:
|
||||
return _BitComparator(self._column, self._flag)
|
||||
def __set__(self, obj, val):
|
||||
if obj:
|
||||
orig = getattr(obj, self._column.name)
|
||||
if val:
|
||||
orig |= self._flag
|
||||
else:
|
||||
orig &= ~(self._flag)
|
||||
setattr(obj, self._column.name, orig)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# Helper for interactive session management
|
||||
from suou.sqlalchemy import bool_column, bound_fk, create_session, declarative_base, BitSelector, unbound_fk
|
||||
from suou.sqlalchemy import create_session, declarative_base
|
||||
|
||||
CSI = create_session_interactively = partial(create_session, app_config.database_url)
|
||||
|
||||
|
|
@ -43,23 +96,21 @@ db = SQLAlchemy(model_class=Base)
|
|||
class User(db.Model):
|
||||
__tablename__ = 'user'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(32), unique=True)
|
||||
email: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
password: Mapped[str] = mapped_column(String(255))
|
||||
join_date: Mapped[int] = mapped_column(DateTime, server_default=func.current_timestamp())
|
||||
karma: Mapped[int] = mapped_column(Integer, server_default=text('1'))
|
||||
is_admin: Mapped[bool] = bool_column()
|
||||
is_disabled: Mapped[int] = bool_column()
|
||||
color_theme: Mapped[int] = mapped_column(SmallInteger, server_default=text('0'))
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(32), unique=True)
|
||||
email = Column(String(256), nullable=True)
|
||||
password = Column(String(255))
|
||||
# will change to server_default in 1.1
|
||||
join_date = Column(DateTime, default=datetime.datetime.now)
|
||||
karma = Column(Integer, default=1)
|
||||
privileges = Column(BigInteger, default=0)
|
||||
is_admin = BitSelector(privileges, 1)
|
||||
restrictions = Column(BigInteger, default=0)
|
||||
is_disabled = BitSelector(restrictions, 1)
|
||||
|
||||
owned_pages: Relationship[List[Page]] = relationship('Page', back_populates='owner')
|
||||
group_memberships: Relationship[UserGroupMembership] = relationship('UserGroupMembership', back_populates='user')
|
||||
contributions: Relationship[PageRevision] = relationship("PageRevision", back_populates='user')
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(username, name='user_username'),
|
||||
)
|
||||
contributions = relationship("PageRevision", back_populates='user')
|
||||
|
||||
# helpers for flask_login
|
||||
@property
|
||||
|
|
@ -119,18 +170,14 @@ PERM_LOCK = PERM_READ
|
|||
class UserGroup(db.Model):
|
||||
__tablename__ = 'usergroup'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(32), unique=True)
|
||||
permissions: Mapped[int] = mapped_column(BigInteger, server_default=text('0'))
|
||||
can_read: Mapped[bool] = BitSelector(permissions, PERM_READ)
|
||||
can_edit: Mapped[bool] = BitSelector(permissions, PERM_EDIT)
|
||||
can_create: Mapped[bool] = BitSelector(permissions, PERM_CREATE)
|
||||
can_set_url: Mapped[bool] = BitSelector(permissions, PERM_SET_URL)
|
||||
can_set_tags: Mapped[bool] = BitSelector(permissions, PERM_SET_TAGS)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(name, name='usergroup_name'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(32), unique=True)
|
||||
permissions = Column(BigInteger, default=0)
|
||||
can_read = BitSelector(permissions, PERM_READ)
|
||||
can_edit = BitSelector(permissions, PERM_EDIT)
|
||||
can_create = BitSelector(permissions, PERM_CREATE)
|
||||
can_set_url = BitSelector(permissions, PERM_SET_URL)
|
||||
can_set_tags = BitSelector(permissions, PERM_SET_TAGS)
|
||||
|
||||
user_memberships: Relationship[UserGroupMembership] = relationship('UserGroupMembership', back_populates='group')
|
||||
page_permissions: Relationship[PagePermission] = relationship('PagePermission', back_populates = 'group')
|
||||
|
|
@ -151,16 +198,15 @@ class UserGroup(db.Model):
|
|||
|
||||
class UserGroupMembership(db.Model):
|
||||
__tablename__ = 'usergroupmembership'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key = True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
group_id: Mapped[int] = mapped_column(Integer, ForeignKey('usergroup.id'))
|
||||
since: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.now)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(user_id, group_id, name="usergroupmembership_user_id_group_id"),
|
||||
UniqueConstraint('user_id', 'group_id'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key = True)
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
group_id = Column(Integer, ForeignKey('usergroup.id'))
|
||||
since = Column(DateTime, default=datetime.datetime.now)
|
||||
|
||||
user: Relationship[User] = relationship("User", back_populates='group_memberships')
|
||||
group: Relationship[UserGroup] = relationship("UserGroup", back_populates='user_memberships')
|
||||
|
||||
|
|
@ -168,26 +214,18 @@ class UserGroupMembership(db.Model):
|
|||
class Page(db.Model):
|
||||
__tablename__ = 'page'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
# v--- url will be moved to a separate table in 1.2 (?)
|
||||
url: Mapped[str | None] = mapped_column(String(64), unique=True)
|
||||
title: Mapped[str] = mapped_column(String(256))
|
||||
touched: Mapped[datetime.datetime] = mapped_column(DateTime)
|
||||
calendar: Mapped[datetime.datetime | None] = mapped_column(DateTime, index=True)
|
||||
owner_id: Mapped[int | None] = mapped_column(Integer, ForeignKey('user.id'))
|
||||
flags: Mapped[int] = mapped_column(BigInteger, server_default=text('0'))
|
||||
is_redirect: Mapped[bool] = BitSelector(flags, 1)
|
||||
is_sync: Mapped[bool] = BitSelector(flags, 2)
|
||||
is_math_enabled: Mapped[bool] = BitSelector(flags, 4)
|
||||
is_locked: Mapped[bool] = BitSelector(flags, 8)
|
||||
is_cw: Mapped[bool] = BitSelector(flags, 16)
|
||||
|
||||
__table_args__ = (
|
||||
Index('page_title', title),
|
||||
Index('page_touched', touched),
|
||||
UniqueConstraint(url, name='page_url'),
|
||||
Index('page_calendar', calendar)
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
url = Column(String(64), unique=True, nullable=True)
|
||||
title = Column(String(256), index=True)
|
||||
touched = Column(DateTime, index=True)
|
||||
calendar = Column(DateTime, index=True, nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey('user.id'), nullable=True)
|
||||
flags = Column(BigInteger, default=0)
|
||||
is_redirect = BitSelector(flags, 1)
|
||||
is_sync = BitSelector(flags, 2)
|
||||
is_math_enabled = BitSelector(flags, 4)
|
||||
is_locked = BitSelector(flags, 8)
|
||||
is_cw = BitSelector(flags, 16)
|
||||
|
||||
revisions: Relationship[List[PageRevision]] = relationship("PageRevision", back_populates = 'page')
|
||||
owner: Relationship[User] = relationship('User', back_populates = 'owned_pages')
|
||||
|
|
@ -199,7 +237,7 @@ class Page(db.Model):
|
|||
|
||||
def latest(self) -> PageRevision:
|
||||
return db.session.execute(
|
||||
select(PageRevision).where(PageRevision.page_id == self.id).order_by(PageRevision.pub_date.desc()).limit(1)
|
||||
db.select(PageRevision).where(PageRevision.page_id == self.id).order_by(PageRevision.pub_date.desc()).limit(1)
|
||||
).scalar()
|
||||
|
||||
def get_url(self):
|
||||
|
|
@ -209,7 +247,6 @@ class Page(db.Model):
|
|||
def by_url(cls, url: str):
|
||||
return db.session.execute(db.select(Page).where(Page.url == url)).scalar()
|
||||
|
||||
@deprecated('usage of inefficient and deprecated remove_tags()')
|
||||
def short_desc(self) -> str:
|
||||
if self.is_cw:
|
||||
return '(Content Warning: we are not allowed to show a description.)'
|
||||
|
|
@ -261,6 +298,10 @@ class Page(db.Model):
|
|||
tags = [x.name for x in self.tags]
|
||||
)
|
||||
|
||||
@not_implemented
|
||||
def ldjson():
|
||||
...
|
||||
|
||||
@deprecated('meta name="keywords" is nowadays ignored by search engines')
|
||||
def seo_keywords(self):
|
||||
kw = []
|
||||
|
|
@ -326,15 +367,19 @@ class Page(db.Model):
|
|||
class PageText(db.Model):
|
||||
__tablename__ = 'pagetext'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
content: Mapped[bytes] = mapped_column(Blob)
|
||||
flags: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
is_gzipped: Mapped[bool] = BitSelector(flags, 2)
|
||||
id = Column(Integer, primary_key=True)
|
||||
content = Column(Blob)
|
||||
flags = Column(BigInteger, default=0)
|
||||
is_utf8 = BitSelector(flags, 1)
|
||||
is_gzipped = BitSelector(flags, 2)
|
||||
def get_content(self) -> str:
|
||||
c = self.content
|
||||
if self.is_gzipped:
|
||||
c = gzip.decompress(c)
|
||||
return c.decode('utf-8', 'replace')
|
||||
if self.is_utf8:
|
||||
return c.decode('utf-8')
|
||||
else:
|
||||
return c.decode('latin-1')
|
||||
@classmethod
|
||||
def create_content(cls, text: str, *, treshold=600, search_dup = True) -> PageText:
|
||||
c: bytes = text.encode('utf-8')
|
||||
|
|
@ -347,6 +392,7 @@ class PageText(db.Model):
|
|||
return item
|
||||
return db.session.execute(insert(cls).values(
|
||||
content = c,
|
||||
is_utf8 = True,
|
||||
is_gzipped = use_gzip
|
||||
).returning(cls)).scalar()
|
||||
|
||||
|
|
@ -355,21 +401,16 @@ class PageText(db.Model):
|
|||
class PageRevision(db.Model):
|
||||
__tablename__ = 'pagerevision'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
page_id: Mapped[int] = bound_fk(Page.id)
|
||||
user_id: Mapped[int | None] = unbound_fk(User.id)
|
||||
comment: Mapped[str] = mapped_column(String(1024), default='')
|
||||
textref_id: Mapped[int] = bound_fk(PageText.id)
|
||||
pub_date: Mapped[datetime.datetime] = mapped_column(DateTime, index=True)
|
||||
length: Mapped[int] = mapped_column()
|
||||
id = Column(Integer, primary_key=True)
|
||||
page_id = Column(Integer, ForeignKey('page.id'))
|
||||
user_id = Column(Integer, ForeignKey('user.id'), nullable=True)
|
||||
comment = Column(String(1024), default='')
|
||||
textref_id = Column(Integer, ForeignKey('pagetext.id'))
|
||||
pub_date = Column(DateTime, index=True)
|
||||
length = Column(Integer)
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_pagerevision_pub_date', pub_date),
|
||||
|
||||
)
|
||||
|
||||
page: Relationship[Page] = relationship("Page", back_populates='revisions')
|
||||
user: Relationship[User] = relationship("User", back_populates='contributions')
|
||||
page = relationship("Page", back_populates='revisions')
|
||||
user = relationship("User", back_populates='contributions')
|
||||
textref: Relationship[PageText] = relationship("PageText")
|
||||
|
||||
|
||||
|
|
@ -398,13 +439,12 @@ class PageRevision(db.Model):
|
|||
class PageTag(db.Model):
|
||||
__tablename__ = 'pagetag'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('page_id', 'name', name='pagetag_page_id_name'),
|
||||
Index('ix_pagetag_name', 'name')
|
||||
UniqueConstraint('page_id', 'name'),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
page_id: Mapped[int] = bound_fk(Page.id)
|
||||
name: Mapped[str] = mapped_column(String(64), index=True)
|
||||
id = Column(Integer, primary_key=True)
|
||||
page_id = Column(Integer, ForeignKey('page.id'))
|
||||
name: Column[str] = Column(String(64), index=True)
|
||||
|
||||
page = relationship('Page', back_populates='tags')
|
||||
|
||||
|
|
@ -415,24 +455,20 @@ class PageTag(db.Model):
|
|||
|
||||
|
||||
class PageProperty(db.Model):
|
||||
"""
|
||||
XXX as of 1.1.0, PageProperty is unused
|
||||
"""
|
||||
__tablename__ = 'pageproperty'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('page_id', 'key', name="pageproperty_page_id_key"),
|
||||
UniqueConstraint('page_id', 'key'),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key = True)
|
||||
page_id: Mapped[int] = mapped_column(Integer, ForeignKey('page.id'))
|
||||
key: Mapped[str] = mapped_column(String(64))
|
||||
value: Mapped[str] = mapped_column(String(8000))
|
||||
id = Column(Integer, primary_key = True)
|
||||
page_id = Column(Integer, ForeignKey('page.id'))
|
||||
key = Column(String(64))
|
||||
value = Column(String(8000))
|
||||
|
||||
page = relationship('Page', back_populates = 'properties')
|
||||
|
||||
|
||||
# XXX PagePropertyDict?
|
||||
@deprecated('is it *really* worth it to implement?')
|
||||
# XXX is it *really* worth it to implement PagePropertyDict?
|
||||
class _PagePropertyDict(Mapping):
|
||||
__slots__ = ('_page', )
|
||||
def __init__(self, page: Page, /) -> tuple[str, Any]:
|
||||
|
|
@ -474,19 +510,16 @@ class _PagePropertyDict(Mapping):
|
|||
else:
|
||||
return self._page.get_prop(key)
|
||||
|
||||
|
||||
class PageLink(db.Model):
|
||||
__tablename__ = 'pagelink'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key = True)
|
||||
from_page_id: Mapped[int] = bound_fk(Page.id)
|
||||
to_page_id: Mapped[int] = bound_fk(Page.id)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('from_page_id', 'to_page_id', name='pagelink_from_page_id_to_page_id'),
|
||||
Index('to_page_id', to_page_id)
|
||||
UniqueConstraint('from_page_id', 'to_page_id'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key = True)
|
||||
from_page_id = Column(Integer, ForeignKey('page.id'))
|
||||
to_page_id = Column(Integer, ForeignKey('page.id'))
|
||||
|
||||
from_page: Relationship[Page] = relationship('Page', foreign_keys=[from_page_id], back_populates='forward_links')
|
||||
to_page: Relationship[Page] = relationship('Page', foreign_keys=[to_page_id], back_populates='back_links')
|
||||
|
||||
|
|
@ -528,23 +561,20 @@ class PageLink(db.Model):
|
|||
|
||||
class PagePermission(db.Model):
|
||||
__tablename__ = 'pagepermission'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key = True)
|
||||
page_id: Mapped[int] = bound_fk(Page.id)
|
||||
group_id: Mapped[int] = bound_fk(UserGroup.id)
|
||||
# v--- will be split in 1.2, otherwise sooner or later
|
||||
permissions: Mapped[int] = mapped_column(BigInteger, server_default=text('0'))
|
||||
can_read: Mapped[bool] = BitSelector(permissions, PERM_READ)
|
||||
can_edit: Mapped[bool] = BitSelector(permissions, PERM_EDIT)
|
||||
can_create: Mapped[bool] = BitSelector(permissions, PERM_CREATE)
|
||||
can_set_url: Mapped[bool] = BitSelector(permissions, PERM_SET_URL)
|
||||
can_set_tags: Mapped[bool] = BitSelector(permissions, PERM_SET_TAGS)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('page_id', 'group_id', name="pagepermission_page_id_group_id"),
|
||||
Index('group_id', group_id)
|
||||
UniqueConstraint('page_id', 'group_id'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key = True)
|
||||
page_id = Column(Integer, ForeignKey('page.id'))
|
||||
group_id = Column(Integer, ForeignKey('usergroup.id'))
|
||||
permissions = Column(BigInteger, default=0)
|
||||
can_read = BitSelector(permissions, PERM_READ)
|
||||
can_edit = BitSelector(permissions, PERM_EDIT)
|
||||
can_create = BitSelector(permissions, PERM_CREATE)
|
||||
can_set_url = BitSelector(permissions, PERM_SET_URL)
|
||||
can_set_tags = BitSelector(permissions, PERM_SET_TAGS)
|
||||
|
||||
page = relationship('Page', back_populates = 'permission_overrides')
|
||||
group = relationship('UserGroup', back_populates = 'page_permissions')
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ def md_and_toc(text) -> tuple[Markup, Any | None]:
|
|||
try:
|
||||
converter: markdown.Markdown = markdown.Markdown(extensions=extensions, extension_configs=extension_configs)
|
||||
markup = Markup(converter.convert(text))
|
||||
toc = Markup(converter.toc)
|
||||
toc = converter.toc
|
||||
return markup, toc
|
||||
except Exception as e:
|
||||
return error_p('Error during rendering: {0}: {1}')
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from salvi.i18n import get_string
|
|||
from ..utils import parse_tag_list
|
||||
from ..renderer import md_and_toc
|
||||
|
||||
from ..models import PERM_CREATE, PERM_EDIT, Page, PageLink, PageRevision, PageText, User, db, is_url_available, is_valid_url, perms_required
|
||||
from ..models import PERM_CREATE, Page, PageLink, PageRevision, PageText, User, db, is_url_available, is_valid_url, perms_required
|
||||
|
||||
current_user: User
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ def create():
|
|||
return savepoint(dict(url=request.args.get('url'), title='', text='', tags='', comment=get_string(g.lang, 'page_created')))
|
||||
|
||||
@bp.route('/edit/<int:id>', methods=['GET', 'POST'])
|
||||
@perms_required(PERM_EDIT, message='You are not authorized to edit pages.')
|
||||
@login_required
|
||||
def edit(id: int):
|
||||
p = db.session.execute(select(Page).where(Page.id == id)).scalar()
|
||||
if p is None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/title.html" import title_tag with context %}
|
||||
|
||||
{% block title %}{{ title_tag(T('bad-request'), false) }}{% endblock %}
|
||||
{% block title %}{{ title_tag(T('Bad Request'), false) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="error-page">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
}</style>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
{% block json_info %}{% endblock %}
|
||||
{% block ldjson %}{% endblock %}
|
||||
</head>
|
||||
<body {% if color_theme %} class="{{ theme_classes(color_theme) }}"{% endif %}>
|
||||
<div id="__top"></div>
|
||||
|
|
@ -62,7 +63,7 @@
|
|||
</div>
|
||||
<footer class="site-footer">
|
||||
<div class="footer-copyright">© 2020-2025 Sakuragasaki46.</div>
|
||||
<div class="footer-liability">{{ T('user-generated-warning')}} <u>{{ T('user-liability-warning') }}</u>.</div>
|
||||
<div class="footer-liability">Any content in this site is user-generated and <u>each user is responsible for what they publish</u>.</div>
|
||||
{% if not g.no_user %}
|
||||
<div class="footer-loggedinas">
|
||||
{% if current_user.is_authenticated %}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@
|
|||
</ul>
|
||||
|
||||
<p>
|
||||
{{ T('show-more-years') }}:
|
||||
Show more years:
|
||||
<ul class="inline">
|
||||
<li>
|
||||
<a href="?till_year={{ till_year + 15 }}">{{ T('in-the-future') }}</a>
|
||||
<a href="?till_year={{ till_year + 15 }}">in the future</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?from_year={{ from_year - 15 }}">{{ T('in-the-past') }}</a>
|
||||
<a href="?from_year={{ from_year - 15 }}">in the past</a>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
|
|
|||
59
salvi/templates/kt_list.html
Normal file
59
salvi/templates/kt_list.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}List of contacts – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>Showing: <strong>{{ cat }}</strong> · <a href="/kt/new">New contact</a></p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Show by:</legend>
|
||||
<p><strong>Letter</strong>:
|
||||
{% set typ_list = 'ABCDEFGHIJKLMNOPQRSTUVWYZ' %}
|
||||
{% for t in typ_list %}
|
||||
<a href="/kt/{{ t }}">{{ t }}</a> ·
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="/kt/">All</a> ·
|
||||
<a href="/kt/expired">Expired</a> ·
|
||||
<a href="/kt/ok">Sane</a>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
{% if count > people.count() %}
|
||||
<p>Showing <strong>{{ people.count() }}</strong> people of <strong>{{ count }}</strong> total.</p>
|
||||
{% if count > pageno * 50 %}
|
||||
<p><a href="?page={{ pageno + 1 }}" rel="nofollow">Next page</a>{% if pageno > 1 %} · <a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a>{% endif %}</p>
|
||||
{% elif pageno > 1 %}
|
||||
<a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p><strong>{{ count }}</strong> people.</p>
|
||||
{% endif %}
|
||||
|
||||
<table class="contactnova-list">
|
||||
<thead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in people %}
|
||||
<tr class="contactnova-status_{{ p.status }}">
|
||||
<td><span class="material-icons">{{ p.status_str() }}</span></td>
|
||||
<td class="contactnova-col-code"><a href="/kt/{{ p.code }}">{{ p.code }}</a></td>
|
||||
<td>{{ p.display_name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if count > people.count() %}
|
||||
<p>Showing <strong>{{ people.count() }}</strong> people of <strong>{{ count }}</strong> total.</p>
|
||||
{% if count > pageno * 50 %}
|
||||
<p><a href="?page={{ pageno + 1 }}" rel="nofollow">Next page</a>{% if pageno > 1 %} · <a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a>{% endif %}</p>
|
||||
{% elif pageno > 1 %}
|
||||
<a href="?page={{ pageno - 1 }}" rel="nofollow">Prev page</a></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p><strong>{{ count }}</strong> people.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
72
salvi/templates/kt_new.html
Normal file
72
salvi/templates/kt_new.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create new contact – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" class="circles-add-form">
|
||||
{% if returnto %}<input type="hidden" name="returnto" value="{{ returnto }}">{% endif %}
|
||||
{% if not pl %}
|
||||
<div>
|
||||
<label>Letter</label>
|
||||
<select id="ktCodeLetter" name="letter">
|
||||
<option value="-" disabled="" selected="">(Choose)</option>
|
||||
{% set typ_list = 'ABCDEFGHIJKLMNOPQRSTUVWYZ' %}
|
||||
{% for t in typ_list %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<label>Code</label>
|
||||
<strong id="ktNewCode">---</strong>
|
||||
</div>
|
||||
<div>
|
||||
<label>Display name</label>
|
||||
<input name="display_name" maxlength="50"{% if pl %} value="{{ pl.display_name }}"{% endif %}>
|
||||
</div>
|
||||
<div>
|
||||
<label>Status</label>
|
||||
<select name="status">
|
||||
{% set statuses = {
|
||||
0: 'Variable',
|
||||
1: 'OK',
|
||||
2: 'Issues',
|
||||
} %}
|
||||
{% for k, v in statuses.items() %}
|
||||
<option value="{{ k }}"{% if pl and pl.status == k %} selected=""{% endif %}>{{ v }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Issues</label>
|
||||
<textarea maxlength="500" name="issues">{{ pl and pl.issues }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Description</label>
|
||||
<textarea maxlength="5000" name="description">{{ pl and pl.description }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label>Due</label>
|
||||
<input name="due" required="" type="date" value="{{ pl.due if pl else pl_date }}" min="2020-01-01" />
|
||||
</div>
|
||||
<input type="submit" id="ktSubmit" value="Save">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
{% if not pl %}
|
||||
ktSubmit.disabled = true;
|
||||
{% endif %}
|
||||
ktCodeLetter.onchange = function(){
|
||||
let x = new XMLHttpRequest;
|
||||
x.open('GET', '/kt/_newcode/' + ktCodeLetter.value);
|
||||
x.onreadystatechange = () => {
|
||||
if (x.readyState === XMLHttpRequest.DONE && x.status == 200) {
|
||||
ktNewCode.textContent = x.responseText;
|
||||
ktSubmit.disabled = false;
|
||||
}
|
||||
};
|
||||
x.send();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
24
salvi/templates/kt_single.html
Normal file
24
salvi/templates/kt_single.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Contact {{ p.code }} – {{ app_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ p.code }}</h1>
|
||||
|
||||
<p>aka: <strong>{{ p.display_name }}</strong></p>
|
||||
|
||||
{% if p.issues %}
|
||||
<p class="contactnova-issues">{{ p.issues }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="contactnova-description">
|
||||
{{ p.description or "No description available." |linebreaks }}
|
||||
</p>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<p>Expires: {{ p.due.strftime('%B %-d, %Y') }}</p>
|
||||
|
||||
<p><a href="/kt/">Back</a> · <a href="/kt/edit/{{ p.code }}?returnto=/kt/{{ p.code }}">Edit contact</a></p>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
{% from "macros/title.html" import title_tag with context %}
|
||||
{% from "macros/nl.html" import nl_list with context %}
|
||||
|
||||
{% block title %}{{ title_tag(T('notes-by-date')) }}{% endblock %}
|
||||
{% block title %}{{ title_tag('Notes by date') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<h1 id="firstHeading">{{ T('notes-by-date') }}</h1>
|
||||
<h1 id="firstHeading">Notes by date</h1>
|
||||
|
||||
<div class="inner-content">
|
||||
{{ nl_list(notes) }}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/title.html" import title_tag with context %}
|
||||
|
||||
{% block title %}{{ title_tag(T('manage-accounts'), False) }}{% endblock %}
|
||||
{% block title %}{{ title_tag('Manage accounts', False) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ T('manage-accounts') }}</h1>
|
||||
<h1>Manage accounts</h1>
|
||||
|
||||
<div class="inner-content">
|
||||
{% if current_user.is_admin %}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<strong>Beware: you are managing sensitive informations.</strong>
|
||||
</p>
|
||||
|
||||
<p class="nl-pagination">{{ T('search-results') }} <strong>{{ users.page * 20 - 19 }}</strong> to <strong>{{ min(users.page * 20, users.total) }}</strong> of <strong>{{ users.total }}</strong> total.</p>
|
||||
<p class="nl-pagination">Showing results <strong>{{ users.page * 20 - 19 }}</strong> to <strong>{{ min(users.page * 20, users.total) }}</strong> of <strong>{{ users.total }}</strong> total.</p>
|
||||
|
||||
<form enctype="multipart/form-data" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
{% elif q %}
|
||||
<h2>{{ T('search-no-results') }} <em>{{ q }}</em></h2>
|
||||
{% else %}
|
||||
<p><u>Please note that search queries <u>do not search for page text</u>.</u></p>
|
||||
<p><u>Please note that search queries do not search for page text.</u></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue