Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
/transcript_42/output/*
!/transcript_42/output/user_local.json
.env
6 changes: 3 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ services:
dockerfile: Dockerfile
ports:
- "127.0.0.1:8000:8000"
networks:
- nginx-network
# networks:
# - nginx-network
env_file:
- .env
volumes:
Expand All @@ -20,4 +20,4 @@ volumes:
driver_opts:
type: 'none'
o: 'bind'
device: './transcript_42/output'
device: '../transcript_42_shared_data'
17 changes: 10 additions & 7 deletions makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@

all: shared-folder docker-up
all: docker-up

docker-up:
docker compose up --build
docker-up: shared-folder
docker compose down
docker compose up --build -d

shared-folder:
mkdir -p ./transcript_42/output
chmod -R 777 ./transcript_42/output
mkdir -p ../transcript_42_shared_data
chmod -R 777 ../transcript_42_shared_data

run-local:
$(PYTHON) -m app.local
run-local: shared-folder
docker compose build transcript_42
docker compose run --rm transcript_42 python3 local.py
echo "The generated files are in ../transcript_42_shared_data"

ssl-certif:
bash reverse_proxy.sh
2 changes: 1 addition & 1 deletion transcript_42/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ COPY . .

WORKDIR /app/app

CMD ["gunicorn", "main:app", "--workers", "1", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000", "--access-logfile", "-", "--access-logformat", "%(t)s %(h)s %(r)s %(s)s %(b)s %(L)ss %(u)s %(q)s", "--error-logfile", "-"]
CMD bash -c "gunicorn main:app --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --access-logfile - --error-logfile - >> /app/output/logs.log 2>&1"
8 changes: 3 additions & 5 deletions transcript_42/app/config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import os
from dotenv import load_dotenv

load_dotenv()
UID = os.getenv("42UID")
SECRET = os.getenv("42SECRET")
UID = os.getenv("FORTYTWO_UID")
SECRET = os.getenv("FORTYTWO_SECRET")
REDIRECT_URI = os.getenv("OAUTH_REDIRECT_URI", "https://transcript42.project-cloud.cloud/oauth_redirect")
AUTH_URL = "https://api.intra.42.fr/oauth/authorize"
TOKEN_URL = "https://api.intra.42.fr/oauth/token"
USER_URL = "https://api.intra.42.fr/v2/me"
LOG_VIEW_PASSWORD = os.getenv("LOG_VIEW_PASSWORD", "changeme")
LOG_PATH = "/var/data/logs.log"
LOG_PATH = "/app/output/logs.log"
4 changes: 2 additions & 2 deletions transcript_42/app/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

# provide the "user_local.json" as your user API call response

with open("/app/output/user_local.json", "r", encoding="utf-8") as f:
with open("/app/local_input/local_user.json", "r", encoding="utf-8") as f:
user_data = json.load(f)
fill_latex_template(user_path = "/app/output/user_local",
user_data = user_data,
date_of_birth = "place-your-own",
location_of_birth = "plcae-your-own",
location_of_birth = "place-your-own",
language = "en",
transcript_type = "core_advanced")
generate_pdf(user_path = "/app/output/user_local")
Expand Down
15 changes: 11 additions & 4 deletions transcript_42/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,24 @@
import shutil
import tempfile

logging.basicConfig(
filename="/app/output/logs.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)

app = FastAPI()

@app.get("/")
def landing_page():
# Construct the OAuth URL for redirect
auth_url = f"{AUTH_URL}?{urllib.parse.urlencode({
params = {
'client_id': UID,
'redirect_uri': REDIRECT_URI,
'response_type': 'code',
'scope': 'public'
})}"
}
auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}"
return render_start_page(auth_url)


Expand All @@ -43,7 +50,7 @@ def create_transcript(
language: str = Form(...),
transcript_type: str = Form(...),
):
user_path = os.path.join("/var/data", f"user_{user_id}")
user_path = os.path.join("/app/output", f"user_{user_id}")
try:
with open(user_path + ".json", "r", encoding="utf-8") as f:
user_data = json.load(f)
Expand Down Expand Up @@ -94,7 +101,7 @@ def get_logs_full(x_api_key: str = Header(...)):
if x_api_key != LOG_VIEW_PASSWORD:
raise HTTPException(status_code=401, detail="Unauthorized")

log_dir = os.path.dirname("/app/output") # directory containing logs
log_dir = "/app/output" # directory containing logs

if not os.path.exists(log_dir):
raise HTTPException(status_code=404, detail="Log directory not found")
Expand Down
39 changes: 30 additions & 9 deletions transcript_42/app/services/fill_latex_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def parse_projects(data: dict, pj_dic: dict, language: str = "en") -> dict:
"name": project_data.get("name"),
"validated": entry.get("validated?"),
"marked_at": marked_at,
"created_at": entry.get("created_at"),
"description": description,
"grade": entry.get("final_mark"),
"hours": project_data.get("hours")
Expand Down Expand Up @@ -267,7 +268,14 @@ def organize_projects_by_category(parsed: dict) -> dict:

def prepare_template_variables(data: dict, parsed: dict, organized: dict, date_of_birth=None, location_of_birth=None, language=None, transcript_type=None) -> dict:
"""Prepare all variables required for the LaTeX template."""
school_address = """Harzer Straße 39\n12059 Berlin\nGERMANY"""
school_address = ""
campus_id = None
for campus in data["campus_users"]:
if campus.get("is_primary"):
campus_id = campus.get("campus_id") or 51
if campus_id == 52:
school_address = """Harzer Straße 39\n12059 Berlin\nGERMANY"""

first_name = data["first_name"].upper()
last_name = data["last_name"].upper()
# Format date_of_birth if provided
Expand All @@ -291,15 +299,27 @@ def prepare_template_variables(data: dict, parsed: dict, organized: dict, date_o
except Exception as e:
passed_selection = f"{month_name} {year}"

target_cursus = next(
(c for c in data["cursus_users"] if c["id"] in {244384, 196717}),
None
)
# target_cursus = next(
# (c for c in data["cursus_users"] if c["id"] in {244384, 196717}),
# None
# )
c_rld = parsed.get("C-piscine-reloaded")
libft = parsed.get("Libft")
core_started = None
if target_cursus:
raw_date = target_cursus["begin_at"]
dt = datetime.fromisoformat(raw_date.replace("Z", "+00:00"))
core_started = dt.strftime("%d.%m.%Y")
if core_started is None:
dates = []
for proj in (c_rld, libft):
if proj and proj.get("created_at"):
dt = datetime.fromisoformat(proj["created_at"].replace("Z", "+00:00"))
dates.append(dt)
if dates:
earliest = min(dates)
core_started = earliest.strftime("%d.%m.%Y")

# if target_cursus:
# raw_date = target_cursus["begin_at"]
# dt = datetime.fromisoformat(raw_date.replace("Z", "+00:00"))
# core_started = dt.strftime("%d.%m.%Y")
tran = parsed.get("ft_transcendence")
exam = parsed.get("Exam Rank 06")
if tran is not None and exam is not None and tran.get("validated") and exam.get("validated"):
Expand All @@ -321,6 +341,7 @@ def prepare_template_variables(data: dict, parsed: dict, organized: dict, date_o
"advanced_completed": advanced_completed,
"language": language,
"transcript_type": transcript_type,
"campus_id": campus_id
}
extra_vars.update(organized)
return extra_vars
Expand Down
6 changes: 6 additions & 0 deletions transcript_42/app/services/generate_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
import subprocess
import logging

logging.basicConfig(
filename="/app/output/logs.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)

def generate_pdf(user_path):
filled_template_path = user_path + ".tex"

Expand Down
33 changes: 24 additions & 9 deletions transcript_42/app/services/render_input_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ def render_profile_form(user_id: str) -> HTMLResponse:
background: #12141a;
margin: 0;
padding: 0;
height: 100vh;

/* Flex to center container vertically and horizontally */
min-height: 100vh;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
color: white;
}}
Expand All @@ -38,12 +36,11 @@ def render_profile_form(user_id: str) -> HTMLResponse:
color: white;
margin: 0;
line-height: 1.2;
}}
}}
h2 {{
margin-top: 4px;
margin-bottom: 30px;
}}

form label {{
display: block;
margin-bottom: 12px;
Expand Down Expand Up @@ -82,6 +79,20 @@ def render_profile_form(user_id: str) -> HTMLResponse:
button:hover {{
background: #0056b3;
}}
.note {{
max-width: 400px;
color: #cfcfcf;
font-size: 14px;
text-align: center;
margin-bottom: 40px;
}}
.note a {{
color: #00babc;
text-decoration: none;
}}
.note a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
Expand All @@ -95,15 +106,15 @@ def render_profile_form(user_id: str) -> HTMLResponse:
<div class="date-group">
<select name="dob_day" required>
<option value="">Day</option>
{''.join(f'<option value="{d}">{d}</option>' for d in range(1, 32))}
{''.join(f'<option value="{{d}}">{{d}}</option>' for d in range(1, 32))}
</select>
<select name="dob_month" required>
<option value="">Month</option>
{''.join(f'<option value="{m}">{m}</option>' for m in range(1, 13))}
{''.join(f'<option value="{{m}}">{{m}}</option>' for m in range(1, 13))}
</select>
<select name="dob_year" required>
<option value="">Year</option>
{''.join(f'<option value="{y}">{y}</option>' for y in range(1940, datetime.now().year - 10))}
{''.join(f'<option value="{{y}}">{{y}}</option>' for y in range(1940, datetime.now().year - 10))}
</select>
</div>
</label>
Expand All @@ -123,6 +134,10 @@ def render_profile_form(user_id: str) -> HTMLResponse:
<button type="submit">Generate Transcript</button>
</form>
</div>
<div class="note">
Only Berlin campus is fully supported so far. If you want to include your campus to have the logo, legal notes, and address featured in the PDF,
<a href="mailto:jubernar@student.42berlin.de">let's talk:</a>
</div>
<script>
document.querySelector('form').addEventListener('submit', function(e) {{
var day = document.querySelector('[name="dob_day"]').value;
Expand Down
21 changes: 20 additions & 1 deletion transcript_42/app/services/render_start_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def render_start_page(auth_url: str) -> HTMLResponse:
justify-content: center;
align-items: center;
color: white;
flex-direction: column;
}}
.container {{
max-width: 400px;
Expand All @@ -28,6 +29,7 @@ def render_start_page(auth_url: str) -> HTMLResponse:
padding: 32px;
box-sizing: border-box;
text-align: center;
margin-bottom: 16px;
}}
h1, h2 {{
margin: 0;
Expand All @@ -53,6 +55,19 @@ def render_start_page(auth_url: str) -> HTMLResponse:
a.button:hover {{
background: #009fa0;
}}
.note {{
max-width: 400px;
color: #cfcfcf;
font-size: 14px;
text-align: center;
}}
.note a {{
color: #00babc;
text-decoration: none;
}}
.note a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
Expand All @@ -61,6 +76,10 @@ def render_start_page(auth_url: str) -> HTMLResponse:
<h2>Academic Transcript</h2>
<a class="button" href="{auth_url}">Login with 42</a>
</div>
<div class="note">
Only Berlin campus is fully supported so far. If you want to include your campus to have the logo, legal notes, and address featured in the PDF,
<a href="mailto:jubernar@student.42berlin.de">let's talk:</a>
</div>
</body>
</html>
""")
""")
Binary file added transcript_42/graphics/42_Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added transcript_42/graphics/42_back.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"id": 136199,
"email": "max_the_dog@student.42berlin.de",
"email": "max_mustermann@student.42berlin.de",
"login": "max",
"first_name": "Max",
"last_name": "Doggo",
"usual_full_name": "Max Doggo",
"last_name": "mustermann",
"usual_full_name": "Max mustermann",
"usual_first_name": null,
"url": "https://api.intra.42.fr/v2/users/max_the_dog",
"url": "https://api.intra.42.fr/v2/users/max_mustermann",
"phone": "hidden",
"displayname": "Max Doggo",
"displayname": "Max mustermann",
"kind": "student",
"image": {
"link": "https://cdn.intra.42.fr/users/79d761cd6a342dba1dfeacaf3f0173a9/max_the_dog.jpg",
"link": "https://cdn.intra.42.fr/users/79d761cd6a342dba1dfeacaf3f0173a9/max_mustermann.jpg",
"versions": {
"large": "https://cdn.intra.42.fr/users/4e0c99018141fe5d1bc8fd63ff2190f3/large_max_the_dog.jpg",
"medium": "https://cdn.intra.42.fr/users/9fd8a15c518feea222dc594adfa74f7c/medium_max_the_dog.jpg",
"small": "https://cdn.intra.42.fr/users/a3b64b3a29c449a4ddd19e53cf707929/small_max_the_dog.jpg",
"micro": "https://cdn.intra.42.fr/users/1879f2b45199cc62f8db2d56a512c4d5/micro_max_the_dog.jpg"
"large": "https://cdn.intra.42.fr/users/4e0c99018141fe5d1bc8fd63ff2190f3/large_max_mustermann.jpg",
"medium": "https://cdn.intra.42.fr/users/9fd8a15c518feea222dc594adfa74f7c/medium_max_mustermann.jpg",
"small": "https://cdn.intra.42.fr/users/a3b64b3a29c449a4ddd19e53cf707929/small_max_mustermann.jpg",
"micro": "https://cdn.intra.42.fr/users/1879f2b45199cc62f8db2d56a512c4d5/micro_max_mustermann.jpg"
}
},
"staff?": false,
Expand Down
4 changes: 2 additions & 2 deletions transcript_42/projects/projects_dict.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
},
"2007": {
"name": "NetPractice",
"description": "Deploy Docker containers and virtual environments for system setup.",
"beschreibung": "Docker-Container und virtuelle Environments für System-Setup deployen.",
"description": "Configure IP routing and subnetting to build functional network connections.",
"beschreibung": "IP-Routing und Subnetze konfigurieren, um funktionale Netzwerke aufzubauen.",
"hours": "50 H"
},
"1338": {
Expand Down
Loading