Skip to content

Commit 59c9d31

Browse files
committed
Integrate Supabase for data management, enhance meta tags in base template, and adjust CSS layout
1 parent 39d9a33 commit 59c9d31

4 files changed

Lines changed: 161 additions & 130 deletions

File tree

main.py

Lines changed: 115 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,31 @@
1212
- Sample data for events and news is kept in-memory for simplicity.
1313
"""
1414

15+
import os
16+
import json
1517
from datetime import date
1618
from typing import List, Optional
19+
from supabase import create_client, Client
20+
from dotenv import load_dotenv
1721

1822
from fastapi import FastAPI, HTTPException, Request
1923
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
2024
from fastapi.staticfiles import StaticFiles
2125
from fastapi.templating import Jinja2Templates
2226
from pydantic import BaseModel
27+
from email_validator import validate_email, EmailNotValidError
2328

2429
app = FastAPI(title="Python Togo")
2530

2631
# Mount static files
2732
app.mount("/static", StaticFiles(directory="static"), name="static")
2833

2934
templates = Jinja2Templates(directory="templates")
35+
load_dotenv()
36+
SUBABASE_URL = os.getenv("SUPABASE_URL")
37+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
3038

39+
supabase: Client = create_client(SUBABASE_URL, SUPABASE_KEY)
3140

3241
# Simple in-memory sample data
3342
SAMPLE_EVENTS = [
@@ -84,7 +93,6 @@
8493
{
8594
"id": 1,
8695
"date": "2025-11-01",
87-
# Optional image URL for this news item; if missing, a placeholder will be used
8896
"image": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763440928/20251117_200618_tsfc73.jpg",
8997
"translations": {
9098
"fr": {
@@ -156,14 +164,6 @@
156164
},
157165
]
158166

159-
SAMPLE_COMMUNITIES = [
160-
{
161-
"id": 1,
162-
"name": "Python Lomé",
163-
"description": "Groupe local basé à Lomé.",
164-
"city": "Lomé",
165-
}
166-
]
167167

168168
TRANSLATIONS = {
169169
"fr": {
@@ -873,12 +873,6 @@ class JoinRequest(BaseModel):
873873
interests: str | None = None
874874

875875

876-
class ContactRequest(BaseModel):
877-
name: str
878-
email: str
879-
subject: str
880-
message: str
881-
882876

883877
class PartnershipRequest(BaseModel):
884878
organization: str
@@ -888,15 +882,6 @@ class PartnershipRequest(BaseModel):
888882
message: Optional[str] = None
889883

890884

891-
class JoinSubmit(BaseModel):
892-
name: str
893-
email: str
894-
city: Optional[str] = None
895-
level: Optional[str] = None
896-
agree_privacy: bool
897-
agree_coc: bool
898-
899-
900885
class ContactSubmit(BaseModel):
901886
name: str
902887
email: str
@@ -906,94 +891,50 @@ class ContactSubmit(BaseModel):
906891
agree_coc: bool
907892

908893

909-
# In-memory holders for partnership requests and partners
910-
PARTNERS = [
911-
{
912-
"name": "Python Software Foundation",
913-
"website": "https://www.python.org/",
914-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/b_rgb:457FAB/c_crop,w_1000,h_563,ar_16:9,g_auto/v1750463364/psf-logo_gqppfi.png",
915-
},
916-
{
917-
"name": "Association Francophone de Python",
918-
"website": "https://www.afpy.org/",
919-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1757104352/afpy_mizqfd.png",
920-
},
921-
{
922-
"name": "Django Software Foundation",
923-
"website": "https://www.djangoproject.com/",
924-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1757104362/django-logo-positive_ziry9u.png",
925-
},
926-
{
927-
"name": "Black Python Devs",
928-
"website": "https://blackpythondevs.com/index.html",
929-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1751558100/bpd_stacked_us5ika.png",
930-
},
931-
{
932-
"name": "O'REILLY",
933-
"website": "https://www.oreilly.com/",
934-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763335940/oreilly_logo_mark_red_efvlhr.svg",
935-
},
936-
{
937-
"name": "GitBook",
938-
"website": "https://www.gitbook.com/",
939-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763336328/GitBook_-_Dark_igckn1.png",
940-
},
941-
{
942-
"name": "Tahaga",
943-
"website": "https://tahaga.com/",
944-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1753966042/Logo_TAHAGA_02_Plan_de_travail_1_5_rh5s9g.jpg",
945-
},
946-
{
947-
"name": "DijiJob",
948-
"website": "https://wearedigijob.com",
949-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755176153/digijoblogo_tkbhns.png",
950-
},
951-
{
952-
"name": "Microsoft Student Ambassadors Togo",
953-
"website": "https://www.youtube.com/@mlsatogo",
954-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754294585/msftatogo_qrtxl9.png",
955-
},
956-
{
957-
"name": "GitHub",
958-
"website": "https://github.com/",
959-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1752712570/GitHub_Logo_pn7gcn.png",
960-
},
961-
{
962-
"name": "Kweku Tech",
963-
"website": "https://www.youtube.com/@KwekuTech",
964-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754092403/kwekutech_v4l7qn.png",
965-
},
966-
{
967-
"name": "AllDotPy",
968-
"website": "https://alldotpy.org/",
969-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755768009/dotpy_blue_transparent_irvs5g.png",
970-
},
971-
{
972-
"name": "IJEAF",
973-
"website": "https://www.linkedin.com/company/ijeaf/",
974-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1755768049/ijeaf_qjqsmf.jpg",
975-
},
976-
{
977-
"name": "TeDxVItChannai",
978-
"website": "https://www.ted.com/tedx/events/19414",
979-
"logo_url": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1754246473/tedxVitchennai_d5bkda.png",
980-
},
981-
]
982-
GALLERIES = [
983-
{
984-
"date": "2025-08-23",
985-
"title": "PyCon Togo 2025",
986-
"image": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1747588996/Group_6_r7n6id.png",
987-
"link": "https://drive.google.com/drive/folders/1Xk8lejAQXBIPjPf1UHnuUmZJ5sEYNPM1?usp=sharing",
988-
},
989-
{
990-
"date": "2024-11-30",
991-
"title": "PyDay Togo 2024",
992-
"image": "https://res.cloudinary.com/dvg7vky5o/image/upload/v1763346354/WUL_0330_rnemnd.jpg",
993-
"link": "https://drive.google.com/drive/folders/1UCZgBjcAaztwCi5R74WRqI8oxbCe1DNC?usp=sharing",
994-
},
995-
]
996-
PARTNERSHIP_REQUESTS: List[dict] = []
894+
def get_data(table):
895+
"""Fetch all data from a given Supabase table.
896+
897+
Parameters
898+
----------
899+
table : str
900+
The name of the table to query.
901+
902+
Returns
903+
-------
904+
list of dict
905+
The list of records from the table, or empty list on error.
906+
"""
907+
try:
908+
response = supabase.table(table).select("*").execute()
909+
return response.data
910+
except Exception:
911+
return []
912+
913+
def insert_data(table, data):
914+
"""Insert data into a given Supabase table.
915+
916+
Parameters
917+
----------
918+
table : str
919+
The name of the table to insert into.
920+
data : dict
921+
The data to insert.
922+
Returns
923+
-------
924+
bool
925+
True if insertion was successful, False otherwise.
926+
"""
927+
try:
928+
supabase.table(table).insert(data).execute()
929+
return True
930+
except Exception as e:
931+
print(f"Error inserting data into {table}: {e}")
932+
return False
933+
934+
PARTNERS = get_data("partners")
935+
936+
GALLERIES = get_data("galleries")
937+
997938
JOIN_REQUESTS: List[dict] = []
998939
CONTACT_MESSAGES: List[dict] = []
999940

@@ -1347,9 +1288,33 @@ async def partnership_submit(request: PartnershipRequest):
13471288
--------
13481289
``POST /api/v1/partnership`` with JSON body
13491290
"""
1350-
PARTNERSHIP_REQUESTS.append(request.dict())
1351-
# Here you would normally send an email or store the request in a DB
1352-
return JSONResponse(content={"status": "received"})
1291+
data = {}
1292+
ct = request.headers.get("content-type", "")
1293+
if "application/json" in ct:
1294+
data = await request.json()
1295+
data = PartnershipRequest(**data)
1296+
else:
1297+
form = await request.form()
1298+
data = dict(form)
1299+
data = PartnershipRequest(**data)
1300+
1301+
try:
1302+
validate_email(data.email, check_deliverability=True)
1303+
except EmailNotValidError as e:
1304+
return JSONResponse(status_code=400, content={"error": "Please use a valide email"})
1305+
1306+
# Normalize boolean fields
1307+
agree_privacy = data.agree_privacy in (True, "true", "True", "on", "1", 1)
1308+
agree_coc = data.agree_coc in (True, "true", "True", "on", "1", 1)
1309+
if not agree_privacy or not agree_coc:
1310+
return JSONResponse(status_code=400, content={"error": "consent_required"})
1311+
1312+
data = json.dumps(data.dict())
1313+
inserted = insert_data("partnershiprequest", data)
1314+
if inserted:
1315+
return JSONResponse(content={"status": "received"})
1316+
else:
1317+
return JSONResponse(content={"status": "Failed"})
13531318

13541319

13551320
@app.post("/api/v1/join")
@@ -1374,19 +1339,29 @@ async def join_submit(request: Request):
13741339
ct = request.headers.get("content-type", "")
13751340
if "application/json" in ct:
13761341
data = await request.json()
1342+
data = JoinRequest(**data)
13771343
else:
13781344
form = await request.form()
13791345
data = dict(form)
1346+
data = JoinRequest(**data)
1347+
1348+
try:
1349+
validate_email(data.email, check_deliverability=True)
1350+
except EmailNotValidError as e:
1351+
return JSONResponse(status_code=400, content={"error": "Please use a valide email"})
13801352

13811353
# Normalize boolean fields
1382-
agree_privacy = data.get("agree_privacy") in (True, "true", "True", "on", "1", 1)
1383-
agree_coc = data.get("agree_coc") in (True, "true", "True", "on", "1", 1)
1354+
agree_privacy = data.agree_privacy in (True, "true", "True", "on", "1", 1)
1355+
agree_coc = data.agree_coc in (True, "true", "True", "on", "1", 1)
13841356
if not agree_privacy or not agree_coc:
13851357
return JSONResponse(status_code=400, content={"error": "consent_required"})
1386-
1387-
# store
1388-
JOIN_REQUESTS.append(data)
1389-
return JSONResponse(content={"status": "received"})
1358+
1359+
data = json.dumps(data.dict())
1360+
inserted = insert_data("members", data)
1361+
if inserted:
1362+
return JSONResponse(content={"status": "received"})
1363+
else:
1364+
return JSONResponse(content={"status": "Failed"})
13901365

13911366

13921367
@app.post("/api/v1/contact")
@@ -1408,19 +1383,33 @@ async def contact_submit(request: Request):
14081383
"""
14091384
data = {}
14101385
ct = request.headers.get("content-type", "")
1386+
14111387
if "application/json" in ct:
14121388
data = await request.json()
1389+
data = ContactSubmit(**data) # validate
14131390
else:
14141391
form = await request.form()
14151392
data = dict(form)
1416-
1417-
agree_privacy = data.get("agree_privacy") in (True, "true", "True", "on", "1", 1)
1418-
agree_coc = data.get("agree_coc") in (True, "true", "True", "on", "1", 1)
1393+
data = ContactSubmit(**data)
1394+
1395+
try:
1396+
validate_email(data.email, check_deliverability=True)
1397+
except EmailNotValidError as e:
1398+
return JSONResponse(status_code=400, content={"error": "Please use a valide email"})
1399+
1400+
agree_privacy = data.agree_coc in (True, "true", "True", "on", "1", 1)
1401+
agree_coc = data.agree_coc in (True, "true", "True", "on", "1", 1)
14191402
if not agree_privacy or not agree_coc:
14201403
return JSONResponse(status_code=400, content={"error": "consent_required"})
14211404

1422-
CONTACT_MESSAGES.append(data)
1423-
return JSONResponse(content={"status": "received"})
1405+
data = json.dumps(data.dict())
1406+
1407+
inserted = insert_data("contacts", data)
1408+
if inserted:
1409+
return JSONResponse(content={"status": "received"})
1410+
else:
1411+
return JSONResponse(content={"status": "Failed"})
1412+
14241413

14251414

14261415
@app.get("/gallery")

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ uvicorn[standard]>=0.23.2
33
jinja2>=3.1.4
44
python-multipart>=0.0.20
55
pydantic>=1.10.0
6+
supabase>=0.7.1
7+
python-dotenv==1.2.1

static/css/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ header {
3939
display: flex;
4040
justify-content: space-between;
4141
align-items: center;
42-
gap: 20px;
42+
gap: 123px;
4343
}
4444

4545
.site-logo img { display:block; width:120px; height:auto; }

0 commit comments

Comments
 (0)