Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jump between steps #561

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion protzilla/disk_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions protzilla/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions protzilla/run_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion protzilla/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
76 changes: 53 additions & 23 deletions protzilla/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {}
Expand All @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
12 changes: 6 additions & 6 deletions tests/ui/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -116,32 +116,32 @@ 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"",
"feature_orientation": "Columns (samples in rows, features in columns)",
}
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",
"feature_orientation": "Columns (samples in rows, features in columns)",
}
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",
"feature_orientation": "Columns (samples in rows, features in columns)",
}
run_standard.step_calculate(parameters)

assert run_standard.current_step.finished
assert run_standard.current_step.calculation_status != "incomplete"
10 changes: 9 additions & 1 deletion ui/runs/forms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
30 changes: 27 additions & 3 deletions ui/runs/static/runs/runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<response.count; i++) {
console.log("gg")
$(`#calculationIcon_${index+i} img`).attr('src', `${staticUrl}${response.status}_icon.svg`);
}

}
});
});
// control calculate button in footer
var calculateButton = $('#calculate_parameters_submit');

calculateButton.click(function() {
var form = $("#calculateForm")[0];
var form = $(".calc_form")[0];

if (form.checkValidity()) {
form.submit();
Expand Down Expand Up @@ -117,4 +140,5 @@ $(document).ready(function () {
saveAccordionState(); // Save the current state to sessionStorage
updateAccordionIcons(); // Update icons after state change
});
});

});
13 changes: 8 additions & 5 deletions ui/runs/templates/runs/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
<script type="text/javascript" src="{% static 'js/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'js/plotly-2.18.0.min.js' %}"></script>
<script type="text/javascript" src="{% static 'runs/runs.js' %}"></script>

<script>
var run_name = "{{ run_name }}";
const staticUrl = "{% static 'img/' %}";
</script>
<script src="https://unpkg.com/[email protected]" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script>
{# TODO 129 Better buttons for analysis and importing #}

Expand Down Expand Up @@ -124,7 +127,7 @@ <h3>{{ display_name }}</h3>
{# if there are plot parameters, display method and plot parameters next to each other #}
{% if plot_form %}
<div class="col">
<form id="calculateForm" action="{% url 'runs:detail' run_name %}" method="post"
<form action="{% url 'runs:detail' run_name %}" id="calc_form_{{current_step_index}}" class="calc_form" method="post"
enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-1">
Expand All @@ -141,7 +144,7 @@ <h3>{{ display_name }}</h3>
</form>
</div>
<div class="col">
<form action="{% url 'runs:plot' run_name %}" id="plot_form" method="post"
<form action="{% url 'runs:plot' run_name %}" id="plot_form_{{current_step_index}}" class="form" method="post"
enctype="multipart/form-data">
{% csrf_token %}
<fieldset id="plotfields">
Expand All @@ -158,7 +161,7 @@ <h3>{{ display_name }}</h3>
{% else %}
<div class="col-7">
{% if step != "plot" %}
<form id="calculateForm" action="{% url 'runs:detail' run_name %}" method="post"
<form action="{% url 'runs:detail' run_name %}" id="calc_form_{{current_step_index}}" class="calc_form" method="post"
enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-2">
Expand Down Expand Up @@ -191,7 +194,7 @@ <h3>{{ display_name }}</h3>
</div>
</form>
{% else %}
<form action="{% url 'runs:plot' run_name %}" id="plot_form" method="post"
<form action="{% url 'runs:plot' run_name %}" id="plot_form_{{current_step_index}}" class="form" method="post"
enctype="multipart/form-data">
{% csrf_token %}
{{ method_dropdown }}
Expand Down
Loading
Loading