Skip to content

Commit 06b2185

Browse files
orabeCopilot
andcommitted
Enhance certificate management by adding new fields and ensuring database schema compatibility
Co-authored-by: Copilot <copilot@github.com>
1 parent 56e4ef8 commit 06b2185

7 files changed

Lines changed: 286 additions & 44 deletions

File tree

assets/css/style.css

Lines changed: 146 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,132 @@ article.active {
375375
animation: verify-pop 260ms ease-out both;
376376
}
377377

378+
.verify-result-card {
379+
display: flex;
380+
flex-direction: column;
381+
gap: 18px;
382+
padding: 18px;
383+
border-radius: 18px;
384+
background: rgba(255, 255, 255, 0.28);
385+
border: 1px solid rgba(16, 81, 50, 0.12);
386+
box-shadow: 0 18px 40px rgba(15, 81, 50, 0.08);
387+
}
388+
389+
.verify-result-success {
390+
background: linear-gradient(180deg, rgba(39, 104, 237, 0.14), rgba(255, 255, 255, 0.2));
391+
border-color: rgba(39, 104, 237, 0.18);
392+
}
393+
394+
.verify-result-revoked {
395+
background: linear-gradient(180deg, rgba(102, 63, 3, 0.12), rgba(255, 255, 255, 0.18));
396+
border-color: rgba(102, 63, 3, 0.16);
397+
}
398+
399+
.verify-result-invalid {
400+
background: linear-gradient(180deg, rgba(184, 12, 0, 0.12), rgba(255, 255, 255, 0.18));
401+
border-color: rgba(184, 12, 0, 0.16);
402+
}
403+
404+
.verify-result-header {
405+
display: flex;
406+
flex-direction: column;
407+
gap: 8px;
408+
}
409+
410+
.verify-result-badge {
411+
width: fit-content;
412+
padding: 5px 10px;
413+
border-radius: 999px;
414+
font-size: var(--fs-8);
415+
font-weight: var(--fw-600);
416+
letter-spacing: 0.06em;
417+
text-transform: uppercase;
418+
color: var(--white-2);
419+
background: rgba(39, 104, 237, 0.18);
420+
border: 1px solid rgba(39, 104, 237, 0.22);
421+
}
422+
423+
.verify-result-header.success .verify-result-badge {
424+
background: rgba(24, 128, 72, 0.16);
425+
border-color: rgba(24, 128, 72, 0.22);
426+
}
427+
428+
.verify-result-header.warning .verify-result-badge {
429+
background: rgba(160, 107, 0, 0.16);
430+
border-color: rgba(160, 107, 0, 0.22);
431+
}
432+
433+
.verify-result-header.danger .verify-result-badge {
434+
background: rgba(184, 12, 0, 0.16);
435+
border-color: rgba(184, 12, 0, 0.22);
436+
}
437+
438+
.verify-result-title {
439+
font-size: var(--fs-2);
440+
font-weight: var(--fw-600);
441+
color: var(--white-2);
442+
line-height: 1.2;
443+
}
444+
445+
.verify-result-description {
446+
color: var(--light-gray);
447+
font-size: var(--fs-6);
448+
line-height: 1.6;
449+
}
450+
451+
.verify-details-grid {
452+
display: grid;
453+
grid-template-columns: repeat(2, minmax(0, 1fr));
454+
gap: 12px;
455+
}
456+
457+
.verify-meta-item {
458+
padding: 12px 14px;
459+
border-radius: 14px;
460+
background: rgba(255, 255, 255, 0.36);
461+
border: 1px solid rgba(255, 255, 255, 0.22);
462+
}
463+
464+
.verify-meta-label {
465+
display: block;
466+
margin-bottom: 4px;
467+
font-size: var(--fs-8);
468+
color: var(--light-gray-70);
469+
text-transform: uppercase;
470+
letter-spacing: 0.05em;
471+
}
472+
473+
.verify-meta-value {
474+
font-size: var(--fs-6);
475+
color: var(--white-2);
476+
font-weight: var(--fw-500);
477+
word-break: break-word;
478+
}
479+
480+
.verify-note {
481+
padding: 14px 16px;
482+
border-radius: 14px;
483+
background: rgba(255, 255, 255, 0.28);
484+
border: 1px solid rgba(255, 255, 255, 0.2);
485+
font-size: var(--fs-6);
486+
line-height: 1.6;
487+
color: var(--light-gray);
488+
}
489+
490+
.verify-note-warn {
491+
background: rgba(184, 12, 0, 0.08);
492+
border-color: rgba(184, 12, 0, 0.12);
493+
}
494+
495+
.verify-result-footer {
496+
display: flex;
497+
justify-content: space-between;
498+
align-items: center;
499+
gap: 12px;
500+
flex-wrap: wrap;
501+
margin-top: 2px;
502+
}
503+
378504
.verify-status.verified,
379505
.verify-status.revoked {
380506
animation: verify-pop 320ms ease-out both, verify-glow 900ms ease-out 1;
@@ -465,7 +591,7 @@ article.active {
465591
}
466592

467593
.verify-contact {
468-
margin-top: 10px;
594+
margin: 0;
469595
color: var(--light-gray);
470596
font-size: var(--fs-8);
471597
}
@@ -474,9 +600,13 @@ article.active {
474600

475601
/* Suggestions and action button */
476602
.verify-suggestion {
477-
margin-top: 8px;
603+
padding: 14px 16px;
604+
border-radius: 14px;
605+
margin: 0;
478606
color: var(--light-gray);
479607
font-size: var(--fs-8);
608+
background: rgba(255, 255, 255, 0.28);
609+
border: 1px solid rgba(255, 255, 255, 0.2);
480610
}
481611

482612
.verify-action-btn {
@@ -496,6 +626,20 @@ article.active {
496626
border-color: rgba(0,0,0,0.08);
497627
}
498628

629+
@media (max-width: 767px) {
630+
.verify-details-grid {
631+
grid-template-columns: 1fr;
632+
}
633+
634+
.verify-result-card {
635+
padding: 16px;
636+
}
637+
638+
.verify-result-title {
639+
font-size: var(--fs-3);
640+
}
641+
}
642+
499643

500644

501645

@@ -2231,18 +2375,6 @@ textarea.form-input::-webkit-resizer {
22312375
* CUSTOM PROPERTY
22322376
*/
22332377

2234-
:root {
2235-
2236-
/**
2237-
* shadow
2238-
*/
2239-
2240-
/* --shadow-1: -4px 8px 24px hsla(0, 0%, 0%, 0.125);
2241-
--shadow-2: 0 16px 30px hsla(0, 0%, 0%, 0.125);
2242-
--shadow-3: 0 16px 40px hsla(0, 0%, 0%, 0.125); */
2243-
2244-
}
2245-
22462378

22472379

22482380
/**

backend/app/crud.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def create_certificate(db: Session, cert_in: schemas.CertificateCreate):
2424
course_title=cert_in.course_title,
2525
completion_date=cert_in.completion_date,
2626
duration_hours=cert_in.duration_hours,
27+
attendance_percentage=cert_in.attendance_percentage,
28+
assignment_completion_percentage=cert_in.assignment_completion_percentage,
29+
course_level=cert_in.course_level,
30+
course_format=cert_in.course_format,
31+
instruction_language=cert_in.instruction_language,
2732
issuer=cert_in.issuer or "MathCodeLab",
2833
instructor=cert_in.instructor or "Mohammad Orabe",
2934
status=models.CertificateStatus.valid,

backend/app/database.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from sqlalchemy import create_engine
2+
from sqlalchemy import create_engine, inspect
33
from sqlalchemy.orm import sessionmaker, declarative_base
44

55
DATABASE_URL = os.getenv("DATABASE_URL")
@@ -25,6 +25,43 @@
2525
Base = declarative_base()
2626

2727

28+
def ensure_certificate_schema(bind=None):
29+
"""Create the certificates table and add any missing columns.
30+
31+
This keeps older PostgreSQL/SQLite databases compatible when the
32+
certificate model gains new optional fields.
33+
"""
34+
target = bind or engine
35+
36+
# Make sure the table exists first.
37+
Base.metadata.create_all(bind=target)
38+
39+
inspector = inspect(target)
40+
if "certificates" not in inspector.get_table_names():
41+
return
42+
43+
existing_columns = {column["name"] for column in inspector.get_columns("certificates")}
44+
missing_statements = []
45+
46+
if "attendance_percentage" not in existing_columns:
47+
missing_statements.append("ALTER TABLE certificates ADD COLUMN attendance_percentage INTEGER")
48+
if "assignment_completion_percentage" not in existing_columns:
49+
missing_statements.append("ALTER TABLE certificates ADD COLUMN assignment_completion_percentage INTEGER")
50+
if "course_level" not in existing_columns:
51+
missing_statements.append("ALTER TABLE certificates ADD COLUMN course_level VARCHAR")
52+
if "course_format" not in existing_columns:
53+
missing_statements.append("ALTER TABLE certificates ADD COLUMN course_format VARCHAR")
54+
if "instruction_language" not in existing_columns:
55+
missing_statements.append("ALTER TABLE certificates ADD COLUMN instruction_language VARCHAR")
56+
57+
if not missing_statements:
58+
return
59+
60+
with target.begin() as connection:
61+
for statement in missing_statements:
62+
connection.exec_driver_sql(statement)
63+
64+
2865
def get_db():
2966
db = SessionLocal()
3067
try:

backend/app/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,24 @@
99

1010
ENABLE_DOCS = os.getenv("ENABLE_DOCS", "false").lower() == "true"
1111

12+
# How to access docs when needed:
13+
# ENABLE_DOCS=true uvicorn app.main:app --reload
14+
# http://127.0.0.1:8000/docs
15+
# Temporary in production (optional):
16+
# ENABLE_DOCS=true
17+
# Then:
18+
# * Redeploy
19+
# * Use /docs
20+
# * Then set back to false
21+
1222
app = FastAPI(
1323
title="MathCodeLab Certificate Verification API",
1424
docs_url="/docs" if ENABLE_DOCS else None,
1525
redoc_url="/redoc" if ENABLE_DOCS else None,
1626
openapi_url="/openapi.json" if ENABLE_DOCS else None,
1727
)
1828

19-
models.Base.metadata.create_all(bind=database.engine)
29+
database.ensure_certificate_schema()
2030

2131
# CORS
2232
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")

backend/app/schemas.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ class CertificateBase(BaseModel):
66
course_title: str
77
completion_date: str
88
duration_hours: int
9+
attendance_percentage: Optional[int] = None
10+
assignment_completion_percentage: Optional[int] = None
11+
course_level: Optional[str] = None
12+
course_format: Optional[str] = None
13+
instruction_language: Optional[str] = None
914
issuer: Optional[str] = "MathCodeLab"
1015
instructor: Optional[str] = "Mohammad Orabe"
1116

backend/scripts/create_certificate.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,50 @@
11
import sys
22
import os
3+
from datetime import datetime
34
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
45
from app import database, models, crud, schemas
56
from sqlalchemy.orm import Session
67

8+
9+
def prompt_int(prompt, min_value=None, max_value=None):
10+
while True:
11+
raw = input(prompt).strip()
12+
try:
13+
value = int(raw)
14+
except ValueError:
15+
print("Please enter a whole number.")
16+
continue
17+
18+
if min_value is not None and value < min_value:
19+
print(f"Please enter a value of at least {min_value}.")
20+
continue
21+
if max_value is not None and value > max_value:
22+
print(f"Please enter a value of at most {max_value}.")
23+
continue
24+
25+
return value
26+
27+
28+
def prompt_date(prompt):
29+
while True:
30+
raw = input(prompt).strip()
31+
try:
32+
datetime.strptime(raw, "%Y-%m-%d")
33+
return raw
34+
except ValueError:
35+
print("Please enter a valid date in YYYY-MM-DD format.")
36+
737
def main():
38+
database.ensure_certificate_schema()
839
db = next(database.get_db())
940
print("Enter certificate details:")
1041

1142
student_name = input("Student name: ")
1243
course_title = input("Course title: ")
13-
completion_date = input("Completion date (YYYY-MM-DD): ")
14-
duration_hours = int(input("Duration (hours): "))
15-
attendance_percentage = int(input("Attendance percentage (0-100): "))
16-
assignment_completion_percentage = int(input("Assignment completion percentage (0-100): "))
44+
completion_date = prompt_date("Completion date (YYYY-MM-DD): ")
45+
duration_hours = prompt_int("Duration (hours): ", min_value=0)
46+
attendance_percentage = prompt_int("Attendance percentage (0-100): ", min_value=0, max_value=100)
47+
assignment_completion_percentage = prompt_int("Assignment completion percentage (0-100): ", min_value=0, max_value=100)
1748
course_level = input("Course level (e.g. Master-level, Bachelor-level, High School-level): ")
1849
course_format = input("Course format (e.g. Online (via Zoom), In-person, Hybrid): ")
1950
instruction_language = input("Instruction language (e.g. English, German, Arabic): ")

0 commit comments

Comments
 (0)