add api routes to crud operation on student models for coordinator
This commit is contained in:
parent
e051f3a101
commit
5607f3cd08
@ -8,6 +8,7 @@ from .config import config
|
||||
from .dependencies import db, ma
|
||||
from .commands.startapp import startapp
|
||||
from .utils import import_models
|
||||
from .api import api_bp
|
||||
|
||||
|
||||
def create_app(config_name: str = None) -> Flask:
|
||||
@ -29,8 +30,7 @@ def create_app(config_name: str = None) -> Flask:
|
||||
|
||||
Migrate(app, db)
|
||||
|
||||
# register blueprints
|
||||
# app.register_blueprint(blueprint)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
# register commands
|
||||
app.cli.add_command(startapp)
|
||||
|
7
backend/app/api.py
Normal file
7
backend/app/api.py
Normal file
@ -0,0 +1,7 @@
|
||||
from flask import Blueprint
|
||||
from .coordinator.routes import bp as coordinator_bp
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
# register blueprints here
|
||||
api_bp.register_blueprint(coordinator_bp)
|
0
backend/app/base/__init__.py
Normal file
0
backend/app/base/__init__.py
Normal file
15
backend/app/base/models.py
Normal file
15
backend/app/base/models.py
Normal file
@ -0,0 +1,15 @@
|
||||
from ..dependencies import db
|
||||
|
||||
|
||||
class Base(db.Model):
|
||||
__abstract__ = True
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
|
||||
class Person(db.Model):
|
||||
__abstract__ = True
|
||||
|
||||
first_name = db.Column(db.String(255), nullable=False)
|
||||
last_name = db.Column(db.String(255), nullable=False)
|
||||
email = db.Column(db.String(120), unique=True)
|
0
backend/app/coordinator/__init__.py
Normal file
0
backend/app/coordinator/__init__.py
Normal file
8
backend/app/coordinator/exceptions.py
Normal file
8
backend/app/coordinator/exceptions.py
Normal file
@ -0,0 +1,8 @@
|
||||
class CSVException(Exception):
|
||||
"""Main csv exception"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidNameOrTypeHeaderException(CSVException):
|
||||
"""Throw if csv file has invalid name or type of header"""
|
||||
pass
|
5
backend/app/coordinator/models.py
Normal file
5
backend/app/coordinator/models.py
Normal file
@ -0,0 +1,5 @@
|
||||
# from ..base.models import Base
|
||||
#
|
||||
#
|
||||
# class Coordinator(Base):
|
||||
# __tablename__ = 'coordinators'
|
7
backend/app/coordinator/routes/__init__.py
Normal file
7
backend/app/coordinator/routes/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
from .students import bp as students_bp
|
||||
|
||||
bp = Blueprint("coordinator", __name__, url_prefix="/coordinator")
|
||||
|
||||
bp.register_blueprint(students_bp)
|
100
backend/app/coordinator/routes/students.py
Normal file
100
backend/app/coordinator/routes/students.py
Normal file
@ -0,0 +1,100 @@
|
||||
from typing import Tuple
|
||||
from random import randint
|
||||
|
||||
from flask import Blueprint, request, make_response, jsonify, Response
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from ...students.models import Student
|
||||
from ..schemas import StudentSchema, StudentEditSchema, StudentCreateSchema
|
||||
from ...dependencies import db
|
||||
from ..utils import parse_csv
|
||||
from ..exceptions import InvalidNameOrTypeHeaderException
|
||||
|
||||
bp = Blueprint("students", __name__, url_prefix="/students")
|
||||
|
||||
|
||||
@bp.route("/", methods=["GET"])
|
||||
def list_students() -> Tuple[dict, int]:
|
||||
students_schema = StudentSchema(many=True)
|
||||
students = Student.query.all()
|
||||
return {"students": students_schema.dump(students)}, 200
|
||||
|
||||
|
||||
@bp.route("/<int:index>/", methods=["GET"])
|
||||
def detail_student(index: int) -> Tuple[dict, int]:
|
||||
student_schema = StudentSchema()
|
||||
student = Student.query.filter_by(index=index).first()
|
||||
if student is not None:
|
||||
return student_schema.dump(student), 200
|
||||
return {"error": f"Student with {index} index doesn't exist!"}, 404
|
||||
|
||||
|
||||
@bp.route("/<int:index>/", methods=["DELETE"])
|
||||
def delete_student(index: int) -> Tuple[dict, int]:
|
||||
student = Student.query.filter_by(index=index).first()
|
||||
if student is not None:
|
||||
db.session.delete(student)
|
||||
db.session.commit()
|
||||
return {"message": "Student was deleted!"}, 202
|
||||
return {"error": f"Student with {index} index doesn't exist!"}, 404
|
||||
|
||||
|
||||
@bp.route("/<int:index>/", methods=["PUT"])
|
||||
def edit_student(index: int) -> Tuple[dict, int]:
|
||||
request_data = request.get_json()
|
||||
student_schema = StudentEditSchema()
|
||||
|
||||
try:
|
||||
data = student_schema.load(request_data)
|
||||
if not data:
|
||||
return {"error": "You have passed empty data!"}, 400
|
||||
|
||||
student_query = Student.query.filter_by(index=index)
|
||||
student = student_query.first()
|
||||
if student is None:
|
||||
return {"error": "Not Found student!"}, 404
|
||||
|
||||
student_query.update(data)
|
||||
db.session.commit()
|
||||
except ValidationError as e:
|
||||
return {"error": e.messages}, 422
|
||||
return {"message": "Student was updated!"}, 200
|
||||
|
||||
|
||||
@bp.route("/", methods=["POST"])
|
||||
def create_student() -> Tuple[dict, int] or Response:
|
||||
request_data = request.get_json()
|
||||
student_schema = StudentCreateSchema()
|
||||
|
||||
try:
|
||||
data = student_schema.load(request_data)
|
||||
|
||||
index = data['index']
|
||||
student = Student.query.filter_by(index=index).first()
|
||||
if student is not None:
|
||||
return {"error": "Student has already exists!"}, 400
|
||||
|
||||
dummy_email = f'student{randint(1, 300_000)}@gmail.com'
|
||||
student = Student(**data, email=dummy_email)
|
||||
db.session.add(student)
|
||||
db.session.commit()
|
||||
except ValidationError as e:
|
||||
return {'error': e.messages}, 422
|
||||
return {"message": "Student was created!"}, 200
|
||||
|
||||
|
||||
@bp.route("/upload/", methods=["POST"])
|
||||
def upload_students() -> Tuple[dict, int]:
|
||||
"""Maybe in the future move to celery workers"""
|
||||
if (uploaded_file := request.files.get('file')) is None:
|
||||
return {"error": "You didn't attach a csv file!"}, 400
|
||||
|
||||
try:
|
||||
students = parse_csv(uploaded_file)
|
||||
except InvalidNameOrTypeHeaderException:
|
||||
return {"error": "Invalid format of csv file!"}, 400
|
||||
|
||||
for student in students:
|
||||
db.session.add(student)
|
||||
db.session.commit()
|
||||
return {"message": "Students was created by uploading csv file!"}, 200
|
37
backend/app/coordinator/schemas.py
Normal file
37
backend/app/coordinator/schemas.py
Normal file
@ -0,0 +1,37 @@
|
||||
from ..dependencies import ma
|
||||
from ..students.models import Student, Group
|
||||
from marshmallow import fields, validate, ValidationError
|
||||
|
||||
|
||||
def validate_index(index):
|
||||
if len(str(index)) > 6:
|
||||
raise ValidationError("Length of index is too long!")
|
||||
elif len(str(index)) < 6:
|
||||
raise ValidationError("Length of index is too short!")
|
||||
|
||||
|
||||
class GroupSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Group
|
||||
include_relationships = False
|
||||
|
||||
|
||||
class StudentSchema(ma.SQLAlchemyAutoSchema):
|
||||
group = fields.Nested(GroupSchema)
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
|
||||
|
||||
class StudentCreateSchema(ma.Schema):
|
||||
first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
|
||||
last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
|
||||
index = fields.Integer(validate=validate_index, required=True)
|
||||
mode = fields.Boolean(required=True)
|
||||
|
||||
|
||||
class StudentEditSchema(ma.Schema):
|
||||
first_name = fields.Str(validate=validate.Length(min=1, max=255))
|
||||
last_name = fields.Str(validate=validate.Length(min=1, max=255))
|
||||
index = fields.Integer(validate=validate_index)
|
||||
mode = fields.Boolean()
|
37
backend/app/coordinator/utils.py
Normal file
37
backend/app/coordinator/utils.py
Normal file
@ -0,0 +1,37 @@
|
||||
from typing import List
|
||||
from random import randint
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from .exceptions import InvalidNameOrTypeHeaderException
|
||||
from ..students.models import Student
|
||||
|
||||
|
||||
def check_columns(df: pd.DataFrame, columns: List[str]) -> bool:
|
||||
flag = True
|
||||
col_types = ['object', 'object', 'int', 'int']
|
||||
print(df.dtypes['first_name'])
|
||||
for name, col_type in zip(columns, col_types):
|
||||
if name not in df and df.dtypes[name].startswith(col_type):
|
||||
flag = False
|
||||
break
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
def parse_csv(file) -> List[Student]:
|
||||
df = pd.read_csv(file)
|
||||
columns = ['first_name', 'last_name', 'index', 'mode']
|
||||
|
||||
if not check_columns(df, columns):
|
||||
raise InvalidNameOrTypeHeaderException
|
||||
|
||||
students = []
|
||||
for _, item in df.iterrows():
|
||||
data = {}
|
||||
for c in columns:
|
||||
data[c] = item[c]
|
||||
data['email'] = f'student{randint(1, 300_000)}@gmail.com'
|
||||
students.append(Student(**data))
|
||||
|
||||
return students
|
0
backend/app/students/__init__.py
Normal file
0
backend/app/students/__init__.py
Normal file
28
backend/app/students/models.py
Normal file
28
backend/app/students/models.py
Normal file
@ -0,0 +1,28 @@
|
||||
from ..dependencies import db
|
||||
from ..base.models import Person, Base
|
||||
|
||||
|
||||
class ProjectSupervisor(Base, Person):
|
||||
__tablename__ = "project_supervisors"
|
||||
|
||||
limit_group = db.Column(db.Integer, default=1, nullable=False)
|
||||
mode = db.Column(db.Boolean, default=True, nullable=False) # True - stationary, False - non-stationary
|
||||
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "groups"
|
||||
|
||||
name = db.Column(db.String(60), nullable=False)
|
||||
project_supervisor_id = db.Column(db.Integer, db.ForeignKey('project_supervisors.id'))
|
||||
project_supervisor = db.relationship('ProjectSupervisor', backref='groups', lazy=True)
|
||||
|
||||
|
||||
class Student(Person):
|
||||
__tablename__ = "students"
|
||||
|
||||
index = db.Column(db.Integer, primary_key=True)
|
||||
first_term = db.Column(db.Integer, default=0, nullable=False)
|
||||
second_term = db.Column(db.Integer, default=0, nullable=False)
|
||||
group_id = db.Column(db.Integer, db.ForeignKey('groups.id'))
|
||||
group = db.relationship('Group', backref='students', lazy=True)
|
||||
mode = db.Column(db.Boolean, default=True, nullable=False) # True - stationary, False - non-stationary
|
0
backend/app/students/routes.py
Normal file
0
backend/app/students/routes.py
Normal file
0
backend/app/students/schemas.py
Normal file
0
backend/app/students/schemas.py
Normal file
59
backend/migrations/versions/45be50e56689_.py
Normal file
59
backend/migrations/versions/45be50e56689_.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 45be50e56689
|
||||
Revises:
|
||||
Create Date: 2022-05-17 19:36:35.976642
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '45be50e56689'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('project_supervisors',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('first_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('last_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('email', sa.String(length=120), nullable=True),
|
||||
sa.Column('limit_group', sa.Integer(), nullable=False),
|
||||
sa.Column('mode', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
op.create_table('groups',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('name', sa.String(length=60), nullable=False),
|
||||
sa.Column('project_supervisor_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['project_supervisor_id'], ['project_supervisors.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('students',
|
||||
sa.Column('first_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('last_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('email', sa.String(length=120), nullable=True),
|
||||
sa.Column('index', sa.Integer(), nullable=False),
|
||||
sa.Column('first_term', sa.Integer(), nullable=False),
|
||||
sa.Column('second_term', sa.Integer(), nullable=False),
|
||||
sa.Column('group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('mode', sa.Boolean(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
||||
sa.PrimaryKeyConstraint('index'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('students')
|
||||
op.drop_table('groups')
|
||||
op.drop_table('project_supervisors')
|
||||
# ### end Alembic commands ###
|
@ -17,7 +17,9 @@ MarkupSafe==2.1.1
|
||||
marshmallow==3.15.0
|
||||
marshmallow-sqlalchemy==0.28.0
|
||||
mccabe==0.6.1
|
||||
numpy==1.22.3
|
||||
packaging==21.3
|
||||
pandas==1.4.2
|
||||
pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pycodestyle==2.8.0
|
||||
@ -25,7 +27,9 @@ pyflakes==2.4.0
|
||||
pyparsing==3.0.9
|
||||
pytest==7.1.2
|
||||
pytest-flask==1.2.0
|
||||
python-dateutil==2.8.2
|
||||
python-dotenv==0.20.0
|
||||
pytz==2022.1
|
||||
six==1.16.0
|
||||
SQLAlchemy==1.4.36
|
||||
tomli==2.0.1
|
||||
|
9
data/students.csv
Normal file
9
data/students.csv
Normal file
@ -0,0 +1,9 @@
|
||||
first_name,last_name,index,mode
|
||||
Patryk,Drzewiński,452790,1
|
||||
Adam,Skowronek,452791,1
|
||||
Mariia,Kuzmenko,452792,1
|
||||
Dominik,Cupał,452793,1
|
||||
Natalia,Wasik,452794,0
|
||||
Michalina,Gaj,452795,0
|
||||
Jan,Kowalski,452796,0
|
||||
Adrian,Kowalski,452797,1
|
|
Loading…
Reference in New Issue
Block a user