From 54ad0fa9d35272325960174e6db4f4dbad1a0eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20H=C3=A9zs=C5=91?= Date: Mon, 8 Jun 2026 11:34:39 +0200 Subject: [PATCH] add Organizations with Regions and update Alt Tech Filtering --- assets/css/style.css | 680 ++++++++++++++++---------------- assets/template/index.html | 47 ++- core/engine.py | 4 + core/utils_db.py | 1 + core/utils_report.py | 7 +- core/utils_report_common.py | 86 ++++ core/utils_report_html.py | 2 + tests/report_fixtures.py | 13 + tests/test_report_pipeline.py | 5 + tests/test_report_transforms.py | 23 ++ 10 files changed, 525 insertions(+), 343 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index fdd2c4f..8e2234f 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,15 +1,15 @@ -/* ======================================================== - Base: Variables - ======================================================== */ +/* ======================================================== + Base: Variables + ======================================================== */ :root { /* Blue */ --blue-100: #dbe6fe; --blue-800: #1e4baf; - /* Green */ - --green-100: #dcfce7; - --green-600: #16a34a; - --green-700: #047854; + /* Green */ + --green-100: #dcfce7; + --green-600: #16a34a; + --green-700: #047854; /* Neutral */ --neutral-50: #f9fbfb; @@ -26,9 +26,9 @@ --primary-800: #115e59; --primary-950: #042f2c; - /* Red */ - --red-50: #fef2f2; - --red-100: #fee2e2; + /* Red */ + --red-50: #fef2f2; + --red-100: #fee2e2; --red-700: #b91c1c; --red-800: #991b1b; @@ -53,9 +53,9 @@ /* Sidebar */ } -/* ======================================================== - Base: Reset - ======================================================== */ +/* ======================================================== + Base: Reset + ======================================================== */ * { margin: 0; padding: 0; @@ -63,12 +63,12 @@ box-sizing: border-box; } -body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; - line-height: 1.375; - font-weight: 400; - color: var(--neutral-900); +body { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + line-height: 1.375; + font-weight: 400; + color: var(--neutral-900); background: var(--neutral-100); text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; @@ -94,9 +94,9 @@ ul { .container { } -/* ======================================================== - Base: Typography - ======================================================== */ +/* ======================================================== + Base: Typography + ======================================================== */ a { text-decoration: none; -webkit-transition: all var(--transition-speed) ease; @@ -169,23 +169,23 @@ main { padding-top: 88px; } -/* Scrollbars */ -::-webkit-scrollbar { +/* Scrollbars */ +::-webkit-scrollbar { width: 5px; border-radius: var(--rounded-sm); } -::-webkit-scrollbar-track { +::-webkit-scrollbar-track { background: var(--neutral-100); border-radius: var(--rounded-sm); } -::-webkit-scrollbar-thumb { +::-webkit-scrollbar-thumb { background: var(--primary-600); border-radius: var(--rounded-sm); } -::-webkit-scrollbar-thumb:hover { +::-webkit-scrollbar-thumb:hover { background: var(--primary-800); border-radius: var(--rounded-sm); } @@ -209,11 +209,11 @@ main { line-height: 1.5; } -/* ======================================================== - Layout: Shared Cards And Charts - ======================================================== */ - -.chart-card-head { +/* ======================================================== + Layout: Shared Cards And Charts + ======================================================== */ + +.chart-card-head { padding: 16px 24px; } @@ -226,131 +226,131 @@ main { font-size: var(--text-label); font-weight: 400; } -.chart-card-head h6 span { - display: inline-block; - font-weight: 500; -} - -.risk-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; +.chart-card-head h6 span { + display: inline-block; + font-weight: 500; +} + +.risk-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; -webkit-box-align: center; -ms-flex-align: center; - align-items: center; - /* padding: 16px 24px; */ -} - -.risk-dashboard { - display: flex; - flex-direction: column; - height: 100%; -} - -.risk-count h2 { - color: var(--neutral-600); - font-size: var(--text-label); - font-weight: 400; -} - -.risk-count .count { - font-size: var(--text-heading-3); - font-weight: 500; - color: var(--neutral-900); - margin: 0; -} - -.chart-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; + align-items: center; + /* padding: 16px 24px; */ +} + +.risk-dashboard { + display: flex; + flex-direction: column; + height: 100%; +} + +.risk-count h2 { + color: var(--neutral-600); + font-size: var(--text-label); + font-weight: 400; +} + +.risk-count .count { + font-size: var(--text-heading-3); + font-weight: 500; + color: var(--neutral-900); + margin: 0; +} + +.chart-container { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - flex: 1; -} - -.chart-box { - height: 350px; -} - -.chart-empty-state { - flex: 1; - min-height: 220px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; -} - -.chart-empty-state-inner { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; -} - -.chart-empty-state i { - font-size: 28px; - line-height: 1; - color: var(--neutral-900); -} - -.chart-empty-state p { - margin: 0; - color: var(--neutral-700); -} - -.alt-tech-empty-state { - min-height: 260px; -} - -.scoring-empty-state { - min-height: 300px; -} - -.scoring-card { - overflow: hidden; -} - -.scoring-inner { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - min-height: 300px; -} - -.chart-wrapper { - max-width: 350px; - overflow: hidden; - position: relative; - border-radius: 12px; - mask-image: linear-gradient(to bottom, black 85%, transparent 100%); - -webkit-mask-image: linear-gradient(to bottom, black 85%, transparent 100%); -} - -.scoring { - width: 100%; - max-width: 420px; - height: 320px !important; - margin: 0 auto; -} - -.form-title { + -ms-flex-align: center; + align-items: center; + flex: 1; +} + +.chart-box { + height: 350px; +} + +.chart-empty-state { + flex: 1; + min-height: 220px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.chart-empty-state-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.chart-empty-state i { + font-size: 28px; + line-height: 1; + color: var(--neutral-900); +} + +.chart-empty-state p { + margin: 0; + color: var(--neutral-700); +} + +.alt-tech-empty-state { + min-height: 260px; +} + +.scoring-empty-state { + min-height: 300px; +} + +.scoring-card { + overflow: hidden; +} + +.scoring-inner { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + min-height: 300px; +} + +.chart-wrapper { + max-width: 350px; + overflow: hidden; + position: relative; + border-radius: 12px; + mask-image: linear-gradient(to bottom, black 85%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, black 85%, transparent 100%); +} + +.scoring { + width: 100%; + max-width: 420px; + height: 320px !important; + margin: 0 auto; +} + +.form-title { font-size: var(--text-heading-3); font-weight: 500; line-height: 1.33; color: var(--neutral-900); } -.shadow-s { +.shadow-s { -webkit-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1); } @@ -359,19 +359,19 @@ main { border: 1px solid var(--neutral-300); } -/* ======================================================== - Components: Buttons - ======================================================== */ - -.dropdown-toggle::after { +/* ======================================================== + Components: Buttons + ======================================================== */ + +.dropdown-toggle::after { float: right; margin-top: 8px; } -/* ======================================================== - Components: Summary, Scoring, Resources, Alt Tech Cards - ======================================================== */ -::-webkit-input-placeholder { +/* ======================================================== + Components: Summary, Scoring, Resources, Alt Tech Cards + ======================================================== */ +::-webkit-input-placeholder { color: var(--neutral-600); font-size: var(--text-body); font-style: normal; @@ -493,10 +493,10 @@ main { border: 0; } -/* ======================================================== - Components: Risk Table - ======================================================== */ -.risk-title-cell { +/* ======================================================== + Components: Risk Table + ======================================================== */ +.risk-title-cell { position: relative; padding-left: 40px !important; cursor: pointer; @@ -609,7 +609,7 @@ main { color: var(--blue-800); } -.btn { +.btn { border-radius: 6px; line-height: 40px; padding: 0 16px; @@ -713,47 +713,47 @@ main { box-shadow: none !important; } -.btn-primary, -.btn-outline-primary { - /* --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-focus-shadow-rgb: 13, 110, 253; - --bs-btn-disabled-bg: transparent; +.btn-primary, +.btn-outline-primary { + /* --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-focus-shadow-rgb: 13, 110, 253; + --bs-btn-disabled-bg: transparent; --bs-btn-active-color: var(--neutral-white); --bs-btn-hover-color: var(--neutral-white); - --bs-gradient: none; */ -} - -.btn-outline-primary { - --bs-btn-color: var(--primary-800); - --bs-btn-border-color: var(--primary-800); - --bs-btn-hover-color: var(--white); - --bs-btn-hover-bg: var(--primary-800); - --bs-btn-hover-border-color: var(--primary-800); - --bs-btn-active-color: var(--white); - --bs-btn-active-bg: var(--primary-800); - --bs-btn-active-border-color: var(--primary-800); - --bs-btn-disabled-color: var(--neutral-400); - --bs-btn-disabled-border-color: var(--neutral-300); - --bs-btn-focus-shadow-rgb: 17, 94, 89; -} - -.btn-outline-primary.show, -.btn-check:checked + .btn-outline-primary, -.btn-check:active + .btn-outline-primary, -.btn-outline-primary:focus, -.btn-outline-primary:focus-visible { - background: var(--primary-800) !important; - border-color: var(--primary-800) !important; - color: var(--white) !important; -} - -.btn-outline-primary.show svg.filter-icon path, -.btn-check:checked + .btn-outline-primary svg.filter-icon path, -.btn-check:active + .btn-outline-primary svg.filter-icon path, -.btn-outline-primary:focus svg.filter-icon path, -.btn-outline-primary:focus-visible svg.filter-icon path { - stroke: var(--white); -} + --bs-gradient: none; */ +} + +.btn-outline-primary { + --bs-btn-color: var(--primary-800); + --bs-btn-border-color: var(--primary-800); + --bs-btn-hover-color: var(--white); + --bs-btn-hover-bg: var(--primary-800); + --bs-btn-hover-border-color: var(--primary-800); + --bs-btn-active-color: var(--white); + --bs-btn-active-bg: var(--primary-800); + --bs-btn-active-border-color: var(--primary-800); + --bs-btn-disabled-color: var(--neutral-400); + --bs-btn-disabled-border-color: var(--neutral-300); + --bs-btn-focus-shadow-rgb: 17, 94, 89; +} + +.btn-outline-primary.show, +.btn-check:checked + .btn-outline-primary, +.btn-check:active + .btn-outline-primary, +.btn-outline-primary:focus, +.btn-outline-primary:focus-visible { + background: var(--primary-800) !important; + border-color: var(--primary-800) !important; + color: var(--white) !important; +} + +.btn-outline-primary.show svg.filter-icon path, +.btn-check:checked + .btn-outline-primary svg.filter-icon path, +.btn-check:active + .btn-outline-primary svg.filter-icon path, +.btn-outline-primary:focus svg.filter-icon path, +.btn-outline-primary:focus-visible svg.filter-icon path { + stroke: var(--white); +} .btn-primary.disabled, .btn-primary:disabled { @@ -768,10 +768,10 @@ main { color: var(--neutral-400); } -/* ======================================================== - Components: Filter Toggles - ======================================================== */ -.toggle-switch { +/* ======================================================== + Components: Filter Toggles + ======================================================== */ +.toggle-switch { position: relative; width: 44px; height: 24px; @@ -797,41 +797,41 @@ main { border-radius: var(--rounded-md); } -.toggle-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 2px; - bottom: 2px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23E5EBEB'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); -} - -.toggle-slider:before { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23E5EBEB'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); +} + +.toggle-slider:before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%23D1DBDB' stroke-width='1.5'/%3E%3Cpath d='M6.5 6.5L13.5 13.5M13.5 6.5L6.5 13.5' stroke='%234B6361' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; +} input:checked + .toggle-slider { background-color: var(--primary-800); } -input:checked + .toggle-slider:before { - -webkit-transform: translateX(20px); - -ms-transform: translateX(20px); - transform: translateX(20px); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='10' fill='%230D948B'/%3E%3Cpath d='M5.5 10.5L8.5 13.5L14.5 7.5' stroke='white' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center; - background-size: 20px 20px; -} - -/* ======================================================== - Components: Tables - ======================================================== */ -table:not("#assessmentsTable") { +input:checked + .toggle-slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Ccircle cx='10' cy='10' r='9.25' fill='white' stroke='%230D948B' stroke-width='1.5'/%3E%3Cpath d='M5.5 10.5L8.5 13.5L14.5 7.5' stroke='%230D948B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + background-size: 20px 20px; +} + +/* ======================================================== + Components: Tables + ======================================================== */ +table:not("#assessmentsTable") { table-layout: fixed; width: 100%; } @@ -857,20 +857,20 @@ tr:last-child td { min-width: 420px; } -.risk-table-container::-webkit-scrollbar { - height: 20px; -} - -.risk-table-container::-webkit-scrollbar-track { +.risk-table-container::-webkit-scrollbar { + height: 20px; +} + +.risk-table-container::-webkit-scrollbar-track { background: var(--white); } -.risk-table-container::-webkit-scrollbar-thumb { +.risk-table-container::-webkit-scrollbar-thumb { background: var(--neutral-300); border-radius: 20px; } -.risk-table-container::-webkit-scrollbar-thumb:hover { +.risk-table-container::-webkit-scrollbar-thumb:hover { background: var(--neutral-600); } @@ -880,84 +880,94 @@ td:first-child { font-weight: 500; } -td .number, -td button { - margin-left: auto; - margin-right: auto; -} - -/* ======================================================== - Layout: Main Content And Forms - ======================================================== */ - -#main-content { - height: 100%; - width: 100%; - overflow: hidden; - padding: 20px 60px 20px 60px; - transition: all 0.3s linear; - -webkit-transition: all 0.3s linear; -} - -.visit span { - font-size: 14px; - word-break: break-all; -} - -.form-control, -.form-select { - color: var(--neutral-600); - font-size: var(--text-body); - font-style: normal; - font-weight: 400; - border-radius: 16px; - border: 1px solid var(--neutral-300); - background: var(--white); - -webkit-transition: border-color 0.3s ease, box-shadow 0.3s ease; - -o-transition: border-color 0.3s ease, box-shadow 0.3s ease; - transition: border-color 0.3s ease, box-shadow 0.3s ease; - -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.form-control:focus, -.form-select:focus { - border-color: var(--primary-800); - box-shadow: 0 0 0 0.2rem rgba(17, 94, 89, 0.12); -} - -.custom-search { - min-width: 240px; - height: 36px; - border-radius: 8px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 3.5C5.96243 3.5 3.5 5.96243 3.5 9C3.5 12.0376 5.96243 14.5 9 14.5C10.519 14.5 11.893 13.8852 12.8891 12.8891C13.8852 11.893 14.5 10.519 14.5 9C14.5 5.96243 12.0376 3.5 9 3.5ZM2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9C16 10.6625 15.4197 12.1906 14.4517 13.3911L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4874 18.0732 17.0126 18.0732 16.7197 17.7803L13.3911 14.4517C12.1906 15.4197 10.6625 16 9 16C5.13401 16 2 12.866 2 9Z' fill='%236B807F'/%3E%3C/svg%3E") !important; - background-repeat: no-repeat !important; - background-position: 16px center !important; - padding: 0 16px 0 48px !important; - width: 400px; -} +td .number, +td button { + margin-left: auto; + margin-right: auto; +} + +/* ======================================================== + Layout: Main Content And Forms + ======================================================== */ + +#main-content { + height: 100%; + width: 100%; + overflow: hidden; + padding: 20px 60px 20px 60px; + transition: all 0.3s linear; + -webkit-transition: all 0.3s linear; +} + +.visit span { + font-size: 14px; + word-break: break-all; +} + +.form-control, +.form-select { + color: var(--neutral-600); + font-size: var(--text-body); + font-style: normal; + font-weight: 400; + border-radius: 8px; + border: 1px solid var(--neutral-300); + background: var(--white); + -webkit-transition: border-color 0.3s ease, box-shadow 0.3s ease; + -o-transition: border-color 0.3s ease, box-shadow 0.3s ease; + transition: border-color 0.3s ease, box-shadow 0.3s ease; + -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.form-select { + -webkit-appearance: none; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23115E59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px 16px; + padding-right: 40px; +} + +.form-control:focus, +.form-select:focus { + border-color: var(--primary-800); + box-shadow: 0 0 0 0.2rem rgba(17, 94, 89, 0.12); +} + +.custom-search { + min-width: 240px; + height: 36px; + border-radius: 8px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 3.5C5.96243 3.5 3.5 5.96243 3.5 9C3.5 12.0376 5.96243 14.5 9 14.5C10.519 14.5 11.893 13.8852 12.8891 12.8891C13.8852 11.893 14.5 10.519 14.5 9C14.5 5.96243 12.0376 3.5 9 3.5ZM2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9C16 10.6625 15.4197 12.1906 14.4517 13.3911L17.7803 16.7197C18.0732 17.0126 18.0732 17.4874 17.7803 17.7803C17.4874 18.0732 17.0126 18.0732 16.7197 17.7803L13.3911 14.4517C12.1906 15.4197 10.6625 16 9 16C5.13401 16 2 12.866 2 9Z' fill='%236B807F'/%3E%3C/svg%3E") !important; + background-repeat: no-repeat !important; + background-position: 16px center !important; + padding: 0 16px 0 48px !important; + width: 400px; +} .btn-clear:hover { text-decoration: underline; } -.alt-tech-card { - background: #f5f5f5; - border-radius: 10px; -} - -.alt-tech-card h3 { - font-size: 18px; - margin: 0; -} - -/* ======================================================== - Components: Alternative Technology Status And Hints - ======================================================== */ - -.verified { - background: var(--green-600); - color: var(--white); +.alt-tech-card { + background: #f5f5f5; + border-radius: 10px; +} + +.alt-tech-card h3 { + font-size: 18px; + margin: 0; +} + +/* ======================================================== + Components: Alternative Technology Status And Hints + ======================================================== */ + +.verified { + background: var(--green-600); + color: var(--white); } .green-100 { background: var(--green-100); @@ -973,19 +983,19 @@ td button { color: var(--neutral-700); font-weight: 500; } -.verified span { - font-size: var(--text-label); -} -.red-700 { - color: var(--red-700); -} -.red-50 { - background: var(--red-50); -} - -.info-hint-box { - position: relative; - display: inline-block; +.verified span { + font-size: var(--text-label); +} +.red-700 { + color: var(--red-700); +} +.red-50 { + background: var(--red-50); +} + +.info-hint-box { + position: relative; + display: inline-block; margin-left: 6px; cursor: pointer; } @@ -1027,18 +1037,18 @@ td button { font-weight: 600; margin-bottom: 6px; } -.hint-hoverbox p { - font-size: 13px; - color: #555; - margin: 0; - line-height: 1.4; -} - -/* ======================================================== - Responsive - ======================================================== */ - -@media (max-width: 991px) { +.hint-hoverbox p { + font-size: 13px; + color: #555; + margin: 0; + line-height: 1.4; +} + +/* ======================================================== + Responsive + ======================================================== */ + +@media (max-width: 991px) { .alt-tech-card div { width: 100%; diff --git a/assets/template/index.html b/assets/template/index.html index 95ea1fa..86dc411 100644 --- a/assets/template/index.html +++ b/assets/template/index.html @@ -553,6 +553,7 @@
Filters
+
+
+ + +
Open Source
@@ -596,24 +608,37 @@
Enterprise Support
{% if alternative_technologies %} {% for alt_tech in - alternative_technologies %} {% set alt_resource = namespace(name="Resource Type " ~ alt_tech.resource_type_id) %} - {% for resource in resource_inventory %} - {% if resource.resource_type|string == alt_tech.resource_type_id|string %} - {% set alt_resource.name = resource.name %} - {% endif %} - {% endfor %} + alternative_technologies %}
-
Category: {{ alt_resource.name }}
+
+ + {{ alt_tech.organization_name }} + {% if alt_tech.organization_url %} + + + + {% endif %} + | + {% if alt_tech.organization_flag %}{{ alt_tech.organization_flag }} {% endif %}{{ alt_tech.organization_country_code }} • {{ alt_tech.organization_region_label }} +

{{ alt_tech.product_name }}

@@ -816,6 +841,7 @@

{{ alt_tech.product_name }}

const applyFiltersBtn = document.getElementById("applyFilters"); const clearFiltersBtn = document.getElementById("clearFilters"); const resourceTypeSelect = document.getElementById("resourceTypeSelect"); + const regionSelect = document.getElementById("regionSelect"); const openSourceSwitch = document.getElementById("openSourceSwitch"); const enterpriseSupportSwitch = document.getElementById( "enterpriseSupportSwitch" @@ -828,6 +854,7 @@

{{ alt_tech.product_name }}

applyFiltersBtn && clearFiltersBtn && resourceTypeSelect && + regionSelect && openSourceSwitch && enterpriseSupportSwitch && searchInput && @@ -866,6 +893,7 @@

{{ alt_tech.product_name }}

function applyAlternativeFilters() { const selectedResourceType = resourceTypeSelect.value; + const selectedRegion = regionSelect.value; const isOpenSource = openSourceSwitch.checked; const hasEnterpriseSupport = enterpriseSupportSwitch.checked; const searchQuery = searchInput.value.trim().toLowerCase(); @@ -874,6 +902,9 @@

{{ alt_tech.product_name }}

const matchesResourceType = selectedResourceType === "all" || box.dataset.resourceType === selectedResourceType; + const matchesRegion = + selectedRegion === "all" || + box.dataset.orgRegion === selectedRegion; const matchesOpenSource = !isOpenSource || box.dataset.openSource === "true"; const matchesEnterpriseSupport = @@ -883,6 +914,7 @@

{{ alt_tech.product_name }}

box.dataset.filtered = matchesResourceType && + matchesRegion && matchesOpenSource && matchesEnterpriseSupport && matchesSearch @@ -899,6 +931,7 @@

{{ alt_tech.product_name }}

clearFiltersBtn.addEventListener("click", function () { resourceTypeSelect.value = "all"; + regionSelect.value = "all"; openSourceSwitch.checked = false; enterpriseSupportSwitch.checked = false; searchInput.value = ""; diff --git a/core/engine.py b/core/engine.py index ca59b9f..6acade9 100644 --- a/core/engine.py +++ b/core/engine.py @@ -437,6 +437,9 @@ def generate_report( risk_definitions = load_data("risk", db_path=db_path) alternatives = load_data("alternative", db_path=db_path) alternative_technologies = load_data("alternativetechnology", db_path=db_path) + alternative_technology_organizations = load_data( + "alternativetechnologyorganization", db_path=db_path + ) resource_inventory = load_data("resource_inventory", db_path=db_path) cost_data = load_data("cost_inventory", db_path=db_path) risk_data = load_data("risk_inventory", db_path=db_path) @@ -481,6 +484,7 @@ def generate_report( alternatives, alternative_technologies, exit_strategy, + alternative_technology_organizations, ) # Generate PDF report diff --git a/core/utils_db.py b/core/utils_db.py index b3202b9..462c4ee 100644 --- a/core/utils_db.py +++ b/core/utils_db.py @@ -17,6 +17,7 @@ "scoring_data", "alternative", "alternativetechnology", + "alternativetechnologyorganization", "risk", } diff --git a/core/utils_report.py b/core/utils_report.py index f68d2b6..270f531 100644 --- a/core/utils_report.py +++ b/core/utils_report.py @@ -73,6 +73,7 @@ def generate_html_report( alternatives: list[dict[str, Any]], alternative_technologies: list[dict[str, Any]], exit_strategy: int, + alternative_technology_organizations: list[dict[str, Any]] | None = None, ) -> str: # Transform resource inventory @@ -117,7 +118,11 @@ def generate_html_report( # Transform alternative technologies alternative_technologies_data = transform_alt_tech_for_html( - resource_inventory, alternatives, alternative_technologies, exit_strategy + resource_inventory, + alternatives, + alternative_technologies, + exit_strategy, + alternative_technology_organizations=alternative_technology_organizations, ) # Scoring Data diff --git a/core/utils_report_common.py b/core/utils_report_common.py index cac8368..8766613 100644 --- a/core/utils_report_common.py +++ b/core/utils_report_common.py @@ -8,6 +8,73 @@ "EUR": "€", } +EU_COUNTRY_CODES = { + "AT", + "BE", + "BG", + "HR", + "CY", + "CZ", + "DK", + "EE", + "FI", + "FR", + "DE", + "GR", + "HU", + "IE", + "IT", + "LV", + "LT", + "LU", + "MT", + "NL", + "PL", + "PT", + "RO", + "SK", + "SI", + "ES", + "SE", +} + +REGION_LABELS = { + "european-union": "European Union", + "united-kingdom": "United Kingdom", + "switzerland": "Switzerland", + "united-states": "United States", + "other": "Other", +} + + +def normalize_country_code(country_code: Any) -> str | None: + if not isinstance(country_code, str): + return None + normalized = country_code.strip().upper() + return normalized if len(normalized) == 2 and normalized.isalpha() else None + + +def country_code_to_region(country_code: Any) -> str: + normalized = normalize_country_code(country_code) + if not normalized: + return "other" + if normalized in EU_COUNTRY_CODES: + return "european-union" + if normalized == "GB": + return "united-kingdom" + if normalized == "CH": + return "switzerland" + if normalized == "US": + return "united-states" + return "other" + + +def country_code_to_flag(country_code: Any) -> str: + normalized = normalize_country_code(country_code) + if not normalized: + return "" + return chr(127397 + ord(normalized[0])) + chr(127397 + ord(normalized[1])) + def sort_cost_data(cost_data: list[dict[str, Any]]) -> list[dict[str, Any]]: return sorted(cost_data, key=lambda x: datetime.strptime(x["month"], "%Y-%m-%d")) @@ -119,6 +186,7 @@ def summarize_alternative_technologies( alternatives: list[dict[str, Any]], alternative_technologies: list[dict[str, Any]], exit_strategy: int, + alternative_technology_organizations: list[dict[str, Any]] | None = None, ) -> dict[str, list[dict[str, Any]]]: active_technologies = { tech["id"]: tech @@ -129,6 +197,9 @@ def summarize_alternative_technologies( grouped_alt_tech: dict[str, list[dict[str, Any]]] = { str(resource["resource_type"]): [] for resource in resource_inventory } + organization_by_id = { + org["id"]: org for org in (alternative_technology_organizations or []) + } for alt in alternatives: if str(alt["strategy_type"]) != str(exit_strategy): @@ -138,6 +209,11 @@ def summarize_alternative_technologies( tech = active_technologies.get(alt["alternative_technology"]) if not tech or resource_type not in grouped_alt_tech: continue + organization = organization_by_id.get(tech.get("organization_id")) + organization_country_code = normalize_country_code( + organization.get("country_code") if organization else None + ) + organization_region = country_code_to_region(organization_country_code) grouped_alt_tech[resource_type].append( { @@ -147,6 +223,16 @@ def summarize_alternative_technologies( "open_source": tech["open_source"] == "t", "support_plan": tech["support_plan"] == "t", "status": tech["status"] == "t", + "organization_name": ( + organization.get("name") if organization else "Unknown Organization" + ), + "organization_url": organization.get("url") if organization else None, + "organization_country_code": organization_country_code or "N/A", + "organization_region": organization_region, + "organization_region_label": REGION_LABELS.get( + organization_region, "Other" + ), + "organization_flag": country_code_to_flag(organization_country_code), } ) diff --git a/core/utils_report_html.py b/core/utils_report_html.py index 18f20d8..6c626c3 100644 --- a/core/utils_report_html.py +++ b/core/utils_report_html.py @@ -42,6 +42,7 @@ def transform_alt_tech_for_html( alternatives: list[dict[str, Any]], alternative_technologies: list[dict[str, Any]], exit_strategy: int, + alternative_technology_organizations: list[dict[str, Any]] | None = None, ) -> list[dict[str, Any]]: alt_tech_data = [] grouped_alt_tech = summarize_alternative_technologies( @@ -49,6 +50,7 @@ def transform_alt_tech_for_html( alternatives, alternative_technologies, exit_strategy, + alternative_technology_organizations=alternative_technology_organizations, ) for resource in resource_inventory: resource_type = str(resource.get("resource_type")) diff --git a/tests/report_fixtures.py b/tests/report_fixtures.py index 63071fc..a3ff6d7 100644 --- a/tests/report_fixtures.py +++ b/tests/report_fixtures.py @@ -57,6 +57,17 @@ def build_report_fixture(): "open_source": "t", "support_plan": "t", "status": "t", + "organization_id": 10, + } + ] + alternative_technology_organizations = [ + { + "id": 10, + "name": "OpenInfra Foundation", + "url": "https://openinfra.org/", + "country_code": "US", + "stability_tier": 5, + "years_in_business": 14, } ] return { @@ -69,6 +80,7 @@ def build_report_fixture(): "risk_data": risk_data, "alternatives": alternatives, "alternative_technologies": alternative_technologies, + "alternative_technology_organizations": alternative_technology_organizations, "exit_strategy": 1, } @@ -96,6 +108,7 @@ def build_empty_report_fixture(): "risk_data": [], "alternatives": [], "alternative_technologies": [], + "alternative_technology_organizations": [], "exit_strategy": 1, } diff --git a/tests/test_report_pipeline.py b/tests/test_report_pipeline.py index edafb4e..536bad6 100644 --- a/tests/test_report_pipeline.py +++ b/tests/test_report_pipeline.py @@ -33,6 +33,7 @@ def test_generate_html_report_creates_expected_output(self): fixture["alternatives"], fixture["alternative_technologies"], fixture["exit_strategy"], + fixture["alternative_technology_organizations"], ) self.assertTrue(Path(html_path).exists()) @@ -41,6 +42,9 @@ def test_generate_html_report_creates_expected_output(self): self.assertIn("Smoke Test Assessment", html) self.assertIn("Amazon Web Services", html) self.assertIn("OpenStack", html) + self.assertIn("OpenInfra Foundation", html) + self.assertIn("All Regions", html) + self.assertIn("data-org-region", html) self.assertIn("EC2 Instance", html) def test_generate_html_report_renders_empty_state_output(self): @@ -59,6 +63,7 @@ def test_generate_html_report_renders_empty_state_output(self): fixture["alternatives"], fixture["alternative_technologies"], fixture["exit_strategy"], + fixture["alternative_technology_organizations"], ) self.assertTrue(Path(html_path).exists()) diff --git a/tests/test_report_transforms.py b/tests/test_report_transforms.py index 3a7e908..0efb361 100644 --- a/tests/test_report_transforms.py +++ b/tests/test_report_transforms.py @@ -86,6 +86,7 @@ def build_alternative_technologies(): "open_source": "t", "support_plan": "t", "status": "t", + "organization_id": 10, }, { "id": 2, @@ -95,6 +96,7 @@ def build_alternative_technologies(): "open_source": "t", "support_plan": "f", "status": "t", + "organization_id": 20, }, { "id": 3, @@ -108,6 +110,23 @@ def build_alternative_technologies(): ] +def build_alternative_technology_organizations(): + return [ + { + "id": 10, + "name": "OpenInfra Foundation", + "url": "https://openinfra.org/", + "country_code": "US", + }, + { + "id": 20, + "name": "European Storage Collective", + "url": "https://example.eu/", + "country_code": "DE", + }, + ] + + class HtmlTransformTests(unittest.TestCase): def test_transform_cost_inventory_for_html_sorts_and_sums_costs(self): months, cost_values, total_cost, currency_code, currency_symbol = ( @@ -151,6 +170,7 @@ def test_transform_alt_tech_for_html_filters_by_strategy_and_status(self): build_alternatives(), build_alternative_technologies(), exit_strategy=1, + alternative_technology_organizations=build_alternative_technology_organizations(), ) self.assertEqual(len(transformed), 2) @@ -158,6 +178,9 @@ def test_transform_alt_tech_for_html_filters_by_strategy_and_status(self): self.assertEqual(transformed[1]["product_name"], "MinIO") self.assertTrue(transformed[0]["open_source"]) self.assertFalse(transformed[1]["support_plan"]) + self.assertEqual(transformed[0]["organization_name"], "OpenInfra Foundation") + self.assertEqual(transformed[0]["organization_region"], "united-states") + self.assertEqual(transformed[1]["organization_region"], "european-union") class JsonTransformTests(unittest.TestCase):