labnotes 0.0.5__tar.gz → 0.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labnotes-0.0.5 → labnotes-0.0.7}/PKG-INFO +1 -1
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/create_app.py +4 -1
- labnotes-0.0.7/src/labnotes/list_runs.py +29 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/load_experiment.py +12 -3
- labnotes-0.0.7/src/labnotes/models.py +9 -0
- labnotes-0.0.7/src/labnotes/parse_run_view.py +20 -0
- labnotes-0.0.7/src/labnotes/sort_href.py +13 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/static/labnotes.css +65 -4
- labnotes-0.0.7/src/labnotes/static/row-link.js +9 -0
- labnotes-0.0.7/src/labnotes/static/view-toggle.js +15 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/templates/experiment.html +22 -4
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/templates/index.html +10 -2
- labnotes-0.0.5/src/labnotes/list_runs.py +0 -12
- {labnotes-0.0.5 → labnotes-0.0.7}/.gitignore +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/CHANGELOG.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/MIGRATIONS.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/README.md +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/pyproject.toml +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/dump_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/open_experiments.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/open_runs.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/read_design.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/read_note.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/study_progress.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/summarize_design.py +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.5 → labnotes-0.0.7}/uv.lock +0 -0
|
@@ -52,7 +52,10 @@ def create_app(experiments_folder: Path, data_folder: Path) -> Starlette:
|
|
|
52
52
|
async def experiment(request: Request) -> Response:
|
|
53
53
|
state = request.app.state
|
|
54
54
|
loaded = await load_experiment(
|
|
55
|
-
state.experiments,
|
|
55
|
+
state.experiments,
|
|
56
|
+
state.runs,
|
|
57
|
+
request.path_params["name"],
|
|
58
|
+
request.query_params,
|
|
56
59
|
)
|
|
57
60
|
if loaded is None:
|
|
58
61
|
raise HTTPException(404, "No such experiment")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dirsql import DirSQL
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
from labnotes.sql_quote import quote_identifier, quote_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def list_runs(
|
|
8
|
+
runs: DirSQL, study: str, completed_run: str, view: RunView
|
|
9
|
+
) -> list[dict[str, object]]:
|
|
10
|
+
where = [f"study = {quote_string(study)}"]
|
|
11
|
+
if view.status:
|
|
12
|
+
negate = {"complete": "", "incomplete": "NOT "}[view.status]
|
|
13
|
+
where.append(f"{negate}({completed_run})")
|
|
14
|
+
where += [
|
|
15
|
+
f"instr(lower(CAST({quote_identifier(column)} AS TEXT)),"
|
|
16
|
+
f" lower({quote_string(value)})) > 0"
|
|
17
|
+
for column, value in view.filters.items()
|
|
18
|
+
]
|
|
19
|
+
direction = "DESC" if view.descending else "ASC"
|
|
20
|
+
if view.sort == "status":
|
|
21
|
+
order = f"labnotes_completed {direction}"
|
|
22
|
+
elif view.sort:
|
|
23
|
+
order = f"{quote_identifier(view.sort)} {direction}"
|
|
24
|
+
else:
|
|
25
|
+
order = "2 DESC"
|
|
26
|
+
return await runs.query(
|
|
27
|
+
f"SELECT ({completed_run}) AS labnotes_completed, * FROM runs"
|
|
28
|
+
f" WHERE {' AND '.join(where)} ORDER BY {order}"
|
|
29
|
+
)
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from collections.abc import Mapping
|
|
2
3
|
from typing import cast
|
|
3
4
|
|
|
4
5
|
from dirsql import DirSQL
|
|
5
6
|
|
|
6
7
|
from labnotes.list_runs import list_runs
|
|
8
|
+
from labnotes.parse_run_view import parse_run_view
|
|
7
9
|
from labnotes.render_markdown import render_markdown
|
|
8
10
|
from labnotes.run_columns import run_columns
|
|
11
|
+
from labnotes.sort_href import sort_href
|
|
9
12
|
from labnotes.sql_quote import quote_string
|
|
10
13
|
from labnotes.study_progress import study_progress
|
|
11
14
|
from labnotes.summarize_design import summarize_design
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
async def load_experiment(
|
|
15
|
-
experiments: DirSQL, runs: DirSQL, name: str
|
|
18
|
+
experiments: DirSQL, runs: DirSQL, name: str, params: Mapping[str, str]
|
|
16
19
|
) -> dict[str, object] | None:
|
|
17
20
|
found = await experiments.query(
|
|
18
21
|
f"SELECT name, study, design FROM experiments WHERE name = {quote_string(name)}"
|
|
@@ -29,10 +32,12 @@ async def load_experiment(
|
|
|
29
32
|
study = None if row["study"] is None else str(row["study"])
|
|
30
33
|
columns = await run_columns(runs)
|
|
31
34
|
progress = await study_progress(runs, study, summary, columns)
|
|
35
|
+
shown = columns[:1] + [key for key in varies if key in columns[1:]]
|
|
36
|
+
view = parse_run_view(params, shown)
|
|
32
37
|
listed = (
|
|
33
38
|
[]
|
|
34
39
|
if study is None
|
|
35
|
-
else await list_runs(runs, study, str(summary["completed_run"]))
|
|
40
|
+
else await list_runs(runs, study, str(summary["completed_run"]), view)
|
|
36
41
|
)
|
|
37
42
|
return {
|
|
38
43
|
"name": row["name"],
|
|
@@ -44,5 +49,9 @@ async def load_experiment(
|
|
|
44
49
|
for note in notes
|
|
45
50
|
],
|
|
46
51
|
"runs_list": listed,
|
|
47
|
-
"run_columns":
|
|
52
|
+
"run_columns": shown,
|
|
53
|
+
"run_view": view,
|
|
54
|
+
"sort_hrefs": {
|
|
55
|
+
column: sort_href(view, column) for column in ["status", *shown]
|
|
56
|
+
},
|
|
48
57
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
|
|
5
|
+
STATUSES = {"complete", "incomplete"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_run_view(params: Mapping[str, str], columns: list[str]) -> RunView:
|
|
9
|
+
sort = params.get("sort")
|
|
10
|
+
status = params.get("status")
|
|
11
|
+
return RunView(
|
|
12
|
+
sort=sort if sort in columns or sort == "status" else None,
|
|
13
|
+
descending=params.get("order") == "desc",
|
|
14
|
+
status=status if status in STATUSES else None,
|
|
15
|
+
filters={
|
|
16
|
+
column: value
|
|
17
|
+
for column in columns
|
|
18
|
+
if (value := params.get(f"f.{column}", "").strip())
|
|
19
|
+
},
|
|
20
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
from labnotes.models import RunView
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sort_href(view: RunView, column: str) -> str:
|
|
7
|
+
params = {"sort": column}
|
|
8
|
+
if view.sort == column and not view.descending:
|
|
9
|
+
params["order"] = "desc"
|
|
10
|
+
if view.status:
|
|
11
|
+
params["status"] = view.status
|
|
12
|
+
params.update({f"f.{name}": value for name, value in view.filters.items()})
|
|
13
|
+
return f"?{urlencode(params)}#runs"
|
|
@@ -163,9 +163,8 @@ h3 {
|
|
|
163
163
|
}
|
|
164
164
|
.ledger td { padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--rule); vertical-align: middle; }
|
|
165
165
|
.ledger tbody tr:last-child td { border-bottom: 0; }
|
|
166
|
-
.ledger tbody tr {
|
|
166
|
+
.ledger tbody tr { cursor: pointer; }
|
|
167
167
|
.ledger tbody tr:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
168
|
-
.ledger .title-link::after { content: ""; position: absolute; inset: 0; }
|
|
169
168
|
.ledger .num { text-align: right; }
|
|
170
169
|
.ledger th.num { text-align: right; }
|
|
171
170
|
|
|
@@ -291,14 +290,14 @@ h3 {
|
|
|
291
290
|
|
|
292
291
|
/* Runs */
|
|
293
292
|
.runs {
|
|
294
|
-
--row:
|
|
293
|
+
--row: 2.75rem repeat(var(--columns), minmax(0, 1fr));
|
|
295
294
|
background: var(--surface);
|
|
296
295
|
border: 1px solid var(--rule);
|
|
297
296
|
border-radius: 10px;
|
|
298
297
|
overflow: hidden;
|
|
299
298
|
font-size: 0.86rem;
|
|
300
299
|
}
|
|
301
|
-
.run-head, .run summary {
|
|
300
|
+
.run-head, .run summary, .run-filters {
|
|
302
301
|
display: grid;
|
|
303
302
|
grid-template-columns: var(--row);
|
|
304
303
|
gap: 1rem;
|
|
@@ -313,6 +312,25 @@ h3 {
|
|
|
313
312
|
color: var(--ink-3);
|
|
314
313
|
border-bottom: 1px solid var(--rule);
|
|
315
314
|
}
|
|
315
|
+
.run-head .sort { color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
316
|
+
.run-head .sort:hover, .run-head .sorted { color: var(--ink); }
|
|
317
|
+
.run-head .arrow { margin-left: 0.3rem; color: var(--accent); }
|
|
318
|
+
.run-filters { padding-top: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); background: var(--paper); }
|
|
319
|
+
.run-filters input, .run-filters select {
|
|
320
|
+
width: 100%;
|
|
321
|
+
min-width: 0;
|
|
322
|
+
font: inherit;
|
|
323
|
+
font-size: 0.82rem;
|
|
324
|
+
padding: 0.2rem 0.45rem;
|
|
325
|
+
color: var(--ink);
|
|
326
|
+
background: var(--surface);
|
|
327
|
+
border: 1px solid var(--rule);
|
|
328
|
+
border-radius: 5px;
|
|
329
|
+
}
|
|
330
|
+
.run-filters select { padding: 0.2rem 0; }
|
|
331
|
+
.run-filters input:focus, .run-filters select:focus { outline: none; border-color: var(--accent); }
|
|
332
|
+
.run-empty { margin: 0; padding: 1rem 1.25rem; }
|
|
333
|
+
h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; color: var(--ink-3); }
|
|
316
334
|
.run { border-bottom: 1px solid var(--rule); }
|
|
317
335
|
.run:last-child { border-bottom: 0; }
|
|
318
336
|
.run summary { cursor: pointer; list-style: none; }
|
|
@@ -338,6 +356,49 @@ h3 {
|
|
|
338
356
|
.record code { background: none; padding: 0; word-break: break-word; }
|
|
339
357
|
.record pre { margin: 0; white-space: pre-wrap; word-break: break-word; max-height: 22rem; overflow: auto; font-size: 0.78rem; color: var(--ink-2); }
|
|
340
358
|
|
|
359
|
+
/* Index view toggle */
|
|
360
|
+
.view-toggle {
|
|
361
|
+
float: right;
|
|
362
|
+
display: inline-flex;
|
|
363
|
+
margin-top: 0.6rem;
|
|
364
|
+
border: 1px solid var(--rule-strong);
|
|
365
|
+
border-radius: 7px;
|
|
366
|
+
overflow: hidden;
|
|
367
|
+
}
|
|
368
|
+
.view-toggle button {
|
|
369
|
+
font: inherit;
|
|
370
|
+
font-size: 0.82rem;
|
|
371
|
+
padding: 0.3rem 0.8rem;
|
|
372
|
+
border: 0;
|
|
373
|
+
background: var(--surface);
|
|
374
|
+
color: var(--ink-2);
|
|
375
|
+
cursor: pointer;
|
|
376
|
+
}
|
|
377
|
+
.view-toggle button + button { border-left: 1px solid var(--rule-strong); }
|
|
378
|
+
.view-toggle button[aria-pressed="true"] { background: var(--accent-soft); color: var(--ink); }
|
|
379
|
+
|
|
380
|
+
/* Index as cards: the same table, laid out as a grid. */
|
|
381
|
+
.ledger[data-view="cards"] { display: block; background: none; border: 0; border-radius: 0; overflow: visible; }
|
|
382
|
+
.ledger[data-view="cards"] thead { display: none; }
|
|
383
|
+
.ledger[data-view="cards"] tbody {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
|
386
|
+
gap: 1rem;
|
|
387
|
+
}
|
|
388
|
+
.ledger[data-view="cards"] tbody tr {
|
|
389
|
+
display: flex;
|
|
390
|
+
flex-direction: column;
|
|
391
|
+
gap: 0.7rem;
|
|
392
|
+
padding: 1.1rem 1.25rem;
|
|
393
|
+
background: var(--surface);
|
|
394
|
+
border: 1px solid var(--rule);
|
|
395
|
+
border-radius: 10px;
|
|
396
|
+
}
|
|
397
|
+
.ledger[data-view="cards"] tbody tr:hover { background: var(--surface); border-color: var(--accent); }
|
|
398
|
+
.ledger[data-view="cards"] td { display: block; padding: 0; border: 0; text-align: left; }
|
|
399
|
+
.ledger[data-view="cards"] .progress-col { width: auto; }
|
|
400
|
+
.ledger[data-view="cards"] td[data-label]::before { content: attr(data-label) " "; color: var(--ink-3); font-size: 0.82rem; }
|
|
401
|
+
|
|
341
402
|
@media (max-width: 52rem) {
|
|
342
403
|
main, .masthead { padding-left: 1.1rem; padding-right: 1.1rem; }
|
|
343
404
|
h1 { font-size: 2rem; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Safari ignores position: relative on <tr>, so a stretched ::after link covers the whole page.
|
|
2
|
+
for (const row of document.querySelectorAll(".ledger tbody tr")) {
|
|
3
|
+
const link = row.querySelector(".title-link");
|
|
4
|
+
row.addEventListener("click", (event) => {
|
|
5
|
+
if (event.target.closest("a") || getSelection().toString()) return;
|
|
6
|
+
if (event.metaKey || event.ctrlKey) window.open(link.href, "_blank");
|
|
7
|
+
else location.href = link.href;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ledger = document.querySelector(".ledger");
|
|
2
|
+
const buttons = document.querySelectorAll(".view-toggle button");
|
|
3
|
+
|
|
4
|
+
function show(view) {
|
|
5
|
+
ledger.dataset.view = view;
|
|
6
|
+
for (const button of buttons) {
|
|
7
|
+
button.setAttribute("aria-pressed", String(button.dataset.view === view));
|
|
8
|
+
}
|
|
9
|
+
localStorage.setItem("labnotes.view", view);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (const button of buttons) {
|
|
13
|
+
button.addEventListener("click", () => show(button.dataset.view));
|
|
14
|
+
}
|
|
15
|
+
show(localStorage.getItem("labnotes.view") ?? "table");
|
|
@@ -121,13 +121,29 @@
|
|
|
121
121
|
|
|
122
122
|
{% if e.study %}
|
|
123
123
|
<section id="runs" class="block">
|
|
124
|
-
|
|
125
|
-
{%
|
|
124
|
+
{% set view = e.run_view %}
|
|
125
|
+
{% set narrowed = view.status or view.filters %}
|
|
126
|
+
<h2>Runs <span class="count num">{{ e.runs_list | length }}{% if narrowed %} of {{ e.runs }}{% endif %}</span>
|
|
127
|
+
{% if narrowed %}<a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a>{% endif %}</h2>
|
|
128
|
+
{% if e.runs %}
|
|
126
129
|
<div class="runs" style="--columns: {{ e.run_columns | length }}">
|
|
127
130
|
<div class="run-head">
|
|
128
|
-
|
|
129
|
-
{%
|
|
131
|
+
{% for column in ['status'] + e.run_columns %}
|
|
132
|
+
{% set sorted = view.sort == column %}
|
|
133
|
+
<a class="sort{{ ' sorted' if sorted }}" href="{{ e.sort_hrefs[column] }}"{% if sorted %} aria-sort="{{ 'descending' if view.descending else 'ascending' }}"{% endif %} title="Sort by {{ column }}">{{ '' if column == 'status' else column }}{% if sorted %}<span class="arrow">{{ '↓' if view.descending else '↑' }}</span>{% endif %}</a>
|
|
134
|
+
{% endfor %}
|
|
130
135
|
</div>
|
|
136
|
+
<form class="run-filters" method="get" action="#runs">
|
|
137
|
+
{% if view.sort %}<input type="hidden" name="sort" value="{{ view.sort }}">{% endif %}
|
|
138
|
+
{% if view.descending %}<input type="hidden" name="order" value="desc">{% endif %}
|
|
139
|
+
<select name="status" aria-label="Filter by status" onchange="this.form.requestSubmit()">
|
|
140
|
+
<option value=""{{ ' selected' if not view.status }}>all</option>
|
|
141
|
+
<option value="complete"{{ ' selected' if view.status == 'complete' }}>✓</option>
|
|
142
|
+
<option value="incomplete"{{ ' selected' if view.status == 'incomplete' }}>✕</option>
|
|
143
|
+
</select>
|
|
144
|
+
{% for column in e.run_columns %}<input type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}">{% endfor %}
|
|
145
|
+
<button type="submit" hidden>Filter</button>
|
|
146
|
+
</form>
|
|
131
147
|
{% for run in e.runs_list %}
|
|
132
148
|
<details class="run">
|
|
133
149
|
<summary>
|
|
@@ -142,6 +158,8 @@
|
|
|
142
158
|
{% endfor %}
|
|
143
159
|
</dl>
|
|
144
160
|
</details>
|
|
161
|
+
{% else %}
|
|
162
|
+
<p class="empty run-empty">No runs match these filters.</p>
|
|
145
163
|
{% endfor %}
|
|
146
164
|
</div>
|
|
147
165
|
{% else %}
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
{% block crumbs %}<span class="crumb">{{ folder }}</span>{% endblock %}
|
|
4
4
|
{% block main %}
|
|
5
5
|
<section class="intro">
|
|
6
|
+
{% if experiments %}
|
|
7
|
+
<div class="view-toggle" role="group" aria-label="View">
|
|
8
|
+
<button type="button" data-view="table" aria-pressed="true">Table</button>
|
|
9
|
+
<button type="button" data-view="cards" aria-pressed="false">Cards</button>
|
|
10
|
+
</div>
|
|
11
|
+
{% endif %}
|
|
6
12
|
<h1>Experiments</h1>
|
|
7
13
|
<p class="lede">{{ experiments | length }} experiment{{ '' if experiments | length == 1 else 's' }},
|
|
8
14
|
{{ experiments | sum(attribute='runs') }} runs recorded.</p>
|
|
9
15
|
</section>
|
|
10
16
|
|
|
11
17
|
{% if experiments %}
|
|
12
|
-
<table class="ledger">
|
|
18
|
+
<table class="ledger" data-view="table">
|
|
13
19
|
<thead>
|
|
14
20
|
<tr>
|
|
15
21
|
<th scope="col">Experiment</th>
|
|
@@ -29,7 +35,7 @@
|
|
|
29
35
|
{% for key, values in experiment.varies.items() %}<span class="factor">{{ key }} <b>{{ values | length }}</b>{% if not loop.last or experiment.runs_per_cell %}<span class="times">×</span>{% endif %}</span> {% endfor %}
|
|
30
36
|
{% if experiment.runs_per_cell %}<span class="factor">runs <b>{{ experiment.runs_per_cell }}</b></span>{% endif %}
|
|
31
37
|
</td>
|
|
32
|
-
<td class="num">{{ experiment.runs }}</td>
|
|
38
|
+
<td class="num" data-label="Runs">{{ experiment.runs }}</td>
|
|
33
39
|
<td class="progress-col">
|
|
34
40
|
{% if experiment.filled is not none and experiment.target %}
|
|
35
41
|
<div class="progress-line">
|
|
@@ -43,6 +49,8 @@
|
|
|
43
49
|
{% endfor %}
|
|
44
50
|
</tbody>
|
|
45
51
|
</table>
|
|
52
|
+
<script src="{{ url_for('static', path='view-toggle.js') }}" defer></script>
|
|
53
|
+
<script src="{{ url_for('static', path='row-link.js') }}" defer></script>
|
|
46
54
|
{% else %}
|
|
47
55
|
<p class="empty">No experiments found. Each experiment is a folder holding a <code>design.yaml</code> and its markdown notes.</p>
|
|
48
56
|
{% endif %}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from dirsql import DirSQL
|
|
2
|
-
|
|
3
|
-
from labnotes.sql_quote import quote_string
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async def list_runs(
|
|
7
|
-
runs: DirSQL, study: str, completed_run: str
|
|
8
|
-
) -> list[dict[str, object]]:
|
|
9
|
-
return await runs.query(
|
|
10
|
-
f"SELECT ({completed_run}) AS labnotes_completed, * FROM runs"
|
|
11
|
-
f" WHERE study = {quote_string(study)} ORDER BY 2 DESC"
|
|
12
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|