Source code for form.upgrade

""" Contains upgrade tasks that are executed when the application is being
upgraded on the server. See :class:`onegov.core.upgrade.upgrade_task`.

"""
# pragma: exclude file
from __future__ import annotations

from depot.io.utils import FileIntent
from io import BytesIO
from onegov.core.crypto import random_token
from onegov.core.orm.types import JSON
from onegov.core.upgrade import upgrade_task
from onegov.core.utils import dictionary_to_binary
from onegov.core.utils import normalize_for_url
from onegov.form import FormDefinitionCollection
from onegov.form import FormFile
from onegov.form import FormSubmission
from onegov.form.parser import ParsedForm
from onegov.form.orm_types import Formcode
from sqlalchemy import Column, Integer, Text, UUID, bindparam, text
from sqlalchemy.engine.reflection import Inspector


from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
    from onegov.core.upgrade import UpgradeContext


@upgrade_task('Enable external form submissions')
[docs] def enable_external_form_submissions(context: UpgradeContext) -> None: context.operations.alter_column('submissions', 'name', nullable=True)
@upgrade_task('Set payment method for existing forms')
[docs] def set_payment_method_for_existing_forms(context: UpgradeContext) -> None: forms = FormDefinitionCollection(context.session) for form in forms.query(): form.payment_method = 'manual'
@upgrade_task('Migrate form submission files to onegov.file')
[docs] def migrate_form_submission_files_to_onegov_file( context: UpgradeContext ) -> None: submission_ids = [ row[0] for row in context.session.execute(text(""" SELECT submission_id FROM submission_files """)) ] if not submission_ids: return submissions = { s.id: s for s in context.session.query(FormSubmission).filter( FormSubmission.id.in_(submission_ids) ) } for row in context.session.execute(text(""" SELECT submission_id, field_id, filedata FROM submission_files """)): submission_id, field_id, filedata = row submission = submissions[submission_id] replacement = FormFile( id=random_token(), name=submission.data[field_id]['filename'], note=field_id, reference=FileIntent( BytesIO(dictionary_to_binary({'data': filedata})), submission.data[field_id]['filename'], submission.data[field_id]['mimetype'], ) ) assert submission.data[field_id]['data'].startswith('@') submission.data[field_id]['data'] = '@' + replacement.id submission.data.changed() # type:ignore[attr-defined] submission.files.append(replacement) context.session.flush() context.session.execute(text('DROP TABLE IF EXISTS submission_files'))
@upgrade_task('Add payment_method to definitions and submissions')
[docs] def add_payment_method_to_definitions_and_submissions( context: UpgradeContext ) -> None: context.add_column_with_defaults( table='forms', column=Column('payment_method', Text, nullable=False), default=lambda form: form.content.get('payment_method', 'manual') ) context.add_column_with_defaults( table='submissions', column=Column('payment_method', Text, nullable=False), default=lambda submission: ( submission.form and submission.form.content.get('payment_method', 'manual') or 'manual' ) ) for form in context.records_per_table('forms'): form.content.pop('payment_method', None)
@upgrade_task('Add meta dictionary to submissions')
[docs] def add_meta_directory_to_submissions(context: UpgradeContext) -> None: context.add_column_with_defaults( table='submissions', column=Column('meta', JSON, nullable=False), default=lambda submission: {} )
@upgrade_task('Add group/order to form definitions')
[docs] def add_group_order_to_form_definitions(context: UpgradeContext) -> None: context.operations.add_column('forms', Column( 'group', Text, nullable=True )) context.add_column_with_defaults( table='forms', column=Column('order', Text, nullable=False, index=True), default=lambda form: normalize_for_url(form.title) )
@upgrade_task('Add registration window columns')
[docs] def add_registration_window_columns(context: UpgradeContext) -> None: context.operations.add_column( 'submissions', Column('claimed', Integer, nullable=True) ) context.operations.add_column( 'submissions', Column('registration_window_id', UUID, nullable=True) ) context.add_column_with_defaults( table='submissions', column=Column('spots', Integer, nullable=False), default=0 )
@upgrade_task('Make form polymorphic type non-nullable')
[docs] def make_form_polymorphic_type_non_nullable(context: UpgradeContext) -> None: if context.has_table('forms'): context.operations.execute(text(""" UPDATE forms SET type = 'generic' WHERE type IS NULL; """)) context.operations.alter_column('forms', 'type', nullable=False)
@upgrade_task('Add title to submission windows')
[docs] def add_title_to_submission_windows(context: UpgradeContext) -> None: if not context.has_column('submission_windows', 'title'): context.add_column_with_defaults( 'submission_windows', Column( 'title', Text, nullable=True ), default=None # type: ignore[arg-type] )
@upgrade_task('Remove no overlapping submission windows constraint')
[docs] def remove_no_overlapping_submission_windows_constraint( context: UpgradeContext ) -> None: if not context.has_table('submission_windows'): return inspector = Inspector(context.operations_connection) # Check unique constraints unique_constraints = inspector.get_unique_constraints('submission_windows') # Check check constraints check_constraints = inspector.get_check_constraints('submission_windows') constraint_exists = any( const['name'] == 'no_overlapping_submission_windows' for const in unique_constraints + check_constraints ) if constraint_exists: context.operations.drop_constraint( 'no_overlapping_submission_windows', 'submission_windows' )
@upgrade_task('Remove state column form survey submissions')
[docs] def remove_state_from_survey_submissions(context: UpgradeContext) -> None: if context.has_table('survey_submissions'): if context.has_column('survey_submissions', 'state'): context.operations.drop_column('survey_submissions', 'state')
@upgrade_task('Switch to JSON serialized form definitions')
[docs] def switch_to_parsed_form(context: UpgradeContext) -> None: table_pairs = ( ('forms', 'submissions'), ('surveys', 'survey_submissions') ) for definitions_table, submissions_table in table_pairs: if not context.has_table(definitions_table): continue # no migration needed, the old column is already gone if not context.has_column(definitions_table, 'definition'): continue # first add the new columns, but make them nullable context.operations.add_column( definitions_table, Column('parsed', Formcode, nullable=True) ) context.operations.add_column( submissions_table, Column('parsed', Formcode, nullable=True) ) # so we maximize the chance of re-using the parser cache # from parsing the submissions, we first create a lookup # for all the definitions definitions: dict[str, str] = { # ruff:ignore[unnecessary-comprehension] name: definition for name, definition in context.session.execute(text( f""" SELECT name, definition FROM {definitions_table} ORDER BY name """ )) } definition_values: list[dict[str, Any]] = [] submission_values: list[dict[str, Any]] = [] current_name: str | None = None for submission_id, name, definition in context.session.execute(text( f""" SELECT id, name, definition FROM {submissions_table} ORDER BY name, created """ )): # after we parsed the final submission is the best time to # parse the corresponding definition, if it still exists if current_name != name: if current_name is not None and ( defn := definitions.pop(current_name, None) ) is not None: definition_values.append({ 'name': current_name, 'parsed': ParsedForm.from_formcode(defn), }) current_name = name submission_values.append({ 'id': submission_id, 'parsed': ParsedForm.from_formcode(definition), }) # parse any definitions that haven't been parsed yet for name, definition in definitions.items(): definition_values.append({ 'name': name, 'parsed': ParsedForm.from_formcode(definition), }) # bulk update the tables with the parsed definitions if definition_values: context.session.execute(text(f""" UPDATE {definitions_table} SET parsed = :parsed WHERE name = :name """).bindparams( bindparam('name', type_=Text), bindparam('parsed', type_=Formcode) ), definition_values) if submission_values: context.session.execute(text(f""" UPDATE {submissions_table} SET parsed = :parsed WHERE id = :id """).bindparams( bindparam('id', type_=UUID), bindparam('parsed', type_=Formcode) ), submission_values) # finally make the columns not nullable and remove # the old columns context.operations.alter_column( definitions_table, 'parsed', nullable=False) context.operations.alter_column( submissions_table, 'parsed', nullable=False) context.operations.drop_column(definitions_table, 'definition') context.operations.drop_column(submissions_table, 'definition')