Printing a dark-mode page lays a full page of toner down for the background and knocks the text out of it in white. On a shopfloor printer that is a cartridge for a handful of guides, and the result is harder to read than the plain version. Somebody would have found this the expensive way. The theme tokens are redefined for print only, in an unscoped block because overriding them has to reach :root. Every guide element already renders through those variables rather than literal colours, so this is a dozen lines instead of a rewrite - the same discipline that made dark mode work without dark-mode code. Backgrounds are dropped rather than printed grey: a callout is identified by its rule and its heading, which cost nothing. Checkboxes print EMPTY even where they are ticked on screen, because a printed guide is a worksheet somebody carries to the bay, and one person's progress is not part of the procedure. The footer gains the print date beside the version, so a stale copy pinned to a noticeboard can be spotted. Also corrects the model and migration found by running this against a real database rather than SQLite: BaseModel contributes createddate, modifieddate and isactive to every table and the migration never created them, so the first read failed on unknown columns; the model had also declared duplicate timestamps on top, now removed with the API field names kept stable. And the slug is 191 characters, not 300 - it is UNIQUE-indexed, MySQL caps an index key at 767 bytes, and utf8mb4 is 4 bytes per character.
115 lines
5.3 KiB
Python
115 lines
5.3 KiB
Python
"""guides plugin baseline: guides, guiderevisions, guideimages.
|
|
|
|
A new plugin, so this builds its tables rather than stamping an anchor over
|
|
core-owned ones (contrast the cutover plugins, whose 0001 is a no-op).
|
|
|
|
Idempotent: each table is created only if it is not already there, so a database
|
|
where an earlier run half-succeeded can be upgraded again without hand-editing
|
|
alembic_version_guides.
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import inspect
|
|
|
|
|
|
revision = 'guides0001baseline'
|
|
down_revision = None
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _tables():
|
|
return set(inspect(op.get_bind()).get_table_names())
|
|
|
|
|
|
def upgrade():
|
|
existing = _tables()
|
|
|
|
if 'guides' not in existing:
|
|
op.create_table(
|
|
'guides',
|
|
sa.Column('guideid', sa.Integer(), nullable=False),
|
|
sa.Column('title', sa.String(length=300), nullable=False),
|
|
# 191 because this is UNIQUE-indexed and MySQL caps an index key at
|
|
# 767 bytes; utf8mb4 is 4 bytes per character.
|
|
sa.Column('slug', sa.String(length=191), nullable=False),
|
|
sa.Column('summary', sa.String(length=1000)),
|
|
sa.Column('tags', sa.String(length=500)),
|
|
sa.Column('status', sa.String(length=20), nullable=False,
|
|
server_default='draft'),
|
|
sa.Column('blocksjson', sa.dialects.mysql.MEDIUMTEXT()
|
|
if op.get_bind().dialect.name == 'mysql' else sa.Text(),
|
|
nullable=False),
|
|
sa.Column('publishedrevisionid', sa.Integer()),
|
|
sa.Column('createdby', sa.Integer()),
|
|
sa.Column('updatedby', sa.Integer()),
|
|
sa.Column('publishedat', sa.DateTime()),
|
|
# BaseModel contributes these to every model in the app; a plugin
|
|
# migration has to spell them out or the table is missing columns
|
|
# the ORM selects on every query.
|
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
|
sa.Column('revision', sa.Integer(), nullable=False, server_default='1'),
|
|
sa.PrimaryKeyConstraint('guideid'),
|
|
sa.UniqueConstraint('slug', name='uq_guides_slug'),
|
|
)
|
|
op.create_index('idx_guides_status', 'guides', ['status'])
|
|
|
|
if 'guiderevisions' not in existing:
|
|
op.create_table(
|
|
'guiderevisions',
|
|
sa.Column('guiderevisionid', sa.Integer(), nullable=False),
|
|
sa.Column('guideid', sa.Integer(), nullable=False),
|
|
sa.Column('revision', sa.Integer(), nullable=False),
|
|
sa.Column('title', sa.String(length=300), nullable=False),
|
|
sa.Column('summary', sa.String(length=1000)),
|
|
sa.Column('tags', sa.String(length=500)),
|
|
sa.Column('blocksjson', sa.dialects.mysql.MEDIUMTEXT()
|
|
if op.get_bind().dialect.name == 'mysql' else sa.Text(),
|
|
nullable=False),
|
|
sa.Column('contenthash', sa.String(length=64), nullable=False),
|
|
sa.Column('note', sa.String(length=500)),
|
|
sa.Column('createdby', sa.Integer()),
|
|
# BaseModel contributes these to every model in the app; a plugin
|
|
# migration has to spell them out or the table is missing columns
|
|
# the ORM selects on every query.
|
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
|
sa.PrimaryKeyConstraint('guiderevisionid'),
|
|
sa.ForeignKeyConstraint(['guideid'], ['guides.guideid'],
|
|
ondelete='CASCADE'),
|
|
)
|
|
op.create_index('idx_guiderevisions_guide', 'guiderevisions', ['guideid'])
|
|
op.create_index('idx_guiderevisions_hash', 'guiderevisions', ['contenthash'])
|
|
|
|
if 'guideimages' not in existing:
|
|
op.create_table(
|
|
'guideimages',
|
|
sa.Column('guideimageid', sa.Integer(), nullable=False),
|
|
sa.Column('guideid', sa.Integer()),
|
|
sa.Column('filename', sa.String(length=255)),
|
|
sa.Column('mimetype', sa.String(length=100), nullable=False),
|
|
sa.Column('bytesize', sa.Integer(), nullable=False),
|
|
sa.Column('data', sa.dialects.mysql.MEDIUMBLOB()
|
|
if op.get_bind().dialect.name == 'mysql' else sa.LargeBinary(),
|
|
nullable=False),
|
|
sa.Column('uploadedby', sa.Integer()),
|
|
# BaseModel contributes these to every model in the app; a plugin
|
|
# migration has to spell them out or the table is missing columns
|
|
# the ORM selects on every query.
|
|
sa.Column('createddate', sa.DateTime(), nullable=False),
|
|
sa.Column('modifieddate', sa.DateTime(), nullable=False),
|
|
sa.Column('isactive', sa.Boolean(), nullable=False),
|
|
sa.PrimaryKeyConstraint('guideimageid'),
|
|
sa.ForeignKeyConstraint(['guideid'], ['guides.guideid'],
|
|
ondelete='CASCADE'),
|
|
)
|
|
op.create_index('idx_guideimages_guide', 'guideimages', ['guideid'])
|
|
|
|
|
|
def downgrade():
|
|
for table in ('guideimages', 'guiderevisions', 'guides'):
|
|
op.drop_table(table)
|