HR-ATS-Portal/backend/org_settings/views.py

185 lines
7.2 KiB
Python

import uuid
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from org_settings.models import ExcludeCompany, ExcludeUniversity, OrgSettings
from org_settings.serializers import (
serialize_exclude_company,
serialize_exclude_university,
serialize_org_setting,
)
VALID_CATEGORIES = (
"general",
"notifications",
"email_templates",
"career_portal",
"branding",
"security",
# REQ-ANL-08: holds `analytics.tth_baseline` ({"days": N, "source": "..."}),
# surfaced by /analytics/kpis/fetch as tth_baseline_* fields.
"analytics",
)
def _as_uuid(value):
if value in (None, ""):
return None
try:
return uuid.UUID(str(value))
except (TypeError, ValueError):
return None
def _user_id(current_user):
if not current_user or not current_user.get("id"):
raise HTTPException(status_code=401, detail="Not authenticated")
uid = _as_uuid(current_user["id"])
if uid is None:
raise HTTPException(status_code=401, detail="Invalid user id")
return uid
class OrgSetting:
def __init__(self, session: AsyncSession):
self.session = session
async def get_settings(self, category=None):
if category and category not in VALID_CATEGORIES:
raise HTTPException(
status_code=422, detail=f"category must be one of {', '.join(VALID_CATEGORIES)}"
)
rows, total = await OrgSettings.fetch_settings(self.session, category=category)
return [serialize_org_setting(r) for r in rows], total
async def update_settings(self, payload, current_user):
items = payload.get("settings") or []
if not items:
raise HTTPException(status_code=400, detail="settings is required")
cleaned = []
for item in items:
key = (item.get("key") or "").strip()
category = (item.get("category") or "").strip()
if not key:
raise HTTPException(status_code=422, detail="key is required")
if category not in VALID_CATEGORIES:
raise HTTPException(
status_code=422, detail=f"category must be one of {', '.join(VALID_CATEGORIES)}"
)
cleaned.append({
"setting_key": key,
"setting_value": item.get("value"),
"category": category,
})
rows = await OrgSettings.upsert_settings(self.session, cleaned, _user_id(current_user))
return [serialize_org_setting(r) for r in rows]
MAX_NAME_LEN = 200
MAX_URL_LEN = 500
def _clean_name(value, *, label="name"):
name = (value or "").strip()
if not name:
raise HTTPException(status_code=422, detail=f"{label} is required")
if len(name) > MAX_NAME_LEN:
raise HTTPException(status_code=422, detail=f"{label} must be {MAX_NAME_LEN} characters or fewer")
return name
def _clean_linkedin_url(value):
url = (value or "").strip() or None
if url is None:
return None
if len(url) > MAX_URL_LEN:
raise HTTPException(status_code=422, detail=f"linkedin_url must be {MAX_URL_LEN} characters or fewer")
lowered = url.lower()
if not (lowered.startswith("http://") or lowered.startswith("https://")):
raise HTTPException(status_code=422, detail="linkedin_url must be an http(s) URL")
return url
class Exclusion:
def __init__(self, session: AsyncSession):
self.session = session
async def get_universities(self):
rows, total = await ExcludeUniversity.fetch_rows(self.session)
return [serialize_exclude_university(r) for r in rows], total
async def create_university(self, payload, current_user):
name = _clean_name(payload.get("name"), label="name")
if await ExcludeUniversity.get_by_name(self.session, name):
raise HTTPException(status_code=409, detail="That university is already excluded")
row = await ExcludeUniversity.insert_row(self.session, {
"name": name,
"created_by": _user_id(current_user),
})
return serialize_exclude_university(row)
async def update_university(self, record_id, payload, current_user):
_user_id(current_user)
fields = {}
if "name" in payload:
name = _clean_name(payload.get("name"), label="name")
existing = await ExcludeUniversity.get_by_name(self.session, name, exclude_id=record_id)
if existing:
raise HTTPException(status_code=409, detail="That university is already excluded")
fields["name"] = name
if not fields:
raise HTTPException(status_code=400, detail="No fields to update")
row = await ExcludeUniversity.update_row(self.session, record_id, fields)
if not row:
raise HTTPException(status_code=404, detail="Excluded university not found")
return serialize_exclude_university(row)
async def delete_university(self, record_id, current_user):
_user_id(current_user)
row = await ExcludeUniversity.soft_delete_row(self.session, record_id)
if not row:
raise HTTPException(status_code=404, detail="Excluded university not found")
return {"id": str(row.id), "deleted": True}
async def get_companies(self):
rows, total = await ExcludeCompany.fetch_rows(self.session)
return [serialize_exclude_company(r) for r in rows], total
async def create_company(self, payload, current_user):
name = _clean_name(payload.get("name"), label="name")
linkedin_url = _clean_linkedin_url(payload.get("linkedin_url"))
if await ExcludeCompany.get_by_name(self.session, name):
raise HTTPException(status_code=409, detail="That company is already excluded")
row = await ExcludeCompany.insert_row(self.session, {
"name": name,
"linkedin_url": linkedin_url,
"created_by": _user_id(current_user),
})
return serialize_exclude_company(row)
async def update_company(self, record_id, payload, current_user):
_user_id(current_user)
fields = {}
if "name" in payload:
name = _clean_name(payload.get("name"), label="name")
existing = await ExcludeCompany.get_by_name(self.session, name, exclude_id=record_id)
if existing:
raise HTTPException(status_code=409, detail="That company is already excluded")
fields["name"] = name
if "linkedin_url" in payload:
fields["linkedin_url"] = _clean_linkedin_url(payload.get("linkedin_url"))
if not fields:
raise HTTPException(status_code=400, detail="No fields to update")
row = await ExcludeCompany.update_row(self.session, record_id, fields)
if not row:
raise HTTPException(status_code=404, detail="Excluded company not found")
return serialize_exclude_company(row)
async def delete_company(self, record_id, current_user):
_user_id(current_user)
row = await ExcludeCompany.soft_delete_row(self.session, record_id)
if not row:
raise HTTPException(status_code=404, detail="Excluded company not found")
return {"id": str(row.id), "deleted": True}