add api routes to crud operation on student models for coordinator

This commit is contained in:
dominik24c 2022-05-17 22:16:45 +02:00
parent e051f3a101
commit 5607f3cd08
18 changed files with 318 additions and 2 deletions

View File

@ -8,6 +8,7 @@ from .config import config
from .dependencies import db, ma from .dependencies import db, ma
from .commands.startapp import startapp from .commands.startapp import startapp
from .utils import import_models from .utils import import_models
from .api import api_bp
def create_app(config_name: str = None) -> Flask: def create_app(config_name: str = None) -> Flask:
@ -29,8 +30,7 @@ def create_app(config_name: str = None) -> Flask:
Migrate(app, db) Migrate(app, db)
# register blueprints app.register_blueprint(api_bp)
# app.register_blueprint(blueprint)
# register commands # register commands
app.cli.add_command(startapp) app.cli.add_command(startapp)

7
backend/app/api.py Normal file
View 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)

View File

View 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)

View File

View 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

View File

@ -0,0 +1,5 @@
# from ..base.models import Base
#
#
# class Coordinator(Base):
# __tablename__ = 'coordinators'

View 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)

View 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

View 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()

View 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

View File

View 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

View File

View File

View 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 ###

View File

@ -17,7 +17,9 @@ MarkupSafe==2.1.1
marshmallow==3.15.0 marshmallow==3.15.0
marshmallow-sqlalchemy==0.28.0 marshmallow-sqlalchemy==0.28.0
mccabe==0.6.1 mccabe==0.6.1
numpy==1.22.3
packaging==21.3 packaging==21.3
pandas==1.4.2
pluggy==1.0.0 pluggy==1.0.0
py==1.11.0 py==1.11.0
pycodestyle==2.8.0 pycodestyle==2.8.0
@ -25,7 +27,9 @@ pyflakes==2.4.0
pyparsing==3.0.9 pyparsing==3.0.9
pytest==7.1.2 pytest==7.1.2
pytest-flask==1.2.0 pytest-flask==1.2.0
python-dateutil==2.8.2
python-dotenv==0.20.0 python-dotenv==0.20.0
pytz==2022.1
six==1.16.0 six==1.16.0
SQLAlchemy==1.4.36 SQLAlchemy==1.4.36
tomli==2.0.1 tomli==2.0.1

9
data/students.csv Normal file
View 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
1 first_name last_name index mode
2 Patryk Drzewiński 452790 1
3 Adam Skowronek 452791 1
4 Mariia Kuzmenko 452792 1
5 Dominik Cupał 452793 1
6 Natalia Wasik 452794 0
7 Michalina Gaj 452795 0
8 Jan Kowalski 452796 0
9 Adrian Kowalski 452797 1