-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from InbarShirizly/api-dev
Api dev
- Loading branch information
Showing
28 changed files
with
370 additions
and
469 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>') |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.