Commit ·
a94f84a
1
Parent(s): e3d540c
Fix enrollment rendering parity and header-safe Step 7 layout
Browse files- app/services/html_builder.py +38 -2
- app/services/normalizer.py +68 -10
- app/static/css/print.css +41 -11
- app/templates/partials/blocks/enrollment_steps.html +18 -11
app/services/html_builder.py
CHANGED
|
@@ -405,11 +405,47 @@ def build_handbook_html(
|
|
| 405 |
"tier_label": tier_label,
|
| 406 |
})
|
| 407 |
|
| 408 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
def _tier_sort(u: dict) -> tuple:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
t = u.get("tier")
|
| 411 |
rank = t if isinstance(t, int) else 99
|
| 412 |
-
return (rank,
|
| 413 |
active_universities.sort(key=_tier_sort)
|
| 414 |
|
| 415 |
# ── Normalise globals ──
|
|
|
|
| 405 |
"tier_label": tier_label,
|
| 406 |
})
|
| 407 |
|
| 408 |
+
# Explicit university display order
|
| 409 |
+
_UNIVERSITY_ORDER: list[str] = [
|
| 410 |
+
"Indiana University of Pennsylvania",
|
| 411 |
+
"Missouri State University",
|
| 412 |
+
"University of Louisville",
|
| 413 |
+
"University of Delaware",
|
| 414 |
+
"Grand Valley State University",
|
| 415 |
+
"Quinnipiac University",
|
| 416 |
+
"William Jessup University",
|
| 417 |
+
"Wilkes University",
|
| 418 |
+
"University of South Dakota",
|
| 419 |
+
"California Baptist University",
|
| 420 |
+
"Illinois State University",
|
| 421 |
+
"Virginia Commonwealth University",
|
| 422 |
+
"Rutgers University-Camden",
|
| 423 |
+
"University of Oklahoma",
|
| 424 |
+
"Saint Louis University",
|
| 425 |
+
"University of Alabama at Birmingham",
|
| 426 |
+
"Oregon State University",
|
| 427 |
+
"Rochester Institute of Technology",
|
| 428 |
+
"Lewis University",
|
| 429 |
+
"Texas State University",
|
| 430 |
+
"Drew University",
|
| 431 |
+
"University of Missouri- Saint Louis",
|
| 432 |
+
"Montana State University",
|
| 433 |
+
"Oklahoma City University",
|
| 434 |
+
"University of Dayton",
|
| 435 |
+
"Webster University",
|
| 436 |
+
"Rockhurst University",
|
| 437 |
+
]
|
| 438 |
+
_uni_order_map = {name.lower().strip(): idx for idx, name in enumerate(_UNIVERSITY_ORDER)}
|
| 439 |
+
|
| 440 |
def _tier_sort(u: dict) -> tuple:
|
| 441 |
+
name_lower = (u.get("name") or "").lower().strip()
|
| 442 |
+
explicit = _uni_order_map.get(name_lower)
|
| 443 |
+
if explicit is not None:
|
| 444 |
+
return (0, explicit, 0)
|
| 445 |
+
# Universities not in the explicit list go after, sorted by tier then alpha
|
| 446 |
t = u.get("tier")
|
| 447 |
rank = t if isinstance(t, int) else 99
|
| 448 |
+
return (1, rank, name_lower, u.get("id", 0))
|
| 449 |
active_universities.sort(key=_tier_sort)
|
| 450 |
|
| 451 |
# ── Normalise globals ──
|
app/services/normalizer.py
CHANGED
|
@@ -363,6 +363,13 @@ def _normalize_steps(steps: list) -> list[dict]:
|
|
| 363 |
step_title = str(s.get("title", s.get("step_title", ""))).strip()
|
| 364 |
body = _normalize_text_content(str(s.get("body", s.get("description", ""))).strip())
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
# Pre-format body with bold emphasis on REGULAR, PRIME, $ amounts
|
| 367 |
# and convert URLs to clickable bold links
|
| 368 |
from markupsafe import Markup
|
|
@@ -398,7 +405,9 @@ def _normalize_steps(steps: list) -> list[dict]:
|
|
| 398 |
})
|
| 399 |
|
| 400 |
qr = str(s.get("qr_url", s.get("qr_image", ""))).strip()
|
| 401 |
-
|
|
|
|
|
|
|
| 402 |
telegram_ref = ""
|
| 403 |
# Check links array for Telegram URLs first
|
| 404 |
for lnk in links:
|
|
@@ -412,19 +421,66 @@ def _normalize_steps(steps: list) -> list[dict]:
|
|
| 412 |
m = re.search(r"(https?://(?:t\.me|telegram\.me)/[^\s<)]+)", body, flags=re.IGNORECASE)
|
| 413 |
if m:
|
| 414 |
telegram_ref = m.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
if telegram_ref:
|
|
|
|
| 416 |
# Use branded Telegram QR from static assets (matches Word doc)
|
| 417 |
# Embed as data URI so it works reliably in Playwright/Docker
|
| 418 |
import base64
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
result.append({
|
| 430 |
"number": step_num,
|
|
@@ -434,6 +490,8 @@ def _normalize_steps(steps: list) -> list[dict]:
|
|
| 434 |
"links": links,
|
| 435 |
"plain_links": plain_links,
|
| 436 |
"qr_url": qr,
|
|
|
|
|
|
|
| 437 |
})
|
| 438 |
return result
|
| 439 |
|
|
|
|
| 363 |
step_title = str(s.get("title", s.get("step_title", ""))).strip()
|
| 364 |
body = _normalize_text_content(str(s.get("body", s.get("description", ""))).strip())
|
| 365 |
|
| 366 |
+
if step_num == 2:
|
| 367 |
+
# Fix malformed source spacing around application fee token.
|
| 368 |
+
body = body.replace("**$20 **that", "**$20** that")
|
| 369 |
+
body = body.replace("**USD 20 **that", "**$20** that")
|
| 370 |
+
body = re.sub(r"\bUSD\s+20\b", "$20", body, flags=re.IGNORECASE)
|
| 371 |
+
body = re.sub(r"\$20\s*that\b", "$20 that", body, flags=re.IGNORECASE)
|
| 372 |
+
|
| 373 |
# Pre-format body with bold emphasis on REGULAR, PRIME, $ amounts
|
| 374 |
# and convert URLs to clickable bold links
|
| 375 |
from markupsafe import Markup
|
|
|
|
| 405 |
})
|
| 406 |
|
| 407 |
qr = str(s.get("qr_url", s.get("qr_image", ""))).strip()
|
| 408 |
+
telegram_url = ""
|
| 409 |
+
telegram_help_text = ""
|
| 410 |
+
if step_num == 1:
|
| 411 |
telegram_ref = ""
|
| 412 |
# Check links array for Telegram URLs first
|
| 413 |
for lnk in links:
|
|
|
|
| 421 |
m = re.search(r"(https?://(?:t\.me|telegram\.me)/[^\s<)]+)", body, flags=re.IGNORECASE)
|
| 422 |
if m:
|
| 423 |
telegram_ref = m.group(1)
|
| 424 |
+
|
| 425 |
+
if not telegram_ref:
|
| 426 |
+
telegram_ref = "https://t.me/+ivtij2xRull4yYmY5"
|
| 427 |
+
|
| 428 |
if telegram_ref:
|
| 429 |
+
telegram_url = telegram_ref
|
| 430 |
# Use branded Telegram QR from static assets (matches Word doc)
|
| 431 |
# Embed as data URI so it works reliably in Playwright/Docker
|
| 432 |
import base64
|
| 433 |
+
if not qr:
|
| 434 |
+
branded_qr = Path(__file__).resolve().parent.parent / "static" / "img" / "telegram_qr.png"
|
| 435 |
+
if branded_qr.exists():
|
| 436 |
+
b64 = base64.b64encode(branded_qr.read_bytes()).decode()
|
| 437 |
+
qr = f"data:image/png;base64,{b64}"
|
| 438 |
+
else:
|
| 439 |
+
qr = (
|
| 440 |
+
"https://api.qrserver.com/v1/create-qr-code/?size=160x160&data="
|
| 441 |
+
+ quote_plus(telegram_ref)
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
followup_re = (
|
| 445 |
+
r"(This telegram group will help you interact with program administrators and "
|
| 446 |
+
r"other prospective students where you can ask any questions you may have about "
|
| 447 |
+
r"the program\.?)"
|
| 448 |
+
)
|
| 449 |
+
m_help = re.search(followup_re, body, flags=re.IGNORECASE)
|
| 450 |
+
if m_help:
|
| 451 |
+
telegram_help_text = m_help.group(1).strip()
|
| 452 |
+
|
| 453 |
+
body = re.sub(r"https?://(?:t\.me|telegram\.me)/[^\s<)]+", "", body, flags=re.IGNORECASE)
|
| 454 |
+
body = re.sub(followup_re, "", body, flags=re.IGNORECASE)
|
| 455 |
+
body = re.sub(r"\n{2,}", "\n", body).strip()
|
| 456 |
+
body_html = Markup(linkify_urls(emphasize_keywords(body))) if body else ""
|
| 457 |
+
|
| 458 |
+
if step_num == 2:
|
| 459 |
+
website_url = ""
|
| 460 |
+
for lnk in links:
|
| 461 |
+
u = str(lnk.get("url", "")).strip()
|
| 462 |
+
if "internationalscholarsprogram.com" in u.lower():
|
| 463 |
+
website_url = u
|
| 464 |
+
break
|
| 465 |
+
if website_url and "internationalscholarsprogram.com" not in body.lower():
|
| 466 |
+
body = re.sub(
|
| 467 |
+
r"\bVisit\s+and\s+submit\s+your\s+application\.",
|
| 468 |
+
"Visit www.internationalscholarsprogram.com and submit your application.",
|
| 469 |
+
body,
|
| 470 |
+
flags=re.IGNORECASE,
|
| 471 |
+
)
|
| 472 |
+
body = re.sub(
|
| 473 |
+
r"\bVisit\s{2,}and\s+submit\s+your\s+application\.",
|
| 474 |
+
"Visit www.internationalscholarsprogram.com and submit your application.",
|
| 475 |
+
body,
|
| 476 |
+
flags=re.IGNORECASE,
|
| 477 |
+
)
|
| 478 |
+
body_html = Markup(linkify_urls(emphasize_keywords(body))) if body else ""
|
| 479 |
+
|
| 480 |
+
links = [
|
| 481 |
+
l for l in links
|
| 482 |
+
if "internationalscholarsprogram.com" not in str(l.get("url", "")).lower()
|
| 483 |
+
]
|
| 484 |
|
| 485 |
result.append({
|
| 486 |
"number": step_num,
|
|
|
|
| 490 |
"links": links,
|
| 491 |
"plain_links": plain_links,
|
| 492 |
"qr_url": qr,
|
| 493 |
+
"telegram_url": telegram_url,
|
| 494 |
+
"telegram_help_text": telegram_help_text,
|
| 495 |
})
|
| 496 |
return result
|
| 497 |
|
app/static/css/print.css
CHANGED
|
@@ -864,10 +864,10 @@ table.programs td a,
|
|
| 864 |
}
|
| 865 |
|
| 866 |
.hb-step {
|
| 867 |
-
margin: 0 0
|
| 868 |
-
padding:
|
| 869 |
-
border-left:
|
| 870 |
-
background:
|
| 871 |
page-break-inside: avoid;
|
| 872 |
break-inside: avoid;
|
| 873 |
}
|
|
@@ -876,23 +876,53 @@ table.programs td a,
|
|
| 876 |
font-size: 10.5pt;
|
| 877 |
font-weight: 700;
|
| 878 |
color: #199970;
|
| 879 |
-
margin: 0 0
|
| 880 |
line-height: 1.25;
|
| 881 |
page-break-after: avoid;
|
| 882 |
break-after: avoid;
|
| 883 |
}
|
| 884 |
|
| 885 |
.hb-step-qr-wrap {
|
| 886 |
-
margin:
|
| 887 |
text-align: center;
|
| 888 |
}
|
| 889 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
.hb-step-qr {
|
| 891 |
display: inline-block;
|
| 892 |
-
width:
|
| 893 |
-
height:
|
| 894 |
-
margin: 0
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
}
|
| 897 |
|
| 898 |
.hb-plain-url {
|
|
@@ -1563,4 +1593,4 @@ p {
|
|
| 1563 |
image-rendering: auto;
|
| 1564 |
-webkit-backface-visibility: hidden;
|
| 1565 |
}
|
| 1566 |
-
}
|
|
|
|
| 864 |
}
|
| 865 |
|
| 866 |
.hb-step {
|
| 867 |
+
margin: 0 0 14pt;
|
| 868 |
+
padding: 0;
|
| 869 |
+
border-left: none;
|
| 870 |
+
background: transparent;
|
| 871 |
page-break-inside: avoid;
|
| 872 |
break-inside: avoid;
|
| 873 |
}
|
|
|
|
| 876 |
font-size: 10.5pt;
|
| 877 |
font-weight: 700;
|
| 878 |
color: #199970;
|
| 879 |
+
margin: 0 0 5pt;
|
| 880 |
line-height: 1.25;
|
| 881 |
page-break-after: avoid;
|
| 882 |
break-after: avoid;
|
| 883 |
}
|
| 884 |
|
| 885 |
.hb-step-qr-wrap {
|
| 886 |
+
margin: 4pt 0 6pt;
|
| 887 |
text-align: center;
|
| 888 |
}
|
| 889 |
|
| 890 |
+
.hb-step-link-line {
|
| 891 |
+
margin: 2pt 0 6pt;
|
| 892 |
+
text-align: left;
|
| 893 |
+
font-size: 10pt;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
.hb-step-link-line a {
|
| 897 |
+
color: #0263A3;
|
| 898 |
+
text-decoration: underline;
|
| 899 |
+
border-bottom: none;
|
| 900 |
+
font-weight: 600;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
.hb-step-qr {
|
| 904 |
display: inline-block;
|
| 905 |
+
width: 72pt;
|
| 906 |
+
height: 72pt;
|
| 907 |
+
margin: 4pt 0 6pt;
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
.hb-telegram-link {
|
| 911 |
+
margin: 2pt 0 6pt;
|
| 912 |
+
text-align: left;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.hb-telegram-link a {
|
| 916 |
+
color: #0263A3;
|
| 917 |
+
text-decoration: underline;
|
| 918 |
+
font-size: 9pt;
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
/* Step 7 often lands at a page top; force safe placement below stamped header area. */
|
| 922 |
+
.hb-step.hb-step-7 {
|
| 923 |
+
page-break-before: always;
|
| 924 |
+
break-before: page;
|
| 925 |
+
padding-top: 14pt;
|
| 926 |
}
|
| 927 |
|
| 928 |
.hb-plain-url {
|
|
|
|
| 1593 |
image-rendering: auto;
|
| 1594 |
-webkit-backface-visibility: hidden;
|
| 1595 |
}
|
| 1596 |
+
}
|
app/templates/partials/blocks/enrollment_steps.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{# Block partial: enrollment_steps — each step visually separated #}
|
| 2 |
{% for step in block.data.steps %}
|
| 3 |
-
<div class="hb-step avoid-break">
|
| 4 |
{% if step.title %}
|
| 5 |
<div class="hb-step-title">Step {{ step.number }}: {{ step.title | e }}</div>
|
| 6 |
{% endif %}
|
|
@@ -10,23 +10,30 @@
|
|
| 10 |
<p class="hb-paragraph">{{ step.body | e }}</p>
|
| 11 |
{% endif %}
|
| 12 |
{% if step.links %}
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
</ul>
|
| 18 |
{% endif %}
|
| 19 |
{% if step.plain_links %}
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
{% endif %}
|
| 26 |
{% if step.qr_url %}
|
| 27 |
<div class="hb-step-qr-wrap">
|
| 28 |
<img class="hb-step-qr" src="{{ step.qr_url | e }}" alt="QR Code" />
|
| 29 |
</div>
|
| 30 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
{% endfor %}
|
|
|
|
| 1 |
{# Block partial: enrollment_steps — each step visually separated #}
|
| 2 |
{% for step in block.data.steps %}
|
| 3 |
+
<div class="hb-step hb-step-{{ step.number }} avoid-break">
|
| 4 |
{% if step.title %}
|
| 5 |
<div class="hb-step-title">Step {{ step.number }}: {{ step.title | e }}</div>
|
| 6 |
{% endif %}
|
|
|
|
| 10 |
<p class="hb-paragraph">{{ step.body | e }}</p>
|
| 11 |
{% endif %}
|
| 12 |
{% if step.links %}
|
| 13 |
+
{% for lnk in step.links %}
|
| 14 |
+
<p class="hb-step-link-line"><a href="{{ lnk.url | e }}" target="_blank" rel="noopener noreferrer">{{ lnk.label | e
|
| 15 |
+
}}</a></p>
|
| 16 |
+
{% endfor %}
|
|
|
|
| 17 |
{% endif %}
|
| 18 |
{% if step.plain_links %}
|
| 19 |
+
{% for plain_url in step.plain_links %}
|
| 20 |
+
<p class="hb-step-link-line"><a href="{{ plain_url | e }}" target="_blank" rel="noopener noreferrer">{{ plain_url |
|
| 21 |
+
e
|
| 22 |
+
}}</a></p>
|
| 23 |
+
{% endfor %}
|
| 24 |
+
{% endif %}
|
| 25 |
+
{% if step.telegram_url %}
|
| 26 |
+
<div class="hb-telegram-link">
|
| 27 |
+
<a href="{{ step.telegram_url | e }}" target="_blank" rel="noopener noreferrer">{{ step.telegram_url | e }}</a>
|
| 28 |
+
</div>
|
| 29 |
{% endif %}
|
| 30 |
{% if step.qr_url %}
|
| 31 |
<div class="hb-step-qr-wrap">
|
| 32 |
<img class="hb-step-qr" src="{{ step.qr_url | e }}" alt="QR Code" />
|
| 33 |
</div>
|
| 34 |
{% endif %}
|
| 35 |
+
{% if step.telegram_help_text %}
|
| 36 |
+
<p class="hb-paragraph">{{ step.telegram_help_text | e }}</p>
|
| 37 |
+
{% endif %}
|
| 38 |
</div>
|
| 39 |
{% endfor %}
|