185 lines
7.2 KiB
Python
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}
|