From 4ff5fb98f807d4bcb5da31715bf809f5dc9da355 Mon Sep 17 00:00:00 2001 From: Adam Wallace Date: Wed, 31 Jul 2024 09:11:40 +0100 Subject: [PATCH] add CRUD for component, page, form and section config (#13) --- .vscode/launch.json | 30 +- app/blueprints/self_serve/data/data_access.py | 129 ++++- app/blueprints/self_serve/routes.py | 169 +++--- .../{build_form.html => create_form.html} | 2 +- .../{build_page.html => create_page.html} | 2 +- ...add_question.html => create_question.html} | 0 ...build_section.html => create_section.html} | 2 +- .../self_serve/templates/index.html | 20 +- app/db/models/application_config.py | 9 +- app/db/queries/application.py | 282 ++++++++++ build.py | 94 ++++ tests/test_clone.py | 6 + tests/test_db_template_CRUD.py | 521 ++++++++++++++++++ 13 files changed, 1173 insertions(+), 93 deletions(-) rename app/blueprints/self_serve/templates/{build_form.html => create_form.html} (99%) rename app/blueprints/self_serve/templates/{build_page.html => create_page.html} (99%) rename app/blueprints/self_serve/templates/{add_question.html => create_question.html} (100%) rename app/blueprints/self_serve/templates/{build_section.html => create_section.html} (99%) create mode 100644 build.py create mode 100644 tests/test_db_template_CRUD.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f849c0..456369a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,15 +10,31 @@ "request": "launch", "module": "flask", "env": { - "FLASK_APP": "app.app.py", - "FLASK_DEBUG": "1" + "FLASK_APP": "app.app.py", + "FLASK_DEBUG": "1" }, - "args": [ - "run", - "--debug" - ], + "args": ["run", "--debug"], "jinja": true, "autoStartBrowser": false - } + }, + { + "name": "Docker Runner FAB", + "type": "python", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5686 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder:funding-service-design-fund-application-builder}", + "remoteRoot": "." + } + ], + "justMyCode": true + } + ] + } + ] } diff --git a/app/blueprints/self_serve/data/data_access.py b/app/blueprints/self_serve/data/data_access.py index 853aaff..499aebe 100644 --- a/app/blueprints/self_serve/data/data_access.py +++ b/app/blueprints/self_serve/data/data_access.py @@ -3,6 +3,7 @@ from app.blueprints.self_serve.data.not_a_db import LISTS from app.blueprints.self_serve.data.not_a_db import PAGES from app.blueprints.self_serve.data.not_a_db import SECTIONS +from app.db.queries.application import insert_new_section saved_responses = [] saved_sections = {} @@ -66,20 +67,134 @@ def get_list_by_id(id: str) -> dict: return LISTS.get(id, None) -def save_question(question: dict): +# TODO Implement front end journey that can use the section/form/page/component CRUD operations +# from app.db.queries.application import insert_new_section +# from app.db.queries.application import insert_new_form +# from app.db.queries.application import insert_new_page +# from app.db.queries.application import insert_new_component + + +def save_template_component(component: dict): + """ + TODO: + Save a template component to the database + Parameters: + component: dict The component to save to the database as a template + Returns: + dict The saved component + + component_config = { + "page_id": component.get("page_id"), + "theme_id": component.get("theme_id"), + "title": component.get("title"), + "hint_text": component.get("hint"), + "options": component.get("options"), + "type": component.get("question_type"), + "template_name": component.get("template_name"), + "is_template": True, + "audit_info": component.get("audit_info"), + "page_index": component.get("page_index"), + "theme_index": component.get("theme_index"), + "conditions": component.get("conditions"), + "runner_component_name": component.get("runner_component_name"), + "list_id": component.get("list_id"), + } + + return insert_new_component(component_config) + """ + + # temp in memory solution COMPONENTS.append( { "json_snippet": { "options": {}, - "type": question["question_type"], - "title": question["title"], - "hint": question["hint"], + "type": component["question_type"], + "title": component["title"], + "hint": component["hint"], }, - "id": question["id"], - "builder_display_name": question["builder_display_name"], + "id": component["id"], + "builder_display_name": component["builder_display_name"], } ) -def save_section(section: dict): +def save_template_page(page: dict): + """ + TODO: + Save a template page to the database + Parameters: + page: dict The page to save to the database as a template + Returns: + dict The saved page + + page_config = { + "form_id": page.get("form_id"), + "name_in_apply_json": { + "en": page.get("form_display_name"), + }, + "template_name": page.get("builder_display_name"), + "is_template": True, + "audit_info": page.get("audit_info"), + "form_index": page.get("form_index"), + "display_path": page.get("display_path"), + "controller": page.get("controller"), + } + + return insert_new_page(page_config) + """ + + # Temp in memory solution + PAGES.append(page) + + +def save_template_form(form: dict): + """ + TODO: + Save a template form to the database + Parameters: + form: dict The form to save to the database as a template + Returns: + dict The saved form + form_config = { + "name_in_apply_json": { + "en": form.get("form_title"), + }, + "is_template": True, + "template_name": form.get("builder_display_name"), + "audit_info": form.get("audit_info"), + "section_id": form.get("section_id"), + "section_index": form.get("section_index"), + "runner_publish_name": None # This is a template + } + + insert_new_form(form_config) + """ + + # Temp in memory solution + FORMS.append(form) + + +def save_template_section(section: dict): + """ + TODO: + Save a template section to the database + Parameters: + section: dict The section to save to the database as a template + Returns: + dict The saved section + + section_config = { + "name_in_apply_json": { + "en": section.get("section_display_name"), + }, + "is_template": True, # Assuming this remains a constant value + "template_name": section.get("builder_display_name"), + "description": section.get("description"), + "audit_info": section.get("audit_info"), + } + + return insert_new_section(section_config) + """ + + # Temp in memory solution SECTIONS.append(section) diff --git a/app/blueprints/self_serve/routes.py b/app/blueprints/self_serve/routes.py index 96afaaa..8fb5125 100644 --- a/app/blueprints/self_serve/routes.py +++ b/app/blueprints/self_serve/routes.py @@ -15,10 +15,10 @@ from app.blueprints.self_serve.data.data_access import get_component_by_name from app.blueprints.self_serve.data.data_access import get_pages_to_display_in_builder from app.blueprints.self_serve.data.data_access import get_saved_forms -from app.blueprints.self_serve.data.data_access import save_form -from app.blueprints.self_serve.data.data_access import save_page -from app.blueprints.self_serve.data.data_access import save_question -from app.blueprints.self_serve.data.data_access import save_section +from app.blueprints.self_serve.data.data_access import save_template_component +from app.blueprints.self_serve.data.data_access import save_template_form +from app.blueprints.self_serve.data.data_access import save_template_page +from app.blueprints.self_serve.data.data_access import save_template_section from app.blueprints.self_serve.forms.form_form import FormForm from app.blueprints.self_serve.forms.page_form import PageForm from app.blueprints.self_serve.forms.question_form import QuestionForm @@ -43,38 +43,6 @@ def index(): return render_template("index.html") -@self_serve_bp.route("/build_form", methods=["GET", "POST"]) -def build_form(): - form = FormForm() - if form.validate_on_submit(): - new_form = { - "builder_display_name": form.builder_display_name.data, - "start_page_guidance": form.start_page_guidance.data, - "form_display_name": form.form_title.data, - "id": human_to_kebab_case(form.form_title.data), - "pages": form.selected_pages.data, - } - save_form(new_form) - flash(message=f'Form {new_form["form_display_name"]} was saved') - return redirect(url_for("self_serve_bp.index")) - - available_pages = [] - pages = get_pages_to_display_in_builder() - for page in pages: - questions = [ - x["json_snippet"]["title"] if (x := get_component_by_name(comp_name)) else comp_name - for comp_name in page["component_names"] - ] - available_pages.append( - { - "id": page["id"], - "display_name": page["builder_display_name"], - "hover_info": {"title": page["form_display_name"], "questions": questions}, - } - ) - return render_template("build_form.html", available_pages=available_pages, form=form) - - @self_serve_bp.route("/download_json", methods=["POST"]) def generate_json(): form_json = generate_form_config_from_request()["form_json"] @@ -129,11 +97,40 @@ def view_form_questions(): return render_template("view_questions.html", section_name=form_config["title"], question_html=html) -@self_serve_bp.route("build_section", methods=["GET", "POST"]) -def build_section(): +@self_serve_bp.route("/section_questions", methods=["POST"]) +def view_section_questions(): + # form_config = generate_form_config_from_request() + # print_data = generate_print_data_for_sections( + # sections=[ + # { + # "section_title": form_config["title"], + # "forms": [{"name": form_config["form_id"], "form_data": form_config["form_json"]}], + # } + # ], + # lang="en", + # ) + # html = print_html(print_data) + # return render_template("view_questions.html", section_name=form_config["title"], question_html=html) + pass + + +# CRUD routes + + +# Create routes +@self_serve_bp.route("section", methods=["GET", "POST", "PUT", "DELETE"]) +def section(): + # TODO: Create frontend routes and connect to middleware + if request.method == "GET": + pass + if request.method == "PUT": + pass + if request.method == "DELETE": + pass + form = SectionForm() if form.validate_on_submit(): - save_section(form.as_dict()) + save_template_section(form.as_dict()) flash(message=f"Section '{form['builder_display_name'].data}' was saved") return redirect(url_for("self_serve_bp.index")) @@ -147,22 +144,60 @@ def build_section(): "hover_info": {"title": f["builder_display_name"], "pages": f["pages"]}, } ) - return render_template("build_section.html", available_forms=available_forms, form=form) + # save to db here + return render_template("create_section.html", available_forms=available_forms, form=form) -@self_serve_bp.route("/add_question", methods=["GET", "POST"]) -def add_question(): - form = QuestionForm() - question = form.as_dict() +@self_serve_bp.route("/form", methods=["GET", "POST", "PUT", "DELETE"]) +def form(): + # TODO: Create frontend routes and connect to middleware + if request.method == "GET": + pass + if request.method == "PUT": + pass + if request.method == "DELETE": + pass + + form = FormForm() if form.validate_on_submit(): - save_question(question) - flash(message=f"Question '{question['title']}' was saved") + new_form = { + "builder_display_name": form.builder_display_name.data, + "start_page_guidance": form.start_page_guidance.data, + "form_display_name": form.form_title.data, + "id": human_to_kebab_case(form.form_title.data), + "pages": form.selected_pages.data, + } + save_template_form(new_form) + flash(message=f'Form {new_form["form_display_name"]} was saved') return redirect(url_for("self_serve_bp.index")) - return render_template("add_question.html", form=form) + available_pages = [] + pages = get_pages_to_display_in_builder() + for page in pages: + questions = [ + x["json_snippet"]["title"] if (x := get_component_by_name(comp_name)) else comp_name + for comp_name in page["component_names"] + ] + available_pages.append( + { + "id": page["id"], + "display_name": page["builder_display_name"], + "hover_info": {"title": page["form_display_name"], "questions": questions}, + } + ) + return render_template("create_form.html", available_pages=available_pages, form=form) + + +@self_serve_bp.route("/page", methods=["GET", "POST", "PUT", "DELETE"]) +def page(): + # TODO: Create frontend routes and connect to middleware + if request.method == "GET": + pass + if request.method == "PUT": + pass + if request.method == "DELETE": + pass -@self_serve_bp.route("/build_page", methods=["GET", "POST"]) -def build_page(): form = PageForm() if form.validate_on_submit(): new_page = { @@ -172,7 +207,7 @@ def build_page(): "component_names": form.selected_components.data, "show_in_builder": True, } - save_page(new_page) + save_template_page(new_page) flash(message=f"Page '{form.builder_display_name.data}' was saved") return redirect(url_for("self_serve_bp.index")) components = get_all_components() @@ -184,21 +219,23 @@ def build_page(): } for c in components ] - return render_template("build_page.html", form=form, available_questions=available_questions) + return render_template("create_page.html", form=form, available_questions=available_questions) -@self_serve_bp.route("/section_questions", methods=["POST"]) -def view_section_questions(): - # form_config = generate_form_config_from_request() - # print_data = generate_print_data_for_sections( - # sections=[ - # { - # "section_title": form_config["title"], - # "forms": [{"name": form_config["form_id"], "form_data": form_config["form_json"]}], - # } - # ], - # lang="en", - # ) - # html = print_html(print_data) - # return render_template("view_questions.html", section_name=form_config["title"], question_html=html) - pass +@self_serve_bp.route("/question", methods=["GET", "PUT", "POST", "DELETE"]) +def question(): + # TODO: Create frontend routes and connect to middleware + if request.method == "GET": + pass + if request.method == "PUT": + pass + if request.method == "DELETE": + pass + + form = QuestionForm() + question = form.as_dict() + if form.validate_on_submit(): + save_template_component(question) + flash(message=f"Question '{question['title']}' was saved") + return redirect(url_for("self_serve_bp.index")) + return render_template("create_question.html", form=form) diff --git a/app/blueprints/self_serve/templates/build_form.html b/app/blueprints/self_serve/templates/create_form.html similarity index 99% rename from app/blueprints/self_serve/templates/build_form.html rename to app/blueprints/self_serve/templates/create_form.html index 12ba792..aaa2ce3 100644 --- a/app/blueprints/self_serve/templates/build_form.html +++ b/app/blueprints/self_serve/templates/create_form.html @@ -208,7 +208,7 @@

} function saveForm() { section_form = document.getElementById("form_form") - section_form.setAttribute("action", "{{url_for('self_serve_bp.build_form')}}") + section_form.setAttribute("action", "{{url_for('self_serve_bp.create_form')}}") section_form.submit() } diff --git a/app/blueprints/self_serve/templates/build_page.html b/app/blueprints/self_serve/templates/create_page.html similarity index 99% rename from app/blueprints/self_serve/templates/build_page.html rename to app/blueprints/self_serve/templates/create_page.html index ff95f02..6b73cb5 100644 --- a/app/blueprints/self_serve/templates/build_page.html +++ b/app/blueprints/self_serve/templates/create_page.html @@ -211,7 +211,7 @@

} function savePage() { section_form = document.getElementById("page_form") - section_form.setAttribute("action", "{{url_for('self_serve_bp.build_page')}}") + section_form.setAttribute("action", "{{url_for('self_serve_bp.create_page')}}") section_form.submit() } diff --git a/app/blueprints/self_serve/templates/add_question.html b/app/blueprints/self_serve/templates/create_question.html similarity index 100% rename from app/blueprints/self_serve/templates/add_question.html rename to app/blueprints/self_serve/templates/create_question.html diff --git a/app/blueprints/self_serve/templates/build_section.html b/app/blueprints/self_serve/templates/create_section.html similarity index 99% rename from app/blueprints/self_serve/templates/build_section.html rename to app/blueprints/self_serve/templates/create_section.html index e385e05..d79fd61 100644 --- a/app/blueprints/self_serve/templates/build_section.html +++ b/app/blueprints/self_serve/templates/create_section.html @@ -207,7 +207,7 @@

} function saveSection() { section_form = document.getElementById("section_form") - section_form.setAttribute("action", "{{url_for('self_serve_bp.build_section')}}") + section_form.setAttribute("action", "{{url_for('self_serve_bp.section')}}") section_form.submit() } diff --git a/app/blueprints/self_serve/templates/index.html b/app/blueprints/self_serve/templates/index.html index 212a18f..1c831d4 100644 --- a/app/blueprints/self_serve/templates/index.html +++ b/app/blueprints/self_serve/templates/index.html @@ -19,18 +19,24 @@

What do you want to do?

-

Application Setup

+

Template Setup

Fund Metadata

    diff --git a/app/db/models/application_config.py b/app/db/models/application_config.py index ad59dde..8bb6001 100644 --- a/app/db/models/application_config.py +++ b/app/db/models/application_config.py @@ -54,7 +54,7 @@ class Section(BaseModel): is_template = Column(Boolean, default=False, nullable=False) audit_info = Column(JSON(none_as_null=True)) forms: Mapped[List["Form"]] = relationship( - "Form", order_by="Form.section_index", collection_class=ordering_list("section_index") + "Form", order_by="Form.section_index", collection_class=ordering_list("section_index"), passive_deletes="all" ) index = Column(Integer()) source_template_id = Column(UUID(as_uuid=True), nullable=True) @@ -85,7 +85,7 @@ class Form(BaseModel): audit_info = Column(JSON(none_as_null=True)) section_index = Column(Integer()) pages: Mapped[List["Page"]] = relationship( - "Page", order_by="Page.form_index", collection_class=ordering_list("form_index") + "Page", order_by="Page.form_index", collection_class=ordering_list("form_index"), passive_deletes="all" ) runner_publish_name = Column(db.String()) source_template_id = Column(UUID(as_uuid=True), nullable=True) @@ -117,7 +117,10 @@ class Page(BaseModel): form_index = Column(Integer()) display_path = Column(String()) components: Mapped[List["Component"]] = relationship( - "Component", order_by="Component.page_index", collection_class=ordering_list("page_index") + "Component", + order_by="Component.page_index", + collection_class=ordering_list("page_index"), + passive_deletes="all", ) source_template_id = Column(UUID(as_uuid=True), nullable=True) controller = Column(String(), nullable=True) diff --git a/app/db/queries/application.py b/app/db/queries/application.py index 727c651..c8ce85f 100644 --- a/app/db/queries/application.py +++ b/app/db/queries/application.py @@ -199,3 +199,285 @@ def clone_single_round(round_id, new_fund_id, new_short_name) -> Round: clone_single_section(section.section_id, cloned_round.round_id) return cloned_round + + +# CRUD operations for Section, Form, Page, and Component +# CRUD SECTION +def insert_new_section(new_section_config): + """ + Inserts a section object based on the provided configuration. + + Parameters: + new_section_config (dict): A dictionary containing the configuration for the new section. + new_section_config keys: + - round_id (str): The ID of the round to which the section belongs. + - name_in_apply_json (dict): The name of the section as it will be in the Application JSON (support multiple languages/keys). + - template_name (str): The name of the template. + - is_template (bool): A flag indicating whether the section is a template. + - source_template_id (str): The ID of the source template. + - audit_info (dict): Audit information for the section. + - index (int): The index of the section. + Returns: + Section: The newly created section object. + """ + section = Section( + section_id=uuid4(), + round_id=new_section_config.get("round_id", None), + name_in_apply_json=new_section_config.get("name_in_apply_json"), + template_name=new_section_config.get("template_name", None), + is_template=new_section_config.get("is_template", False), + source_template_id=new_section_config.get("source_template_id", None), + audit_info=new_section_config.get("audit_info", {}), + index=new_section_config.get("index"), + ) + db.session.add(section) + db.session.commit() + return section + + +def update_section(section_id, new_section_config): + section = db.session.query(Section).where(Section.section_id == section_id).one_or_none() + if section: + # Define a list of allowed keys to update + allowed_keys = ["round_id", "name_in_apply_json", "template_name", "is_template", "audit_info", "index"] + + for key, value in new_section_config.items(): + # Update the section if the key is allowed + if key in allowed_keys: + setattr(section, key, value) + + db.session.commit() + return section + + +def delete_section(section_id): + section = db.session.query(Section).where(Section.section_id == section_id).one_or_none() + db.session.delete(section) + db.session.commit() + return section + + +# CRUD FORM +def insert_new_form(new_form_config): + """ + Inserts a form object based on the provided configuration. + + Parameters: + new_form_config (dict): A dictionary containing the configuration for the new form. + new_form_config keys: + - section_id (str): The ID of the section to which the form belongs. + - name_in_apply_json (dict): The name of the form as it will be in the Application JSON (support multiple languages/keys). + - is_template (bool): A flag indicating whether the form is a template. + - template_name (str): The name of the template. + - source_template_id (str): The ID of the source template. + - audit_info (dict): Audit information for the form. + - section_index (int): The index of the form within the section. + - runner_publish_name (bool): The path of the form in the form runner (kebab case). + Returns: + Form: The newly created form object. + """ + + form = Form( + form_id=uuid4(), + section_id=new_form_config.get("section_id", None), + name_in_apply_json=new_form_config.get("name_in_apply_json"), + is_template=new_form_config.get("is_template", False), + template_name=new_form_config.get("template_name", None), + source_template_id=new_form_config.get("source_template_id", None), + audit_info=new_form_config.get("audit_info", {}), + section_index=new_form_config.get("section_index"), + runner_publish_name=new_form_config.get("runner_publish_name", None), + ) + db.session.add(form) + db.session.commit() + return form + + +def update_form(form_id, new_form_config): + form = db.session.query(Form).where(Form.form_id == form_id).one_or_none() + if form: + # Define a list of allowed keys to update + allowed_keys = [ + "section_id", + "name_in_apply_json", + "template_name", + "is_template", + "audit_info", + "section_index", + "runner_publish_name", + ] + + # Iterate over the new_form_config dictionary + for key, value in new_form_config.items(): + # Update the form if the key is allowed + if key in allowed_keys: + setattr(form, key, value) + + db.session.commit() + return form + + +def delete_form(form_id): + form = db.session.query(Form).where(Form.form_id == form_id).one_or_none() + db.session.delete(form) + db.session.commit() + return form + + +# CRUD PAGE +def insert_new_page(new_page_config): + """ + Inserts a page object based on the provided configuration. + + Parameters: + new_page_config (dict): A dictionary containing the configuration for the new page. + new_page_config keys: + - form_id (str): The ID of the form to which the page belongs. + - name_in_apply_json (str): The name of the page as it will be in the Application JSON. + - template_name (str): The name of the template. + - is_template (bool): A flag indicating whether the page is a template. + - source_template_id (str): The ID of the source template. + - audit_info (dict): Audit information for the page. + - form_index (int): The index of the page within the form. + - display_path (str): The form runner display path of the page (kebab case). + - controller (str): The form runner controller path for the page (e.g. './pages/summary.js'). + Returns: + Page: The newly created page object. + """ + page = Page( + page_id=uuid4(), + form_id=new_page_config.get("form_id", None), + name_in_apply_json=new_page_config.get("name_in_apply_json"), + template_name=new_page_config.get("template_name", None), + is_template=new_page_config.get("is_template", False), + source_template_id=new_page_config.get("source_template_id", None), + audit_info=new_page_config.get("audit_info", {}), + form_index=new_page_config.get("form_index"), + display_path=new_page_config.get("display_path"), + controller=new_page_config.get("controller", None), + ) + db.session.add(page) + db.session.commit() + return page + + +def update_page(page_id, new_page_config): + page = db.session.query(Page).where(Page.page_id == page_id).one_or_none() + if page: + # Define a list of allowed keys to update + allowed_keys = [ + "form_id", + "name_in_apply_json", + "template_name", + "is_template", + "audit_info", + "form_index", + "display_path", + "controller", + ] + + for key, value in new_page_config.items(): + # Update the page if the key is allowed + if key in allowed_keys: + setattr(page, key, value) + + db.session.commit() + return page + + +def delete_page(page_id): + page = db.session.query(Page).where(Page.page_id == page_id).one_or_none() + db.session.delete(page) + db.session.commit() + return page + + +# CRUD COMPONENT +def insert_new_component(new_component_config: dict): + """ + Inserts a component object based on the provided configuration. + + Parameters: + new_component_config (dict): A dictionary containing the configuration for the new component. + new_component_config keys: + - page_id (str): The ID of the page to which the component belongs. + - theme_id (str): The ID of the theme to which the component belongs. + - title (str): The title of the component. + - hint_text (str): The hint text for the component. + - options (dict): The options such as classes, prefix etc + - type (str): The type of the component. + - template_name (str): The name of the template. + - is_template (bool): A flag indicating whether the component is a template. + - source_template_id (str): The ID of the source template. + - audit_info (dict): Audit information for the component. + - page_index (int): The index of the component within the page. + - theme_index (int): The index of the component within the theme. + - conditions (dict): The conditions such as potential routes based on the components value (can specify page path). + - runner_component_name (str): The name of the runner component. + - list_id (str): The ID of the list to which the component belongs. + Returns: + Component: The newly created component object. + """ + # Instantiate the Component object with the provided and default values + component = Component( + component_id=uuid4(), + page_id=new_component_config.get("page_id", None), + theme_id=new_component_config.get("theme_id", None), + title=new_component_config.get("title"), + hint_text=new_component_config.get("hint_text"), + options=new_component_config.get("options", {}), + type=new_component_config.get("type"), + is_template=new_component_config.get("is_template", False), + template_name=new_component_config.get("template_name", None), + source_template_id=new_component_config.get("source_template_id", None), + audit_info=new_component_config.get("audit_info", {}), + page_index=new_component_config.get("page_index"), + theme_index=new_component_config.get("theme_index"), + conditions=new_component_config.get("conditions", []), + runner_component_name=new_component_config.get("runner_component_name"), + list_id=new_component_config.get("list_id", None), + ) + + # Add the component to the session and commit + db.session.add(component) + db.session.commit() + + # Return the created component object or its ID based on your requirements + return component + + +def update_component(component_id, new_component_config): + component = db.session.query(Component).where(Component.component_id == component_id).one_or_none() + if component: + # Define a list of allowed keys to update to prevent updating unintended fields + allowed_keys = [ + "page_id", + "theme_id", + "title", + "hint_text", + "options", + "type", + "template_name", + "is_template", + "audit_info", + "page_index", + "theme_index", + "conditions", + "runner_component_name", + "list_id", + ] + + for key, value in new_component_config.items(): + # Update the component if the key is allowed + if key in allowed_keys: + setattr(component, key, value) + + db.session.commit() + return component + + +def delete_component(component_id): + component = db.session.query(Component).where(Component.component_id == component_id).one_or_none() + db.session.delete(component) + db.session.commit() + return component diff --git a/build.py b/build.py new file mode 100644 index 0000000..17d4ece --- /dev/null +++ b/build.py @@ -0,0 +1,94 @@ +import glob +import os +import shutil +import urllib.request +import zipfile + +import static_assets + + +def build_govuk_assets(static_dist_root="app/static/dist"): + DIST_ROOT = "./" + static_dist_root + GOVUK_DIR = "/govuk-frontend" + GOVUK_URL = "https://github.com/alphagov/govuk-frontend/releases/download/v5.4.0/release-v5.4.0.zip" + ZIP_FILE = "./govuk_frontend.zip" + DIST_PATH = DIST_ROOT + GOVUK_DIR + ASSETS_DIR = "/assets" + ASSETS_PATH = DIST_PATH + ASSETS_DIR + + # Checks if GovUK Frontend Assets already built + if os.path.exists(DIST_PATH): + print("GovUK Frontend assets already built.If you require a rebuild manually run build.build_govuk_assets") + return True + + # Download zips from GOVUK_URL + # There is a known problem on Mac where one must manually + # run the script "Install Certificates.command" found + # in the python application folder for this to work. + + print("Downloading static file zip.") + urllib.request.urlretrieve(GOVUK_URL, ZIP_FILE) # nosec + + # Attempts to delete the old files, states if + # one doesn't exist. + + print("Deleting old " + DIST_PATH) + try: + shutil.rmtree(DIST_PATH) + except FileNotFoundError: + print("No old " + DIST_PATH + " to remove.") + + # Extract the previously downloaded zip to DIST_PATH + + print("Unzipping file to " + DIST_PATH + "...") + with zipfile.ZipFile(ZIP_FILE, "r") as zip_ref: + zip_ref.extractall(DIST_PATH) + + # Move files from ASSETS_PATH to DIST_PATH + + print("Moving files from " + ASSETS_PATH + " to " + DIST_PATH) + for file_to_move in os.listdir(ASSETS_PATH): + shutil.move("/".join([ASSETS_PATH, file_to_move]), DIST_PATH) + + # Update relative paths + + print("Updating relative paths in css files to " + GOVUK_DIR) + cwd = os.getcwd() + os.chdir(DIST_PATH) + for css_file in glob.glob("*.css"): + + # Read in the file + with open(css_file, "r") as file: + filedata = file.read() + + # Replace the target string + filedata = filedata.replace(ASSETS_DIR, ASSETS_DIR + GOVUK_DIR) + + # Write the file out again + with open(css_file, "w") as file: + file.write(filedata) + os.chdir(cwd) + + # Copy css + os.makedirs("./app/static/dist/styles") + + # Copy over JS source + os.makedirs("./app/static/dist/js") + + # Delete temp files + print("Deleting " + ASSETS_PATH) + shutil.rmtree(ASSETS_PATH) + os.remove(ZIP_FILE) + + +def build_all(static_dist_root="app/static/dist", remove_existing=False): + if remove_existing: + relative_dist_root = "./" + static_dist_root + if os.path.exists(relative_dist_root): + shutil.rmtree(relative_dist_root) + build_govuk_assets(static_dist_root=static_dist_root) + static_assets.build_bundles(static_folder=static_dist_root) + + +if __name__ == "__main__": + build_all(remove_existing=True) diff --git a/tests/test_clone.py b/tests/test_clone.py index 30ef412..e44ef9d 100644 --- a/tests/test_clone.py +++ b/tests/test_clone.py @@ -169,6 +169,8 @@ def test_clone_single_component(flask_test_client, _db): assert result new_id = result.component_id + assert old_id != new_id + # check can retrieve new component assert _db.session.get(Component, new_id) @@ -286,6 +288,8 @@ def test_clone_page_no_components(seed_dynamic_data, _db): assert result new_id = result.page_id + assert old_id != new_id + # check new page exists new_page_from_db = _db.session.query(Page).where(Page.page_id == new_id).one_or_none() assert new_page_from_db @@ -363,6 +367,8 @@ def test_clone_page_with_components(seed_dynamic_data, _db): assert result new_id = result.page_id + assert old_page_id != new_id + # check new page exists new_page_from_db = _db.session.query(Page).where(Page.page_id == new_id).one_or_none() assert new_page_from_db diff --git a/tests/test_db_template_CRUD.py b/tests/test_db_template_CRUD.py new file mode 100644 index 0000000..960d418 --- /dev/null +++ b/tests/test_db_template_CRUD.py @@ -0,0 +1,521 @@ +import uuid +from copy import deepcopy + +from app.db.models import ComponentType +from app.db.models.application_config import Component +from app.db.models.application_config import Form +from app.db.models.application_config import Page +from app.db.models.application_config import Section +from app.db.queries.application import delete_component +from app.db.queries.application import delete_form +from app.db.queries.application import delete_page +from app.db.queries.application import delete_section +from app.db.queries.application import insert_new_component +from app.db.queries.application import insert_new_form +from app.db.queries.application import insert_new_page +from app.db.queries.application import insert_new_section +from app.db.queries.application import update_component +from app.db.queries.application import update_form +from app.db.queries.application import update_page +from app.db.queries.application import update_section + +new_template_section_config = { + "round_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Section Name"}, + "template_name": "Template Name", + "is_template": True, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "index": 1, +} + +new_section_config = { + "round_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Template Section Name"}, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "index": 1, +} + + +def test_insert_new_section(flask_test_client, _db, clear_test_data, seed_dynamic_data): + # Access actual round_id from seed_dynamic_data (could also be None) + round_id = seed_dynamic_data["rounds"][0].round_id + + # Update the configs with the round_id + new_template_section_config["round_id"] = round_id + new_section_config["round_id"] = round_id + + new_section = insert_new_section(new_section_config) + template_section = insert_new_section(new_template_section_config) + + assert isinstance(template_section, Section) + assert template_section.round_id == new_template_section_config["round_id"] + assert template_section.name_in_apply_json == new_template_section_config["name_in_apply_json"] + assert template_section.template_name == new_template_section_config["template_name"] + assert template_section.is_template == True + assert new_section.source_template_id == None + assert template_section.audit_info == new_template_section_config["audit_info"] + assert template_section.index == new_template_section_config["index"] + + assert isinstance(new_section, Section) + assert new_section.round_id == new_section_config["round_id"] + assert new_section.name_in_apply_json == new_section_config["name_in_apply_json"] + assert new_section.template_name == None + assert new_section.is_template == False + assert new_section.source_template_id == None + assert new_section.audit_info == new_section_config["audit_info"] + assert new_section.index == new_section_config["index"] + + +def test_update_section(flask_test_client, _db, clear_test_data, seed_dynamic_data): + round_id = seed_dynamic_data["rounds"][0].round_id + new_section_config["round_id"] = round_id + new_section = insert_new_section(new_section_config) + + assert new_section.round_id == new_section_config["round_id"] + assert new_section.name_in_apply_json == new_section_config["name_in_apply_json"] + assert new_section.template_name == None + assert new_section.is_template == False + assert new_section.source_template_id == None + assert new_section.audit_info == new_section_config["audit_info"] + assert new_section.index == new_section_config["index"] + + # Update new_section_config + updated_section_config = deepcopy(new_section_config) + updated_section_config["name_in_apply_json"] = {"en": "Updated Section Name"} + updated_section_config["audit_info"] = {"created_by": "Jonny Doe", "created_at": "2024-01-02"} + + updated_section = update_section(new_section.section_id, updated_section_config) + # write assertions for updated_section + assert isinstance(updated_section, Section) + assert updated_section.round_id == updated_section_config["round_id"] + assert updated_section.name_in_apply_json == updated_section_config["name_in_apply_json"] + assert updated_section.audit_info == updated_section_config["audit_info"] + + +def test_delete_section(flask_test_client, _db, clear_test_data, seed_dynamic_data): + round_id = seed_dynamic_data["rounds"][0].round_id + new_section_config["round_id"] = round_id + new_section = insert_new_section(new_section_config) + + assert isinstance(new_section, Section) + assert new_section.audit_info == new_section_config["audit_info"] + + delete_section(new_section.section_id) + assert _db.session.query(Section).filter(Section.section_id == new_section.section_id).one_or_none() == None + + +from sqlalchemy.exc import IntegrityError + + +def test_failed_delete_section_with_fk_to_forms(flask_test_client, _db, clear_test_data, seed_dynamic_data): + new_section_config["round_id"] = None + section = insert_new_section(new_section_config) + # CREATE FK link to Form + new_form_config["section_id"] = section.section_id + form = insert_new_form(new_form_config) + # check inserted form has same section_id + assert form.section_id == section.section_id + assert isinstance(section, Section) + assert section.audit_info == new_section_config["audit_info"] + + try: + delete_section(form.section_id) + assert False, "Expected IntegrityError was not raised" + except IntegrityError: + _db.session.rollback() # Rollback the failed transaction to maintain DB integrity + assert True # Explicitly pass the test to indicate the expected error was caught + + existing_section = _db.session.query(Section).filter(Section.section_id == section.section_id).one_or_none() + assert existing_section is not None, "Section was unexpectedly deleted" + + +new_form_config = { + "section_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Form Name"}, + "is_template": False, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "section_index": 1, + "runner_publish_name": "test-form", +} + +new_template_form_config = { + "section_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Template Form Name"}, + "template_name": "Form Template Name", + "is_template": True, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "section_index": 1, + "runner_publish_name": None, # This is a template +} + + +def test_insert_new_form(flask_test_client, _db, clear_test_data, seed_dynamic_data): + round_id = seed_dynamic_data["rounds"][0].round_id + new_section_config["round_id"] = round_id + new_section = insert_new_section(new_section_config) + # Point to a section that exists in the db + new_form_config["section_id"] = new_section.section_id # *Does not need to belong to a section + new_template_form_config["section_id"] = new_section.section_id # *Does not need to belong to a section + + new_template_form = insert_new_form(new_template_form_config) + assert isinstance(new_template_form, Form) + assert new_template_form.section_id == new_template_form_config["section_id"] + assert new_template_form.name_in_apply_json == new_template_form_config["name_in_apply_json"] + assert new_template_form.template_name == new_template_form_config["template_name"] + assert new_template_form.is_template == True + assert new_template_form.source_template_id == None + assert new_template_form.audit_info == new_template_form_config["audit_info"] + assert new_template_form.section_index == new_template_form_config["section_index"] + assert new_template_form.runner_publish_name == None + + new_form = insert_new_form(new_form_config) + assert isinstance(new_form, Form) + assert new_form.section_id == new_form_config["section_id"] + assert new_form.name_in_apply_json == new_form_config["name_in_apply_json"] + assert new_form.template_name == None + assert new_form.source_template_id == None # not cloned, its a new non-template form + assert new_form.is_template == False + assert new_form.audit_info == new_form_config["audit_info"] + assert new_form.section_index == new_form_config["section_index"] + assert new_form.runner_publish_name == new_form_config["runner_publish_name"] + + new_form_config["section_index"] = 2 + new_form = insert_new_form(new_form_config) + assert new_form.section_index == new_form_config["section_index"] + + +def test_update_form(flask_test_client, _db, clear_test_data, seed_dynamic_data): + round_id = seed_dynamic_data["rounds"][0].round_id + new_section_config["round_id"] = round_id + new_section = insert_new_section(new_section_config) + new_form_config["section_id"] = new_section.section_id + new_form = insert_new_form(new_form_config) + + assert new_form.section_id == new_form_config["section_id"] + assert new_form.name_in_apply_json == new_form_config["name_in_apply_json"] + assert new_form.template_name == None + assert new_form.is_template == False + assert new_form.source_template_id == None + assert new_form.audit_info == new_form_config["audit_info"] + assert new_form.section_index == new_form_config["section_index"] + assert new_form.runner_publish_name == new_form_config["runner_publish_name"] + + # Update new_form_config + updated_form_config = deepcopy(new_form_config) + updated_form_config["name_in_apply_json"] = {"en": "Updated Form Name"} + updated_form_config["audit_info"] = {"created_by": "Jonny Doe", "created_at": "2024-01-02"} + + updated_form = update_form(new_form.form_id, updated_form_config) + + assert isinstance(updated_form, Form) + assert updated_form.section_id == updated_form_config["section_id"] + assert updated_form.name_in_apply_json == updated_form_config["name_in_apply_json"] + assert updated_form.audit_info == updated_form_config["audit_info"] + + +def test_delete_form(flask_test_client, _db, clear_test_data, seed_dynamic_data): + round_id = seed_dynamic_data["rounds"][0].round_id + new_section_config["round_id"] = round_id + new_section = insert_new_section(new_section_config) + new_form_config["section_id"] = new_section.section_id + new_form = insert_new_form(new_form_config) + + assert isinstance(new_form, Form) + assert new_form.audit_info == new_form_config["audit_info"] + + delete_form(new_form.form_id) + assert _db.session.query(Form).filter(Form.form_id == new_form.form_id).one_or_none() == None + + +def test_failed_delete_form_with_fk_to_page(flask_test_client, _db, clear_test_data, seed_dynamic_data): + new_form_config["section_id"] = None + form = insert_new_form(new_form_config) + # CREATE FK link to Form + new_page_config["form_id"] = form.form_id + page = insert_new_page(new_page_config) + + try: + delete_form(page.form_id) + assert False, "Expected IntegrityError was not raised" + except IntegrityError: + _db.session.rollback() # Rollback the failed transaction to maintain DB integrity + assert True # Explicitly pass the test to indicate the expected error was caught + + existing_form = _db.session.query(Form).filter(Form.form_id == form.form_id).one_or_none() + assert existing_form is not None, "Form was unexpectedly deleted" + + +new_page_config = { + "form_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Page Name"}, + "is_template": False, + "template_name": None, + "source_template_id": None, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "form_index": 1, + "display_path": "test-page", + "controller": "./test-controller", +} + +new_template_page_config = { + "form_id": uuid.uuid4(), + "name_in_apply_json": {"en": "Template Page Name"}, + "is_template": True, + "template_name": "Page Template Name", + "source_template_id": None, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "form_index": 1, + "display_path": "test-page", + "controller": None, +} + + +def test_insert_new_page(flask_test_client, _db, clear_test_data, seed_dynamic_data): + + new_form_config["section_id"] = None + new_form = insert_new_form(new_form_config) + + new_page_config["form_id"] = new_form.form_id # *Does not need to belong to a form + new_template_page_config["form_id"] = None # *Does not need to belong to a form + + new_template_page = insert_new_page(new_template_page_config) + assert isinstance(new_template_page, Page) + assert new_template_page.form_id is None + assert new_template_page.name_in_apply_json == new_template_page_config["name_in_apply_json"] + assert new_template_page.template_name == new_template_page_config["template_name"] + assert new_template_page.is_template == True + assert new_template_page.source_template_id == None + assert new_template_page.audit_info == new_template_page_config["audit_info"] + assert new_template_page.form_index == new_template_page_config["form_index"] + assert new_template_page.display_path == new_page_config["display_path"] + assert new_template_page.controller == new_template_page_config["controller"] + + new_page = insert_new_page(new_page_config) + assert isinstance(new_page, Page) + assert new_page.form_id == new_page_config["form_id"] + assert new_page.name_in_apply_json == new_page_config["name_in_apply_json"] + assert new_page.template_name == None + assert new_page.is_template == False + assert new_page.source_template_id == None + assert new_page.audit_info == new_page_config["audit_info"] + assert new_page.form_index == new_page_config["form_index"] + assert new_page.display_path == new_page_config["display_path"] + assert new_page.controller == new_page_config["controller"] + + +def test_update_page(flask_test_client, _db, clear_test_data, seed_dynamic_data): + new_page_config["form_id"] = None + new_page = insert_new_page(new_page_config) + + assert new_page.form_id is None + assert new_page.name_in_apply_json == new_page_config["name_in_apply_json"] + assert new_page.template_name == None + assert new_page.is_template == False + assert new_page.source_template_id == None + assert new_page.audit_info == new_page_config["audit_info"] + assert new_page.form_index == new_page_config["form_index"] + assert new_page.display_path == new_page_config["display_path"] + assert new_page.controller == new_page_config["controller"] + + # Update new_page_config + updated_page_config = deepcopy(new_page_config) + updated_page_config["name_in_apply_json"] = {"en": "Updated Page Name"} + updated_page_config["audit_info"] = {"created_by": "Jonny Doe", "created_at": "2024-01-02"} + + updated_page = update_page(new_page.page_id, updated_page_config) + + assert isinstance(updated_page, Page) + assert updated_page.form_id == updated_page_config["form_id"] + assert updated_page.name_in_apply_json == updated_page_config["name_in_apply_json"] + assert updated_page.audit_info == updated_page_config["audit_info"] + + +def test_delete_page(flask_test_client, _db, clear_test_data, seed_dynamic_data): + new_page_config["form_id"] = None + new_page = insert_new_page(new_page_config) + + assert isinstance(new_page, Page) + assert new_page.audit_info == new_page_config["audit_info"] + + delete_page(new_page.page_id) + assert _db.session.query(Page).filter(Page.page_id == new_page.page_id).one_or_none() == None + + +def test_failed_delete_page_with_fk_to_component(flask_test_client, _db, clear_test_data, seed_dynamic_data): + new_page_config["form_id"] = None + new_page = insert_new_page(new_page_config) + # CREATE FK link to Component + new_component_config["page_id"] = new_page.page_id + new_component_config["list_id"] = None + new_component_config["theme_id"] = None + component = insert_new_component(new_component_config) + # check inserted component has same page_id + assert component.page_id == new_page.page_id + assert isinstance(new_page, Page) + assert new_page.audit_info == new_page_config["audit_info"] + + try: + delete_page(component.page_id) + assert False, "Expected IntegrityError was not raised" + except IntegrityError: + _db.session.rollback() # Rollback the failed transaction to maintain DB integrity + assert True # Explicitly pass the test to indicate the expected error was caught + + existing_page = _db.session.query(Page).filter(Page.page_id == new_page.page_id).one_or_none() + assert existing_page is not None, "Page was unexpectedly deleted" + + +new_component_config = { + "page_id": uuid.uuid4(), + "theme_id": uuid.uuid4(), + "title": "Component Title", + "hint_text": "Component Hint Text", + "options": {"hideTitle": False, "classes": "test-class"}, + "type": ComponentType.TEXT_FIELD, + "is_template": False, + "template_name": None, + "source_template_id": None, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "page_index": 1, + "theme_index": 1, + "conditions": [ + { + "name": "organisation_other_names_no", + "value": "false", # this must be lowercaes or the navigation doesn't work + "operator": "is", + "destination_page_path": "CONTINUE", + }, + { + "name": "organisation_other_names_yes", + "value": "true", # this must be lowercaes or the navigation doesn't work + "operator": "is", + "destination_page_path": "organisation-alternative-names", + }, + ], + "runner_component_name": "test-component", + "list_id": uuid.uuid4(), +} + + +new_template_component_config = { + "page_id": uuid.uuid4(), + "theme_id": uuid.uuid4(), + "title": "Template Component Title", + "hint_text": "Template Component Hint Text", + "options": {"hideTitle": False, "classes": "test-class"}, + "type": ComponentType.TEXT_FIELD, + "is_template": True, + "template_name": "Component Template Name", + "source_template_id": None, + "audit_info": {"created_by": "John Doe", "created_at": "2022-01-01"}, + "page_index": 1, + "theme_index": 2, + "conditions": [ + { + "name": "path_start_no", + "value": "false", # this must be lowercaes or the navigation doesn't work + "operator": "is", + "destination_page_path": "path-1", + }, + { + "name": "path_start_yes", + "value": "true", # this must be lowercaes or the navigation doesn't work + "operator": "is", + "destination_page_path": "path-2", + }, + ], + "runner_component_name": "test-component", + "list_id": uuid.uuid4(), +} + + +def test_insert_new_component(flask_test_client, _db, clear_test_data, seed_dynamic_data): + page_id = seed_dynamic_data["pages"][0].page_id + list_id = seed_dynamic_data["lists"][0].list_id + theme_id = seed_dynamic_data["themes"][0].theme_id + new_component_config["page_id"] = page_id + new_template_component_config["page_id"] = None + new_component_config["list_id"] = list_id + new_template_component_config["list_id"] = list_id + new_component_config["theme_id"] = theme_id + new_template_component_config["theme_id"] = theme_id + + component = insert_new_component(new_component_config) + assert isinstance(component, Component) + assert component.page_id == new_component_config["page_id"] + assert component.theme_id == new_component_config["theme_id"] + assert component.title == new_component_config["title"] + assert component.hint_text == new_component_config["hint_text"] + assert component.options == new_component_config["options"] + assert component.type == new_component_config["type"] + assert component.is_template == False + assert component.template_name == None + assert component.source_template_id == None + assert component.audit_info == new_component_config["audit_info"] + assert component.page_index == new_component_config["page_index"] + assert component.theme_index == new_component_config["theme_index"] + assert component.conditions == new_component_config["conditions"] + assert component.runner_component_name == new_component_config["runner_component_name"] + assert component.list_id == new_component_config["list_id"] + + template_component = insert_new_component(new_template_component_config) + assert isinstance(template_component, Component) + assert template_component.page_id is None + assert template_component.theme_id == new_template_component_config["theme_id"] + assert template_component.title == new_template_component_config["title"] + assert template_component.hint_text == new_template_component_config["hint_text"] + assert template_component.options == new_template_component_config["options"] + assert template_component.type == new_template_component_config["type"] + assert template_component.is_template == True + assert template_component.template_name == new_template_component_config["template_name"] + assert template_component.source_template_id == None + assert template_component.audit_info == new_template_component_config["audit_info"] + assert template_component.page_index == new_template_component_config["page_index"] + assert template_component.theme_index == new_template_component_config["theme_index"] + assert template_component.conditions == new_template_component_config["conditions"] + assert template_component.runner_component_name == new_template_component_config["runner_component_name"] + assert template_component.list_id == new_template_component_config["list_id"] + + +def test_update_component(flask_test_client, _db, clear_test_data, seed_dynamic_data): + page_id = seed_dynamic_data["pages"][0].page_id + list_id = seed_dynamic_data["lists"][0].list_id + theme_id = seed_dynamic_data["themes"][0].theme_id + new_component_config["page_id"] = page_id + new_component_config["list_id"] = list_id + new_component_config["theme_id"] = theme_id + + component = insert_new_component(new_component_config) + + assert component.title == new_component_config["title"] + assert component.audit_info == new_component_config["audit_info"] + assert component.is_template == False + + # Update new_component_config + updated_component_config = deepcopy(new_component_config) + updated_component_config["title"] = "Updated Component Title" + updated_component_config["audit_info"] = {"created_by": "Adam Doe", "created_at": "2024-01-02"} + + updated_component = update_component(component.component_id, updated_component_config) + + assert isinstance(updated_component, Component) + assert updated_component.title == updated_component_config["title"] + assert updated_component.audit_info == updated_component_config["audit_info"] + assert updated_component.is_template == False + + +def test_delete_component(flask_test_client, _db, clear_test_data, seed_dynamic_data): + page_id = seed_dynamic_data["pages"][0].page_id + list_id = seed_dynamic_data["lists"][0].list_id + theme_id = seed_dynamic_data["themes"][0].theme_id + new_component_config["page_id"] = page_id + new_component_config["list_id"] = list_id + new_component_config["theme_id"] = theme_id + + component = insert_new_component(new_component_config) + + assert isinstance(component, Component) + assert component.audit_info == new_component_config["audit_info"] + + delete_component(component.component_id) + assert _db.session.query(Component).filter(Component.component_id == component.component_id).one_or_none() == None