Skip to content
Open
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
66 changes: 59 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,65 @@
.env
secret*
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.egg
*.egg-info/
dist/
build/

# Environnements virtuels
.venv/
venv/
__pycache__/
*.pyc
env/
ENV/

# Variables d'environnement — ne jamais commiter
.env
app/.env
!*.env.example
!app/.env.example

# Secrets
secret*
*.key
*.pem

# Bases de données locales
*.db
*.sqlite3

# Logs
*.log
logs/

# IDEs
.vscode/
.idea/
*.iml
*.sublime-project
*.sublime-workspace

# OS
.DS_Store
Thumbs.db
desktop.ini

# Tests
.coverage
.pytest_cache/
.mypy_cache/
.tox/
htmlcov/
coverage.xml
mock*
fake*
test*
tests/
.vscode/
.idea/
.DS_Store

# Divers
*.bak
*.tmp
*.swp
*.swo
20 changes: 13 additions & 7 deletions app/core/imagekit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@
from imagekitio import ImageKit
from app.core.settings import settings

_client: ImageKit | None = None

imagekit = ImageKit(
private_key=settings.imagekit_private_key
)

URL_ENDPOINT = settings.imagekit_url_endpoint
def _get_client() -> ImageKit:
global _client
if _client is None:
if not settings.imagekit_private_key:
raise RuntimeError(
"IMAGEKIT_PRIVATE_KEY is not set. Configure it to use ImageKit."
)
_client = ImageKit(private_key=settings.imagekit_private_key)
return _client


def upload_image_base64_url(image_name, base64_string, folder=""):
def upload_image_base64_url(image_name: str, base64_string: str, folder: str = ""):
try:
upload_response = imagekit.files.upload(
client = _get_client()
upload_response = client.files.upload(
file=base64.b64decode(base64_string),
file_name=image_name,
folder="/pythontogo/" + folder.lstrip("/"),

)
return upload_response
except Exception as e:
Expand Down
22 changes: 11 additions & 11 deletions app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@
smtp_port=config("SMTP_PORT", default=587, cast=int),
smtp_user=config("SMTP_USER", default="user"),
smtp_password=config("SMTP_PASSWORD", default="password"),
paydunya_public_key=config("PAYDUNYA_PUBLIC_KEY"),
paydunya_private_key=config("PAYDUNYA_PRIVATE_KEY"),
paydunya_token=config("PAYDUNYA_TOKEN"),
paydunya_master_key=config("PAYDUNYA_MASTER_KEY"),
imagekit_private_key=config("IMAGEKIT_PRIVATE_KEY"),
imagekit_public_key=config("IMAGEKIT_PUBLIC_KEY"),
imagekit_url_endpoint=config("IMAGEKIT_URL_ENDPOINT"),
student_pass_template_url=config("STUDENT_PASS_TEMPLATE_URL"),
professional_pass_template_url=config("PROFESSIONAL_PASS_TEMPLATE_URL"),
premium_pass_template_url=config("PREMIUM_PASS_TEMPLATE_URL"),
dinner_pass_template_url=config("DINNER_PASS_TEMPLATE_URL")
paydunya_public_key=config("PAYDUNYA_PUBLIC_KEY", default=None),
paydunya_private_key=config("PAYDUNYA_PRIVATE_KEY", default=None),
paydunya_token=config("PAYDUNYA_TOKEN", default=None),
paydunya_master_key=config("PAYDUNYA_MASTER_KEY", default=None),
imagekit_private_key=config("IMAGEKIT_PRIVATE_KEY", default=None),
imagekit_public_key=config("IMAGEKIT_PUBLIC_KEY", default=None),
imagekit_url_endpoint=config("IMAGEKIT_URL_ENDPOINT", default=None),
student_pass_template_url=config("STUDENT_PASS_TEMPLATE_URL", default=None),
professional_pass_template_url=config("PROFESSIONAL_PASS_TEMPLATE_URL", default=None),
premium_pass_template_url=config("PREMIUM_PASS_TEMPLATE_URL", default=None),
dinner_pass_template_url=config("DINNER_PASS_TEMPLATE_URL", default=None)
)


Expand Down
6 changes: 3 additions & 3 deletions app/database/generate_sql_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ def normalize_value(value):

Returns:
-------
The normalized value, ready for use in SQL queries. For dictionaries, it returns a Jsonb object.
The normalized value, ready for use in SQL queries. For dictionaries and lists, it returns a Jsonb object.
"""
if isinstance(value, dict):
if isinstance(value, (dict, list)):
return Jsonb(value)
return value


def normalize_data(data: dict):
return {
k: str(v) if not isinstance(
v, (int, float, bool, dict, type(None))) else v
v, (int, float, bool, dict, list, type(None))) else v
for k, v in data.items()
}

Expand Down
33 changes: 33 additions & 0 deletions app/database/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@
'manual_correction'
);
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_location_enum') THEN
CREATE TYPE job_location_enum AS ENUM ('remote', 'onsite', 'hybrid');
END IF;

IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'contract_type_enum') THEN
CREATE TYPE contract_type_enum AS ENUM ('full-time', 'part-time', 'internship', 'contract');
END IF;
END
$$;
"""
Expand Down Expand Up @@ -417,13 +425,38 @@
ON DELETE CASCADE
);""",

"""
CREATE TABLE IF NOT EXISTS job_offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
company VARCHAR(255) NOT NULL,
logo_url TEXT,
location job_location_enum NOT NULL,
contract_type contract_type_enum NOT NULL,
apply_url TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
salary_range VARCHAR(255),
application_deadline TIMESTAMPTZ,
tags JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_job_offers_event
FOREIGN KEY (event_id)
REFERENCES events(id)
ON DELETE CASCADE,
CONSTRAINT uq_job_offers_event_company_title UNIQUE (event_id, company, title)
);""",

]


CREATE_INDEX_QUERIES = [
"CREATE INDEX IF NOT EXISTS idx_sponsors_partners_event_id ON sponsors_partners(event_id);",
"CREATE INDEX IF NOT EXISTS idx_api_keys_event_id ON api_keys(event_id);",
"CREATE INDEX IF NOT EXISTS idx_job_offers_event_id ON job_offers(event_id);",
"CREATE INDEX IF NOT EXISTS idx_job_offers_is_active ON job_offers(is_active);",
]


Expand Down
8 changes: 7 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,20 @@ async def lifespan(app: FastAPI):
await app.state.redis_client.close()


_is_dev = settings.env in ["dev", "local", "development"]

app = FastAPI(
title=settings.app_name,
version="2.1.0",
license_info={
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
lifespan=lifespan)
lifespan=lifespan,
openapi_url="/openapi.json" if _is_dev else None,
docs_url="/docs" if _is_dev else None,
redoc_url="/redoc" if _is_dev else None,
)


app.add_middleware(
Expand Down
2 changes: 2 additions & 0 deletions app/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from app.routers.tickets import api_router as tickets_router
from app.routers.registrations import api_router as registrations_router
from app.routers.helper import app_router as helper_router
from app.routers.job_offers import api_router as job_offers_router
from fastapi import APIRouter
from app.core.security import verify_api_key

Expand All @@ -31,4 +32,5 @@
api_routers.include_router(checkout_router)
api_routers.include_router(registrations_router)
api_routers.include_router(tickets_router)
api_routers.include_router(job_offers_router)
api_routers.include_router(helper_router)
111 changes: 111 additions & 0 deletions app/routers/job_offers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status

from app.database.connection import get_db_connection
from app.schemas.models import JobOfferCreate, JobOfferSummary, JobOfferUpdate, MessageResponse
from app.utils.job_offers import (
add_job_offer,
delete_job_offer,
get_all_job_offers,
get_job_offer_by_id,
get_job_offers_by_event,
update_job_offer,
)
from app.core.settings import logger


api_router = APIRouter(prefix="/job-offers", tags=["job-offers"])


@api_router.post(
"/create/{event_code}",
response_model=MessageResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_job_offer(
job_offer: JobOfferCreate,
event_code: str,
background_tasks: BackgroundTasks,
db=Depends(get_db_connection),
):
"""Create a new job offer for an event."""
try:
return await add_job_offer(db, job_offer, event_code, background_tasks)
except Exception as e:
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.get("/list/{event_code}", response_model=list[JobOfferSummary])
async def list_job_offers(event_code: str, db=Depends(get_db_connection)):
"""List all active job offers for a specific event."""
try:
job_offers = await get_job_offers_by_event(db, event_code)
if not job_offers:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No job offers found for this event",
)
return job_offers
except Exception as e:
logger.error(f"Error listing job offers: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.get("/list", response_model=list[JobOfferSummary])
async def list_all_job_offers(db=Depends(get_db_connection)):
"""List all job offers across all events."""
try:
return await get_all_job_offers(db)
except Exception as e:
logger.error(f"Error listing all job offers: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.get("/{job_offer_id}", response_model=JobOfferSummary)
async def get_job_offer(job_offer_id: str, db=Depends(get_db_connection)):
"""Retrieve a job offer by its ID."""
try:
return await get_job_offer_by_id(db, job_offer_id)
except Exception as e:
logger.error(f"Error retrieving job offer {job_offer_id}: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.put("/update/{job_offer_id}", response_model=MessageResponse)
async def update_job_offer_details(
job_offer_id: str,
job_offer_update: JobOfferUpdate,
background_tasks: BackgroundTasks,
db=Depends(get_db_connection),
):
"""Update an existing job offer."""
try:
return await update_job_offer(db, job_offer_id, job_offer_update, background_tasks)
except Exception as e:
logger.error(f"Error updating job offer {job_offer_id}: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")


@api_router.delete("/delete/{job_offer_id}", response_model=MessageResponse)
async def delete_job_offer_by_id(
job_offer_id: str,
background_tasks: BackgroundTasks,
db=Depends(get_db_connection),
):
"""Delete a job offer by its ID."""
try:
return await delete_job_offer(db, job_offer_id, background_tasks)
except Exception as e:
logger.error(f"Error deleting job offer {job_offer_id}: {str(e)}")
if isinstance(e, HTTPException):
raise e
raise HTTPException(status_code=500, detail="Internal server error")
22 changes: 11 additions & 11 deletions app/schemas/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ class Config(BaseModel):
smtp_port: int = 587
smtp_user: str = "user"
smtp_password: str = "password"
paydunya_public_key: str
paydunya_private_key: str
paydunya_token: str
paydunya_master_key: str
imagekit_public_key: str
imagekit_private_key: str
imagekit_url_endpoint: str
student_pass_template_url: str
professional_pass_template_url: str
premium_pass_template_url: str
dinner_pass_template_url: str
paydunya_public_key: str | None = None
paydunya_private_key: str | None = None
paydunya_token: str | None = None
paydunya_master_key: str | None = None
imagekit_public_key: str | None = None
imagekit_private_key: str | None = None
imagekit_url_endpoint: str | None = None
student_pass_template_url: str | None = None
professional_pass_template_url: str | None = None
premium_pass_template_url: str | None = None
dinner_pass_template_url: str | None = None
Loading