View edits

This commit is contained in:
Xander 2025-03-27 09:57:13 +02:00
parent 1a3f5b7c1a
commit 5c207b4771
17 changed files with 13572 additions and 2696 deletions

12562
logs/app.log

File diff suppressed because it is too large Load Diff

0
model/__init__.py Normal file
View File

View File

@ -20,7 +20,9 @@ load_dotenv()
DATABASE_URL = os.getenv('DATABASE_URL') # Должно быть "mysql+aiomysql://..." или "postgresql+asyncpg://..." DATABASE_URL = os.getenv('DATABASE_URL') # Должно быть "mysql+aiomysql://..." или "postgresql+asyncpg://..."
# Создаём асинхронный движок # Создаём асинхронный движок
async_engine = create_async_engine(DATABASE_URL, echo=True) # async_engine = create_async_engine(DATABASE_URL, echo=True)
async_engine = create_async_engine(DATABASE_URL, echo=True, pool_recycle=1800, pool_size=10, pool_pre_ping=True )
# Создаём фабрику сессий # Создаём фабрику сессий
async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) async_session = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
@ -124,6 +126,7 @@ class Job(Base):
active = Column(Boolean, default=True) # Вакансия активна? active = Column(Boolean, default=True) # Вакансия активна?
about = Column(Text, nullable=True) about = Column(Text, nullable=True)
date_posted = Column(DateTime) date_posted = Column(DateTime)
data_requested = Column(DateTime, default=datetime.utcnow)
text = Column(Text, nullable=True) text = Column(Text, nullable=True)

View File

@ -8,6 +8,7 @@ dependencies = [
"aiomysql>=0.2.0", "aiomysql>=0.2.0",
"aiosqlite>=0.21.0", "aiosqlite>=0.21.0",
"alembic>=1.14.1", "alembic>=1.14.1",
"beautifulsoup4>=4.13.3",
"fastapi>=0.115.8", "fastapi>=0.115.8",
"jinja2>=3.1.5", "jinja2>=3.1.5",
"linkedin-api>=2.3.1", "linkedin-api>=2.3.1",
@ -16,6 +17,7 @@ dependencies = [
"pymysql>=1.1.1", "pymysql>=1.1.1",
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"python-multipart>=0.0.20", "python-multipart>=0.0.20",
"requests>=2.32.3",
"schedule>=1.2.2", "schedule>=1.2.2",
"sqlalchemy>=2.0.38", "sqlalchemy>=2.0.38",
"uvicorn>=0.34.0", "uvicorn>=0.34.0",

View File

@ -7,7 +7,7 @@ from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import json import json
from model.database import get_async_session, Client from model.database import get_async_session, Client
from utils.clients import upsert_client, del_jobs, add_jobs, get_applied_jobs from utils.clients import upsert_client, del_jobs, add_jobs, get_applied_jobs, get_filtered_jobs
from typing import Union from typing import Union
import asyncio import asyncio
@ -87,6 +87,23 @@ async def client(data: JsonData, x_api_key: str = Header(...), db: Session = Dep
print("Job Level:", ", ".join(job_level_values)) print("Job Level:", ", ".join(job_level_values))
print("Job Type:", ", ".join(job_type_values)) print("Job Type:", ", ".join(job_type_values))
print("Location Type:", ", ".join(location_type_values)) print("Location Type:", ", ".join(location_type_values))
# Пример использования функции
jobs = await get_filtered_jobs(
db,
user_job_titles=["Electronics Engineer", "Hardware Engineer"],
minimum_annual_salary=None,
salary_currency=None,
user_location_type=None,
user_locations=["Burnaby, Canada", "Vancouver, Canada", "Toronto, Canada"],
user_levels=["Mid", "Senior", "Manager"],
user_job_types=["Full-time", "Permanent"]
)
# Выводим вакансии
for job in jobs:
print(job.job_title, job.location, job.job_level, job.job_type)
# ads = await add_jobs(db, user_id) # ads = await add_jobs(db, user_id)
@ -101,7 +118,7 @@ async def client(data: JsonData, x_api_key: str = Header(...), db: Session = Dep
# print(f"Неверный формат данных: {type(data.json_data)}") # print(f"Неверный формат данных: {type(data.json_data)}")
raise HTTPException(status_code=400, detail="Invalid data format") raise HTTPException(status_code=400, detail="Invalid data format")
return {"message": "JSON получен", "data": 'get_jobs'} return {"message": "JSON получен", "data": jobs }

File diff suppressed because it is too large Load Diff

View File

@ -6429,10 +6429,11 @@ github.com style (c) Vasily Polovnyov <vast@whiteants.net>
background-color: #fff; background-color: #fff;
background-color: rgba(255, 255, 255, var(--bg-opacity)); background-color: rgba(255, 255, 255, var(--bg-opacity));
} }
/* background: linear-gradient(to bottom, #1C3FAA, #2B51B4); 7b52fc*/
@media (max-width: 1279px) { @media (max-width: 1279px) {
.login { .login {
background: linear-gradient(to bottom, #1C3FAA, #2B51B4); background: linear-gradient(to bottom, #7b52fc, #7b52fc);
background-repeat: no-repeat; background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
} }
@ -6951,7 +6952,7 @@ select.input.input--lg {
} }
html { html {
background: #1C3FAA; background: #7a61fe;
} }
body { body {

BIN
static/dist/images/333.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -4,8 +4,8 @@
<rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/> <rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
</clipPath> </clipPath>
<linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox"> <linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#2b51b4"/> <stop offset="0" stop-color="#7a61fe"/>
<stop offset="1" stop-color="#1c3faa"/> <stop offset="1" stop-color="#7a61fe"/>
</linearGradient> </linearGradient>
</defs> </defs>
<g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)"> <g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -24,7 +24,7 @@
<nav class="side-nav"> <nav class="side-nav">
<a href="/" class="intro-x flex items-center pl-5 pt-4"> <a href="/" class="intro-x flex items-center pl-5 pt-4">
<img alt="Midone Tailwind HTML Admin Template" class="w-6" src="/static/dist/images/logo.svg"> <img alt="Midone Tailwind HTML Admin Template" class="w-6" src="/static/dist/images/logo.svg">
<span class="hidden xl:block text-white text-lg ml-3"> LI<span class="font-medium">NK</span> </span> <span class="hidden xl:block text-white text-lg ml-3"> Turbo<span class="font-medium">Аpply</span> </span>
</a> </a>
<div class="side-nav__devider my-6"></div> <div class="side-nav__devider my-6"></div>
<ul> <ul>
@ -39,7 +39,7 @@
<li> <li>
<a href="/users" class="side-menu {% if current_path.startswith('/users') %}side-menu--active{% endif %}"> <a href="/users" class="side-menu {% if current_path.startswith('/users') %}side-menu--active{% endif %}">
<div class="side-menu__icon"> <i data-feather="users"></i> </div> <div class="side-menu__icon"> <i data-feather="users"></i> </div>
<div class="side-menu__title"> Users <i data-feather="chevron-down" class="side-menu__sub-icon"></i> </div> <div class="side-menu__title"> Users </div>
</a> </a>
<ul class=""> <ul class="">
</li> </li>
@ -53,14 +53,14 @@
<div class="content"> <div class="content">
<div class="top-bar"> <div class="top-bar">
<!-- BEGIN: Breadcrumb --> <!-- BEGIN: Breadcrumb -->
<div class="-intro-x breadcrumb mr-auto hidden sm:flex"> <a href="" class="">Application</a> <i data-feather="chevron-right" class="breadcrumb__icon"></i> <a href="" class="breadcrumb--active">{{size}}</a> </div> <div class="-intro-x breadcrumb mr-auto hidden sm:flex"> </div>
<!-- END: Breadcrumb --> <!-- END: Breadcrumb -->
<!-- BEGIN: Account Menu --> <!-- BEGIN: Account Menu -->
<div class="intro-x dropdown w-8 h-8 relative"> <div class="intro-x dropdown w-8 h-8 relative">
<div class="dropdown-toggle w-8 h-8 rounded-full overflow-hidden shadow-lg image-fit zoom-in"> <div class="dropdown-toggle w-8 h-8 rounded-full overflow-hidden shadow-lg image-fit zoom-in">
<img alt="Midone Tailwind HTML Admin Template" src="/static/dist/images/profile-12.jpg"> <img alt="Midone Tailwind HTML Admin Template" src="/static/dist/images/333.png">
</div> </div>
<div class="dropdown-box mt-10 absolute w-56 top-0 right-0 z-20"> <div class="dropdown-box mt-10 absolute w-56 top-0 right-0 z-20">
<div class="dropdown-box__content box bg-theme-38 text-white"> <div class="dropdown-box__content box bg-theme-38 text-white">

View File

@ -50,16 +50,16 @@ License: You must have a valid license purchased only from themeforest(the above
<div class="hidden xl:flex flex-col min-h-screen"> <div class="hidden xl:flex flex-col min-h-screen">
<a href="" class="-intro-x flex items-center pt-5"> <a href="" class="-intro-x flex items-center pt-5">
<img alt="Midone Tailwind HTML Admin Template" class="w-6" src="/static/dist/images/logo.svg"> <img alt="Midone Tailwind HTML Admin Template" class="w-6" src="/static/dist/images/logo.svg">
<span class="text-white text-lg ml-3"> LI<span class="font-medium">NK</span> </span> <span class="text-white text-lg ml-3"> Turbo<span class="font-medium">Аpply</span> </span>
</a> </a>
<div class="my-auto"> <div class="my-auto">
<img alt="Midone Tailwind HTML Admin Template" class="-intro-x w-1/2 -mt-16" src="/static/dist/images/illustration.svg"> <img alt="Midone Tailwind HTML Admin Template" class="-intro-x w-1/2 -mt-16" src="/static/dist/images/illustration.svg">
<div class="-intro-x text-white font-medium text-4xl leading-tight mt-10"> <div class="-intro-x text-white font-medium text-4xl leading-tight mt-10">
Admin panel Admin panel
<br> <br>
LINK. TurboАpply
</div> </div>
<div class="-intro-x mt-5 text-lg text-white">Manage all your e-commerce accounts in one place</div> <div class="-intro-x mt-5 text-lg text-white">Data Handler Portal</div>
</div> </div>
</div> </div>
<!-- END: Login Info --> <!-- END: Login Info -->
@ -72,7 +72,7 @@ License: You must have a valid license purchased only from themeforest(the above
<div class="intro-x mt-2 text-gray-500 xl:hidden text-center">A few more clicks to sign in to your account. Manage all your e-commerce accounts in one place</div> <div class="intro-x mt-2 text-gray-500 xl:hidden text-center">A few more clicks to sign in to your account. Manage all your e-commerce accounts in one place</div>
<div class="intro-x mt-8"> <div class="intro-x mt-8">
<form method="post" action="/login"> <form method="post" action="/login">
<input type="text" name="username" class="intro-x login__input input input--lg border border-gray-300 block" placeholder="Email"> <input type="text" name="username" class="intro-x login__input input input--lg border border-gray-300 block" placeholder="Login">
<input type="password" name="password" class="intro-x login__input input input--lg border border-gray-300 block mt-4" placeholder="Password"> <input type="password" name="password" class="intro-x login__input input input--lg border border-gray-300 block mt-4" placeholder="Password">
<div class="intro-x mt-5 xl:mt-8 text-center xl:text-left"> <div class="intro-x mt-5 xl:mt-8 text-center xl:text-left">
<button class="button button--lg w-full xl:w-32 text-white bg-theme-1 xl:mr-3" type="submit">Login</button> <button class="button button--lg w-full xl:w-32 text-white bg-theme-1 xl:mr-3" type="submit">Login</button>
@ -84,16 +84,14 @@ License: You must have a valid license purchased only from themeforest(the above
<input type="checkbox" class="input border mr-2" id="remember-me"> <input type="checkbox" class="input border mr-2" id="remember-me">
<label class="cursor-pointer select-none" for="remember-me">Remember me</label> <label class="cursor-pointer select-none" for="remember-me">Remember me</label>
</div> </div>
<a href="#">Forgot Password?</a>
</div> </div>
<!--<div class="intro-x mt-5 xl:mt-8 text-center xl:text-left"> <!--<div class="intro-x mt-5 xl:mt-8 text-center xl:text-left">
<button class="button button--lg w-full xl:w-32 text-white bg-theme-1 xl:mr-3">Login</button> <button class="button button--lg w-full xl:w-32 text-white bg-theme-1 xl:mr-3">Login</button>
<button class="button button--lg w-full xl:w-32 text-gray-700 border border-gray-300 mt-3 xl:mt-0">Sign up</button> <button class="button button--lg w-full xl:w-32 text-gray-700 border border-gray-300 mt-3 xl:mt-0">Sign up</button>
</div>--> </div>-->
<div class="intro-x mt-10 xl:mt-24 text-gray-700 text-center xl:text-left"> <div class="intro-x mt-10 xl:mt-24 text-gray-700 text-center xl:text-left">
By signin up, you agree to our
<br>
<a class="text-theme-1" href="">Terms and Conditions</a> & <a class="text-theme-1" href="">Privacy Policy</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,22 +6,26 @@
<div class="intro-y flex flex-col sm:flex-row items-center mt-8"> <div class="intro-y flex flex-col sm:flex-row items-center mt-8">
<h2 class="text-lg font-medium mr-auto"> <h2 class="text-lg font-medium mr-auto">
Datatable Tasks
</h2> </h2>
<button class="button text-white bg-theme-1 shadow-md mr-2" onclick="window.location.href='/productprom'">Unasigned Jobs</button>
<button class="button text-white bg-theme-1 shadow-md mr-2" onclick="window.location.href='/productnoprom'">My Jobs</button>
<button class="button text-white bg-theme-1 shadow-md mr-2" onclick="window.location.href='/productrozetka'">All Jobs</button>
<button class="button text-white bg-theme-1 shadow-md mr-2" onclick="window.location.href='/productnorozetka'">Outstanding Jobs</button>
</div> </div>
<!-- BEGIN: Datatable --> <!-- BEGIN: Datatable -->
<div class="intro-y datatable-wrapper box p-5 mt-5"> <div class="intro-y datatable-wrapper box p-5 mt-5">
<table class="table table-report table-report--bordered display datatable w-full"> <table class="table table-report table-report--bordered display datatable w-full">
<thead> <thead>
<tr> <tr>
<th class="border-b-2 whitespace-no-wrap">TITLE</th> <th class="border-b-2 whitespace-no-wrap">TITLE &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">COMPANY</th> <th class="border-b-2 text-center whitespace-no-wrap">COMPANY &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">CLIENT</th> <th class="border-b-2 text-center whitespace-no-wrap">CLIENT &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">Requested on</th> <th class="border-b-2 text-center whitespace-no-wrap">Requested on &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">Posted on</th> <th class="border-b-2 text-center whitespace-no-wrap">Posted on &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">STATUS</th> <th class="border-b-2 text-center whitespace-no-wrap">STATUS &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">Assignee</th> <th class="border-b-2 text-center whitespace-no-wrap">Assignee &#10760;</th>
<th class="border-b-2 text-center whitespace-no-wrap">Applied on</th> <th class="border-b-2 text-center whitespace-no-wrap">Applied on</th>
@ -42,7 +46,7 @@
</td> </td>
<td class="text-center border-b">Requested on</td> <td class="text-center border-b">Requested on</td>
<td class="text-center border-b">Posted on</td> <td class="text-center border-b">{{job.job.date_posted}}</td>
<td class="text-center border-b"> <td class="text-center border-b">
<select class="select2" onchange="updateStatus(this, '{{ job.id }}')"> <select class="select2" onchange="updateStatus(this, '{{ job.id }}')">
<option value="Scheduled" {% if job.status == "Scheduled" %}selected{% endif %}>Scheduled</option> <option value="Scheduled" {% if job.status == "Scheduled" %}selected{% endif %}>Scheduled</option>

View File

@ -40,7 +40,7 @@
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select></div> </select></div>
<button type="submit" class="button bg-theme-1 text-white mt-5">Login</button> <button type="submit" class="button bg-theme-1 text-white mt-5">Register</button>
</form> </form>

View File

@ -10,7 +10,8 @@ import time
from utils.logging_setup import configure_global_logging from utils.logging_setup import configure_global_logging
from datetime import datetime from datetime import datetime
import requests
from bs4 import BeautifulSoup
import asyncio import asyncio
import json import json
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -26,6 +27,10 @@ load_dotenv()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
}
# # Ваши учетные данные LinkedIn # # Ваши учетные данные LinkedIn
@ -49,13 +54,13 @@ def pars_jobs(geo):
] ]
# file_path = "search_jobes2.json" file_path = "search_jobes2.json"
# with open(file_path, "w", encoding="utf-8") as json_file:
# json.dump(search_jobes, json_file, indent=4, ensure_ascii=False)
file_path = "search_jobes3.json"
with open(file_path, "w", encoding="utf-8") as json_file: with open(file_path, "w", encoding="utf-8") as json_file:
json.dump(search_jobs, json_file, indent=4, ensure_ascii=False) json.dump(search_jobes, json_file, indent=4, ensure_ascii=False)
# file_path = "search_jobes3.json"
# with open(file_path, "w", encoding="utf-8") as json_file:
# json.dump(search_jobs, json_file, indent=4, ensure_ascii=False)
print(f"Результаты успешно сохранены в {file_path}") print(f"Результаты успешно сохранены в {file_path}")
@ -64,6 +69,25 @@ def add_to_bd():
#[ ]: Написать функцию записи в БД #[ ]: Написать функцию записи в БД
pass pass
async def typess(url):
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")
# Находим все элементы с описанием критериев вакансии
criteria = soup.find_all("span", class_="description__job-criteria-text")
if len(criteria) >= 2:
level = criteria[0].get_text(strip=True) # Уровень должности
job_type = criteria[1].get_text(strip=True) # Тип занятости
else:
level = "Не найдено"
job_type = "Не найдено"
# print(f"Job Level: {level}")
# print(f"Type of Employment: {job_type}")
return level, job_type
async def get_job(db: AsyncSession, job_id: str): async def get_job(db: AsyncSession, job_id: str):
try: try:
jobs = api.get_job(job_id) jobs = api.get_job(job_id)
@ -107,6 +131,9 @@ async def get_job(db: AsyncSession, job_id: str):
# job.location_type = localized_name # job.location_type = localized_name
# job.entity_urn = entity_urn # job.entity_urn = entity_urn
level, job_type = await typess(link)
# Проверка, есть ли вакансия в базе # Проверка, есть ли вакансия в базе
query = select(Job).filter(Job.job_id == job_id) query = select(Job).filter(Job.job_id == job_id)
result = await db.execute(query) result = await db.execute(query)
@ -123,6 +150,8 @@ async def get_job(db: AsyncSession, job_id: str):
job.date_posted = listed_ats job.date_posted = listed_ats
job.link_company = company_url job.link_company = company_url
job.location_type = localized_name job.location_type = localized_name
job.job_level = level
job.job_type = job_type
else: else:
logging.info(f"🆕 Добавление вакансии {job_id} в базу...") logging.info(f"🆕 Добавление вакансии {job_id} в базу...")
job = Job( job = Job(
@ -200,31 +229,19 @@ async def get_vakansi():
# geo = '100025096' geo = '100025096'
# pars_jobs(geo) # pars_jobs(geo)
# pars_jobs(geo)
# get_vakansi()
# async def main(): # async def main():
# # get_vakansi() # await get_vakansi()
# # logging.info("WORK")
# # jobs =
# # async def main():
# async for db in get_async_session(): # Асинхронный генератор сессий
# query = select(Job).filter(Job.active == 2)
# result = await db.execute(query)
# job = result.scalars().first()
# for j in job:
# ids = j.job_id
# print(ids)
# # await get_job(db, '4192842821')
# # break # Выход после первого использования
# # pars_jobs(geo)
# if __name__ == "__main__": # if __name__ == "__main__":
# asyncio.run(main()) # asyncio.run(main())
@ -267,7 +284,7 @@ async def get_vakansi():
async def main(): async def main():
async for db in get_async_session(): # Асинхронный генератор сессий async for db in get_async_session(): # Асинхронный генератор сессий
query = select(Job).filter(Job.active == 2) query = select(Job).filter(Job.active == 6)
result = await db.execute(query) result = await db.execute(query)
jobs = result.scalars().all() # Получаем ВСЕ записи в виде списка jobs = result.scalars().all() # Получаем ВСЕ записи в виде списка
@ -276,4 +293,60 @@ async def main():
await get_job(db, job.job_id) await get_job(db, job.job_id)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
# async def get_or_create_jobs(db: AsyncSession, job_id: int, titles: str):
# """ Проверяет, существует ли запись, если нет — создаёт """
# try:
# query = select(Job).filter(Job.job_id == job_id)
# result = await db.execute(query)
# job = result.scalars().first()
# if not job:
# job = Job(
# job_id=job_id,
# job_title=titles
# )
# db.add(job)
# await db.commit()
# await db.refresh(job)
# return job # Возвращаем объект
# except Exception as e:
# await db.rollback() # Откатываем транзакцию в случае ошибки
# print(f"Ошибка при добавлении вакансии {job_id}: {e}")
# return None
# async def get_vakansi():
# """ Читает данные из JSON и записывает их в БД """
# file_path = "search_jobes2.json"
# try:
# with open(file_path, "r", encoding="utf-8") as json_file:
# data = json.load(json_file)
# except Exception as e:
# print(f"Ошибка чтения JSON: {e}")
# return
# async for session in get_async_session(): # Создаём сессию здесь!
# for d in data:
# title = d.get("title", "")
# job_id = d.get("entityUrn", "")
# if job_id:
# await get_or_create_jobs(session, int(job_id), title) # Сохраняем в БД
# print(f"{title} {job_id}")
# async def main():
# await get_vakansi() # Здесь должен быть await!
# if __name__ == "__main__":
# asyncio.run(main())

View File

@ -120,4 +120,67 @@ async def get_applied_jobs(db, client_id: int):
"jobLink": job.link "jobLink": job.link
}) })
print(jobs_list) print(jobs_list)
return jobs_list return jobs_list
async def get_filtered_jobs(db: AsyncSession, user_job_titles, minimum_annual_salary, salary_currency,
user_location_type, user_locations, user_levels, user_job_types): #
# Строим фильтры для каждого из параметров пользователя
filters = []
# Фильтрация по job_title (примерное совпадение)
if user_job_titles:
title_filters = [Job.job_title.ilike(f"%{title}%") for title in user_job_titles]
filters.append(or_(*title_filters))
# Фильтрация по minimum_annual_salary (если указано)
if minimum_annual_salary is not None:
filters.append(Job.minimum_annual_salary >= minimum_annual_salary)
# Фильтрация по salary_currency (если указано)
if salary_currency is not None:
filters.append(Job.salary_currency == salary_currency)
# Фильтрация по location_type (если указано)
if user_location_type:
filters.append(Job.location_type == user_location_type)
# Фильтрация по location (разделяем по каждому городу)
if user_locations:
location_filters = [Job.location.ilike(location) for location in user_locations]
filters.append(or_(*location_filters))
# Фильтрация по job_level (если указаны)
if user_levels:
filters.append(Job.job_level.in_(user_levels))
# Фильтрация по job_type (если указаны)
if user_job_types:
filters.append(Job.job_type.in_(user_job_types))
# Выполняем запрос с применением всех фильтров
query = select(Job).filter(*filters)
# Выполняем асинхронный запрос
result = await db.execute(query)
# Получаем все результаты
jobs = result.scalars().all()
return jobs
# # Пример использования функции
# jobs = get_filtered_jobs(
# session=session,
# user_job_titles=["Electronics Engineer", "Hardware Engineer"],
# minimum_annual_salary=None,
# salary_currency=None,
# user_location_type=None,
# user_locations=["Burnaby, Canada", "Vancouver, Canada", "Toronto, Canada"],
# user_levels=["Mid", "Senior", "Manager"],
# user_job_types=["Full-time", "Permanent"]
# )
# # Выводим вакансии
# for job in jobs:
# print(job.job_title, job.location, job.job_level, job.job_type)

69
utils/search.py Normal file
View File

@ -0,0 +1,69 @@
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'model')))
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import joinedload
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from model.database import Client, AppliedJob, Job
def get_filtered_jobs(session: AsyncSession, user_job_titles, minimum_annual_salary, salary_currency,
user_location_type, user_locations, user_levels, user_job_types):
# Строим фильтры для каждого из параметров пользователя
filters = []
# Фильтрация по job_title (примерное совпадение)
if user_job_titles:
filters.append(Job.job_title.in_(user_job_titles))
# Фильтрация по minimum_annual_salary (если указано)
if minimum_annual_salary is not None:
filters.append(Job.minimum_annual_salary >= minimum_annual_salary)
# Фильтрация по salary_currency (если указано)
if salary_currency is not None:
filters.append(Job.salary_currency == salary_currency)
# Фильтрация по location_type (если указано)
if user_location_type:
filters.append(Job.location_type == user_location_type)
# Фильтрация по location (разделяем по каждому городу)
if user_locations:
location_filters = [Job.location.ilike(location) for location in user_locations]
filters.append(or_(*location_filters))
# Фильтрация по job_level (если указаны)
if user_levels:
filters.append(Job.job_level.in_(user_levels))
# Фильтрация по job_type (если указаны)
if user_job_types:
filters.append(Job.job_type.in_(user_job_types))
# Выполняем запрос с применением всех фильтров
query = session.query(Job).filter(*filters)
# Получаем все результаты
jobs = query.all()
return jobs
# Пример использования функции
jobs = get_filtered_jobs(
session=session,
user_job_titles=["Electronics Engineer", "Hardware Engineer"],
minimum_annual_salary=None,
salary_currency=None,
user_location_type=None,
user_locations=["Burnaby, Canada", "Vancouver, Canada", "Toronto, Canada"],
user_levels=["Mid", "Senior", "Manager"],
user_job_types=["Full-time", "Permanent"]
)
# Выводим вакансии
for job in jobs:
print(job.job_title, job.location, job.job_level, job.job_type)

View File

@ -9,6 +9,7 @@ dependencies = [
{ name = "aiomysql" }, { name = "aiomysql" },
{ name = "aiosqlite" }, { name = "aiosqlite" },
{ name = "alembic" }, { name = "alembic" },
{ name = "beautifulsoup4" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "linkedin-api" }, { name = "linkedin-api" },
@ -17,6 +18,7 @@ dependencies = [
{ name = "pymysql" }, { name = "pymysql" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "requests" },
{ name = "schedule" }, { name = "schedule" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "uvicorn" }, { name = "uvicorn" },
@ -27,6 +29,7 @@ requires-dist = [
{ name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiomysql", specifier = ">=0.2.0" },
{ name = "aiosqlite", specifier = ">=0.21.0" }, { name = "aiosqlite", specifier = ">=0.21.0" },
{ name = "alembic", specifier = ">=1.14.1" }, { name = "alembic", specifier = ">=1.14.1" },
{ name = "beautifulsoup4", specifier = ">=4.13.3" },
{ name = "fastapi", specifier = ">=0.115.8" }, { name = "fastapi", specifier = ">=0.115.8" },
{ name = "jinja2", specifier = ">=3.1.5" }, { name = "jinja2", specifier = ">=3.1.5" },
{ name = "linkedin-api", specifier = ">=2.3.1" }, { name = "linkedin-api", specifier = ">=2.3.1" },
@ -35,6 +38,7 @@ requires-dist = [
{ name = "pymysql", specifier = ">=1.1.1" }, { name = "pymysql", specifier = ">=1.1.1" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "python-multipart", specifier = ">=0.0.20" }, { name = "python-multipart", specifier = ">=0.0.20" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "schedule", specifier = ">=1.2.2" }, { name = "schedule", specifier = ">=1.2.2" },
{ name = "sqlalchemy", specifier = ">=2.0.38" }, { name = "sqlalchemy", specifier = ">=2.0.38" },
{ name = "uvicorn", specifier = ">=0.34.0" }, { name = "uvicorn", specifier = ">=0.34.0" },