Skip to content
Merged
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
28 changes: 28 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,33 @@
ON DELETE CASCADE
);""",

"""
CREATE TABLE IF NOT EXISTS job_offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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,
country VARCHAR(255),
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(),
);""",

]


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_is_active ON job_offers(is_active);",
"CREATE INDEX IF NOT EXISTS idx_job_offers_company ON job_offers(company);",
]


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()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Steventog, merci beaucoup pour ta contribution.



_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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On'a besoin de la docs pour l'intregration. pour le moment on va le garderet travailler a mettre une authentification au lieux de le bloquer carrement.

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)
103 changes: 103 additions & 0 deletions app/routers/job_offers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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_active_job_offers,
get_all_job_offers,
get_job_offer_by_id,
update_job_offer,
)
from app.core.settings import logger


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


@api_router.post("/create", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_job_offer(
job_offer: JobOfferCreate,
background_tasks: BackgroundTasks,
db=Depends(get_db_connection),
):
"""Create a new job offer."""
try:
return await add_job_offer(db, job_offer, 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/active", response_model=list[JobOfferSummary])
async def list_active_job_offers(db=Depends(get_db_connection)):
"""List all active job offers."""
try:
job_offers = await get_active_job_offers(db)
if not job_offers:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No active job offers found",
)
return job_offers
except Exception as e:
logger.error(f"Error listing active 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 (admin)."""
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:
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:
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:
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