Skip to content

Commit

Permalink
Merge pull request #18 from InbarShirizly/api-dev
Browse files Browse the repository at this point in the history
Api dev
  • Loading branch information
InbarShirizly authored Oct 4, 2020
2 parents 9111569 + f49f203 commit a3056c2
Show file tree
Hide file tree
Showing 28 changed files with 370 additions and 469 deletions.
24 changes: 24 additions & 0 deletions Server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# zoom-attendance-check API

API doc can be found [here](https://documenter.getpostman.com/view/4335694/TVRg694k)

## Requirements
1. Install python 3 and pip
2. `pip install -r requirements.txt`
3. `python run.py`

API will be running now at: `http://localhost:5000`

## Project structure
The server code is inside Server package, and can be runned with the external module `run.py`.
We decided to use `flask-restful` extenstion in order to create the endpoints, and `flaks-sqlalchemy` in order to create the database.

The inside the `Server` package is organzied into the following sub-packages:

- `api` - package which is responsible for the api endpoints
- `utils` - general utils for the application

also there are:

- `models.py` - ORM models for the database
- `config.py` - genreal settings for the application
12 changes: 4 additions & 8 deletions Server/Server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
from Server.config import FlaskConfig
from flask_httpauth import HTTPBasicAuth

app = Flask(__name__)
app.config.from_object(FlaskConfig)

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'users.login'
login_manager.login_message_category = 'info'
auth = HTTPBasicAuth(app)


from Server.users.routes import users
from Server.classrooms.routes import classrooms
from Server.api import api_blueprint

app.register_blueprint(users)
app.register_blueprint(classrooms)
app.register_blueprint(api_blueprint)

7 changes: 7 additions & 0 deletions Server/Server/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from flask_restful import Api
from flask import Blueprint

api_blueprint = Blueprint('api', __name__, url_prefix='/api')
api = Api(api_blueprint)

from Server.api import auth, clasrooms, reports, student_status
Binary file not shown.
Binary file added Server/Server/api/__pycache__/auth.cpython-38.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
31 changes: 31 additions & 0 deletions Server/Server/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from Server import auth, bcrypt, db
from flask_restful import Resource, reqparse, abort
from Server.api import api
from Server.models import get_user, TeacherModel


register_argparse = reqparse.RequestParser()
register_argparse.add_argument("username", type=str, help="Username is required", location='json', required=True)
register_argparse.add_argument("email", type=str, help="Email is required", location='json', required=True)
register_argparse.add_argument("password", type=str, help="Password is required", location='json', required=True)


class RegisterResource(Resource):
def post(self):
args = register_argparse.parse_args()
if TeacherModel.query.filter_by(username=args['username']).first():
return abort(400, message="Username already taken")
if TeacherModel.query.filter_by(email=args['email']).first():
return abort(400, message="Email already taken")

user = TeacherModel(
username=args['username'],
email=args['email'],
password=bcrypt.generate_password_hash(args['password'])
)
db.session.add(user)
db.session.commit()
return '', 204


api.add_resource(RegisterResource, "/register")
94 changes: 94 additions & 0 deletions Server/Server/api/clasrooms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from Server.api import api
from flask_restful import Resource, reqparse, abort, fields, marshal
from Server import auth, db
from Server.models import TeacherModel, ClassroomModel
from werkzeug.datastructures import FileStorage
from Server.utils import parser
from Server.utils.utils import create_students_df
import pandas as pd

# Fields:
classrooms_list_fields = { # Fields list of classrooms
'name': fields.String,
'id': fields.Integer
}
class StudentItemField(fields.Raw): # Custom field to parse StudentModel object

def format(self, value):
without_none = {k: v for k, v in value.__dict__ .items() if v is not None} # Getting only attributes which are not None
del without_none['_sa_instance_state']
del without_none['class_id']
return without_none

classroom_resource_fields = { # Fields for a single classroom
'name': fields.String,
'students': fields.List(StudentItemField)
}


# arg parsing:
classrooms_post_argparse = reqparse.RequestParser()
classrooms_post_argparse.add_argument('name', type=str, help="Name of the class is required", required=True)
classrooms_post_argparse.add_argument('students_file', type=FileStorage, location='files', help="Student file is required", required=True)

classroom_put_argparse = reqparse.RequestParser()
classroom_put_argparse.add_argument('new_name', type=str, help="New name is required in order to update", location="json", required=True)


class ClassroomsResource(Resource):
method_decorators = [auth.login_required]

def get(self, class_id=None):
if class_id is None:
return marshal(auth.current_user().classrooms, classrooms_list_fields)

current_class = ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() # Making sure the class belongs to the current user
if current_class is None:
abort(400, message="Invalid class id")
return marshal(current_class, classroom_resource_fields)

def post(self, class_id=None):
if class_id:
return abort(404, message="Invalid route")
args = classrooms_post_argparse.parse_args()
filename, stream = args['students_file'].filename.replace('"', ""), args['students_file'].stream #TODO: replace here because of postman post request
students_df = create_students_df(filename, stream)
students = parser.parse_df(students_df)

new_class = ClassroomModel(name=args['name'], teacher=auth.current_user())
db.session.add(new_class)
db.session.commit()

students['class_id'] = pd.Series([new_class.id] * students.shape[0])
students.to_sql('student', con=db.engine, if_exists="append", index=False)
return {'class_id': new_class.id}

def put(self, class_id=None):
if class_id is None:
return abort(404, message="Invalid route")
args = classroom_put_argparse.parse_args()
current_class = ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() # Making sure the class belongs to the current user
if current_class is None:
abort(400, message="Invalid class id")
current_class.name = args['new_name']
db.session.commit()
return "", 204

def delete(self, class_id=None):
if class_id is None: # Deleting all classes
teacher_classes_id = db.session.query(ClassroomModel.id).filter_by(teacher=auth.current_user()).all()
for class_data in teacher_classes_id:
current_class = ClassroomModel.query.filter_by(id=class_data.id, teacher=auth.current_user()).first()
db.session.delete(current_class)
db.session.commit()
return "", 204

current_class = ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() # Making sure the class belongs to the current user
if current_class is None:
abort(400, message="Invalid class id")

db.session.delete(current_class)
db.session.commit()
return "", 204

api.add_resource(ClassroomsResource, "/classrooms", "/classrooms/<int:class_id>")
105 changes: 105 additions & 0 deletions Server/Server/api/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from Server.api import api
from flask_restful import Resource, reqparse, abort, marshal, fields
from Server.utils.attendance_check import Attendance
from werkzeug.datastructures import FileStorage
from Server import db, auth
from datetime import datetime
import pandas as pd
from Server.models import StudentModel, ClassroomModel, ReportModel, SessionModel, ZoomNamesModel, StudentStatus
from Server.utils.utils import create_chat_df


# marshals:
reports_list_fields = { # Fields list of classrooms
'description': fields.String,
'id': fields.Integer
}

student_status_field = {
'status': fields.Integer,
'student_name': fields.String(attribute='student.name'),
'status_id': fields.Integer(attribute="id")
}

# args:
report_post_args = reqparse.RequestParser()
report_post_args.add_argument('description', type=str)
report_post_args.add_argument('chat_file', type=FileStorage, help="Chat file is required", location='files', required=True)
report_post_args.add_argument('time_delta', default=1, type=int)
report_post_args.add_argument('date', default=datetime.now().date(), type=lambda x: datetime.strptime(x, '%d/%m/%y'))
report_post_args.add_argument('first_sentence', type=str, help='First sentence is required in order to understand when does the check starts', required=True)
report_post_args.add_argument('not_included_zoom_users', default=[], type=str, help='Must be a list of strings with zoom names', action="append")


class ReportsResource(Resource):
method_decorators = [auth.login_required]

def get(self, class_id, report_id=None): # TODO: create decorator that validates class_id
if ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() is None:
abort(400, message="Invalid class id")
if report_id is None:
return marshal(ReportModel.query.filter_by(class_id=class_id).all(), reports_list_fields)
report = ReportModel.query.filter_by(class_id=class_id, id=report_id).first()
if report is None:
abort(400, message="Invalid report id")
return marshal(report.student_statuses, student_status_field)

def post(self, class_id, report_id=None):
args = report_post_args.parse_args()
if ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() is None:
abort(400, message="Invalid class id")
if report_id:
abort(404, message="Invalid route")

students_df = pd.read_sql(StudentModel.query.filter_by(class_id=class_id).statement, con=db.engine)


chat_file = args['chat_file'].stream.read().decode("utf-8").split("\n")
chat_df = create_chat_df(chat_file)
report_object = Attendance(chat_df, students_df, ['name', "id_number", "phone"], args['time_delta'], args['first_sentence'], args['not_included_zoom_users'])

message_time = report_object.first_message_time
report_time = datetime(args["date"].year, args["date"].month, args["date"].day,
message_time.hour, message_time.minute, message_time.second)

new_report = ReportModel(description=args['description'], report_time=report_time, class_id=class_id)
db.session.add(new_report)
db.session.commit()

student_status_df = report_object.student_status_table(new_report.id)
student_status_df.to_sql('student_status', con=db.engine, if_exists="append", index=False)

for session_object in report_object.report_sessions:
session_table = SessionModel(start_time=session_object._first_message_time, report_id=new_report.id)
db.session.add(session_table)
db.session.commit()

zoom_names_df = session_object.zoom_names_table(session_table.id)
zoom_names_df.to_sql('zoom_names', con=db.engine, if_exists="append", index=False)

zoom_names_df = pd.read_sql(ZoomNamesModel.query.filter_by(session_id=session_table.id).statement, con=db.engine)
session_chat_df = session_object.chat_table(zoom_names_df)
session_chat_df.to_sql('chat', con=db.engine, if_exists="append", index=False)

return {"report_id": new_report.id}

def delete(self, class_id, report_id=None):
if ClassroomModel.query.filter_by(id=class_id, teacher=auth.current_user()).first() is None:
abort(400, message="Invalid class id")
if report_id is None: # Deleting all reports of class
class_reports_id = db.session.query(ReportModel.id).filter_by(class_id=class_id).all()
for report_data in class_reports_id:
current_report = ReportModel.query.filter_by(id=report_data.id).first()
db.session.delete(current_report)
db.session.commit()
return "", 204

current_report = ReportModel.query.filter_by(id=report_id).first() # Making sure the class belongs to the current user
if current_report is None:
abort(400, message="Invalid report id")

db.session.delete(current_report)
db.session.commit()
return "", 204

api.add_resource(ReportsResource, '/classrooms/<int:class_id>/reports', '/classrooms/<int:class_id>/reports/<int:report_id>')
29 changes: 29 additions & 0 deletions Server/Server/api/student_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from Server.api import api
from flask_restful import Resource, reqparse, abort
from Server import db, auth
from Server.models import StudentStatus

STATUS_CHOICES = (0, 1, 2)

# arg parsing:
student_status_argparse = reqparse.RequestParser()
student_status_argparse.add_argument('new_status', type=int,
help=f"Bad choice, please pick one of this choices {STATUS_CHOICES}",
required=True, location="json", choices=STATUS_CHOICES)

class StudentStatusResource(Resource):
method_decorators = [auth.login_required]

def put(self, status_id):
args = student_status_argparse.parse_args()
status = StudentStatus.query.get(status_id)
if status is None or status.report.classroom.teacher != auth.current_user():
abort(400, message="Invalid status id")

status.status = args["new_status"]
db.session.commit()

return "", 204


api.add_resource(StudentStatusResource, '/status/<int:status_id>')
17 changes: 0 additions & 17 deletions Server/Server/classrooms/forms.py

This file was deleted.

Loading

0 comments on commit a3056c2

Please sign in to comment.