diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 5650819..4139fd7 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -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) diff --git a/backend/app/api.py b/backend/app/api.py new file mode 100644 index 0000000..1524d99 --- /dev/null +++ b/backend/app/api.py @@ -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) diff --git a/backend/app/base/__init__.py b/backend/app/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/base/models.py b/backend/app/base/models.py new file mode 100644 index 0000000..7f52ac2 --- /dev/null +++ b/backend/app/base/models.py @@ -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) diff --git a/backend/app/coordinator/__init__.py b/backend/app/coordinator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/coordinator/exceptions.py b/backend/app/coordinator/exceptions.py new file mode 100644 index 0000000..a02e7ba --- /dev/null +++ b/backend/app/coordinator/exceptions.py @@ -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 diff --git a/backend/app/coordinator/models.py b/backend/app/coordinator/models.py new file mode 100644 index 0000000..02de479 --- /dev/null +++ b/backend/app/coordinator/models.py @@ -0,0 +1,5 @@ +# from ..base.models import Base +# +# +# class Coordinator(Base): +# __tablename__ = 'coordinators' diff --git a/backend/app/coordinator/routes/__init__.py b/backend/app/coordinator/routes/__init__.py new file mode 100644 index 0000000..6252558 --- /dev/null +++ b/backend/app/coordinator/routes/__init__.py @@ -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) diff --git a/backend/app/coordinator/routes/students.py b/backend/app/coordinator/routes/students.py new file mode 100644 index 0000000..2b4597e --- /dev/null +++ b/backend/app/coordinator/routes/students.py @@ -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("//", 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("//", 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("//", 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 diff --git a/backend/app/coordinator/schemas.py b/backend/app/coordinator/schemas.py new file mode 100644 index 0000000..255e85e --- /dev/null +++ b/backend/app/coordinator/schemas.py @@ -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() diff --git a/backend/app/coordinator/utils.py b/backend/app/coordinator/utils.py new file mode 100644 index 0000000..7a49886 --- /dev/null +++ b/backend/app/coordinator/utils.py @@ -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 diff --git a/backend/app/students/__init__.py b/backend/app/students/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/students/models.py b/backend/app/students/models.py new file mode 100644 index 0000000..6512db3 --- /dev/null +++ b/backend/app/students/models.py @@ -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 diff --git a/backend/app/students/routes.py b/backend/app/students/routes.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/students/schemas.py b/backend/app/students/schemas.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/migrations/versions/45be50e56689_.py b/backend/migrations/versions/45be50e56689_.py new file mode 100644 index 0000000..0dad58f --- /dev/null +++ b/backend/migrations/versions/45be50e56689_.py @@ -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 ### diff --git a/backend/requirements.txt b/backend/requirements.txt index 3d705eb..d486409 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/data/students.csv b/data/students.csv new file mode 100644 index 0000000..8ba79b7 --- /dev/null +++ b/data/students.csv @@ -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 \ No newline at end of file