From 028ec7bba116d848e9570d67db6643abe9a4c295 Mon Sep 17 00:00:00 2001 From: Philipp Hendel Date: Wed, 27 Nov 2024 14:11:09 +0100 Subject: [PATCH 1/3] auto save forms on edit and display correct calculation status per step --- protzilla/disk_operator.py | 5 ++- protzilla/run.py | 5 +++ protzilla/run_v2.py | 5 +++ protzilla/runner.py | 2 +- protzilla/steps.py | 48 +++++++++++++-------- tests/ui/test_views.py | 12 +++--- ui/runs/forms/base.py | 8 +++- ui/runs/static/runs/runs.js | 24 ++++++++++- ui/runs/templates/runs/details.html | 12 +++--- ui/runs/templates/runs/sidebar_section.html | 5 ++- ui/runs/urls.py | 1 + ui/runs/views.py | 19 ++++++-- ui/runs/views_helper.py | 3 ++ 13 files changed, 111 insertions(+), 38 deletions(-) diff --git a/protzilla/disk_operator.py b/protzilla/disk_operator.py index ce7f07add..3d7060aca 100644 --- a/protzilla/disk_operator.py +++ b/protzilla/disk_operator.py @@ -93,6 +93,7 @@ class KEYS: STEP_PLOTS = "plots" STEP_INSTANCE_IDENTIFIER = "instance_identifier" STEP_TYPE = "type" + STEP_IS_CALCULATED = "is_calculated" DF_MODE = "df_mode" @@ -176,7 +177,7 @@ def check_file_validity(self, file: Path, steps: StepManager) -> bool: if steps.current_step.instance_identifier in file.name: return False return any( - step.instance_identifier in file.name and step.finished + step.instance_identifier in file.name and step.calculation_status!="incomplete" for step in steps.all_steps ) @@ -213,6 +214,7 @@ def _read_step(self, step_data: dict, steps: StepManager) -> Step: step.output = self._read_outputs(step_data.get(KEYS.STEP_OUTPUTS, {})) step.plots = self._read_plots(step_data.get(KEYS.STEP_PLOTS, [])) step.form_inputs = step_data.get(KEYS.STEP_FORM_INPUTS, {}) + #step.calculated = step_data.get(KEYS.STEP_IS_CALCULATED,{}) return step def _write_step(self, step: Step, workflow_mode: bool = False) -> dict: @@ -232,6 +234,7 @@ def _write_step(self, step: Step, workflow_mode: bool = False) -> dict: instance_identifier=step.instance_identifier, output=step.output ) step_data[KEYS.STEP_MESSAGES] = step.messages.messages + #step_data[KEYS.STEP_IS_CALCULATED] = step.calculated return step_data def _read_outputs(self, output: dict) -> Output: diff --git a/protzilla/run.py b/protzilla/run.py index 1f451e89d..9ff6bddc5 100644 --- a/protzilla/run.py +++ b/protzilla/run.py @@ -144,6 +144,11 @@ def step_remove( def step_calculate(self, inputs: dict | None = None) -> None: self.steps.current_step.calculate(self.steps, inputs) + @error_handling + @auto_save + def update_inputs(self, inputs: dict) -> None: + self.steps.current_step.updateInputs(inputs) + @error_handling @auto_save def step_plot(self, inputs: dict | None = None) -> None: diff --git a/protzilla/run_v2.py b/protzilla/run_v2.py index c4b6835ba..06f0c0be5 100644 --- a/protzilla/run_v2.py +++ b/protzilla/run_v2.py @@ -138,6 +138,11 @@ def step_remove( def step_calculate(self, inputs: dict | None = None) -> None: self.steps.current_step.calculate(self.steps, inputs) + @error_handling + @auto_save + def update_inputs(self, inputs: dict) -> None: + self.steps.current_step.updateInputs(inputs) + @error_handling @auto_save def step_plot(self) -> None: diff --git a/protzilla/runner.py b/protzilla/runner.py index 50cbf1274..f5b2c776e 100644 --- a/protzilla/runner.py +++ b/protzilla/runner.py @@ -89,7 +89,7 @@ def compute_workflow(self): log_messages(self.run.current_messages) self.run.current_messages.clear() - if not step.finished: + if step.calculation_status!="complete": break self.run.step_next() diff --git a/protzilla/steps.py b/protzilla/steps.py index d5fb124e3..65ca19224 100644 --- a/protzilla/steps.py +++ b/protzilla/steps.py @@ -7,6 +7,7 @@ from enum import Enum from io import BytesIO from pathlib import Path +from typing import Literal import pandas as pd import plotly @@ -29,6 +30,7 @@ class Step: method_description: str = None input_keys: list[str] = [] output_keys: list[str] = [] + calculation_status: Literal["complete","outdated","incomplete"] = "incomplete" def __init__(self, instance_identifier: str | None = None): self.form_inputs: dict = {} @@ -54,6 +56,11 @@ def __eq__(self, other): and self.output == other.output ) + def updateInputs(self, inputs: dict) -> None: + if inputs: + self.inputs = inputs.copy() + self.form_inputs = self.inputs.copy() + def calculate(self, steps: StepManager, inputs: dict) -> None: """ Core calculation method for all steps, receives the inputs from the front-end and calculates the output. @@ -63,10 +70,7 @@ def calculate(self, steps: StepManager, inputs: dict) -> None: :return: None """ steps._clear_future_steps() - - if inputs: - self.inputs = inputs.copy() - self.form_inputs = self.inputs.copy() + self.updateInputs(inputs) try: self.messages.clear() @@ -113,6 +117,7 @@ def calculate(self, steps: StepManager, inputs: dict) -> None: trace=format_trace(traceback.format_exception(e)), ) ) + self.calculation_status = "complete" def method(self, **kwargs) -> dict: raise NotImplementedError("This method must be implemented in a subclass.") @@ -198,16 +203,18 @@ def validate_outputs( return False return True - @property - def finished(self) -> bool: - """ - Return whether the step has valid outputs and is therefore considered finished. - Plot steps without required outputs are considered finished if they have plots. - :return: True if the step is finished, False otherwise - """ - if len(self.output_keys) == 0: - return not self.plots.empty - return self.validate_outputs(soft_check=True) + #@property + #def calculation_status(self) -> Literal["done","outdated","missing_inputs"]: + # if () + # def finished(self) -> bool: + # """ + # Return whether the step has valid outputs and is therefore considered finished. + # Plot steps without required outputs are considered finished if they have plots. + # :return: True if the step is finished, False otherwise + # """ + # if len(self.output_keys) == 0: + # return not self.plots.empty + # return self.validate_outputs(soft_check=True) class Output: @@ -512,7 +519,7 @@ def preprocessed_output(self) -> Output: if self.current_section == "data_preprocessing": return ( self.current_step.output - if self.current_step.finished + if self.current_step.calculation_status!="incomplete" else self.previous_steps[-1].output ) return self.data_preprocessing[-1].output @@ -621,10 +628,13 @@ def goto_step(self, step_index: int, section: str) -> None: step = self.all_steps_in_section(section)[step_index] new_step_index = self.all_steps.index(step) - if new_step_index < self.current_step_index: - self.current_step_index = new_step_index - else: - raise ValueError("Cannot go to a step that is after the current step") + #if new_step_index < self.current_step_index: + self.current_step_index = new_step_index + # else: + # step.calculate(self, step.form_inputs) + # self.next_step() + # self.goto_step(step_index, section) + #raise ValueError("Cannot go to a step that is after the current step") def name_current_step_instance(self, new_instance_identifier: str) -> None: """ diff --git a/tests/ui/test_views.py b/tests/ui/test_views.py index eb0aece68..480ae6691 100644 --- a/tests/ui/test_views.py +++ b/tests/ui/test_views.py @@ -106,7 +106,7 @@ def test_all_button_parameters(): def test_step_finished(run_standard): - assert not run_standard.current_step.finished + assert not run_standard.current_step.calculation_status != "incomplete" parameters = { "file_path": f"{PROJECT_PATH}/tests/proteinGroups_small_cut.txt", @@ -116,11 +116,11 @@ def test_step_finished(run_standard): } run_standard.step_calculate(parameters) - assert run_standard.current_step.finished + assert run_standard.current_step.calculation_status != "incomplete" run_standard.step_next() - assert not run_standard.current_step.finished + assert not run_standard.current_step.calculation_status != "incomplete" parameters = { "file_path": f"", @@ -128,7 +128,7 @@ def test_step_finished(run_standard): } run_standard.step_calculate(parameters) - assert not run_standard.current_step.finished + assert not run_standard.current_step.calculation_status != "incomplete" parameters = { "file_path": f"{PROJECT_PATH}/tests/nonexistent_file.txt", @@ -136,7 +136,7 @@ def test_step_finished(run_standard): } run_standard.step_calculate(parameters) - assert not run_standard.current_step.finished + assert not run_standard.current_step.calculation_status != "incomplete" parameters = { "file_path": f"{PROJECT_PATH}/tests/metadata_cut_columns.csv", @@ -144,4 +144,4 @@ def test_step_finished(run_standard): } run_standard.step_calculate(parameters) - assert run_standard.current_step.finished + assert run_standard.current_step.calculation_status != "incomplete" diff --git a/ui/runs/forms/base.py b/ui/runs/forms/base.py index ba6b93853..270085d43 100644 --- a/ui/runs/forms/base.py +++ b/ui/runs/forms/base.py @@ -91,10 +91,16 @@ def replace_file_fields_with_paths(self, pretty_file_names: bool) -> None: label=field.label, initial=file_name_to_show ) - def submit(self, run: Run) -> None: + def add_missing_fields(self)->None: # add the missing fields to the form for field_name, field in self.initial_fields.items(): if field_name not in self.fields: self.fields[field_name] = field self.cleaned_data[field_name] = None + def submit(self, run: Run) -> None: + self.add_missing_fields() run.step_calculate(self.cleaned_data) + + def update_form(self, run: Run) -> None: + self.add_missing_fields() + run.update_inputs(self.cleaned_data) \ No newline at end of file diff --git a/ui/runs/static/runs/runs.js b/ui/runs/static/runs/runs.js index 58637bdbd..9b7ff1aae 100644 --- a/ui/runs/static/runs/runs.js +++ b/ui/runs/static/runs/runs.js @@ -32,4 +32,26 @@ $(document).ready(function () { let id = $(this).attr("id"); $('#chosen-' + id).text(this.files[0].name); }); -}); \ No newline at end of file + + //save forms on change + $('.form').on( "change", function() { + var triggeredForm = $(this); + var formId = triggeredForm.attr('id'); + var index = formId.split("_").pop(); + console.log(formId,index) + + $.ajax({ + url: `/runs/${run_name}/display_not_calculated`, // The URL for the Django view + type: 'POST', + headers: { + 'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val() // CSRF token for security + }, + data: $(this).serialize(), + success: function(response) { + $(`#myCircle_${index}`).attr("fill", response === "incomplete" ? "red" : (response === "complete" ? "green":"yellow")); + } + }); + }); + +}); + diff --git a/ui/runs/templates/runs/details.html b/ui/runs/templates/runs/details.html index 5809d356c..ec09b6377 100644 --- a/ui/runs/templates/runs/details.html +++ b/ui/runs/templates/runs/details.html @@ -9,7 +9,9 @@ - + {# TODO 129 Better buttons for analysis and importing #} @@ -126,7 +128,7 @@

{{ display_name }}

{# if there are plot parameters, display method and plot parameters next to each other #} {% if plot_form %}
-
{% csrf_token %}
@@ -145,7 +147,7 @@

{{ display_name }}

-
{% csrf_token %}
@@ -159,7 +161,7 @@

{{ display_name }}

{% else %}
{% if step != "plot" %} - {% csrf_token %}
@@ -191,7 +193,7 @@

{{ display_name }}

{% else %} -
{% csrf_token %} {{ method_dropdown }} diff --git a/ui/runs/templates/runs/sidebar_section.html b/ui/runs/templates/runs/sidebar_section.html index feafb381e..8722770b3 100644 --- a/ui/runs/templates/runs/sidebar_section.html +++ b/ui/runs/templates/runs/sidebar_section.html @@ -1,4 +1,4 @@ -{# params: run_name section{id, name, finished, selected, possible_steps{id, name, methods{id, name, description}}, steps{id, name, finished, selected, index, method_name}} #} +{# params: run_name section{id, finished, name, selected, possible_steps{id, name, methods{id, name, description}}, steps{id, name, finished, selected, index, index_global, method_name, circle_color}} #} {% block js %} {# TODO 129 Better buttons for analysis and importing #} @@ -128,7 +129,7 @@

{{ display_name }}

{# if there are plot parameters, display method and plot parameters next to each other #} {% if plot_form %}
- {% csrf_token %}
@@ -161,7 +162,7 @@

{{ display_name }}

{% else %}
{% if step != "plot" %} - {% csrf_token %}
diff --git a/ui/runs/templates/runs/sidebar_section.html b/ui/runs/templates/runs/sidebar_section.html index 8722770b3..a31f56c19 100644 --- a/ui/runs/templates/runs/sidebar_section.html +++ b/ui/runs/templates/runs/sidebar_section.html @@ -1,4 +1,5 @@ -{# params: run_name section{id, finished, name, selected, possible_steps{id, name, methods{id, name, description}}, steps{id, name, finished, selected, index, index_global, method_name, circle_color}} #} +{% load static %} +{# params: run_name section{id, finished, name, selected, possible_steps{id, name, methods{id, name, description}}, steps{id, name, finished, selected, index, index_global, method_name, calculation_icon_path}} #} {% block js %}