Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c4e102e
added basic construct of settings page
ronjakrg Nov 24, 2024
8d76417
added selection of sections and forms for plot settings
ronjakrg Nov 25, 2024
c1fd7d9
moved settings file to own folder, added navigation to sidebar and sm…
ronjakrg Nov 28, 2024
d8653e0
merged dev into this branch
ronjakrg Dec 2, 2024
701b0a7
Added footer and returning to last view after saving or canceling
ronjakrg Dec 10, 2024
7e4aedc
small bugfix and refactoring
ronjakrg Dec 10, 2024
49bb332
Fixed alignment
ronjakrg Dec 19, 2024
33fc8ac
enabled saving and loading settings, split settings into sections and…
ronjakrg Jan 3, 2025
9c4dd4d
connected and enhanced plotly template, added small ui changes
ronjakrg Jan 3, 2025
813bcd1
minor changes
ronjakrg Jan 3, 2025
fc14808
moved plot_template to settings folder
ronjakrg Jan 6, 2025
c3f87f7
restructured plot settings html, enabled choosing a custom font
ronjakrg Jan 7, 2025
b52c7cd
included file format from settings in plot download
ronjakrg Jan 7, 2025
85e0129
added plot height and width into plot template
ronjakrg Jan 8, 2025
8acc7d7
added plot settings preview
ronjakrg Jan 16, 2025
c962278
moved js code from <script> into own js file
ronjakrg Jan 16, 2025
c49b60a
moved plot preview in column, some formatting
ronjakrg Jan 21, 2025
06cade2
adjusted spacing, added convertion from mm to px
ronjakrg Jan 21, 2025
451ce4e
fixed behaviour of unsaved changes modal
ronjakrg Jan 24, 2025
893a935
added default yamls as fallback templates
ronjakrg Jan 27, 2025
7697fe1
removed plot.yaml
ronjakrg Jan 27, 2025
a42961f
adjusted gitignore for ignoring individual settings
ronjakrg Jan 27, 2025
0ffe07b
small bugfix
ronjakrg Jan 28, 2025
1fd2d8c
added check if list of exported plots is empty
ronjakrg Feb 1, 2025
6f0aeaa
enhanced plot download by including scaling to experienced size, adde…
ronjakrg Feb 1, 2025
da9e056
fixed broken tiff & eps download with plotly figures
ronjakrg Feb 1, 2025
33ccabe
removed general section, added temporary databases section
ronjakrg Feb 1, 2025
7ffbf4c
added font scaling to displayed plots
ronjakrg Feb 1, 2025
27d4c46
added requested changes
ronjakrg Feb 9, 2025
b4c837a
added tests
ronjakrg Feb 9, 2025
42aac62
attempt to fix tests that were successful locally
ronjakrg Feb 9, 2025
f905a24
removed unused imports
ronjakrg Feb 9, 2025
cc450ca
added requested changes
ronjakrg Feb 11, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
user_data/runs/*
user_data/workflows/*
user_data/external_data/*
user_data/settings/*
!user_data/settings/plots_default.yaml
!user_data/settings/databases_default.yaml
user_data/debug/*
ui/static/admin/*
ui/uploads/*
Expand Down
1 change: 1 addition & 0 deletions protzilla/constants/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
USER_DATA_PATH = Path(PROJECT_PATH, "user_data")
RUNS_PATH = USER_DATA_PATH / "runs"
WORKFLOWS_PATH = USER_DATA_PATH / "workflows"
SETTINGS_PATH = USER_DATA_PATH / "settings"
EXTERNAL_DATA_PATH = Path(PROJECT_PATH, "user_data/external_data")
WORKFLOW_META_PATH = Path(PROJECT_PATH, "protzilla/constants/workflow_meta.json")
UI_PATH = Path(PROJECT_PATH, "ui")
Expand Down
1 change: 0 additions & 1 deletion protzilla/data_preprocessing/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from protzilla.data_preprocessing.plots_helper import generate_tics
from protzilla.utilities import default_intensity_column
from protzilla.utilities.plot_template import *
from protzilla.constants.colors import *

def create_pie_plot(
Expand Down
41 changes: 28 additions & 13 deletions protzilla/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from pathlib import Path

import pandas as pd
import plotly
import plotly.io as pio
import plotly.graph_objects as go
from PIL import Image

from protzilla.utilities import format_trace
Expand Down Expand Up @@ -277,33 +278,47 @@ def __repr__(self):
def empty(self) -> bool:
return len(self.plots) == 0

def export(self, format_):
def export(self, settings: dict) -> list:
"""
Converts all plots from this step to files according to the format and size in the Plotly template.
An exported plot is represented as BytesIO object containing binary image data.
:param settings: Dict containing the plot settings.
:return: List of all exported plots.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vielleicht noch hinzufügen was genau ein exportierter Plot ist, also ob es die raw files sind oder metadaten als Plot objekt oder so?

"""
from ui.settings.plot_template import get_scale_factor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ist es absichtlich, dass das erst hier importiert wird?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja, tatsächlich Absicht. Wenn ich das oben importiert habe, kam dieser Error (hier nur ein Ausschnitt):

File "/home/ronja/git/PROTzilla2/ui/runs/urls.py", line 3, in <module>
    from . import views
  File "/home/ronja/git/PROTzilla2/ui/runs/views.py", line 22, in <module>
    from protzilla.run import Run, get_available_run_names 
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ronja/git/PROTzilla2/ui/../protzilla/run.py", line 8, in <module>
    from protzilla.steps import Messages, Output, Plots, Step
  File "/home/ronja/git/PROTzilla2/ui/../protzilla/steps.py", line 17, in <module>
    from ui.settings.plot_template import get_scale_factor
  File "/home/ronja/git/PROTzilla2/ui/../ui/settings/plot_template.py", line 5, in <module>
    from protzilla.disk_operator import YamlOperator
  File "/home/ronja/git/PROTzilla2/ui/../protzilla/disk_operator.py", line 14, in <module>
    from protzilla.steps import Messages, Output, Plots, Step, StepManager
ImportError: cannot import name 'Messages' from partially initialized module 'protzilla.steps' (most likely due to a circular import) (/home/ronja/git/PROTzilla2/ui/../protzilla/steps.py)

Also steps.py möchte Messages, Output, etc. und get_scale_factor aus dem plot_template.py importieren. Dort wird der YamlOperator aus disk_operator.py importiert, der wiederum möchte unter anderem Step aus steps.py importieren, wo dann wieder Messages, Output, etc. importiert werden soll und da fängt das ganze wieder von vorne an ... Also vermeidet das Importieren an dieser Stelle diesen circular import, weil es erst geladen wird, wenn es benötigt wird. :)

Ich fand diesen Workaround auch etwas befremdlich, habe es aber hier gefunden:

It is sometimes necessary to move imports to a function or class to avoid problems with circular imports. Gordon McMillan says:

Circular imports are fine where both modules use the “import ” form of import. They fail when the 2nd module wants to grab a name out of the first (“from module import name”) and the import is at the top level. That’s because names in the 1st are not yet available, because the first module is busy importing the 2nd.

In this case, if the second module is only used in one function, then the import can easily be moved into that function. By the time the import is called, the first module will have finished initializing, and the second module can do its import.

(Sorry für die lange Antwort, aber finde es irgendwie total nice, wie wir in unseren Teilaufgaben so random stuff lernen - und vielleicht ist das ja für noch wen interessant. ^^)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oki danke für die Erklärung!! Ja, das habe ich auch erst mit Protzilla gelernt, dass man das tatsächlich so macht :D

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also ich fand die lange Antwort interessant :D

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ach, du liest hier auch mit? 👋 Das freut mich zu hören haha (:

exports = []
format_ = settings["file_format"]

for plot in self.plots:
if isinstance(plot, plotly.graph_objs.Figure):
if format_ in ["eps", "tiff"]:
png_binary = plotly.io.to_image(plot, format="png", scale=4)
img = Image.open(BytesIO(png_binary)).convert("RGB")
scale_factor = get_scale_factor(plot, settings)
# For Plotly GO Figure
if isinstance(plot, go.Figure):
if format_ in ["tiff", "eps"]:
binary_png = pio.to_image(plot, format="png", scale=scale_factor)
img = Image.open(BytesIO(binary_png)).convert("RGB")
binary = BytesIO()
if format_ == "tiff":
img.save(binary, format="tiff", compression="tiff_lzw")
else:
elif format_ == "eps":
img.save(binary, format=format_)
binary.seek(0)
exports.append(binary)
else:
binary_string = plotly.io.to_image(plot, format=format_, scale=4)
exports.append(BytesIO(binary_string))
binary_png = pio.to_image(plot, format=format_, scale=scale_factor)
exports.append(BytesIO(binary_png))
elif isinstance(plot, dict) and "plot_base64" in plot:
plot = plot["plot_base64"]

if isinstance(plot, bytes): # base64 encoded plots
if format_ in ["eps", "tiff"]:
# TO DO: Include scale_factor here
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ist das ToDo noch aktuell?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ich habe aktuell keinen Überblick, wo wir diese andere Art von Plots verwenden und wäre stark dafür, sie einfach rauszunehmen und stattdessen einheitlich Plotly verwenden. Es existiert bereits das Issue "Use Plotly Template in all Sections #549", kann aber gerne noch einen Hinweis auf dieses To Do dort einfügen.

# For base64 encoded plot
if isinstance(plot, bytes):
if format_ in ["tiff", "eps"]:
img = Image.open(BytesIO(base64.b64decode(plot))).convert("RGB")
binary = BytesIO()
if format_ == "tiff":
img.save(binary, format="tiff", compression="tiff_lzw")
else:
img.save(binary, format=format_)
elif format_ == "eps":
img.save(binary, format="eps")
binary.seek(0)
exports.append(binary)
elif format_ in ["png", "jpg"]:
Expand Down
34 changes: 0 additions & 34 deletions protzilla/utilities/plot_template.py

This file was deleted.

45 changes: 45 additions & 0 deletions protzilla/utilities/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import pandas as pd
import psutil

from django.http import QueryDict

# recipie from https://docs.python.org/3/library/itertools.html
def unique_justseen(iterable, key=None):
Expand Down Expand Up @@ -136,3 +137,47 @@ def get_file_name_from_upload_path(upload_path: str) -> str:
base_name = file_name_randomized.split("_")[0]
file_extension = file_name_randomized.split(".")[-1]
return f"{base_name}.{file_extension}"


def parameters_from_post(post: QueryDict) -> dict:
"""
Removes token from dict and converts the remaining entries into suitable data formats.
:param post: Django dict containing POST data.
:return: Dict containing the parameters in suitable formats.
"""
d = dict(post)
if "csrfmiddlewaretoken" in d:
del d["csrfmiddlewaretoken"]
parameters = {}
for k, v in d.items():
if len(v) > 1:
# only used for named_output parameters and multiselect fields
parameters[k] = v
else:
parameters[k] = convert_str_if_possible(v[0])
return parameters


def convert_str_if_possible(s):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hier gerne auch doc-string und return type :)

Copy link
Collaborator Author

@ronjakrg ronjakrg Feb 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for parameters_from_post, aber ergänze ich gern! :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Danke, dass du es trotzdem ergänzt hast ^^

"""
Converts an input value into suitable representation as a string.
:param s: Input value.
:return: Converted input value.
"""
try:
f = float(s)
return int(f) if int(f) == f else f
except ValueError:
if s == "checked":
# s is a checkbox
return True
if re.fullmatch(r"\d+(\.\d+)?(\|\d+(\.\d+)?)*", s):
# s is a multi-numeric input e.g. 1-0.12-5
numbers_str = re.findall(r"\d+(?:\.\d+)?", s)
numbers = []
for num in numbers_str:
num = float(num)
num = int(num) if int(num) == num else num
numbers.append(num)
return numbers
return s
41 changes: 41 additions & 0 deletions tests/ui/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pytest
import plotly.graph_objects as go

from protzilla.constants.colors import PLOT_PRIMARY_COLOR, PLOT_SECONDARY_COLOR
from protzilla.constants.paths import SETTINGS_PATH
from ui.settings.plot_template import (
determine_font,
resize_for_display,
get_scale_factor
)


@pytest.fixture
def sample_params():
return {
"section_id": "plots",
"file_format": "png",
"width": 85,
"height": 60,
"custom_font": "Ubuntu Mono",
"font": "Arial",
"heading_size": 11,
"text_size": 8
}

def test_determine_font(sample_params):
assert determine_font(sample_params) == "Arial"
sample_params["font"] = "Custom"
assert determine_font(sample_params) == "Ubuntu Mono"

def test_resize_for_display(sample_params):
result = resize_for_display(sample_params)
assert result["display_width"] == 600 # SCALED_WIDTH
assert result["display_heading_size"] == 27

def test_get_scale_factor(sample_params):
fig = go.Figure()
fig.update_layout(width=600)
scale = get_scale_factor(fig, sample_params)
assert isinstance(scale, float)
assert round(scale, 3) == 1.673
6 changes: 5 additions & 1 deletion ui/main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"runs",
"settings",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -127,7 +128,10 @@

STATIC_URL = "static/"

STATICFILES_DIRS = [BASE_DIR / "static"]
STATICFILES_DIRS = [
BASE_DIR / "static",
BASE_DIR / "settings/static"
]

# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
Expand Down
1 change: 1 addition & 0 deletions ui/main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
urlpatterns = [
path("", views.index),
path("runs/", include("runs.urls")),
path("settings/", include("settings.urls")),
path("databases", views.databases, name="databases"),
path("databases/upload", views.database_upload, name="database_upload"),
path("databases/delete", views.database_delete, name="database_delete"),
Expand Down
1 change: 1 addition & 0 deletions ui/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def index(request):


def databases(request):
request.session['last_view'] = "databases"
databases = uniprot_databases()
df_infos = {}
if database_metadata_path.exists():
Expand Down
6 changes: 6 additions & 0 deletions ui/runs/static/runs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,10 @@ html, body {
padding: 10px 8%;
margin-top: 10px;
background-color: rgb(255, 255, 255);
}
.plot-wrapper {
width: fit-content;
height: fit-content;
border: 1px solid #e8edf3;
border-radius: .375rem;
}
22 changes: 10 additions & 12 deletions ui/runs/templates/runs/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ <h3>{{ display_name }}</h3>
{% if current_plots %}
<div class="mt-4" id="plots">
{% for plot in current_plots %}
<div class="plot-wrapper">
<div class="plot-wrapper mb-2">
{{ plot|safe }}
</div>
{% endfor %}
Expand All @@ -225,15 +225,13 @@ <h3>{{ display_name }}</h3>
<div class="d-flex col">
{% if current_plots %}
<form action="{% url 'runs:download_plots' run_name %}">
<select class="form-select mb-2" name="format">
<option value="png" selected="selected">png</option>
<option value="tiff">tiff</option>
<option value="svg">svg</option>
<option value="pdf">pdf</option>
<option value="eps">eps</option>
<option value="jpg">jpg</option>
</select>
<button class="btn btn-grey">Download plots</button>
<button class="btn btn-grey d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-2" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
</svg>
Download plots
</button>
</form>
{% endif %}
</div>
Expand Down Expand Up @@ -278,11 +276,11 @@ <h5 class="modal-title" id="calculationInProgressModalLabel">Do you want back?</
Do you want to proceed?
<hr>
<a href="{% url 'runs:back' run_name %}" id = "backButton" class="btn btn-red mr-auto">Back</a>
<button id= "cancel" type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button id="cancel" type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if not last_step %}
<button class="btn btn-red" {% if not results_exist %} disabled {% endif %} >Next</button>
Expand Down
17 changes: 14 additions & 3 deletions ui/runs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
format_trace,
get_memory_usage,
name_to_title,
parameters_from_post,
)
from protzilla.workflow import get_available_workflow_names
from protzilla.constants.paths import WORKFLOWS_PATH
Expand All @@ -38,13 +39,14 @@
make_name_field,
make_sidebar,
)
from ui.runs.views_helper import display_message, display_messages, parameters_from_post
from ui.runs.views_helper import display_message, display_messages

from .form_mapping import (
get_empty_plot_form_by_method,
get_filled_form_by_method,
get_filled_form_by_request,
)
from ui.settings.views import load_settings

active_runs: dict[str, Run] = {}

Expand All @@ -68,6 +70,9 @@ def detail(request: HttpRequest, run_name: str):
active_runs[run_name] = Run(run_name)
run: Run = active_runs[run_name]

request.session['last_view'] = "runs:detail"
request.session['run_name'] = run_name

# section, step, method = run.current_run_location()
# end_of_run = not step

Expand Down Expand Up @@ -173,6 +178,9 @@ def index(request: HttpRequest, index_error: bool = False):
:return: the rendered index page
:rtype: HttpResponse
"""

request.session['last_view'] = "runs:index"

return render(
request,
"runs/index.html",
Expand Down Expand Up @@ -447,11 +455,14 @@ def download_plots(request: HttpRequest, run_name: str):
if run_name not in active_runs:
active_runs[run_name] = Run(run_name)
run = active_runs[run_name]
format_ = request.GET["format"]
settings = load_settings("plots")
format_ = settings["file_format"]
index = run.steps.current_step_index
section = run.current_step.section
operation = run.current_step.operation
exported = run.current_plots.export(format_=format_)
exported = run.current_plots.export(settings)
if len(exported) == 0:
raise RuntimeError("List of exported plots is empty.")
if len(exported) == 1:
filename = f"{index}-{section}-{operation}.{format_}"
return FileResponse(exported[0], filename=filename, as_attachment=True)
Expand Down
Loading
Loading