diff --git a/protzilla/disk_operator.py b/protzilla/disk_operator.py index ce7f07add..e41d5bcbe 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_CALCULATION_STATUS = "calculation_status" 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.calculation_status = step_data.get(KEYS.STEP_CALCULATION_STATUS,"incomplete") 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_CALCULATION_STATUS] = step.calculation_status return step_data def _read_outputs(self, output: dict) -> Output: diff --git a/protzilla/run.py b/protzilla/run.py index 1f451e89d..7d205756e 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: @@ -162,6 +167,10 @@ def step_previous(self) -> None: def step_goto(self, step_index: int, section: str) -> None: self.steps.goto_step(step_index, section) + @error_handling + def step_set_outdated(self) -> int: + return self.steps.set_steps_outdated() + @error_handling @auto_save def step_change_method(self, new_method: str) -> None: diff --git a/protzilla/run_v2.py b/protzilla/run_v2.py index f0499ac01..f1df31c88 100644 --- a/protzilla/run_v2.py +++ b/protzilla/run_v2.py @@ -147,6 +147,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 fa206a197..5c78c57cd 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 eecf0d045..2e446c5e8 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. @@ -62,21 +69,21 @@ def calculate(self, steps: StepManager, inputs: dict) -> None: :param inputs: These inputs will be supplied to the method. Only keys in the input_keys of the method class will actually be supplied to the method :return: None """ - steps._clear_future_steps() - - if inputs: - self.inputs = inputs.copy() - self.form_inputs = self.inputs.copy() - + stepIndex = steps.all_steps.index(self) + previousStep = steps.all_steps[stepIndex-1] + print(self, previousStep) + if (previousStep.calculation_status == "outdated" ): + previousStep.calculate(steps,inputs) + #steps._clear_future_steps() + if (steps.current_step_index == stepIndex): + self.updateInputs(inputs) try: self.messages.clear() self.insert_dataframes(steps, self.inputs) self.validate_inputs() - output_dict = self.method(self.inputs) self.handle_outputs(output_dict) self.handle_messages(output_dict) - self.validate_outputs() except NotImplementedError as e: self.messages.append( @@ -113,6 +120,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 +206,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: @@ -467,10 +477,23 @@ def all_steps_in_section(self, section: str) -> list[Step]: return self.sections[section] else: raise ValueError(f"Unknown section {section}") + + def set_steps_outdated(self) -> None: + print("steps",self.following_steps) + count=0 + for step in self.following_steps: + if (step.calculation_status == "complete"): + step.calculation_status = "outdated" + count+=1 + return count @property def previous_steps(self) -> list[Step]: return self.all_steps[: self.current_step_index] + + @property + def following_steps(self) -> list[Step]: + return self.all_steps[self.current_step_index :] @property def current_step(self) -> Step: @@ -515,7 +538,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 @@ -624,10 +647,14 @@ 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 (step.calculation_status != "incomplete"): + #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: """ @@ -668,3 +695,6 @@ def _clear_future_steps(self, index: int | None = None) -> None: step.output = Output() step.messages = Messages() step.plots = Plots() + + # def _calculate_previous_steps(self, steps: StepManager, index: int) -> None: + # for step in 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..1b75318ae 100644 --- a/ui/runs/forms/base.py +++ b/ui/runs/forms/base.py @@ -91,10 +91,18 @@ 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: + print("submit", self) + self.add_missing_fields() run.step_calculate(self.cleaned_data) + + def update_form(self, run: Run) -> None: + print("update", self) + 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 4fbb32670..d6127a0e3 100644 --- a/ui/runs/static/runs/runs.js +++ b/ui/runs/static/runs/runs.js @@ -32,11 +32,34 @@ $(document).ready(function () { $('#chosen-' + id).text(this.files[0].name); }); - // control calculate button in footer + + //save forms on change + $('.calc_form').on( "change", function() { + var triggeredForm = $(this); + var formId = triggeredForm.attr('id'); + var index = Number(formId.split("_").pop()); + + $.ajax({ + url: `/runs/${run_name}/update_form`, // The URL for the Django view + type: 'POST', + headers: { + 'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val() // CSRF token for security + }, + data: $(this).serialize(), + success: function(response) { + for (let i=0; i - + {# TODO 129 Better buttons for analysis and importing #} @@ -124,7 +127,7 @@

{{ display_name }}

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

{{ display_name }}

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

{{ display_name }}

{% else %}
{% if step != "plot" %} - {% csrf_token %}
@@ -191,7 +194,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 b76528b8a..cf2973fe5 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, name, finished, selected, possible_steps{id, name, methods{id, name, description}}, steps{id, name, finished, selected, index, method_name}} #} +{% 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 %}