copied the code from the working repo
This commit is contained in:
5
mtucijobsbackend/.env.example
Normal file
5
mtucijobsbackend/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
database_hostname =
|
||||
database_port =
|
||||
database_password =
|
||||
database_name =
|
||||
database_username =
|
||||
3
mtucijobsbackend/.gitignore
vendored
Normal file
3
mtucijobsbackend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
venv/
|
||||
.env
|
||||
6
mtucijobsbackend/Dockerfile
Normal file
6
mtucijobsbackend/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM python:3.9-alpine
|
||||
WORKDIR /code
|
||||
COPY ./requirements.txt /code/requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
COPY . .
|
||||
CMD ["fastapi", "run", "./app/main.py", "--root-path", "/api/"]
|
||||
0
mtucijobsbackend/app/__init__.py
Normal file
0
mtucijobsbackend/app/__init__.py
Normal file
21
mtucijobsbackend/app/config.py
Normal file
21
mtucijobsbackend/app/config.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
database_hostname: str
|
||||
database_port: str
|
||||
database_password: str
|
||||
database_name: str
|
||||
database_username: str
|
||||
access_key: str
|
||||
secret_key: str
|
||||
endpoint: str
|
||||
secret_key2: str
|
||||
algorithm: str
|
||||
access_token_expire_minutes: int
|
||||
refresh_token_expire_days: int
|
||||
x_api_key: str
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
150
mtucijobsbackend/app/crud.py
Normal file
150
mtucijobsbackend/app/crud.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from fastapi import status, HTTPException, Depends, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from sqlalchemy import func
|
||||
from . import models
|
||||
from typing import List, Dict
|
||||
from datetime import date
|
||||
import requests
|
||||
|
||||
def get_group_pattern(year):
|
||||
today = date.today()
|
||||
current_year = int(today.strftime("%Y"))
|
||||
current_month = int(today.strftime("%m"))
|
||||
|
||||
if current_month > 5:
|
||||
year -= 1
|
||||
|
||||
if year <= 0:
|
||||
return "_"
|
||||
|
||||
pattern = "^[:alpha:]"
|
||||
|
||||
for i in range(year):
|
||||
pattern += "|" + str(current_year % 100 - i)
|
||||
|
||||
return pattern
|
||||
|
||||
def send_webhook_background(event_type: str, data: Dict):
|
||||
webhook_url = "http://bot:3006/webhook"
|
||||
payload = {
|
||||
"event": event_type,
|
||||
"data": data
|
||||
}
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
print(f"Failed to send webhook: {e}")
|
||||
|
||||
def send_webhook_response_updated(data: Dict):
|
||||
event_type = "response_updated"
|
||||
send_webhook_background(event_type, data)
|
||||
|
||||
def update_hardskills(entity_id: int, hardskills: List[str], is_job: bool, db: Session):
|
||||
model = models.JobsHard_skills if is_job else models.StudentsHard_skills
|
||||
id_field = 'JobID' if is_job else 'StudentID'
|
||||
|
||||
# Удаление существующих навыков
|
||||
db.query(model).filter(getattr(model, id_field) == entity_id).delete()
|
||||
db.flush()
|
||||
|
||||
# Добавление новых навыков
|
||||
hardskills_query = db.query(models.Hard_skills).filter(models.Hard_skills.Title.in_(hardskills))
|
||||
new_hardskills = [model(**{id_field: entity_id, 'Hard_skillID': hardskill.Hard_skillID}) for hardskill in hardskills_query]
|
||||
db.add_all(new_hardskills)
|
||||
db.flush()
|
||||
|
||||
def update_matches(entity_id: int, is_job: bool, db: Session, background_tasks: BackgroundTasks) -> List[Dict]:
|
||||
if is_job:
|
||||
matches = db.query(
|
||||
models.JobsHard_skills.JobID,
|
||||
models.StudentsHard_skills.StudentID,
|
||||
func.count(models.StudentsHard_skills.Hard_skillID).label("count_students_skills")
|
||||
).join(models.JobsHard_skills, models.JobsHard_skills.Hard_skillID == models.StudentsHard_skills.Hard_skillID).\
|
||||
filter(models.JobsHard_skills.JobID == entity_id).\
|
||||
group_by(models.JobsHard_skills.JobID, models.StudentsHard_skills.StudentID).\
|
||||
all()
|
||||
|
||||
count_jobs_skills = db.query(func.count(models.JobsHard_skills.Hard_skillID)).filter(models.JobsHard_skills.JobID == entity_id).scalar()
|
||||
db.query(models.Matches).filter(models.Matches.JobID == entity_id).delete()
|
||||
else:
|
||||
matches = db.query(
|
||||
models.StudentsHard_skills.StudentID,
|
||||
models.JobsHard_skills.JobID,
|
||||
func.count(models.StudentsHard_skills.Hard_skillID).label("count_students_skills")
|
||||
).join(models.Jobs, models.Jobs.JobID == models.JobsHard_skills.JobID).\
|
||||
join(models.StudentsHard_skills, models.JobsHard_skills.Hard_skillID == models.StudentsHard_skills.Hard_skillID).\
|
||||
filter(models.Jobs.Archive == False, models.StudentsHard_skills.StudentID == entity_id).\
|
||||
group_by(models.StudentsHard_skills.StudentID, models.JobsHard_skills.JobID).\
|
||||
all()
|
||||
|
||||
db.query(models.Matches).filter(models.Matches.StudentID == entity_id).delete()
|
||||
|
||||
db.flush()
|
||||
|
||||
updated_matches = []
|
||||
for match in matches:
|
||||
if is_job:
|
||||
MATCH = match.count_students_skills / count_jobs_skills * 100
|
||||
updated_matches.append({
|
||||
"job_id": match.JobID,
|
||||
"student_id": match.StudentID,
|
||||
"match_percentage": MATCH
|
||||
})
|
||||
else:
|
||||
count_jobs_skills = db.query(func.count(models.JobsHard_skills.Hard_skillID)).filter(models.JobsHard_skills.JobID == match.JobID).scalar()
|
||||
MATCH = match.count_students_skills / count_jobs_skills * 100
|
||||
updated_matches.append({
|
||||
"job_id": match.JobID,
|
||||
"student_id": match.StudentID,
|
||||
"match_percentage": MATCH
|
||||
})
|
||||
|
||||
new_match = models.Matches(StudentID=match.StudentID, JobID=match.JobID, Match=MATCH)
|
||||
db.add(new_match)
|
||||
|
||||
db.flush()
|
||||
|
||||
if updated_matches:
|
||||
background_tasks.add_task(send_webhook_background, "matches_updated", {
|
||||
"entity_type": "job" if is_job else "student",
|
||||
"entity_id": entity_id,
|
||||
"updated_matches": updated_matches
|
||||
})
|
||||
|
||||
return updated_matches
|
||||
|
||||
def update_record(model, model_id: int, updated_data: dict, db: Session, background_tasks: BackgroundTasks):
|
||||
query = db.query(model).filter(model.ResponseID == model_id)
|
||||
record = query.first()
|
||||
|
||||
if not record:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
query.update(updated_data, synchronize_session=False)
|
||||
db.flush()
|
||||
|
||||
commit_transaction(db)
|
||||
|
||||
post_update_data = {
|
||||
"response_id": record.ResponseID,
|
||||
"student_id": record.StudentID,
|
||||
"job_id": record.JobID,
|
||||
"status": record.Status,
|
||||
"comment": record.Comment,
|
||||
"link": record.Link
|
||||
}
|
||||
|
||||
if post_update_data:
|
||||
background_tasks.add_task(send_webhook_response_updated, post_update_data)
|
||||
|
||||
return query.first()
|
||||
|
||||
def commit_transaction(db):
|
||||
try:
|
||||
db.commit()
|
||||
except:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||
|
||||
47
mtucijobsbackend/app/database.py
Normal file
47
mtucijobsbackend/app/database.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from .config import settings
|
||||
from .models import Hard_skills
|
||||
|
||||
DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}'
|
||||
|
||||
engine = create_engine(DATABASE_URL)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def init_db(session: Session):
|
||||
|
||||
titles = [
|
||||
'C', 'C++', 'C#', 'Java', 'Python', 'Ruby', 'Ruby on Rails', 'R', 'Matlab', 'Django', 'NetBeans',
|
||||
'Scala', 'JavaScript', 'TypeScript', 'Go', 'Software Development', 'Application Development',
|
||||
'Web Applications', 'Object Oriented Programming', 'Aspect Oriented Programming', 'Concurrent Programming',
|
||||
'Mobile Development (iOS)', 'Mobile Development (Android)', 'Data Science', 'Data Analytics', 'Data Mining',
|
||||
'Data Visualization', 'Machine Learning', 'TensorFlow', 'PyTorch', 'Keras', 'Theano', 'Statistical Analysis',
|
||||
'Bayesian Analysis', 'Regression Analysis', 'Time Series Analysis', 'Clustering', 'K-means', 'KNN', 'Decision Trees',
|
||||
'Random Forest', 'Dimensionality Reduction', 'PCA', 'SVD', 'Gradient Descent', 'Stochastic Gradient Descent',
|
||||
'Outlier Detection', 'Frequent Itemset Mining', 'SQL', 'NoSQL', 'SQL Server', 'MS SQL Server', 'Apache Hadoop',
|
||||
'Apache Spark', 'Apache Airflow', 'Apache Impala', 'Apache Drill', 'HTML', 'CSS', 'React', 'Angular', 'Vue.js',
|
||||
'Node.js', 'Express.js', 'REST', 'SOAP', 'Web Platforms', 'System Architecture', 'Distributed Computing', 'AWS',
|
||||
'AWS Glue', 'Azure', 'Google Cloud Platform', 'Docker', 'Kubernetes', 'UNIX', 'Linux', 'Windows', 'MacOS',
|
||||
'Embedded Hardware', 'Debugging', 'Unit Testing', 'Integration Testing', 'System Testing', 'Code Review', 'Git',
|
||||
'SVN', 'CI/CD', 'Software Documentation', 'IDE', 'CASE Tools', 'Computational Complexity', 'Algorithm Design',
|
||||
'Data Structures', 'Mathematical Modeling', 'Statistics', 'Technical Writing', 'Technical Support', 'System Design',
|
||||
'System Development', 'Technical Guidance', 'Client Interface', 'Vendor Interface', 'Emerging Technologies', 'Jira',
|
||||
'Trello', 'Software Architecture', 'Word', 'Excel'
|
||||
]
|
||||
|
||||
for title in titles:
|
||||
exist_hardskill = session.query(Hard_skills).filter(Hard_skills.Title == title).first()
|
||||
|
||||
if not exist_hardskill:
|
||||
hard_skills_object = Hard_skills(Title=title)
|
||||
session.add(hard_skills_object)
|
||||
session.commit()
|
||||
32
mtucijobsbackend/app/main.py
Normal file
32
mtucijobsbackend/app/main.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
from .routers import auth, user, service, student, job
|
||||
from .database import init_db, engine
|
||||
from .models import Base
|
||||
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
with Session(engine) as session:
|
||||
init_db(session)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(auth.router)
|
||||
app.include_router(user.router)
|
||||
app.include_router(student.router)
|
||||
app.include_router(job.router)
|
||||
app.include_router(service.router)
|
||||
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {"message": "I'm ok!"}
|
||||
87
mtucijobsbackend/app/models.py
Normal file
87
mtucijobsbackend/app/models.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, ARRAY, UniqueConstraint
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
class Base(DeclarativeBase): pass
|
||||
|
||||
class Users(Base):
|
||||
__tablename__ = 'Users'
|
||||
UserID = Column(Integer, primary_key=True, autoincrement=True)
|
||||
Email = Column(String(166), nullable=False, unique=True)
|
||||
Hashed_password = Column(String(200), nullable=False)
|
||||
|
||||
class Students(Base):
|
||||
__tablename__ = 'Students'
|
||||
StudentID = Column(Integer, primary_key=True, autoincrement=True)
|
||||
Name = Column(String(155), nullable=False)
|
||||
Type = Column(String(60), nullable=False)
|
||||
Faculties = Column(String(70), nullable=False)
|
||||
Group = Column(String(20), nullable=False)
|
||||
Year = Column(Integer, nullable=False)
|
||||
Experience_specialty = Column(Boolean, nullable=False)
|
||||
Time = Column(ARRAY(String(3)), nullable=False)
|
||||
Soft_skills = Column(String(155), nullable=False)
|
||||
Link = Column(String(155), nullable=False)
|
||||
Email = Column(String(166), nullable=False)
|
||||
Phone_number = Column(String(16), nullable=False)
|
||||
|
||||
class Jobs(Base):
|
||||
__tablename__ = 'Jobs'
|
||||
JobID = Column(Integer, primary_key=True, autoincrement=True)
|
||||
UserID = Column(Integer, ForeignKey(
|
||||
"Users.UserID", ondelete="CASCADE"), nullable=False)
|
||||
Company_name = Column(String(155), nullable=False)
|
||||
Link_to_job = Column(String(155), nullable=True)
|
||||
Job_name = Column(String(155), nullable=False)
|
||||
Year = Column(String(1), nullable=False)
|
||||
Qualification = Column(Boolean, nullable=False)
|
||||
Salary_after_interview = Column(Boolean, nullable=False)
|
||||
Salary = Column(Integer, nullable=False)
|
||||
Email = Column(String(155), nullable=False)
|
||||
Archive = Column(Boolean, nullable=False)
|
||||
Responsibilities = Column(String(255), nullable=False)
|
||||
Time = Column(ARRAY(String), nullable=False)
|
||||
|
||||
class Hard_skills(Base):
|
||||
__tablename__ = 'Hard_skills'
|
||||
Hard_skillID = Column(Integer, primary_key=True, nullable=False, autoincrement=True)
|
||||
Title = Column(String, nullable=False, unique=True)
|
||||
|
||||
class StudentsHard_skills(Base):
|
||||
__tablename__ = 'StudentsHard_skills'
|
||||
|
||||
StudentID = Column(Integer, ForeignKey(
|
||||
"Students.StudentID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
Hard_skillID = Column(Integer, ForeignKey(
|
||||
"Hard_skills.Hard_skillID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
|
||||
class JobsHard_skills(Base):
|
||||
__tablename__ = 'JobsHard_skills'
|
||||
|
||||
JobID = Column(Integer, ForeignKey(
|
||||
"Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
Hard_skillID = Column(Integer, ForeignKey(
|
||||
"Hard_skills.Hard_skillID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
|
||||
class Responses(Base):
|
||||
__tablename__ = 'Responses'
|
||||
|
||||
ResponseID = Column(Integer, primary_key=True, autoincrement=True)
|
||||
StudentID = Column(Integer, ForeignKey(
|
||||
"Students.StudentID", onupdate="CASCADE", ondelete="CASCADE"))
|
||||
JobID = Column(Integer, ForeignKey(
|
||||
"Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE"))
|
||||
Status = Column(String(50), nullable=True)
|
||||
Comment = Column(String(700), nullable=True)
|
||||
Link = Column(String(155), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('StudentID', 'JobID', name='unique_student_job_for_responses'),
|
||||
)
|
||||
|
||||
class Matches(Base):
|
||||
__tablename__ = 'Matches'
|
||||
StudentID = Column(Integer, ForeignKey(
|
||||
"Students.StudentID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
JobID = Column(Integer, ForeignKey(
|
||||
"Jobs.JobID", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
|
||||
Match = Column(Integer, nullable=False)
|
||||
51
mtucijobsbackend/app/routers/auth.py
Normal file
51
mtucijobsbackend/app/routers/auth.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, Depends, status, HTTPException, Response
|
||||
from fastapi.security.oauth2 import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import database, schemas, models, security, utils
|
||||
|
||||
router = APIRouter(tags=['Authentication'])
|
||||
|
||||
|
||||
@router.post('/login', response_model=schemas.Token)
|
||||
def login(user_credentials: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)):
|
||||
|
||||
user = db.query(models.Users).filter(
|
||||
models.Users.Email == user_credentials.username).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials")
|
||||
|
||||
if not utils.verify(user_credentials.password, user.Hashed_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid Credentials")
|
||||
|
||||
access_token = security.create_access_token(data={
|
||||
"UserID": user.UserID,
|
||||
"Email": user.Email
|
||||
})
|
||||
|
||||
refresh_token = security.create_refresh_token(data={
|
||||
"UserID": user.UserID,
|
||||
})
|
||||
|
||||
return {"access_token": access_token, "refresh_token": refresh_token}
|
||||
|
||||
@router.post('/refresh', response_model=schemas.Token, response_model_exclude_none=True )
|
||||
def refresh_access_token(refresh_token: str, db: Session = Depends(database.get_db)):
|
||||
user_id = security.verify_refresh_token(refresh_token)
|
||||
|
||||
user = db.query(models.Users).filter(
|
||||
models.Users.UserID == user_id).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
access_token = security.create_access_token(data={
|
||||
"UserID": user.UserID,
|
||||
"Email": user.Email
|
||||
})
|
||||
|
||||
return {"access_token": access_token}
|
||||
247
mtucijobsbackend/app/routers/job.py
Normal file
247
mtucijobsbackend/app/routers/job.py
Normal file
@@ -0,0 +1,247 @@
|
||||
from fastapi import status, HTTPException, Response, APIRouter, Depends, BackgroundTasks, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import not_, func
|
||||
from typing import List, Annotated
|
||||
from ..database import get_db
|
||||
from .. import models, schemas, security, crud
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/jobs",
|
||||
dependencies=[Depends(security.verify_access_token)],
|
||||
responses={401: {"description": "Unauthorized"}},
|
||||
tags=['Jobs']
|
||||
)
|
||||
|
||||
@router.get("/", response_model=List[schemas.JobGet])
|
||||
def get_jobs(db: Session = Depends(get_db)):
|
||||
jobs = db.query(models.Jobs).all()
|
||||
|
||||
if not jobs:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response: List[schemas.JobGet] = []
|
||||
|
||||
for job in jobs:
|
||||
hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\
|
||||
join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.JobsHard_skills.JobID == job.JobID).all()
|
||||
|
||||
Hardskills = [hardskill.Title for hardskill in hardskills]
|
||||
response += [schemas.JobGet(JobID=job.JobID, UserID = job.UserID, Job_name=job.Job_name, Company_name=job.Company_name,
|
||||
Link_to_job=job.Link_to_job, Year=job.Year, Qualification=job.Qualification, Time=job.Time,
|
||||
Salary_after_interview=job.Salary_after_interview, Salary=job.Salary, Email=job.Email, Archive=job.Archive,
|
||||
Responsibilities=job.Responsibilities, Hardskills=Hardskills)]
|
||||
|
||||
return response
|
||||
|
||||
@router.get("/{id}", response_model=schemas.JobGet)
|
||||
def get_job(id: int, db: Session = Depends(get_db)):
|
||||
job = db.query(models.Jobs).filter(models.Jobs.JobID == id).first()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\
|
||||
join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.JobsHard_skills.JobID == id).all()
|
||||
|
||||
Hardskills = [hardskill.Title for hardskill in hardskills]
|
||||
response = schemas.JobGet(JobID=job.JobID, UserID = job.UserID, Job_name=job.Job_name, Company_name=job.Company_name,
|
||||
Link_to_job=job.Link_to_job, Year=job.Year, Qualification=job.Qualification, Time=job.Time,
|
||||
Salary_after_interview=job.Salary_after_interview, Salary=job.Salary, Email=job.Email, Archive=job.Archive,
|
||||
Responsibilities=job.Responsibilities, Hardskills=Hardskills)
|
||||
|
||||
return response
|
||||
|
||||
@router.get("/matches/{id}", response_model=List[schemas.MatchJob])
|
||||
def get_matches(id: int, db: Session = Depends(get_db)):
|
||||
matches = db.query(models.Matches).filter(models.Matches.JobID == id).order_by(models.Matches.Match.desc()).all()
|
||||
matches = db.query(
|
||||
models.Students.StudentID,
|
||||
models.Students.Name,
|
||||
models.Students.Type,
|
||||
models.Students.Faculties,
|
||||
models.Students.Group,
|
||||
models.Students.Year,
|
||||
models.Students.Experience_specialty,
|
||||
models.Students.Time,
|
||||
models.Students.Soft_skills,
|
||||
models.Students.Link,
|
||||
models.Students.Email,
|
||||
models.Students.Phone_number,
|
||||
models.Matches.Match
|
||||
).join(models.Matches, models.Students.StudentID == models.Matches.StudentID).\
|
||||
filter(models.Matches.JobID == id).\
|
||||
order_by(models.Matches.Match.desc()).\
|
||||
all()
|
||||
|
||||
if not matches:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return matches
|
||||
|
||||
@router.get("/hardskills/{id}", response_model=List[schemas.JobsHardskill])
|
||||
def get_jobs_hardskills(id: int, db: Session = Depends(get_db)):
|
||||
hardskills = db.query(models.JobsHard_skills.Hard_skillID, models.Hard_skills.Title).\
|
||||
join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.JobsHard_skills.JobID == id).all()
|
||||
|
||||
if not hardskills:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return hardskills
|
||||
|
||||
@router.get("/students-search/", response_model=List[schemas.StudentUpdate])
|
||||
def get_students(
|
||||
year: int = None,
|
||||
time: Annotated[list[str], Query()] = [],
|
||||
hardskills: Annotated[list[str], Query()] = [],
|
||||
faculties: str = None,
|
||||
experience_specialty: bool = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
|
||||
if hardskills:
|
||||
students = db.query(
|
||||
models.Students.StudentID,
|
||||
models.Students.Name,
|
||||
models.Students.Type,
|
||||
models.Students.Faculties,
|
||||
models.Students.Group,
|
||||
models.Students.Уear,
|
||||
models.Students.Experience_specialty,
|
||||
models.Students.Time,
|
||||
models.Students.Soft_skills,
|
||||
models.Students.Link,
|
||||
models.Students.Email,
|
||||
models.Students.Phone_number,
|
||||
).join(models.StudentsHard_skills, models.Students.StudentID == models.StudentsHard_skills.StudentID).\
|
||||
join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.Hard_skills.Title.in_(hardskills)).distinct(models.Students.StudentID)
|
||||
else:
|
||||
students = db.query(models.Students).filter(models.Students.Experience_specialty == experience_specialty)
|
||||
|
||||
if time:
|
||||
students = students.filter(models.Students.Time.op("&&")(time))
|
||||
|
||||
if year:
|
||||
students = students.filter(models.Students.Year >= year)
|
||||
|
||||
if faculties:
|
||||
students = students.filter(models.Students.Faculties.ilike("%" + faculties + "%"))
|
||||
|
||||
response: List[schemas.StudentUpdate] = []
|
||||
|
||||
for student in students:
|
||||
hardskills = db.query(models.StudentsHard_skills.Hard_skillID, models.Hard_skills.Title).\
|
||||
join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.StudentsHard_skills.StudentID == student.StudentID).all()
|
||||
|
||||
Hardskills = [hardskill.Title for hardskill in hardskills]
|
||||
response += [schemas.StudentUpdate(Name=student.Name, Type=student.Type, Faculties=student.Faculties,
|
||||
Group=student.Group, Year=student.Year, Experience_specialty=student.Experience_specialty,
|
||||
Time=student.Time, Soft_skills=student.Soft_skills, Link=student.Link, Email=student.Email,
|
||||
Phone_number=student.Phone_number, Hardskills=Hardskills)]
|
||||
|
||||
return response
|
||||
|
||||
@router.get("/responses/{id}", response_model=List[schemas.Response])
|
||||
def get_responses(id: int, db: Session = Depends(get_db)):
|
||||
responses = db.query(models.Responses).filter(models.Responses.JobID == id).all()
|
||||
|
||||
if not responses:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return responses
|
||||
|
||||
@router.get("/responses/", response_model=List[schemas.Response])
|
||||
def get_responses_all_unprocessed(db: Session = Depends(get_db)):
|
||||
responses = db.query(
|
||||
models.Responses.ResponseID,
|
||||
models.Responses.StudentID,
|
||||
models.Responses.JobID,
|
||||
models.Responses.Status,
|
||||
models.Responses.Comment,
|
||||
models.Responses.Link,
|
||||
).join(models.Students, models.Students.StudentID == models.Responses.StudentID).\
|
||||
filter(models.Responses.Status == "Ожидает рассмотрения").\
|
||||
order_by(models.Students.Year.asc()).\
|
||||
all() # order_by работате и на буквы в начале группы. Так, сейчас должен работать по идеи
|
||||
|
||||
if not responses:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return responses
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.Job)
|
||||
def create_job(
|
||||
job: schemas.JobCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.Users = Depends(security.get_current_user)
|
||||
):
|
||||
|
||||
new_job = models.Jobs(UserID=current_user.UserID, **job.model_dump(exclude='Hardskills'))
|
||||
db.add(new_job)
|
||||
db.flush()
|
||||
|
||||
crud.update_hardskills(new_job.JobID, job.Hardskills, is_job=True, db=db)
|
||||
|
||||
if not new_job.Archive:
|
||||
crud.update_matches(new_job.JobID, is_job=True, db=db, background_tasks=background_tasks)
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return new_job
|
||||
|
||||
@router.put("/{id}", response_model=schemas.Job)
|
||||
def update_job(
|
||||
id: int,
|
||||
updated_job: schemas.JobUpdate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.Users = Depends(security.get_current_user)
|
||||
):
|
||||
|
||||
job_query = db.query(models.Jobs).filter(models.Jobs.JobID == id)
|
||||
job = job_query.first()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
updated_data = updated_job.model_dump(exclude='Hardskills')
|
||||
updated_data["UserID"] = current_user.UserID
|
||||
|
||||
job_query.update(updated_data, synchronize_session=False)
|
||||
db.flush()
|
||||
|
||||
crud.update_hardskills(id, updated_job.Hardskills, is_job=True, db=db)
|
||||
|
||||
if not updated_job.Archive:
|
||||
crud.update_matches(id, is_job=True, db=db, background_tasks=background_tasks)
|
||||
else:
|
||||
db.query(models.Matches).filter(models.Matches.JobID == id).delete()
|
||||
db.flush()
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return job_query.first()
|
||||
|
||||
@router.put("/responses/{id}", status_code=status.HTTP_200_OK)
|
||||
def update_response(id: int, updated_response: schemas.ResponseUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
updated_record = crud.update_record(models.Responses, id, updated_response.model_dump(), db, background_tasks)
|
||||
return updated_record
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_job(id: int, db: Session = Depends(get_db)):
|
||||
job_query = db.query(models.Jobs).filter(models.Jobs.JobID == id)
|
||||
job = job_query.first()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
job_query.delete()
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
55
mtucijobsbackend/app/routers/service.py
Normal file
55
mtucijobsbackend/app/routers/service.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from fastapi import status, HTTPException, APIRouter, Depends, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from minio import Minio
|
||||
from typing import List
|
||||
from io import BytesIO
|
||||
from os import remove
|
||||
from starlette.background import BackgroundTask
|
||||
from ..database import get_db
|
||||
from ..storage import get_client
|
||||
from .. import models, schemas, security
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/services",
|
||||
dependencies=[Depends(security.verify_api_key)],
|
||||
tags=['Services']
|
||||
)
|
||||
|
||||
@router.get("/resume/{filename}")
|
||||
def get_resume(filename: str, client: Minio = Depends(get_client)):
|
||||
response = client.fget_object("tgjobs", filename, filename)
|
||||
|
||||
if not response:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
task = BackgroundTask(remove, filename)
|
||||
|
||||
return FileResponse(path=filename, filename=response.object_name, media_type=response.content_type, background=task)
|
||||
|
||||
@router.get("/hardskills/", response_model=List[schemas.Hard_skill])
|
||||
def get_all_hardskills(db: Session = Depends(get_db)):
|
||||
hardskills = db.query(models.Hard_skills).filter(models.Hard_skills.Title.ilike("%")).all()
|
||||
|
||||
if not hardskills:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return hardskills
|
||||
|
||||
@router.get("/hardskills/{title}", response_model=List[schemas.Hard_skill])
|
||||
def get_hardskills(title: str, db: Session = Depends(get_db)):
|
||||
hardskills = db.query(models.Hard_skills).filter(models.Hard_skills.Title.ilike("%" + title + "%")).all()
|
||||
|
||||
if not hardskills:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return hardskills
|
||||
|
||||
@router.post("/resume/")
|
||||
async def upload_resume_to_cloud(file: UploadFile, client: Minio = Depends(get_client)):
|
||||
response = client.put_object("tgjobs", file.filename, BytesIO(await file.read()), file.size, file.content_type)
|
||||
|
||||
if not response:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||
|
||||
return {"filename": response.object_name}
|
||||
197
mtucijobsbackend/app/routers/student.py
Normal file
197
mtucijobsbackend/app/routers/student.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from fastapi import status, HTTPException, Response, APIRouter, Depends, Query, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Annotated
|
||||
from ..database import get_db
|
||||
from .. import models, schemas, security, crud
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/students",
|
||||
dependencies=[Depends(security.verify_api_key)],
|
||||
tags=['Students']
|
||||
)
|
||||
|
||||
@router.get("/", response_model=List[schemas.Student])
|
||||
def get_students(db: Session = Depends(get_db)):
|
||||
students = db.query(models.Students)
|
||||
|
||||
return students
|
||||
|
||||
@router.get("/{id}", response_model=schemas.Student)
|
||||
def get_student(id: int, db: Session = Depends(get_db)):
|
||||
student = db.query(models.Students).filter(models.Students.StudentID == id).first()
|
||||
|
||||
if not student:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return student
|
||||
|
||||
@router.get("/responses/{id}", response_model=List[schemas.Response])
|
||||
def get_responses(id: int, db: Session = Depends(get_db)):
|
||||
responses = db.query(models.Responses).filter(models.Responses.StudentID == id).all()
|
||||
|
||||
if not responses:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return responses
|
||||
|
||||
@router.get("/matches/{id}", response_model=List[schemas.MatchStudent])
|
||||
def get_matches(id: int, db: Session = Depends(get_db)):
|
||||
matches = db.query(
|
||||
models.Jobs.JobID,
|
||||
models.Jobs.Company_name,
|
||||
models.Jobs.Link_to_job,
|
||||
models.Jobs.Job_name,
|
||||
models.Jobs.Year,
|
||||
models.Jobs.Qualification,
|
||||
models.Jobs.Time,
|
||||
models.Jobs.Salary_after_interview,
|
||||
models.Jobs.Salary,
|
||||
models.Jobs.Email,
|
||||
models.Jobs.Responsibilities,
|
||||
models.Matches.Match
|
||||
).join(models.Matches, models.Jobs.JobID == models.Matches.JobID).\
|
||||
filter(models.Matches.StudentID == id).\
|
||||
order_by(models.Matches.Match.desc()).\
|
||||
all()
|
||||
|
||||
if not matches:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return matches
|
||||
|
||||
@router.get("/hardskills/{id}", response_model=List[schemas.StudentsHardskill])
|
||||
def get_students_hardskills(id: int, db: Session = Depends(get_db)):
|
||||
hardskills = db.query(models.StudentsHard_skills.Hard_skillID, models.Hard_skills.Title).\
|
||||
join(models.Hard_skills, models.StudentsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.StudentsHard_skills.StudentID == id).all()
|
||||
|
||||
if not hardskills:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return hardskills
|
||||
|
||||
@router.get("/jobs-search/", response_model=List[schemas.Jobs_search])
|
||||
def get_jobs(year: str = None, qualification: bool = None, time: Annotated[list[str], Query()] = [], salary: int = None, hardskills: Annotated[list[str], Query()] = [], search: str = None, db: Session = Depends(get_db)):
|
||||
|
||||
if hardskills:
|
||||
jobs = db.query(
|
||||
models.Jobs.Job_name,
|
||||
models.Jobs.Company_name,
|
||||
models.Jobs.Link_to_job,
|
||||
models.Jobs.Year,
|
||||
models.Jobs.Qualification,
|
||||
models.Jobs.Time,
|
||||
models.Jobs.Salary,
|
||||
models.Jobs.Salary_after_interview,
|
||||
models.Jobs.Email,
|
||||
models.Jobs.Responsibilities,
|
||||
models.Jobs.JobID,
|
||||
models.JobsHard_skills.JobID,
|
||||
models.Hard_skills.Title
|
||||
).join(models.JobsHard_skills, models.Jobs.JobID == models.JobsHard_skills.JobID).\
|
||||
join(models.Hard_skills, models.JobsHard_skills.Hard_skillID == models.Hard_skills.Hard_skillID).\
|
||||
filter(models.Hard_skills.Title.in_(hardskills), models.Jobs.Qualification == qualification, models.Jobs.Archive == False).distinct(models.Jobs.JobID)
|
||||
else:
|
||||
jobs = db.query(models.Jobs).filter(models.Jobs.Qualification == qualification, models.Jobs.Archive == False)
|
||||
|
||||
if year:
|
||||
jobs = jobs.filter(models.Jobs.Year >= year)
|
||||
|
||||
if salary:
|
||||
jobs = jobs.filter(models.Jobs.Salary >= salary)
|
||||
|
||||
if time:
|
||||
jobs = jobs.filter(models.Jobs.Time.op("&&")(time))
|
||||
|
||||
if search:
|
||||
jobs = jobs.filter(models.Jobs.Job_name.match(search))
|
||||
|
||||
return jobs.all()
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.Student)
|
||||
def create_student(student: schemas.StudentCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
new_student = models.Students(**student.model_dump(exclude='Hardskills'))
|
||||
db.add(new_student)
|
||||
db.flush()
|
||||
|
||||
crud.update_hardskills(new_student.StudentID, student.Hardskills, is_job=False, db=db)
|
||||
crud.update_matches(new_student.StudentID, is_job=False, db=db, background_tasks=background_tasks)
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return new_student
|
||||
|
||||
@router.post("/responses/", status_code=status.HTTP_201_CREATED, response_model=schemas.Response)
|
||||
def add_to_responses(response: schemas.ResponseCreate, db: Session = Depends(get_db)):
|
||||
new_response = models.Responses(**response.model_dump())
|
||||
db.add(new_response)
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return new_response
|
||||
|
||||
@router.put("/{id}", response_model=schemas.Student)
|
||||
def update_student(id: int, updated_student: schemas.StudentUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
student_query = db.query(models.Students).filter(models.Students.StudentID == id)
|
||||
student = student_query.first()
|
||||
|
||||
if not student:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
student_query.update(updated_student.model_dump(exclude='Hardskills'))
|
||||
db.flush()
|
||||
|
||||
crud.update_hardskills(id, updated_student.Hardskills, is_job=False, db=db)
|
||||
crud.update_matches(id, is_job=False, db=db, background_tasks=background_tasks)
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return student_query.first()
|
||||
|
||||
@router.put("/responses/{id}", status_code=status.HTTP_200_OK)
|
||||
def update_response(id: int, updated_response: schemas.ResponseUpdate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)):
|
||||
updated_record = crud.update_record(models.Responses, id, updated_response.model_dump(), db, background_tasks)
|
||||
return updated_record
|
||||
|
||||
@router.patch("/{id}", status_code=status.HTTP_200_OK)
|
||||
def update_students_link(id: int, Link: str, db: Session = Depends(get_db)):
|
||||
student_query = db.query(models.Students).filter(models.Students.StudentID == id)
|
||||
student = student_query.first()
|
||||
|
||||
if not student:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
student_query.update({models.Students.Link: Link})
|
||||
db.flush()
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return student_query.first()
|
||||
|
||||
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_student(id: int, db: Session = Depends(get_db)):
|
||||
student_query = db.query(models.Students).filter(models.Students.StudentID == id)
|
||||
student = student_query.first()
|
||||
|
||||
if not student:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
student_query.delete()
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@router.delete("/responses/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_responses(id: int, db: Session = Depends(get_db)):
|
||||
responses_query = db.query(models.Responses).filter(models.Responses.ResponseID == id)
|
||||
response = responses_query.first()
|
||||
|
||||
if not response:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
responses_query.delete()
|
||||
|
||||
crud.commit_transaction(db)
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
60
mtucijobsbackend/app/routers/user.py
Normal file
60
mtucijobsbackend/app/routers/user.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import FastAPI, status, HTTPException, Depends, APIRouter
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import models, schemas, security, utils
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=['Users']
|
||||
)
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.User)
|
||||
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
|
||||
existing_user = db.query(models.Users).filter(models.Users.Email == user.Email).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User with this email already exists"
|
||||
)
|
||||
|
||||
hashed_password = utils.hash(user.Hashed_password)
|
||||
|
||||
user_data = user.model_dump()
|
||||
user_data["Hashed_password"] = hashed_password
|
||||
|
||||
new_user = models.Users(**user_data)
|
||||
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
@router.put("/{email}", response_model=schemas.User)
|
||||
def update_user(email: str, updated_user: schemas.UserUpdate, db: Session = Depends(get_db), current_user: models.Users = Depends(security.get_current_user)):
|
||||
|
||||
if current_user.Email != email:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
existing_user = db.query(models.Users).filter(models.Users.Email == email).first()
|
||||
if not existing_user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
if updated_user.Email:
|
||||
email_user = db.query(models.Users).filter(models.Users.Email == updated_user.Email).first()
|
||||
if email_user and email_user.Email != email:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already in use")
|
||||
|
||||
if updated_user.Hashed_password:
|
||||
hashed_password = utils.hash(updated_user.Hashed_password)
|
||||
updated_user.Hashed_password = hashed_password
|
||||
|
||||
for key, value in updated_user.model_dump(exclude_unset=True).items():
|
||||
setattr(existing_user, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(existing_user)
|
||||
|
||||
return existing_user
|
||||
146
mtucijobsbackend/app/schemas.py
Normal file
146
mtucijobsbackend/app/schemas.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
class StudentUpdate(BaseModel):
|
||||
Name: str
|
||||
Type: str
|
||||
Faculties: str
|
||||
Group: str
|
||||
Course: Optional[int]
|
||||
Experience_specialty: bool
|
||||
Time: List[str]
|
||||
Soft_skills: str
|
||||
Link: str
|
||||
Email: str
|
||||
Phone_number: str
|
||||
Hardskills: List[str]
|
||||
|
||||
class Student(BaseModel):
|
||||
StudentID: int
|
||||
Name: str
|
||||
Type: str
|
||||
Faculties: str
|
||||
Group: str
|
||||
Course: Optional[int]
|
||||
Experience_specialty: bool
|
||||
Time: List[str]
|
||||
Soft_skills: str
|
||||
Link: str
|
||||
Email: str
|
||||
Phone_number: str
|
||||
|
||||
class StudentCreate(Student):
|
||||
Hardskills: List[str]
|
||||
|
||||
class MatchJob(Student):
|
||||
Match: int
|
||||
|
||||
class Jobs_search(BaseModel):
|
||||
JobID: int
|
||||
Company_name: str
|
||||
Link_to_job: Optional[str] = None
|
||||
Job_name: str
|
||||
Year: str
|
||||
Qualification: bool
|
||||
Time: List[str]
|
||||
Salary_after_interview: bool
|
||||
Salary: int
|
||||
Email: str
|
||||
Responsibilities: str
|
||||
|
||||
class MatchStudent(Jobs_search):
|
||||
Match: int
|
||||
|
||||
class JobBase(BaseModel):
|
||||
Company_name: str
|
||||
Link_to_job: Optional[str] = None
|
||||
Job_name: str
|
||||
Year: str
|
||||
Qualification: bool
|
||||
Salary_after_interview: bool
|
||||
Salary: int
|
||||
Email: str
|
||||
Archive: bool
|
||||
Responsibilities: str
|
||||
Time: List[str]
|
||||
Hardskills: List[str]
|
||||
|
||||
class JobCreate(JobBase):
|
||||
pass
|
||||
|
||||
class JobUpdate(JobBase):
|
||||
pass
|
||||
|
||||
class Job(BaseModel):
|
||||
JobID: Optional[int]
|
||||
UserID: int
|
||||
Company_name: Optional[str]
|
||||
Link_to_job: Optional[str] = None
|
||||
Job_name: str
|
||||
Year: str
|
||||
Qualification: bool
|
||||
Time: List[str]
|
||||
Salary_after_interview: bool
|
||||
Salary: int
|
||||
Email: str
|
||||
Archive: bool
|
||||
Responsibilities: str
|
||||
|
||||
class JobGet(Job):
|
||||
Hardskills: List[str]
|
||||
|
||||
class ResponseCreate(BaseModel):
|
||||
StudentID: int
|
||||
JobID: int
|
||||
Status: Optional[str] = "Ожидает рассмотрения"
|
||||
Comment: Optional[str] = None
|
||||
Link: Optional[str] = None
|
||||
|
||||
class ResponseUpdate(BaseModel):
|
||||
Status: Optional[str] = "Ожидает рассмотрения"
|
||||
Comment: Optional[str] = None
|
||||
Link: Optional[str] = None
|
||||
|
||||
class Response(ResponseCreate):
|
||||
ResponseID: int
|
||||
|
||||
class Hard_skill(BaseModel):
|
||||
Hard_skillID: int
|
||||
Title: str
|
||||
|
||||
class StudentsHardskillCreate(BaseModel):
|
||||
Title: str
|
||||
|
||||
class StudentsHardskill(BaseModel):
|
||||
Hard_skillID: int
|
||||
Title: str
|
||||
|
||||
class JobsHardskillCreate(BaseModel):
|
||||
Title: str
|
||||
|
||||
class JobsHardskill(BaseModel):
|
||||
Hard_skillID: int
|
||||
Title: str
|
||||
|
||||
class UserBase(BaseModel):
|
||||
Email: Optional[str]
|
||||
Hashed_password: Optional[str]
|
||||
|
||||
class UserCreate(UserBase):
|
||||
pass
|
||||
|
||||
class UserUpdate(UserBase):
|
||||
pass
|
||||
|
||||
class User(BaseModel):
|
||||
Email: str
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: Union[str, None] = None
|
||||
token_type: str = "Bearer"
|
||||
|
||||
class TokenData(BaseModel):
|
||||
UserID: int
|
||||
Email: str
|
||||
90
mtucijobsbackend/app/security.py
Normal file
90
mtucijobsbackend/app/security.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from . import schemas, database, models
|
||||
from fastapi import Depends, status, HTTPException, Security
|
||||
from fastapi.security import OAuth2PasswordBearer, APIKeyHeader
|
||||
from sqlalchemy.orm import Session
|
||||
from .config import settings
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login')
|
||||
header_scheme = APIKeyHeader(name="X-API-KEY")
|
||||
|
||||
SECRET_KEY = settings.secret_key2
|
||||
ALGORITHM = settings.algorithm
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = settings.refresh_token_expire_days
|
||||
X_API_KEY = settings.x_api_key
|
||||
|
||||
def verify_api_key(api_key_header: str = Security(header_scheme)):
|
||||
if api_key_header == X_API_KEY:
|
||||
return {"status": "OK"}
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def create_access_token(data: dict):
|
||||
to_encode = data.copy()
|
||||
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update(
|
||||
{"type": "access"})
|
||||
to_encode.update(
|
||||
{"exp": expire})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
return encoded_jwt
|
||||
|
||||
def create_refresh_token(data: dict):
|
||||
to_encode = data.copy()
|
||||
|
||||
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
to_encode.update(
|
||||
{"type": "refresh"})
|
||||
to_encode.update(
|
||||
{"exp": expire})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
return encoded_jwt
|
||||
|
||||
def decode_and_verify_token(token: str, expected_type: str):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
token_type: str = payload.get("type")
|
||||
if token_type != expected_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid token type: expected {expected_type}, got {token_type}")
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def verify_access_token(access_token: str = Depends(oauth2_scheme)):
|
||||
payload = decode_and_verify_token(access_token, expected_type="access")
|
||||
|
||||
user_id: str = payload.get("UserID")
|
||||
email: str = payload.get("Email")
|
||||
|
||||
token_data = schemas.TokenData(UserID=user_id, Email=email)
|
||||
|
||||
return token_data
|
||||
|
||||
def verify_refresh_token(refresh_token: str):
|
||||
payload = decode_and_verify_token(refresh_token, expected_type="refresh")
|
||||
|
||||
user_id: str = payload.get("UserID")
|
||||
|
||||
return user_id
|
||||
|
||||
|
||||
def get_current_user(token_data: schemas.TokenData = Depends(verify_access_token), db: Session = Depends(database.get_db)):
|
||||
user = db.query(models.Users).filter(models.Users.UserID == token_data.UserID).first()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
return user
|
||||
21
mtucijobsbackend/app/storage.py
Normal file
21
mtucijobsbackend/app/storage.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
from .config import settings
|
||||
|
||||
def get_client():
|
||||
try:
|
||||
client = Minio(
|
||||
endpoint=settings.endpoint,
|
||||
secure=False,
|
||||
access_key=settings.access_key,
|
||||
secret_key=settings.secret_key
|
||||
)
|
||||
|
||||
found = client.bucket_exists("tgjobs")
|
||||
|
||||
if not found:
|
||||
client.make_bucket("tgjobs")
|
||||
|
||||
return client
|
||||
except S3Error as exc:
|
||||
print("error occurred.", exc)
|
||||
8
mtucijobsbackend/app/utils.py
Normal file
8
mtucijobsbackend/app/utils.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from passlib.context import CryptContext
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def hash(password: str):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def verify(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
61
mtucijobsbackend/requirements.txt
Normal file
61
mtucijobsbackend/requirements.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.4.0
|
||||
argon2-cffi==23.1.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
bcrypt==4.1.3
|
||||
certifi==2024.6.2
|
||||
cffi==1.16.0
|
||||
click==8.1.7
|
||||
colorama==0.4.6
|
||||
cryptography==42.0.8
|
||||
dnspython==2.6.1
|
||||
ecdsa==0.19.0
|
||||
email_validator==2.1.2
|
||||
exceptiongroup==1.2.1
|
||||
fastapi==0.111.0
|
||||
fastapi-cli==0.0.4
|
||||
greenlet==3.0.3
|
||||
h11==0.14.0
|
||||
httpcore==1.0.5
|
||||
httptools==0.6.1
|
||||
httpx==0.27.0
|
||||
idna==3.7
|
||||
iniconfig==2.0.0
|
||||
Jinja2==3.1.4
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==2.1.5
|
||||
mdurl==0.1.2
|
||||
minio==7.2.7
|
||||
orjson==3.10.5
|
||||
packaging==24.1
|
||||
passlib==1.7.4
|
||||
pluggy==1.5.0
|
||||
psycopg2-binary==2.9.9
|
||||
pyasn1==0.6.0
|
||||
pycparser==2.22
|
||||
pycryptodome==3.20.0
|
||||
pydantic==2.7.4
|
||||
pydantic-settings==2.3.3
|
||||
pydantic_core==2.18.4
|
||||
Pygments==2.18.0
|
||||
pytest==8.2.2
|
||||
python-dotenv==1.0.1
|
||||
python-jose==3.3.0
|
||||
python-multipart==0.0.9
|
||||
PyYAML==6.0.1
|
||||
rich==13.7.1
|
||||
rsa==4.9
|
||||
shellingham==1.5.4
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
SQLAlchemy==2.0.30
|
||||
starlette==0.37.2
|
||||
tomli==2.0.1
|
||||
typer==0.12.3
|
||||
typing_extensions==4.12.2
|
||||
ujson==5.10.0
|
||||
urllib3==2.2.2
|
||||
uvicorn==0.30.1
|
||||
watchfiles==0.22.0
|
||||
websockets==12.0
|
||||
requests==2.32.3
|
||||
0
mtucijobsbackend/tests/__init__.py
Normal file
0
mtucijobsbackend/tests/__init__.py
Normal file
36
mtucijobsbackend/tests/conftest.py
Normal file
36
mtucijobsbackend/tests/conftest.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.main import app
|
||||
from app.config import settings
|
||||
from app.database import get_db, Base
|
||||
from app import schemas, models
|
||||
|
||||
# DATABASE_URL = f'postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}:{settings.database_port}/{settings.database_name}_test'
|
||||
DATABASE_URL = f'postgresql://postgres:2003@localhost:5444/tg_jobs_test'
|
||||
engine = create_engine(DATABASE_URL)
|
||||
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
@pytest.fixture()
|
||||
def session():
|
||||
print("my session fixture ran")
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
db = TestingSessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(session):
|
||||
def override_get_db():
|
||||
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
yield TestClient(app)
|
||||
158
mtucijobsbackend/tests/test_students.py
Normal file
158
mtucijobsbackend/tests/test_students.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import pytest
|
||||
from app import schemas, models
|
||||
|
||||
@pytest.fixture()
|
||||
def create_hardskills(session):
|
||||
hardskill1 = models.Hard_skills(Title="Python")
|
||||
hardskill2 = models.Hard_skills(Title="Java")
|
||||
session.add(hardskill1)
|
||||
session.add(hardskill2)
|
||||
session.commit()
|
||||
session.refresh(hardskill1)
|
||||
session.refresh(hardskill2)
|
||||
return [hardskill1, hardskill2]
|
||||
|
||||
Glob_StudentID = 1 # Важно!!! Данная переменная не должна совпадать с StudentID из student_data_test.
|
||||
|
||||
@pytest.fixture
|
||||
def student_data_test():
|
||||
return {
|
||||
"StudentID": 1234567890,
|
||||
"Name": "Журавлёв Василий Иванович",
|
||||
"Type": "Работу",
|
||||
"Group": "БУТ2101",
|
||||
"Time": ["20", "30", "40"],
|
||||
"Soft_skills": "коммуникабельность, работа в команде, адаптивность",
|
||||
"Link": "https://Vasiliy.com",
|
||||
"Email": "Vasiliy@gmail.com"
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def job_data_test(create_hardskills):
|
||||
hardskills = create_hardskills
|
||||
return {
|
||||
"job": {
|
||||
"Company_name": "АСУ-ВЭИ",
|
||||
"Job_name": "Работа с ПЛК",
|
||||
"Year": "3",
|
||||
"Qualification": False,
|
||||
"Soft_skills": "Работа в команде",
|
||||
"Salary": 25000,
|
||||
"Email": "info@asu-vei.ru",
|
||||
"Archive": False,
|
||||
"Responsibilities": "Разработка методических пособий, для работы с ПЛК. Тестирование scada системы"
|
||||
},
|
||||
"hardskills": [
|
||||
{
|
||||
"JobID": 1,
|
||||
"Hard_skillID": hardskills[0].Hard_skillID
|
||||
},
|
||||
{
|
||||
"JobID": 1,
|
||||
"Hard_skillID": hardskills[1].Hard_skillID
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def favourite_data_test(create_student, create_job):
|
||||
student_id = create_student["StudentID"]
|
||||
return {
|
||||
"StudentID": student_id,
|
||||
"JobID": 1
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def create_favourite(client, favourite_data_test):
|
||||
response = client.post("/students/favourites/", json=favourite_data_test)
|
||||
assert response.status_code == 201
|
||||
return response.json()
|
||||
|
||||
@pytest.fixture
|
||||
def create_job(client, job_data_test):
|
||||
response = client.post("/jobs/", json=job_data_test)
|
||||
assert response.status_code == 201
|
||||
return response.json()
|
||||
|
||||
@pytest.fixture
|
||||
def create_student(client, student_data_test):
|
||||
response = client.post("/students/", json={"student": student_data_test})
|
||||
assert response.status_code == 201
|
||||
return response.json()
|
||||
|
||||
def test_root(client):
|
||||
res = client.get("/")
|
||||
assert res.json().get('message') == "I'm ok!"
|
||||
assert res.status_code == 200
|
||||
|
||||
def test_get_jobs(client):
|
||||
response = client.get("/students/")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_students(client):
|
||||
response = client.get("/students/")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_student_by_id(client, create_student):
|
||||
student_id = create_student["StudentID"]
|
||||
response = client.get(f"/students/{student_id}")
|
||||
assert response.status_code == 200
|
||||
student = schemas.Student(**response.json())
|
||||
assert student.StudentID == student_id
|
||||
|
||||
# Возможно стоит удалить этот тест
|
||||
def test_get_student_by_id_not_exist(client, create_student):
|
||||
response = client.get(f"/students/{Glob_StudentID}")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_create_student(client, student_data_test):
|
||||
response = client.post("/students/", json={"student": student_data_test})
|
||||
assert response.status_code == 201
|
||||
new_student = schemas.Student(**response.json())
|
||||
assert new_student.Name == "Журавлёв Василий Иванович"
|
||||
|
||||
def test_update_student(client, create_student):
|
||||
student_id = create_student["StudentID"]
|
||||
updated_data = {
|
||||
"Name": "Журавлёв Владимир Иванович",
|
||||
"Type": "Стажировку",
|
||||
"Group": "БУТ2101",
|
||||
"Time": ["20"],
|
||||
"Soft_skills": "коммуникабельность, адаптивность",
|
||||
"Link": "https://Vladimir.com",
|
||||
"Email": "Vladimir@gmail.com"
|
||||
}
|
||||
response = client.put(f"/students/{student_id}", json=updated_data)
|
||||
assert response.status_code == 200
|
||||
updated_student = schemas.Student(**response.json())
|
||||
assert updated_student.Name == "Журавлёв Владимир Иванович"
|
||||
assert updated_student.Type == "Стажировку"
|
||||
assert updated_student.Group == "БУТ2101"
|
||||
assert updated_student.Time == ["20"]
|
||||
assert updated_student.Soft_skills == "коммуникабельность, адаптивность"
|
||||
assert updated_student.Link == "https://Vladimir.com"
|
||||
assert updated_student.Email == "Vladimir@gmail.com"
|
||||
|
||||
def test_delete_student(client, create_student):
|
||||
student_id = create_student["StudentID"]
|
||||
response = client.delete(f"/students/{student_id}")
|
||||
assert response.status_code == 204
|
||||
|
||||
response = client.get(f"/students/{student_id}")
|
||||
assert response.status_code == 404
|
||||
|
||||
# Для этого теста нужно создать job
|
||||
def test_add_to_favourites(client, create_favourite):
|
||||
student_id = create_favourite["StudentID"]
|
||||
job_id = create_favourite["JobID"]
|
||||
assert student_id == student_id
|
||||
assert job_id == 1
|
||||
|
||||
# Для этого теста наверное надо ещё заполнить таблицу Matches, так что думаю этот тест не очень стабильный, но о работает)))
|
||||
# def test_get_favourites(client, create_student, create_job, create_favourite):
|
||||
# student_id = create_student["StudentID"]
|
||||
# response = client.get(f"/students/favourites/{student_id}")
|
||||
|
||||
# # assert response.status_code == 200 # как это может ломать тест?
|
||||
# favourites = response.json()
|
||||
# assert len(favourites) > 0
|
||||
Reference in New Issue
Block a user