From cd1129ea72843e1dfd3f02ddccda19aa96fdc512 Mon Sep 17 00:00:00 2001 From: Kavalar Date: Thu, 12 Mar 2026 19:23:54 +0300 Subject: [PATCH] first --- .gitignore | 6 + CACHEDIR.TAG | 4 + main.py | 16 + pyvenv.cfg | 8 + server.py | 1169 +++++++++++++++++++++++++++++++++ templates/admin.html | 685 +++++++++++++++++++ templates/index.html | 265 ++++++++ templates/login.html | 407 ++++++++++++ templates/profile.html | 820 +++++++++++++++++++++++ templates/resumes.html | 497 ++++++++++++++ templates/vacancies.html | 512 +++++++++++++++ templates/vacancy_detail.html | 303 +++++++++ 12 files changed, 4692 insertions(+) create mode 100644 .gitignore create mode 100644 CACHEDIR.TAG create mode 100644 main.py create mode 100644 pyvenv.cfg create mode 100644 server.py create mode 100644 templates/admin.html create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/profile.html create mode 100644 templates/resumes.html create mode 100644 templates/vacancies.html create mode 100644 templates/vacancy_detail.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d9c3b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# created by virtualenv automatically +.idea +bin +lib + +rabota_today.db \ No newline at end of file diff --git a/CACHEDIR.TAG b/CACHEDIR.TAG new file mode 100644 index 0000000..837feef --- /dev/null +++ b/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by Python virtualenv. +# For information about cache directory tags, see: +# https://bford.info/cachedir/ \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5596b44 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Shift+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..367ece4 --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,8 @@ +home = /usr/bin +implementation = CPython +version_info = 3.10.12.final.0 +virtualenv = 20.31.2 +include-system-site-packages = false +base-prefix = /usr +base-exec-prefix = /usr +base-executable = /usr/bin/python3.10 diff --git a/server.py b/server.py new file mode 100644 index 0000000..bafc342 --- /dev/null +++ b/server.py @@ -0,0 +1,1169 @@ +# main.py (полная версия с тегами и админкой) +from fastapi import FastAPI, HTTPException, Depends, status, Request, Form +from fastapi.middleware.cors import CORSMiddleware +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, EmailStr +from typing import Optional, List +from datetime import datetime, timedelta +import jwt +import sqlite3 +import hashlib +import os +from contextlib import contextmanager +import uvicorn +from pathlib import Path + +app = FastAPI(title="Rabota.Today API", version="2.0.0") + +# Настройки +SECRET_KEY = "your-secret-key-here-change-in-production" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 дней + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +security = HTTPBearer() + +# Создаем директории +BASE_DIR = Path(__file__).parent +TEMPLATES_DIR = BASE_DIR / "templates" +STATIC_DIR = BASE_DIR / "static" + +TEMPLATES_DIR.mkdir(exist_ok=True) +STATIC_DIR.mkdir(exist_ok=True) +(STATIC_DIR / "css").mkdir(exist_ok=True) +(STATIC_DIR / "js").mkdir(exist_ok=True) + +# Подключаем статические файлы +app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + + +# ========== МОДЕЛИ БАЗЫ ДАННЫХ ========== + +def init_db(): + """Инициализация базы данных""" + with get_db() as conn: + cursor = conn.cursor() + + # Таблица пользователей + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + full_name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + phone TEXT NOT NULL, + telegram TEXT, + password_hash TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('employee', 'employer', 'admin')), + is_admin BOOLEAN DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Таблица вакансий + cursor.execute(""" + CREATE TABLE IF NOT EXISTS vacancies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + salary TEXT, + description TEXT, + contact TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + views INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + """) + + # Таблица резюме + cursor.execute(""" + CREATE TABLE IF NOT EXISTS resumes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER UNIQUE NOT NULL, + desired_position TEXT, + about_me TEXT, + desired_salary TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + views INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + """) + + # Таблица тегов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + category TEXT CHECK(category IN ('skill', 'industry', 'position', 'other')) DEFAULT 'skill', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Связь тегов с вакансиями + cursor.execute(""" + CREATE TABLE IF NOT EXISTS vacancy_tags ( + vacancy_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (vacancy_id, tag_id), + FOREIGN KEY (vacancy_id) REFERENCES vacancies (id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE + ) + """) + + # Связь тегов с резюме + cursor.execute(""" + CREATE TABLE IF NOT EXISTS resume_tags ( + resume_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + PRIMARY KEY (resume_id, tag_id), + FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE + ) + """) + + # Таблица опыта работы + cursor.execute(""" + CREATE TABLE IF NOT EXISTS work_experience ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + resume_id INTEGER NOT NULL, + position TEXT NOT NULL, + company TEXT NOT NULL, + period TEXT, + FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE + ) + """) + + # Таблица образования + cursor.execute(""" + CREATE TABLE IF NOT EXISTS education ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + resume_id INTEGER NOT NULL, + institution TEXT NOT NULL, + specialty TEXT, + graduation_year TEXT, + FOREIGN KEY (resume_id) REFERENCES resumes (id) ON DELETE CASCADE + ) + """) + + # Таблица откликов + cursor.execute(""" + CREATE TABLE IF NOT EXISTS applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vacancy_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'pending', + FOREIGN KEY (vacancy_id) REFERENCES vacancies (id), + FOREIGN KEY (user_id) REFERENCES users (id) + ) + """) + + # Создаем админа по умолчанию + admin_password = hash_password("admin123") + cursor.execute(""" + INSERT OR IGNORE INTO users (full_name, email, phone, password_hash, role, is_admin) + VALUES ('Admin', 'admin@rabota.today', '+70000000000', ?, 'admin', 1) + """, (admin_password,)) + + # Добавляем популярные теги + default_tags = [ + ('Python', 'skill'), + ('JavaScript', 'skill'), + ('React', 'skill'), + ('Vue.js', 'skill'), + ('Node.js', 'skill'), + ('Java', 'skill'), + ('C++', 'skill'), + ('PHP', 'skill'), + ('HTML/CSS', 'skill'), + ('SQL', 'skill'), + ('Менеджер', 'position'), + ('Разработчик', 'position'), + ('Дизайнер', 'position'), + ('Маркетолог', 'position'), + ('Аналитик', 'position'), + ('IT', 'industry'), + ('Финансы', 'industry'), + ('Медицина', 'industry'), + ('Образование', 'industry'), + ('Продажи', 'industry'), + ] + + for tag_name, category in default_tags: + cursor.execute("INSERT OR IGNORE INTO tags (name, category) VALUES (?, ?)", (tag_name, category)) + + conn.commit() + print("✅ База данных инициализирована") + + +@contextmanager +def get_db(): + """Контекстный менеджер для работы с БД""" + conn = sqlite3.connect("rabota_today.db") + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.close() + + +# ========== МОДЕЛИ PYDANTIC ========== + +class UserRegister(BaseModel): + full_name: str + email: EmailStr + phone: str + telegram: Optional[str] = None + password: str + role: str + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class TokenResponse(BaseModel): + access_token: str + token_type: str + user_id: int + full_name: str + role: str + is_admin: bool + + +class Tag(BaseModel): + id: Optional[int] = None + name: str + category: str = "skill" + + +class VacancyCreate(BaseModel): + title: str + salary: Optional[str] = None + description: Optional[str] = None + contact: Optional[str] = None + tags: List[str] = [] # список названий тегов + + +class VacancyResponse(BaseModel): + id: int + title: str + salary: Optional[str] + description: Optional[str] + contact: Optional[str] + created_at: str + is_active: bool + views: int + tags: List[Tag] = [] + company_name: Optional[str] = None + + +class WorkExperience(BaseModel): + position: str + company: str + period: Optional[str] = None + + +class Education(BaseModel): + institution: str + specialty: Optional[str] = None + graduation_year: Optional[str] = None + + +class ResumeCreate(BaseModel): + desired_position: Optional[str] = None + about_me: Optional[str] = None + desired_salary: Optional[str] = None + work_experience: List[WorkExperience] = [] + education: List[Education] = [] + tags: List[str] = [] # список названий тегов + + +class ResumeResponse(BaseModel): + id: int + user_id: int + desired_position: Optional[str] + about_me: Optional[str] + desired_salary: Optional[str] + work_experience: List[WorkExperience] + education: List[Education] + tags: List[Tag] = [] + views: int + updated_at: str + full_name: Optional[str] = None + + +# ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========== + +def hash_password(password: str) -> str: + """Хеширование пароля""" + salt = "rabota.today.salt" + return hashlib.sha256((password + salt).encode()).hexdigest() + + +def verify_password(password: str, hash: str) -> bool: + """Проверка пароля""" + return hash_password(password) == hash + + +def create_access_token(data: dict): + """Создание JWT токена""" + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + """Получение текущего пользователя из токена""" + token = credentials.credentials + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id = payload.get("sub") + if user_id is None: + raise HTTPException(status_code=401, detail="Невалидный токен") + return int(user_id) + except jwt.PyJWTError: + raise HTTPException(status_code=401, detail="Невалидный токен") + + +def get_current_user_optional(token: Optional[str] = None): + """Опциональное получение пользователя (для публичных страниц)""" + if not token: + return None + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return int(payload.get("sub")) + except: + return None + + +def get_tags_for_vacancy(cursor, vacancy_id): + """Получение тегов для вакансии""" + cursor.execute(""" + SELECT t.* FROM tags t + JOIN vacancy_tags vt ON t.id = vt.tag_id + WHERE vt.vacancy_id = ? + """, (vacancy_id,)) + return [dict(tag) for tag in cursor.fetchall()] + + +def get_tags_for_resume(cursor, resume_id): + """Получение тегов для резюме""" + cursor.execute(""" + SELECT t.* FROM tags t + JOIN resume_tags rt ON t.id = rt.tag_id + WHERE rt.resume_id = ? + """, (resume_id,)) + return [dict(tag) for tag in cursor.fetchall()] + + +def add_tags_to_vacancy(cursor, vacancy_id, tag_names): + """Добавление тегов к вакансии""" + for tag_name in tag_names: + # Ищем или создаем тег + cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) + tag = cursor.fetchone() + + if tag: + tag_id = tag["id"] + else: + cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) + tag_id = cursor.lastrowid + + # Связываем тег с вакансией + cursor.execute(""" + INSERT OR IGNORE INTO vacancy_tags (vacancy_id, tag_id) + VALUES (?, ?) + """, (vacancy_id, tag_id)) + + +def add_tags_to_resume(cursor, resume_id, tag_names): + """Добавление тегов к резюме""" + for tag_name in tag_names: + cursor.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) + tag = cursor.fetchone() + + if tag: + tag_id = tag["id"] + else: + cursor.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) + tag_id = cursor.lastrowid + + cursor.execute(""" + INSERT OR IGNORE INTO resume_tags (resume_id, tag_id) + VALUES (?, ?) + """, (resume_id, tag_id)) + + +# ========== HTML СТРАНИЦЫ ========== + +@app.get("/", response_class=HTMLResponse) +async def get_index(): + """Главная страница""" + file_path = TEMPLATES_DIR / "index.html" + if not file_path.exists(): + return HTMLResponse( + content="

Rabota.Today API

Сервер работает. Создайте файл templates/index.html

") + return FileResponse(file_path) + + +@app.get("/login", response_class=HTMLResponse) +async def get_login(): + """Страница входа""" + file_path = TEMPLATES_DIR / "login.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Страница входа

Создайте файл templates/login.html

") + + +@app.get("/register", response_class=HTMLResponse) +async def get_register(): + """Страница регистрации""" + file_path = TEMPLATES_DIR / "register.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Страница регистрации

Создайте файл templates/register.html

") + + +@app.get("/profile", response_class=HTMLResponse) +async def get_profile(): + """Страница профиля""" + file_path = TEMPLATES_DIR / "profile.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Профиль

Создайте файл templates/profile.html

") + + +@app.get("/vacancies", response_class=HTMLResponse) +async def get_vacancies(): + """Страница со списком вакансий""" + file_path = TEMPLATES_DIR / "vacancies.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Вакансии

Создайте файл templates/vacancies.html

") + + +@app.get("/vacancy/{vacancy_id}", response_class=HTMLResponse) +async def get_vacancy_detail(vacancy_id: int): + """Страница конкретной вакансии""" + file_path = TEMPLATES_DIR / "vacancy_detail.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content=f"

Вакансия #{vacancy_id}

Создайте файл templates/vacancy_detail.html

") + + +@app.get("/resumes", response_class=HTMLResponse) +async def get_resumes(): + """Страница со списком резюме""" + file_path = TEMPLATES_DIR / "resumes.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Резюме

Создайте файл templates/resumes.html

") + + +@app.get("/resume/{resume_id}", response_class=HTMLResponse) +async def get_resume_detail(resume_id: int): + """Страница конкретного резюме""" + file_path = TEMPLATES_DIR / "resume_detail.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content=f"

Резюме #{resume_id}

Создайте файл templates/resume_detail.html

") + + +@app.get("/admin", response_class=HTMLResponse) +async def get_admin(): + """Страница админки""" + file_path = TEMPLATES_DIR / "admin.html" + return FileResponse(file_path) if file_path.exists() else HTMLResponse( + content="

Админка

Создайте файл templates/admin.html

") + + +# ========== API ЭНДПОИНТЫ ========== + +@app.post("/api/register", response_model=TokenResponse) +async def register(user: UserRegister): + """Регистрация нового пользователя""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT id FROM users WHERE email = ?", (user.email,)) + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Email уже зарегистрирован") + + password_hash = hash_password(user.password) + is_admin = 1 if user.email == "admin@rabota.today" else 0 + + cursor.execute(""" + INSERT INTO users (full_name, email, phone, telegram, password_hash, role, is_admin) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (user.full_name, user.email, user.phone, user.telegram, password_hash, user.role, is_admin)) + + user_id = cursor.lastrowid + conn.commit() + + token = create_access_token({"sub": str(user_id), "role": user.role, "is_admin": is_admin}) + + return { + "access_token": token, + "token_type": "bearer", + "user_id": user_id, + "full_name": user.full_name, + "role": user.role, + "is_admin": bool(is_admin) + } + + +@app.post("/api/login", response_model=TokenResponse) +async def login(user_data: UserLogin): + """Вход в систему""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM users WHERE email = ?", (user_data.email,)) + user = cursor.fetchone() + + if not user or not verify_password(user_data.password, user["password_hash"]): + raise HTTPException(status_code=401, detail="Неверный email или пароль") + + token = create_access_token({"sub": str(user["id"]), "role": user["role"], "is_admin": user["is_admin"]}) + + return { + "access_token": token, + "token_type": "bearer", + "user_id": user["id"], + "full_name": user["full_name"], + "role": user["role"], + "is_admin": bool(user["is_admin"]) + } + + +@app.get("/api/tags") +async def get_all_tags(category: Optional[str] = None): + """Получение всех тегов""" + with get_db() as conn: + cursor = conn.cursor() + if category: + cursor.execute("SELECT * FROM tags WHERE category = ? ORDER BY name", (category,)) + else: + cursor.execute("SELECT * FROM tags ORDER BY category, name") + tags = cursor.fetchall() + return [dict(tag) for tag in tags] + + +@app.post("/api/tags") +async def create_tag(tag: Tag, user_id: int = Depends(get_current_user)): + """Создание нового тега (только для админа)""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, админ ли пользователь + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Только администратор может создавать теги") + + try: + cursor.execute("INSERT INTO tags (name, category) VALUES (?, ?)", (tag.name, tag.category)) + conn.commit() + tag_id = cursor.lastrowid + cursor.execute("SELECT * FROM tags WHERE id = ?", (tag_id,)) + return dict(cursor.fetchone()) + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="Тег с таким названием уже существует") + + +# ========== ЭНДПОИНТЫ ДЛЯ ВАКАНСИЙ ========== + +@app.post("/api/vacancies", response_model=VacancyResponse) +async def create_vacancy( + vacancy: VacancyCreate, + user_id: int = Depends(get_current_user) +): + """Создание новой вакансии""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employer": + raise HTTPException(status_code=403, detail="Только работодатели могут создавать вакансии") + + cursor.execute(""" + INSERT INTO vacancies (user_id, title, salary, description, contact) + VALUES (?, ?, ?, ?, ?) + """, (user_id, vacancy.title, vacancy.salary, vacancy.description, vacancy.contact)) + + vacancy_id = cursor.lastrowid + + # Добавляем теги + if vacancy.tags: + add_tags_to_vacancy(cursor, vacancy_id, vacancy.tags) + + conn.commit() + + cursor.execute("SELECT * FROM vacancies WHERE id = ?", (vacancy_id,)) + new_vacancy = dict(cursor.fetchone()) + new_vacancy["tags"] = get_tags_for_vacancy(cursor, vacancy_id) + + return new_vacancy + + +@app.get("/api/vacancies", response_model=List[VacancyResponse]) +async def get_my_vacancies(user_id: int = Depends(get_current_user)): + """Получение всех вакансий текущего пользователя""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT v.*, u.full_name as company_name + FROM vacancies v + JOIN users u ON v.user_id = u.id + WHERE v.user_id = ? AND v.is_active = 1 + ORDER BY v.created_at DESC + """, (user_id,)) + vacancies = cursor.fetchall() + + result = [] + for v in vacancies: + vacancy_dict = dict(v) + vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) + result.append(vacancy_dict) + + return result + + +@app.get("/api/vacancies/all") +async def get_all_vacancies( + page: int = 1, + search: str = None, + tags: str = None, # теги через запятую + min_salary: int = None, + sort: str = "newest" +): + """Получение всех вакансий с фильтрацией по тегам""" + with get_db() as conn: + cursor = conn.cursor() + + query = """ + SELECT DISTINCT v.*, u.full_name as company_name + FROM vacancies v + JOIN users u ON v.user_id = u.id + WHERE v.is_active = 1 + """ + params = [] + + if search: + query += " AND (v.title LIKE ? OR v.description LIKE ?)" + params.extend([f"%{search}%", f"%{search}%"]) + + if tags: + tag_list = [t.strip() for t in tags.split(',')] + placeholders = ','.join(['?'] * len(tag_list)) + query += f""" + AND v.id IN ( + SELECT vt.vacancy_id + FROM vacancy_tags vt + JOIN tags t ON vt.tag_id = t.id + WHERE t.name IN ({placeholders}) + GROUP BY vt.vacancy_id + HAVING COUNT(DISTINCT t.name) = ? + ) + """ + params.extend(tag_list + [len(tag_list)]) + + if min_salary: + query += " AND v.salary LIKE ?" + params.append(f"%{min_salary}%") + + if sort == "salary_desc": + query += " ORDER BY v.salary DESC" + elif sort == "salary_asc": + query += " ORDER BY v.salary ASC" + else: + query += " ORDER BY v.created_at DESC" + + # Пагинация + limit = 10 + offset = (page - 1) * limit + query += " LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + cursor.execute(query, params) + vacancies = cursor.fetchall() + + result = [] + for v in vacancies: + vacancy_dict = dict(v) + vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) + result.append(vacancy_dict) + + # Получаем общее количество + cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") + total = cursor.fetchone()[0] + + return { + "vacancies": result, + "total_pages": (total + limit - 1) // limit, + "current_page": page + } + + +@app.get("/api/vacancies/{vacancy_id}") +async def get_vacancy(vacancy_id: int, token: Optional[str] = None): + """Получение конкретной вакансии""" + user_id = get_current_user_optional(token) + + with get_db() as conn: + cursor = conn.cursor() + + # Увеличиваем счетчик просмотров + cursor.execute("UPDATE vacancies SET views = views + 1 WHERE id = ?", (vacancy_id,)) + + cursor.execute(""" + SELECT v.*, u.full_name as company_name, u.email, u.telegram + FROM vacancies v + JOIN users u ON v.user_id = u.id + WHERE v.id = ? + """, (vacancy_id,)) + + vacancy = cursor.fetchone() + if not vacancy: + raise HTTPException(status_code=404, detail="Вакансия не найдена") + + result = dict(vacancy) + result["tags"] = get_tags_for_vacancy(cursor, vacancy_id) + + # Проверяем, откликался ли пользователь + if user_id: + cursor.execute(""" + SELECT id FROM applications + WHERE vacancy_id = ? AND user_id = ? + """, (vacancy_id, user_id)) + result["has_applied"] = cursor.fetchone() is not None + + conn.commit() + return result + + +@app.post("/api/vacancies/{vacancy_id}/apply") +async def apply_to_vacancy( + vacancy_id: int, + user_id: int = Depends(get_current_user) +): + """Отклик на вакансию""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT id FROM vacancies WHERE id = ?", (vacancy_id,)) + if not cursor.fetchone(): + raise HTTPException(status_code=404, detail="Вакансия не найдена") + + cursor.execute(""" + SELECT id FROM applications + WHERE vacancy_id = ? AND user_id = ? + """, (vacancy_id, user_id)) + + if cursor.fetchone(): + raise HTTPException(status_code=400, detail="Вы уже откликались на эту вакансию") + + cursor.execute(""" + INSERT INTO applications (vacancy_id, user_id) + VALUES (?, ?) + """, (vacancy_id, user_id)) + + conn.commit() + return {"message": "Отклик отправлен"} + + +# ========== ЭНДПОИНТЫ ДЛЯ РЕЗЮМЕ ========== + +@app.post("/api/resume", response_model=ResumeResponse) +async def create_or_update_resume( + resume: ResumeCreate, + user_id: int = Depends(get_current_user) +): + """Создание или обновление резюме""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT role FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or user["role"] != "employee": + raise HTTPException(status_code=403, detail="Только соискатели могут создавать резюме") + + cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (user_id,)) + existing = cursor.fetchone() + + if existing: + cursor.execute(""" + UPDATE resumes + SET desired_position = ?, about_me = ?, desired_salary = ?, updated_at = CURRENT_TIMESTAMP + WHERE user_id = ? + """, (resume.desired_position, resume.about_me, resume.desired_salary, user_id)) + + resume_id = existing["id"] + + cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,)) + cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,)) + cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,)) + else: + cursor.execute(""" + INSERT INTO resumes (user_id, desired_position, about_me, desired_salary) + VALUES (?, ?, ?, ?) + """, (user_id, resume.desired_position, resume.about_me, resume.desired_salary)) + + resume_id = cursor.lastrowid + + for exp in resume.work_experience: + cursor.execute(""" + INSERT INTO work_experience (resume_id, position, company, period) + VALUES (?, ?, ?, ?) + """, (resume_id, exp.position, exp.company, exp.period)) + + for edu in resume.education: + cursor.execute(""" + INSERT INTO education (resume_id, institution, specialty, graduation_year) + VALUES (?, ?, ?, ?) + """, (resume_id, edu.institution, edu.specialty, edu.graduation_year)) + + # Добавляем теги + if resume.tags: + add_tags_to_resume(cursor, resume_id, resume.tags) + + conn.commit() + + cursor.execute("SELECT * FROM resumes WHERE id = ?", (resume_id,)) + resume_data = dict(cursor.fetchone()) + + cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,)) + resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] + + cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", + (resume_id,)) + resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] + + resume_data["tags"] = get_tags_for_resume(cursor, resume_id) + + return resume_data + + +@app.get("/api/resume", response_model=ResumeResponse) +async def get_my_resume(user_id: int = Depends(get_current_user)): + """Получение резюме текущего пользователя""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT * FROM resumes WHERE user_id = ?", (user_id,)) + resume = cursor.fetchone() + + if not resume: + raise HTTPException(status_code=404, detail="Резюме не найдено") + + resume_data = dict(resume) + + cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume["id"],)) + resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] + + cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", + (resume["id"],)) + resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] + + resume_data["tags"] = get_tags_for_resume(cursor, resume["id"]) + + return resume_data + + +@app.get("/api/resumes/all") +async def get_all_resumes( + page: int = 1, + search: str = None, + tags: str = None, + min_salary: int = None +): + """Получение всех резюме с фильтрацией по тегам""" + with get_db() as conn: + cursor = conn.cursor() + + query = """ + SELECT DISTINCT r.*, u.full_name + FROM resumes r + JOIN users u ON r.user_id = u.id + WHERE 1=1 + """ + params = [] + + if search: + query += " AND (r.desired_position LIKE ? OR r.about_me LIKE ?)" + params.extend([f"%{search}%", f"%{search}%"]) + + if tags: + tag_list = [t.strip() for t in tags.split(',')] + placeholders = ','.join(['?'] * len(tag_list)) + query += f""" + AND r.id IN ( + SELECT rt.resume_id + FROM resume_tags rt + JOIN tags t ON rt.tag_id = t.id + WHERE t.name IN ({placeholders}) + GROUP BY rt.resume_id + HAVING COUNT(DISTINCT t.name) = ? + ) + """ + params.extend(tag_list + [len(tag_list)]) + + if min_salary: + query += " AND r.desired_salary LIKE ?" + params.append(f"%{min_salary}%") + + # Пагинация + limit = 10 + offset = (page - 1) * limit + query += " ORDER BY r.updated_at DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + + cursor.execute(query, params) + resumes = cursor.fetchall() + + result = [] + for r in resumes: + resume_dict = dict(r) + resume_dict["tags"] = get_tags_for_resume(cursor, r["id"]) + cursor.execute("SELECT COUNT(*) FROM work_experience WHERE resume_id = ?", (r["id"],)) + resume_dict["experience_count"] = cursor.fetchone()[0] + result.append(resume_dict) + + cursor.execute("SELECT COUNT(*) FROM resumes") + total = cursor.fetchone()[0] + + return { + "resumes": result, + "total_pages": (total + limit - 1) // limit, + "current_page": page + } + + +@app.get("/api/resumes/{resume_id}") +async def get_resume(resume_id: int): + """Получение конкретного резюме""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute(""" + SELECT r.*, u.full_name, u.email, u.phone, u.telegram + FROM resumes r + JOIN users u ON r.user_id = u.id + WHERE r.id = ? + """, (resume_id,)) + + resume = cursor.fetchone() + if not resume: + raise HTTPException(status_code=404, detail="Резюме не найдено") + + # Увеличиваем счетчик просмотров + cursor.execute("UPDATE resumes SET views = views + 1 WHERE id = ?", (resume_id,)) + + resume_data = dict(resume) + + cursor.execute("SELECT position, company, period FROM work_experience WHERE resume_id = ?", (resume_id,)) + resume_data["work_experience"] = [dict(exp) for exp in cursor.fetchall()] + + cursor.execute("SELECT institution, specialty, graduation_year FROM education WHERE resume_id = ?", + (resume_id,)) + resume_data["education"] = [dict(edu) for edu in cursor.fetchall()] + + resume_data["tags"] = get_tags_for_resume(cursor, resume_id) + + conn.commit() + return resume_data + + +# ========== АДМИНСКИЕ ЭНДПОИНТЫ ========== + +@app.get("/api/admin/stats") +async def get_admin_stats(user_id: int = Depends(get_current_user)): + """Получение статистики для админки""" + with get_db() as conn: + cursor = conn.cursor() + + # Проверяем, админ ли пользователь + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен") + + stats = {} + + cursor.execute("SELECT COUNT(*) FROM users") + stats["total_users"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'") + stats["total_employees"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'") + stats["total_employers"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") + stats["active_vacancies"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM resumes") + stats["total_resumes"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM applications") + stats["total_applications"] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM tags") + stats["total_tags"] = cursor.fetchone()[0] + + # Последние регистрации + cursor.execute(""" + SELECT id, full_name, email, role, created_at + FROM users + ORDER BY created_at DESC + LIMIT 10 + """) + stats["recent_users"] = [dict(u) for u in cursor.fetchall()] + + # Популярные теги + cursor.execute(""" + SELECT t.name, COUNT(vt.vacancy_id) as vacancy_count, COUNT(rt.resume_id) as resume_count + FROM tags t + LEFT JOIN vacancy_tags vt ON t.id = vt.tag_id + LEFT JOIN resume_tags rt ON t.id = rt.tag_id + GROUP BY t.id + ORDER BY (COUNT(vt.vacancy_id) + COUNT(rt.resume_id)) DESC + LIMIT 20 + """) + stats["popular_tags"] = [dict(t) for t in cursor.fetchall()] + + return stats + + +@app.get("/api/admin/users") +async def get_all_users(user_id: int = Depends(get_current_user)): + """Получение всех пользователей (для админа)""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен") + + cursor.execute(""" + SELECT id, full_name, email, phone, telegram, role, is_admin, created_at + FROM users + ORDER BY created_at DESC + """) + users = cursor.fetchall() + return [dict(u) for u in users] + + +@app.delete("/api/admin/users/{target_user_id}") +async def delete_user(target_user_id: int, user_id: int = Depends(get_current_user)): + """Удаление пользователя (для админа)""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен") + + cursor.execute("DELETE FROM users WHERE id = ?", (target_user_id,)) + conn.commit() + return {"message": "Пользователь удален"} + + +@app.get("/api/admin/vacancies") +async def get_all_vacancies_admin(user_id: int = Depends(get_current_user)): + """Получение всех вакансий (для админа)""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен") + + cursor.execute(""" + SELECT v.*, u.full_name as company_name + FROM vacancies v + JOIN users u ON v.user_id = u.id + ORDER BY v.created_at DESC + """) + vacancies = cursor.fetchall() + + result = [] + for v in vacancies: + vacancy_dict = dict(v) + vacancy_dict["tags"] = get_tags_for_vacancy(cursor, v["id"]) + result.append(vacancy_dict) + + return result + + +@app.delete("/api/admin/vacancies/{vacancy_id}") +async def delete_vacancy_admin(vacancy_id: int, user_id: int = Depends(get_current_user)): + """Удаление вакансии (для админа)""" + with get_db() as conn: + cursor = conn.cursor() + + cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,)) + user = cursor.fetchone() + if not user or not user["is_admin"]: + raise HTTPException(status_code=403, detail="Доступ запрещен") + + cursor.execute("DELETE FROM vacancies WHERE id = ?", (vacancy_id,)) + conn.commit() + return {"message": "Вакансия удалена"} + + +# ========== ПОЛЬЗОВАТЕЛЬСКИЕ ЭНДПОИНТЫ ========== + +@app.get("/api/user") +async def get_user_info(user_id: int = Depends(get_current_user)): + """Получение информации о текущем пользователе""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT id, full_name, email, phone, telegram, role, is_admin, created_at + FROM users WHERE id = ? + """, (user_id,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + return dict(user) + + +@app.get("/api/users/{target_user_id}/contact") +async def get_user_contact( + target_user_id: int, + current_user: int = Depends(get_current_user) +): + """Получение контактных данных пользователя""" + with get_db() as conn: + cursor = conn.cursor() + cursor.execute("SELECT email, phone, telegram FROM users WHERE id = ?", (target_user_id,)) + user = cursor.fetchone() + + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + return dict(user) + + +# Инициализация базы данных +init_db() + +if __name__ == "__main__": + print("🚀 Запуск сервера на http://localhost:8000") + print("📚 Документация API: http://localhost:8000/docs") + print("👤 Админ: admin@rabota.today / admin123") + print("📁 HTML страницы должны быть в папке templates/") + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..513429b --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,685 @@ + + + + + + Админ-панель | Rabota.Today + + + + +
+
+ + +
+ + +
+
+
Пользователи
+
0
+
+
+
Соискатели
+
0
+
+
+
Работодатели
+
0
+
+
+
Вакансии
+
0
+
+
+
Резюме
+
0
+
+
+
Отклики
+
0
+
+
+ + +
+
Пользователи
+
Вакансии
+
Резюме
+
Теги
+
+ + +
+ +
+

Управление пользователями

+ + + + + + + + + + + + + +
IDИмяEmailТелефонРольДата регистрацииДействия
+
+ + + + + + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..4180cf3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,265 @@ + + + + + + Rabota.Today - Ярмарка вакансий + + + + + +
+
+ + +
+ +
+

Найди работу мечты

+

Тысячи вакансий и резюме на одной платформе

+ +
+ +
+
+ +
1,234
+
активных вакансий
+
+
+ +
5,678
+
соискателей
+
+
+ +
456
+
компаний
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..61eac60 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,407 @@ + + + + + + Вход | Rabota.Today + + + + +
+
+

+ + Rabota.Today +

+

Вход в личный кабинет

+
+ +
+
+
Вход
+
Регистрация
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+ + +
+ + + + + +
+
+ + + + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..84acdf3 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,820 @@ + + + + + + Профиль | Rabota.Today + + + + +
+
+ + +
+ +
+ +
+
+ +
+
Загрузка...
+
+ +
+
+
0
+
просмотров
+
+
+
0
+
в избранном
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
Главное
+
Вакансии
+
Резюме
+
+ + +
+

Добро пожаловать!

+

Здесь будет ваша активность и статистика

+ +
+

Быстрые действия

+
+ + +
+
+
+ + +
+
+

Мои вакансии

+ +
+
+
Загрузка вакансий...
+
+
+ + +
+
+

Мое резюме

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +

Опыт работы

+
+ + +

Образование

+
+ +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/templates/resumes.html b/templates/resumes.html new file mode 100644 index 0000000..556d3f7 --- /dev/null +++ b/templates/resumes.html @@ -0,0 +1,497 @@ + + + + + + Резюме | Rabota.Today + + + + +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Загрузка резюме...
+
+ + +
+ + + + \ No newline at end of file diff --git a/templates/vacancies.html b/templates/vacancies.html new file mode 100644 index 0000000..1099f3c --- /dev/null +++ b/templates/vacancies.html @@ -0,0 +1,512 @@ + + + + + + Вакансии | Rabota.Today + + + + +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
Загрузка вакансий...
+
+ + +
+ + + + \ No newline at end of file diff --git a/templates/vacancy_detail.html b/templates/vacancy_detail.html new file mode 100644 index 0000000..183c5cd --- /dev/null +++ b/templates/vacancy_detail.html @@ -0,0 +1,303 @@ + + + + + + Вакансия | Rabota.Today + + + + +
+
+ + +
+ + Назад к вакансиям + +
+
Загрузка...
+
+
+ + + + \ No newline at end of file