Skip to content

Commit dbc23e8

Browse files
committed
update
1 parent eb9b386 commit dbc23e8

17 files changed

Lines changed: 3258 additions & 325 deletions

about.html

Lines changed: 553 additions & 0 deletions
Large diffs are not rendered by default.

assets/.DS_Store

6 KB
Binary file not shown.

assets/css/main.css

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
@import url(fontawesome-all.min.css);
22
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,400italic,600italic|Roboto+Slab:400,700");
3-
/*
4-
Editorial by HTML5 UP
5-
html5up.net | @ajlkn
6-
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
3+
/*
4+
Editorial by HTML5 UP
5+
html5up.net | @ajlkn
6+
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
77
*/
88
html, body, div, span, applet, object,
99
iframe, h1, h2, h3, h4, h5, h6, p, blockquote,
@@ -1006,6 +1006,19 @@ header.major > :last-child {
10061006
header.main > :last-child {
10071007
margin: 0 0 1em 0; }
10081008

1009+
/* Breadcrumb stile Editorial */
1010+
#header .breadcrumbs { display:flex; align-items:center; }
1011+
#header .breadcrumbs ol {
1012+
list-style:none; margin:0; padding:0;
1013+
display:flex; flex-wrap:wrap; gap:.35rem;
1014+
}
1015+
#header .breadcrumbs li+li::before {
1016+
content:"/"; opacity:.45; margin:0 .25rem;
1017+
}
1018+
#header .breadcrumbs a { text-decoration:none; border-bottom:0; }
1019+
#header .breadcrumbs [aria-current="page"] { font-weight:600; opacity:.9; }
1020+
1021+
10091022
/* Form */
10101023
form {
10111024
margin: 0 0 2em 0; }
@@ -1280,6 +1293,23 @@ ul {
12801293
ul.alt li:first-child {
12811294
border-top: 0;
12821295
padding-top: 0; }
1296+
ul.alt .no-alt {
1297+
/* ripristina una lista normale per la ul annidata */
1298+
list-style: disc;
1299+
padding-left: 1.25em;
1300+
margin: 0.5em 0 0.75em 1.25em; /* margini più compatti nella annidata */
1301+
}
1302+
1303+
ul.alt .no-alt li {
1304+
/* annulla lo stile a righe di ul.alt */
1305+
border-top: 0;
1306+
padding: 0.25em 0;
1307+
}
1308+
1309+
/* (facoltativo) evita troppo spazio sopra la prima voce annidata */
1310+
ul.alt .no-alt li:first-child {
1311+
padding-top: 0;
1312+
}
12831313

12841314
dl {
12851315
margin: 0 0 2em 0; }

assets/js/a.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
(async function () {
2+
const listEl = document.getElementById('thesesList');
3+
const statusEl = document.getElementById('status');
4+
const lastUpdatedEl = document.getElementById('lastUpdated');
5+
const btn = document.getElementById('refreshBtn');
6+
7+
async function loadData({ bustCache = false } = {}) {
8+
try {
9+
statusEl.textContent = 'Caricamento in corso…';
10+
const url = '/data/theses.json' + (bustCache ? `?t=${Date.now()}` : '');
11+
const res = await fetch(url, { cache: 'no-store' });
12+
if (!res.ok) throw new Error('Errore di rete ' + res.status);
13+
const data = await res.json();
14+
render(data);
15+
statusEl.textContent = `Trovate ${data.count ?? data.items?.length ?? 0} tesi.`;
16+
} catch (e) {
17+
statusEl.textContent = 'Impossibile caricare i dati. Riprova.';
18+
console.error(e);
19+
}
20+
}
21+
22+
function render(data) {
23+
listEl.innerHTML = '';
24+
lastUpdatedEl.textContent = data.updated_at ? `Ultimo aggiornamento: ${new Date(data.updated_at).toLocaleString()}` : '';
25+
const items = Array.isArray(data.items) ? data.items : [];
26+
items.forEach(item => {
27+
const li = document.createElement('li');
28+
li.className = 'thesis-item';
29+
const title = item.title || '(titolo non disponibile)';
30+
const a = document.createElement('a');
31+
a.href = item.url || '#';
32+
a.target = '_blank';
33+
a.rel = 'noopener';
34+
a.textContent = title;
35+
const meta = document.createElement('div');
36+
meta.className = 'thesis-meta';
37+
const bits = [];
38+
if (item.author) bits.push(`Autore: ${item.author}`);
39+
if (item.degree) bits.push(`Corso: ${item.degree}`);
40+
if (item.year_or_date) bits.push(`Anno/Data: ${item.year_or_date}`);
41+
meta.textContent = bits.join(' • ');
42+
li.appendChild(a);
43+
if (bits.length) li.appendChild(meta);
44+
listEl.appendChild(li);
45+
});
46+
}
47+
48+
btn.addEventListener('click', () => loadData({ bustCache: true }));
49+
loadData(); // iniziale
50+
})();

assets/js/breadcumbs.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
By Laura Pollacci
3+
*/
4+
5+
/* Per "project site", attivare questa riga (oppure mettere data-basepath sull'<html>):
6+
document.documentElement.dataset.basepath = "/nome-repo/";
7+
*/
8+
9+
(function () {
10+
const base = document.documentElement.dataset.basepath || "/";
11+
12+
// === 1) MAPPA DEL SITO ===
13+
// Path normalizzati: sempre con / iniziale e finale ("/", "/sezione/", "/sezione/pagina/")
14+
const siteMap = {
15+
"/": { title: "Home" },
16+
17+
// ESEMPI:
18+
"/portfolio/": { title: "Pagina principale", parent: "/" },
19+
"/portfolio/app-x/": { title: "Sottopagina", parent: "/portfolio/" },
20+
"/about/": { title: "Chi sono", parent: "/" },
21+
"/contatti/": { title: "Contatti", parent: "/" }
22+
};
23+
24+
// === 2) COSTRUZIONE BREADCRUMB ===
25+
const hostBase = (function () {
26+
// unisce basepath e path assoluto del sito
27+
return function join(path) {
28+
// path è tipo "/qualcosa/"
29+
if (base === "/") return path;
30+
return base.replace(/\/$/, "") + path; // "/repo" + "/x/" => "/repo/x/"
31+
};
32+
})();
33+
34+
function normalizePath(pathname) {
35+
let p = pathname;
36+
if (base !== "/" && p.startsWith(base)) p = p.slice(base.length - 1);
37+
p = p.replace(/index\.html?$/i, "");
38+
if (p === "" || p === "/") return "/";
39+
if (!p.endsWith("/")) p += "/";
40+
if (!p.startsWith("/")) p = "/" + p;
41+
return p;
42+
}
43+
44+
const el = document.getElementById("breadcrumbs");
45+
if (!el) return;
46+
47+
const current = normalizePath(location.pathname);
48+
49+
// Se la pagina non è in siteMap, usa <title> come fallback e mette genitore Home
50+
const startNode = siteMap[current] || { title: (document.title || ""), parent: "/" };
51+
52+
// Risali ai genitori finché esistono
53+
const chain = [];
54+
let node = startNode, cursor = current;
55+
while (node) {
56+
chain.push({ path: cursor, title: node.title });
57+
if (!node.parent) break;
58+
cursor = node.parent;
59+
node = siteMap[cursor];
60+
}
61+
62+
// Assicura che Home sia in testa
63+
if (!chain.some(c => c.path === "/") && siteMap["/"]) {
64+
chain.push({ path: "/", title: siteMap["/"].title });
65+
}
66+
chain.reverse();
67+
68+
// Caso Home: mostra solo "Home"
69+
if (current === "/") {
70+
el.innerHTML = '<ol><li aria-current="page">' + (siteMap["/"]?.title || "Home") + '</li></ol>';
71+
return;
72+
}
73+
74+
// Render HTML
75+
const ol = document.createElement("ol");
76+
chain.forEach((c, i) => {
77+
const li = document.createElement("li");
78+
const last = i === chain.length - 1;
79+
if (last) {
80+
li.textContent = c.title;
81+
li.setAttribute("aria-current", "page");
82+
} else {
83+
const a = document.createElement("a");
84+
a.href = hostBase(c.path);
85+
a.textContent = c.title;
86+
li.appendChild(a);
87+
}
88+
ol.appendChild(li);
89+
});
90+
el.appendChild(ol);
91+
})();

assets/sass/.DS_Store

6 KB
Binary file not shown.

assets/update-theses.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Update Theses List
2+
3+
on:
4+
schedule:
5+
# Ogni lunedì alle 06:00 UTC (le 08:00 circa a Pisa in ora legale)
6+
- cron: "0 6 * * 1"
7+
workflow_dispatch: {} # consente l'avvio manuale dal tab "Actions"
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
scrape:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.11"
23+
24+
- name: Install deps
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install requests beautifulsoup4 lxml
28+
29+
- name: Scrape ETD and build JSON
30+
run: |
31+
python scripts/parse_etd.py
32+
33+
- name: Commit changes
34+
run: |
35+
if [ -n "$(git status --porcelain data/theses.json)" ]; then
36+
git config user.name "github-actions[bot]"
37+
git config user.email "github-actions[bot]@users.noreply.github.com"
38+
git add data/theses.json
39+
git commit -m "chore: update theses.json [skip ci]"
40+
git push
41+
else
42+
echo "No changes."
43+
fi

assets/workflows/update-theses.yml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
'''
4+
Estrae le tesi da archivio UniPi filtrate per relatore e genera:
5+
- assets/theses_pollacci.json
6+
- assets/theses_pollacci.html (frammento HTML)
7+
Pensato per essere eseguito da GitHub Actions.
8+
'''
9+
import json, time, os, sys
10+
from urllib.parse import urljoin, urlencode, urlparse
11+
import requests
12+
from bs4 import BeautifulSoup
13+
14+
BASE = "https://etd.adm.unipi.it/ETD-db/ETD-search/search_by_advisor"
15+
ADVISOR = os.getenv("ETD_ADVISOR", "Pollacci") # cambialo se serve
16+
RATE_DELAY = float(os.getenv("RATE_DELAY", "0.6"))
17+
UA = "thesis-list-updater/1.0 (+contact: youremail@example.com)"
18+
19+
session = requests.Session()
20+
session.headers.update({"User-Agent": UA, "Accept-Language": "it,en;q=0.8"})
21+
22+
def build_url(advisor, extra=None):
23+
qs = {"advisor_name": advisor}
24+
if extra:
25+
qs.update(extra)
26+
return f"{BASE}?{urlencode(qs)}"
27+
28+
def fetch(url):
29+
r = session.get(url, timeout=30)
30+
r.raise_for_status()
31+
return BeautifulSoup(r.text, "html.parser"), r.url
32+
33+
def parse_rows(soup):
34+
"""
35+
Cerca una tabella risultati e ne estrae le righe. È scritto in modo 'resiliente'
36+
rispetto a piccoli cambi di markup.
37+
"""
38+
rows = []
39+
table = soup.find("table")
40+
if not table:
41+
return rows
42+
# salta l'header
43+
for tr in table.select("tr")[1:]:
44+
tds = tr.find_all("td")
45+
if len(tds) < 2:
46+
continue
47+
author = tds[0].get_text(" ", strip=True)
48+
title = tds[1].get_text(" ", strip=True)
49+
degree = tds[2].get_text(" ", strip=True) if len(tds) > 2 else ""
50+
committee = tds[3].get_text(" ", strip=True) if len(tds) > 3 else ""
51+
link = None
52+
a = tr.find("a", href=True)
53+
if a:
54+
link = urljoin(BASE, a["href"])
55+
rows.append({
56+
"author": author,
57+
"title": title,
58+
"degree": degree,
59+
"committee": committee,
60+
"url": link
61+
})
62+
return rows
63+
64+
def find_next(soup):
65+
"""
66+
Trova il link 'successiva/next' o un link di paginazione compatibile.
67+
"""
68+
# 1) Cerca ancore con testo tipico
69+
for a in soup.find_all("a", href=True):
70+
label = a.get_text(" ", strip=True).lower()
71+
if label in {"successiva", "next", ">", "»", ">>"}:
72+
href = a["href"]
73+
if "search_by_advisor" in href:
74+
return urljoin(BASE, href)
75+
# 2) Fallback: qualunque link allo stesso endpoint con parametri di pagina
76+
for a in soup.find_all("a", href=True):
77+
href = a["href"]
78+
if ("search_by_advisor" in href and "advisor_name=" in href
79+
and any(p in href for p in ("offset", "start", "from", "page", "first"))):
80+
return urljoin(BASE, href)
81+
return None
82+
83+
def ensure_dirs():
84+
os.makedirs("assets", exist_ok=True)
85+
86+
def render_html(items):
87+
out = []
88+
out.append("<ul class=\"theses-list\">")
89+
for r in items:
90+
li = f"<li><strong>{r['author']}</strong> — «{r['title']}»"
91+
if r.get("degree"):
92+
li += f" <em>({r['degree']})</em>"
93+
if r.get("url"):
94+
li += f" — <a href=\"{r['url']}\" target=\"_blank\" rel=\"noopener\">scheda</a>"
95+
li += "</li>"
96+
out.append(li)
97+
out.append("</ul>")
98+
return "\n".join(out)
99+
100+
def main():
101+
ensure_dirs()
102+
url = build_url(ADVISOR)
103+
seen = set()
104+
items = []
105+
106+
while url and url not in seen:
107+
seen.add(url)
108+
soup, final_url = fetch(url)
109+
items.extend(parse_rows(soup))
110+
nxt = find_next(soup)
111+
url = nxt
112+
time.sleep(RATE_DELAY)
113+
if not url:
114+
break
115+
116+
# ordina alfabeticamente per autore (puoi cambiare qui il criterio)
117+
items.sort(key=lambda x: (x.get("author","").lower(), x.get("title","").lower()))
118+
119+
with open("assets/theses_pollacci.json", "w", encoding="utf-8") as f:
120+
json.dump(items, f, ensure_ascii=False, indent=2)
121+
122+
with open("assets/theses_pollacci.html", "w", encoding="utf-8") as f:
123+
f.write(render_html(items))
124+
125+
print(f"Scritti {len(items)} record.")
126+
127+
if __name__ == "__main__":
128+
sys.exit(main())

0 commit comments

Comments
 (0)