diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f11a4b..0b40585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/alembic/README b/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index a283d9d..0000000 --- a/alembic/env.py +++ /dev/null @@ -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() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index fbc4b07..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -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"} diff --git a/alembic/versions/ae0587e14725_.py b/alembic/versions/ae0587e14725_.py deleted file mode 100644 index a9c6a70..0000000 --- a/alembic/versions/ae0587e14725_.py +++ /dev/null @@ -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 ### diff --git a/alembic/versions/ebde30d24167_.py b/alembic/versions/ebde30d24167_.py deleted file mode 100644 index 222417e..0000000 --- a/alembic/versions/ebde30d24167_.py +++ /dev/null @@ -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 ### diff --git a/i18n/salvi.en.json b/i18n/salvi.en.json index 996c8a6..2376fe5 100644 --- a/i18n/salvi.en.json +++ b/i18n/salvi.en.json @@ -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}!", diff --git a/i18n/salvi.it.json b/i18n/salvi.it.json index 6580835..711625d 100644 --- a/i18n/salvi.it.json +++ b/i18n/salvi.it.json @@ -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", diff --git a/pyproject.toml b/pyproject.toml index 5dca1b1..ffd0921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/salvi/__init__.py b/salvi/__init__.py index 4afe92b..f6ada0d 100644 --- a/salvi/__init__.py +++ b/salvi/__init__.py @@ -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) ... diff --git a/salvi/constants.py b/salvi/constants.py index d37ff4f..a5b425b 100644 --- a/salvi/constants.py +++ b/salvi/constants.py @@ -9,7 +9,7 @@ PING_RE = r'(? 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,21 +510,18 @@ 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') + to_page: Relationship[Page] = relationship('Page', foreign_keys=[to_page_id], back_populates='back_links') @classmethod def parse_links(cls, from_page, text: str, erase=True): @@ -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') diff --git a/salvi/renderer.py b/salvi/renderer.py index 4d4df75..b286048 100644 --- a/salvi/renderer.py +++ b/salvi/renderer.py @@ -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}') diff --git a/salvi/routes/edit.py b/salvi/routes/edit.py index 0901229..4be78de 100644 --- a/salvi/routes/edit.py +++ b/salvi/routes/edit.py @@ -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/', 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: diff --git a/salvi/templates/400.html b/salvi/templates/400.html index dc58259..1e02791 100644 --- a/salvi/templates/400.html +++ b/salvi/templates/400.html @@ -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 %}
diff --git a/salvi/templates/base.html b/salvi/templates/base.html index f3539d1..9f3deb1 100644 --- a/salvi/templates/base.html +++ b/salvi/templates/base.html @@ -25,6 +25,7 @@ } {% block json_info %}{% endblock %} + {% block ldjson %}{% endblock %}
@@ -62,7 +63,7 @@