Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions protzilla/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ def current_plots(self) -> Plots | None:
@property
def current_outputs(self) -> Output:
return self.steps.current_step.output

@property
def current_filtered_data(self) -> Output:
return self.steps.current_step.datatable_filtered_data

@property
def current_step(self) -> Step | None:
Expand Down
1 change: 1 addition & 0 deletions protzilla/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(self, instance_identifier: str | None = None):
self.inputs: dict = {}
self.messages: Messages = Messages([])
self.output: Output = Output()
self.datatable_filtered_data: dict = {}
self.plots: Plots = Plots()
self.instance_identifier = instance_identifier

Expand Down
83 changes: 39 additions & 44 deletions ui/runs/templates/runs/tables.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,65 @@
{% block css %}
<link rel="stylesheet" type="text/css" href="{% static 'css/datatables-1.13.4.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'runs/style.css' %}">

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" >
{% endblock %}

{% block js %}
<script type="text/javascript" src="{% static 'js/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'js/datatables-1.13.4.min.js' %}"></script>

<script type="text/javascript" src="{% static 'js/datatables-controls.js' %}"></script>
<script>
$(document).ready(function () {
$.ajax({
url: "{% url 'runs:tables_content' run_name index key %}",
data: "{{ clean_ids }}",
type: "GET",
success: function (response) {
let tr = $("<tr>");
response.columns.forEach(element => {
tr.append($("<th>").text(element))
});
$("#datatable").html($("<thead>").append(tr))
.DataTable({
data: response.data,
columnDefs: [{
targets: "_all",
render: function (data, type, row) {
if (type !== 'display') {
return data;
}
if (typeof data === 'string' && data.startsWith("https://")) {
return data.split(' ').map(url => `<a href="${url}" target="_blank">${url.split('/').pop()}</a>`).join(' ')
}
if (typeof data === 'number' && (!Number.isInteger(data) || data >= 1e4)) {
return data.toPrecision(4);
}
return `<div style="word-break:break-all">${data}</div>`;
},
}],
});
}
});
$('#tables_dropdown').on("change", function () {
window.location.href = $(this).val()
})
});
const RUNS_TABLES_CONTENT_URL = "{% url 'runs:tables_content' run_name index key %}";
const CLEAN_IDS = "{{ clean_ids }}";
</script>
{% endblock %}

{% block content %}
<div class="p-3">
<p>
Index {{ index|add:1 }}, Section {{ section }}, step {{ step }}, method {{ method }}
<br>
</p>

<div class="d-flex justify-content-between">
<div class="input-group mb-3 w-50">
<span class="input-group-text">Choose table</span>
{% include 'runs/field_select_with_label.html' with key="tables_dropdown" categories=options only %}
</div>
<div>
{% if clean_ids %}
<a class="mb-3 btn btn-grey" href="{% url 'runs:tables' run_name index key %}">Enable Isoforms</a>
{% else %}
<a class="mb-3 btn btn-grey" href="?clean-ids">Disable Isoforms</a>
{% endif %}
<a class="mb-3 btn btn-grey" href="{% url 'runs:download_table' run_name index key %}">Download Table</a>
{% if clean_ids %}
<a class="mb-3 btn btn-grey" href="{% url 'runs:tables' run_name index key %}">Enable Isoforms</a>
{% else %}
<a class="mb-3 btn btn-grey" href="?clean-ids">Disable Isoforms</a>
{% endif %}
<a class="mb-3 btn btn-grey" href="{% url 'runs:download_table' run_name index key %}">Download Table</a>
</div>
</div>

<div class="input-group mb-3 w-50">
<input type="text" id="searchInput" class="form-control" placeholder="Type search query here..." >
<button class="btn btn-grey ms-1" id="search-btn">Search</button>
</div>

<div style="overflow-x: auto;">
<table id="datatable" class="display"></table>
</div>

<div class="d-flex justify-content-between">
<div class="d-flex justify-content-between">
<div class="input-group mt-3 w-auto">
<span class="input-group-text">Rows per page</span>
<select id="rowsPerPage" class="form-select form-select-sm">
<option value="10" selected>10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div id="page-info" class="mt-3 ms-3 d-flex align-items-center"></div>
</div>
<div id="pagination" class="pagination-buttons d-flex justify-content-end mt-3"></div>
</div>
<table id="datatable" class="display"></table>
</div>
</div>
{% endblock %}
67 changes: 54 additions & 13 deletions ui/runs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.shortcuts import render
from django.urls import reverse
from django.conf import settings
from django.http import JsonResponse

from protzilla.run import Run, get_available_run_names
from protzilla.run_v2 import delete_run_folder
Expand All @@ -29,6 +30,8 @@
format_trace,
get_memory_usage,
name_to_title,
clean_uniprot_id,
unique_justseen
)
from protzilla.workflow import get_available_workflow_names
from protzilla.constants.paths import WORKFLOWS_PATH
Expand All @@ -38,7 +41,7 @@
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, parameters_from_post, get_filtered_data, set_filtered_data

from .form_mapping import (
get_empty_plot_form_by_method,
Expand Down Expand Up @@ -515,29 +518,67 @@ def navigate(request, run_name: str):
run.step_goto(index, section_name)
return HttpResponseRedirect(reverse("runs:detail", args=(run_name,)))


def tables_content(request, run_name, index, key):
"""
Handles the content of a table during a run, including filtering, searching, sorting, and pagination.

:param request: the request object
:param run_name: the name of the run
:param index: the index of the current step
:param key: the key of the datatable

:return: a JSON response containing the table data
"""

if run_name not in active_runs:
active_runs[run_name] = Run(run_name)
run = active_runs[run_name]
# TODO this will change with df_mode implementation
if index < len(run.steps.previous_steps):
outputs = run.steps.previous_steps[index].output[key]
else:
outputs = run.current_outputs[key]
out = outputs.replace(np.nan, None)

filtered_data = get_filtered_data(run, index, key)

if request.GET.get("is_new_search", "false").lower() == "true":
filtered_data = get_filtered_data(run, index, key, reset=True)
search_query = request.GET.get("search_query", "").lower()
if search_query:
mask = filtered_data .astype(str).stack().str.contains(search_query, case=False, na=False).unstack()
filtered_data = filtered_data [mask.any(axis=1)]
set_filtered_data(run, index, key, filtered_data )

if request.GET.get("is_new_sorting", "false").lower() == "true":
sorting_column_idx = int(request.GET.get("sorting_column_index", "0").lower())
is_sort_ascending = request.GET.get("is_sort_ascending", "true").lower() == "true"
column_name = filtered_data .columns[sorting_column_idx]
filtered_data = filtered_data .sort_values(by=column_name, ascending=is_sort_ascending)
set_filtered_data(run, index, key, filtered_data )

if "clean-ids" in request.GET:
for column in out.columns:
for column in filtered_data .columns:
if "protein" in column.lower():
out[column] = out[column].map(
filtered_data [column] = filtered_data [column].map(
lambda group: ";".join(
unique_justseen(map(clean_uniprot_id, group.split(";")))
)
)
return JsonResponse(
dict(columns=out.to_dict("split")["columns"], data=out.to_dict("split")["data"])
)

current_page = int(request.GET.get("current_page", 1))
rows_per_page = int(request.GET.get("rows_per_page", 10))

total_items = len(filtered_data )
total_pages = (total_items + rows_per_page - 1) // rows_per_page
start_idy = (current_page - 1) * rows_per_page
end_idy = start_idy + rows_per_page
paginated_data = filtered_data .iloc[start_idy:end_idy]

response_data = {
"columns": paginated_data.to_dict("split")["columns"],
"data": paginated_data.to_dict("split")["data"],
"page": current_page,
"total_pages": total_pages,
"total_items": total_items,
"start_item": start_idy + 1,
"end_item": min(end_idy, total_items)
}
return JsonResponse(response_data)


def change_method(request, run_name):
Expand Down
29 changes: 29 additions & 0 deletions ui/runs/views_helper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from django.contrib import messages
import numpy as np

import ui.runs.form_mapping as form_map
from protzilla.steps import StepManager
Expand Down Expand Up @@ -149,3 +150,31 @@ def clear_messages(request):
for message in messages.get_messages(request):
pass
storage.used = True


def get_filtered_data(run, index, key, reset=False):
if index < len(run.steps.previous_steps):
if key not in run.steps.previous_steps[index].datatable_filtered_output or reset:
outputs = run.steps.previous_steps[index].output[key]
filtered_data = outputs.copy()
filtered_data = filtered_data.replace(np.nan, None)
run.steps.previous_steps[index].datatable_filtered_output[key] = filtered_data
else:
filtered_data = run.steps.previous_steps[index].datatable_filtered_output[key]

else:
if key not in run.current_filtered_data or reset:
outputs = run.current_outputs[key]
filtered_data = outputs.copy()
filtered_data = filtered_data.replace(np.nan, None)
run.current_filtered_data[key] = filtered_data
else:
filtered_data = run.current_filtered_data[key]

return filtered_data

def set_filtered_data(run, index, key, filtered_data):
if index < len(run.steps.previous_steps):
run.steps.previous_steps[index].datatable_filtered_output[key] = filtered_data
else:
run.current_filtered_data[key] = filtered_data
Loading
Loading