diff --git a/Server/README.md b/Server/README.md index 98b7a7d..5bdc81d 100644 --- a/Server/README.md +++ b/Server/README.md @@ -10,10 +10,10 @@ API doc can be found [here](https://documenter.getpostman.com/view/4335694/TVRg6 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`. +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: +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 diff --git a/Server/Server/__init__.py b/Server/Server/__init__.py index 4d3eaf6..9c381ba 100644 --- a/Server/Server/__init__.py +++ b/Server/Server/__init__.py @@ -1,7 +1,7 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt -from Server.config import FlaskConfig +from server.config import FlaskConfig from flask_httpauth import HTTPBasicAuth app = Flask(__name__) @@ -12,7 +12,7 @@ auth = HTTPBasicAuth(app) -from Server.api import api_blueprint +from server.api import api_blueprint app.register_blueprint(api_blueprint) diff --git a/Server/Server/api/__init__.py b/Server/Server/api/__init__.py index 174bf0f..da338f0 100644 --- a/Server/Server/api/__init__.py +++ b/Server/Server/api/__init__.py @@ -4,4 +4,4 @@ api_blueprint = Blueprint('api', __name__, url_prefix='/api') api = Api(api_blueprint) -from Server.api import auth, clasrooms, reports, student_status +from server.api import auth, clasrooms, reports, student_status diff --git a/Server/Server/api/__pycache__/__init__.cpython-38.pyc b/Server/Server/api/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 6fc85eb..0000000 Binary files a/Server/Server/api/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/Server/Server/api/__pycache__/auth.cpython-38.pyc b/Server/Server/api/__pycache__/auth.cpython-38.pyc deleted file mode 100644 index 4f17d14..0000000 Binary files a/Server/Server/api/__pycache__/auth.cpython-38.pyc and /dev/null differ diff --git a/Server/Server/api/__pycache__/clasrooms.cpython-38.pyc b/Server/Server/api/__pycache__/clasrooms.cpython-38.pyc deleted file mode 100644 index 93f38bb..0000000 Binary files a/Server/Server/api/__pycache__/clasrooms.cpython-38.pyc and /dev/null differ diff --git a/Server/Server/api/__pycache__/reports.cpython-38.pyc b/Server/Server/api/__pycache__/reports.cpython-38.pyc deleted file mode 100644 index f564043..0000000 Binary files a/Server/Server/api/__pycache__/reports.cpython-38.pyc and /dev/null differ diff --git a/Server/Server/api/auth.py b/Server/Server/api/auth.py index 58ef9da..98da864 100644 --- a/Server/Server/api/auth.py +++ b/Server/Server/api/auth.py @@ -1,22 +1,24 @@ -from Server import auth, bcrypt, db +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) +from server.api import api +from server.models.orm import get_user, TeacherModel +from server.config import RestErrors class RegisterResource(Resource): + def __init__(self): + super().__init__() + self._post_args = reqparse.RequestParser() + self._post_args.add_argument("username", type=str, help="Username is required", location='json', required=True) + self._post_args.add_argument("email", type=str, help="Email is required", location='json', required=True) + self._post_args.add_argument("password", type=str, help="Password is required", location='json', required=True) + def post(self): - args = register_argparse.parse_args() + args = self._post_args.parse_args() if TeacherModel.query.filter_by(username=args['username']).first(): - return abort(400, message="Username already taken") + return abort(400, message=RestErrors.USERNAME_TAKEN) if TeacherModel.query.filter_by(email=args['email']).first(): - return abort(400, message="Email already taken") + return abort(400, message=RestErrors.EMAIL_TAKEN) user = TeacherModel( username=args['username'], diff --git a/Server/Server/api/clasrooms.py b/Server/Server/api/clasrooms.py index f4a8309..c982aca 100644 --- a/Server/Server/api/clasrooms.py +++ b/Server/Server/api/clasrooms.py @@ -1,56 +1,41 @@ -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 server.api import api +from flask_restful import Resource, reqparse, abort, marshal +from server import auth, db +from server.models.orm import TeacherModel, ClassroomModel from werkzeug.datastructures import FileStorage -from Server.utils import parser -from Server.utils.utils import create_students_df +from server.parsing import parser +from server.parsing.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) +from server.config import RestErrors +from server.models.marshals import classrooms_list_fields, classroom_resource_fields class ClassroomsResource(Resource): method_decorators = [auth.login_required] + def __init__(self): + super().__init__() + + self._post_args = reqparse.RequestParser() + self._post_args.add_argument('name', type=str, help="Name of the class is required", required=True) + self._post_args.add_argument('students_file', type=FileStorage, location='files', help="Student file is required", required=True) + + self._put_args = reqparse.RequestParser() + self._put_args.add_argument('new_name', type=str, help="New name is required in order to update", location="json", required=True) + 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") + abort(400, message=RestErrors.INVALID_CLASS) 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() + return abort(404, message=RestErrors.INVALID_ROUTE) + args = self._post_args.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) @@ -65,11 +50,11 @@ def post(self, class_id=None): def put(self, class_id=None): if class_id is None: - return abort(404, message="Invalid route") - args = classroom_put_argparse.parse_args() + return abort(404, message=RestErrors.INVALID_ROUTE) + args = self._put_args.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") + abort(400, message=RestErrors.INVALID_CLASS) current_class.name = args['new_name'] db.session.commit() return "", 204 @@ -85,7 +70,7 @@ def delete(self, class_id=None): 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") + abort(400, message=RestErrors.INVALID_CLASS) db.session.delete(current_class) db.session.commit() diff --git a/Server/Server/api/reports.py b/Server/Server/api/reports.py index bee2f08..3670d31 100644 --- a/Server/Server/api/reports.py +++ b/Server/Server/api/reports.py @@ -1,60 +1,47 @@ -from Server.api import api -from flask_restful import Resource, reqparse, abort, marshal, fields -from Server.utils.attendance_check import Attendance +from server.api import api +from flask_restful import Resource, reqparse, abort, marshal +from server.parsing.attendance_check import Attendance from werkzeug.datastructures import FileStorage -from Server import db, auth +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") +from server.models.orm import StudentModel, ClassroomModel, ReportModel, SessionModel, ZoomNamesModel, StudentStatus +from server.parsing.utils import create_chat_df +from server.api.utils import validate_classroom +from server.config import RestErrors +from server.models.marshals import student_status_field, reports_list_fields class ReportsResource(Resource): - method_decorators = [auth.login_required] + method_decorators = [validate_classroom, 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") + def __init__(self): + super().__init__() + self._post_args = reqparse.RequestParser() + self._post_args.add_argument('description', type=str) + self._post_args.add_argument('chat_file', type=FileStorage, help="Chat file is required", location='files', required=True) + self._post_args.add_argument('time_delta', default=1, type=int) + self._post_args.add_argument('date', default=datetime.now().date(), type=lambda x: datetime.strptime(x, '%d/%m/%y')) + self._post_args.add_argument('first_sentence', type=str, help='First sentence is required in order to understand when does the check starts', required=True) + self._post_args.add_argument('not_included_zoom_users', default=[], type=str, help='Must be a list of strings with zoom names', action="append") + + def get(self, class_id, report_id=None): 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") + abort(400, message=RestErrors.INVALID_REPORT) 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") + abort(404, message=RestErrors.INVALID_REPORT) + args = self._post_args.parse_args() 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_file = args['chat_file'].stream.read().decode("utf-8").split("\n") #TODO: check this in test 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']) @@ -84,8 +71,6 @@ def post(self, class_id, report_id=None): 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: @@ -96,7 +81,7 @@ def delete(self, class_id, report_id=None): 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") + abort(400, message=RestErrors.INVALID_REPORT) db.session.delete(current_report) db.session.commit() diff --git a/Server/Server/api/student_status.py b/Server/Server/api/student_status.py index 13fdcf7..ff2d43a 100644 --- a/Server/Server/api/student_status.py +++ b/Server/Server/api/student_status.py @@ -1,24 +1,31 @@ -from Server.api import api +from server.api import api from flask_restful import Resource, reqparse, abort -from Server import db, auth -from Server.models import StudentStatus +from server import db, auth +from server.models.orm import StudentStatus +from server.config import RestErrors 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 __init__(self): + super().__init__() + + self._put_args = reqparse.RequestParser() + self._put_args.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 + ) + def put(self, status_id): - args = student_status_argparse.parse_args() + args = self._put_args.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") + abort(400, message=RestErrors.INVALID_STATUS) status.status = args["new_status"] db.session.commit() diff --git a/Server/Server/api/utils.py b/Server/Server/api/utils.py new file mode 100644 index 0000000..9e59c66 --- /dev/null +++ b/Server/Server/api/utils.py @@ -0,0 +1,20 @@ +from functools import wraps +from flask_restful import abort +from server import auth +from server.models.orm import ClassroomModel +from server import app +from server.config import RestErrors + +# This decorator fucntion will make sure that the classroom belongs to the current user +def validate_classroom(fnc): + def inner(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") + return fnc(class_id, report_id) + return inner + + +# Error handler for 404 +@app.errorhandler(404) +def not_found(error): + return {"message": RestErrors.INVALID_ROUTE} \ No newline at end of file diff --git a/Server/Server/api/validators.py b/Server/Server/api/validators.py new file mode 100644 index 0000000..e69de29 diff --git a/Server/Server/config.py b/Server/Server/config.py index f2768b6..6c0ada5 100644 --- a/Server/Server/config.py +++ b/Server/Server/config.py @@ -4,7 +4,6 @@ class FlaskConfig: class ParseConfig: - FILE_COLS_DICT = { "name": ["שם התלמיד", "תלמידים", "שמות", "שם", "סטודנט"], "id_number": ["תעודת זהות", "ת.ז.", "ת.ז", "תז"], @@ -13,4 +12,13 @@ class ParseConfig: "org_class": ["כיתה"] } MASHOV_COLS = ["name", "org_class"] - GENDER_DICT = {1: ["זכר", "ז", "(ז)"], 0: ["נקבה", "נ", "(נ)"]} \ No newline at end of file + GENDER_DICT = {1: ["זכר", "ז", "(ז)"], 0: ["נקבה", "נ", "(נ)"]} + + +class RestErrors: + INVALID_ROUTE = "Route does't exist" + INVALID_CLASS = "Invalid class id" + INVALID_REPORT = "Invalid report id" + INVALID_STATUS = "Invalid status id" + USERNAME_TAKEN = "Username already taken" + EMAIL_TAKEN = "Email already taken" \ No newline at end of file diff --git a/Server/Server/models/custom_fields.py b/Server/Server/models/custom_fields.py new file mode 100644 index 0000000..7c25550 --- /dev/null +++ b/Server/Server/models/custom_fields.py @@ -0,0 +1,9 @@ +from flask_restful import fields + + +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 \ No newline at end of file diff --git a/Server/Server/models/marshals.py b/Server/Server/models/marshals.py new file mode 100644 index 0000000..79603cb --- /dev/null +++ b/Server/Server/models/marshals.py @@ -0,0 +1,23 @@ +from flask_restful import fields +from server.models.custom_fields import StudentItemField + +# Fields for classrom.py +classrooms_list_fields = { # Fields list of classrooms + 'name': fields.String, + 'id': fields.Integer +} +classroom_resource_fields = { # Fields for a single classroom + 'name': fields.String, + 'students': fields.List(StudentItemField) +} + +# Fields for report.py +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") +} \ No newline at end of file diff --git a/Server/Server/models.py b/Server/Server/models/orm.py similarity index 99% rename from Server/Server/models.py rename to Server/Server/models/orm.py index fc0282c..b2cd466 100644 --- a/Server/Server/models.py +++ b/Server/Server/models/orm.py @@ -1,4 +1,4 @@ -from Server import db, bcrypt, auth +from server import db, bcrypt, auth from flask_login import UserMixin diff --git a/Server/Server/parsing/__init__.py b/Server/Server/parsing/__init__.py new file mode 100644 index 0000000..9e2a7ba --- /dev/null +++ b/Server/Server/parsing/__init__.py @@ -0,0 +1,8 @@ +from server.config import ParseConfig +from server.parsing.loading_classroom_file import ParseClassFile +from collections import namedtuple + + +parser = ParseClassFile.from_object(ParseConfig) + +AttendanceMetaData = namedtuple('meta_data', ['filter_modes', 'time_delta', 'start_sentence', 'not_included_zoom_users']) diff --git a/Server/Server/utils/attendance_check.py b/Server/Server/parsing/attendance_check.py similarity index 52% rename from Server/Server/utils/attendance_check.py rename to Server/Server/parsing/attendance_check.py index 117480c..c71f72c 100644 --- a/Server/Server/utils/attendance_check.py +++ b/Server/Server/parsing/attendance_check.py @@ -1,5 +1,7 @@ import numpy as np import pandas as pd +from server.parsing.session import Session +#from server.parsing import AttendanceMetaData class Attendance: @@ -11,7 +13,7 @@ class Attendance: """ def __init__(self, chat_df, students_df, filter_modes, time_delta, start_sentence, not_included_zoom_users): """ - :param chat_df: zoom chat in df (df) + :param chat_df: zoom chat (df) :param students_df: student class raw data (df) :param filter_modes: filters the user picked for parsing the text file (list of str) :param time_delta: max time from start sentence to the last message to parse in each session in minutes (int) @@ -19,26 +21,54 @@ def __init__(self, chat_df, students_df, filter_modes, time_delta, start_sentenc :param not_included_zoom_users: zoom names that will not be considered (list of str) :return: data frame with the data from the chat """ + meta_data = AttendanceMetaData(filter_modes=filter_modes, time_delta=time_delta, + start_sentence=start_sentence, not_included_zoom_users=not_included_zoom_users) self.first_message_time = chat_df["time"].sort_values().iloc[0] # get time of first message in the chat - start_indices = chat_df.index[chat_df['message'].apply(lambda string: start_sentence.lower() in string.lower())] #TODO: slice by time or by next message + start_indices = Attendance.get_start_indices(chat_df, meta_data) df_students_for_report = students_df.set_index("id").astype(str).reset_index() # set all columns to str except the id self._df_students = df_students_for_report self._sessions = [] - for start_index in start_indices: - df_session = Attendance.get_df_of_time_segment(chat_df, start_index, time_delta) - self._sessions.append(Session(self._df_students, df_session, filter_modes, not_included_zoom_users)) + for ind in range(len(start_indices)): + df_session = Attendance.get_df_of_time_segment(chat_df, start_indices, ind, time_delta) + self._sessions.append(Session(self._df_students, df_session, meta_data)) @staticmethod - def get_df_of_time_segment(df, start_index, time_delta): - + def get_start_indices(df, meta_data): + """ + find start indices - when one of the "teachers" writes the "start_sentence" + :param df: zoom chat (df) + :param meta_data: configurations of the user + :return: list of indices of start of session + """ + not_included_zoom_users_filt = df['zoom_name'].str.contains('|'.join(meta_data.not_included_zoom_users)) + not_included_zoom_users_df = df[not_included_zoom_users_filt] + check_sentence = lambda string: meta_data.start_sentence.lower() in string.lower() + start_indices = not_included_zoom_users_df.index[not_included_zoom_users_df['message'].apply(check_sentence)] + return start_indices + + + @staticmethod + def get_df_of_time_segment(df, start_indices, ind, time_delta): + """ + + :param df: + :param start_indices: + :param ind: + :param time_delta: + :return: + """ + if ind < len(start_indices) - 1: + df = df.iloc[start_indices[ind]:start_indices[ind + 1], :] + time_delta = np.timedelta64(time_delta, 'm') - time_segment_start = df.loc[start_index, "time"] + time_segment_start = df.loc[start_indices[ind], "time"] time_filt = (df["time"] >= time_segment_start) & \ (df["time"] <= time_segment_start + time_delta) - return df.loc[time_filt] + relevant_df = df.loc[time_filt] + return relevant_df @property def report_sessions(self): @@ -56,55 +86,13 @@ def student_status_table(self, report_id): return df_status_report.loc[:, ["student_id", "report_id", "status"]] -class Session: - - def __init__(self, students_df, df_session_chat, filter_modes, not_included_zoom_users): - - self._first_message_time = df_session_chat["time"].sort_values().iloc[0] - self._relevant_chat = self.get_participants_in_session(students_df, filter_modes, df_session_chat, not_included_zoom_users) - - @ staticmethod - def get_participants_in_session(df_students, filter_modes, df_chat, not_included_zoom_users): - """ - finds students that attendant to the session. runs over each mode which represent different way to declare that - the student attendant (for example: phone number, ID). merges this data to the csv table with the zoom name that - added it - :param df_chat: that table of the chat for the specific session - :return: df of the attendance in the session - """ - final_df = None - for mode in filter_modes: - merged_df = pd.merge(df_students, df_chat.reset_index(), left_on=mode, right_on="message", how="left") - final_df = pd.concat([merged_df, final_df]) - - final_df.sort_values(by="time", inplace=True) - df_participated = final_df.groupby("zoom_name").first().reset_index() - df_participated["index"] = df_participated["index"].astype(int) - df_participated = df_participated.loc[:, ["id", "zoom_name", "time", "message", "index"]].set_index("index") - - filt = df_chat['zoom_name'].str.contains('|'.join(not_included_zoom_users)) - df_relevant_chat = pd.merge(df_chat[~filt], df_participated, how="left") - - df_relevant_chat["relevant"] = df_relevant_chat["id"].apply(lambda x: 1 if x == x else 0) - df_relevant_chat["id"] = df_relevant_chat["id"].apply(lambda x: int(x) if x == x else -1) - return df_relevant_chat - - - def zoom_names_table(self, session_id): - zoom_df = self._relevant_chat.loc[:, ["zoom_name", "id"]].rename(columns={"zoom_name": "name", "id": "student_id"}) - zoom_df['session_id'] = pd.Series([session_id] * zoom_df.shape[0]) - return zoom_df.sort_values(by="student_id", ascending=False).groupby("name").first().reset_index() - - def chat_table(self, zoom_df): - relevant_chat = self._relevant_chat.drop(columns=["id"]) - chat_session_table = pd.merge(relevant_chat, zoom_df, left_on="zoom_name", right_on="name") - return chat_session_table.drop(columns=["zoom_name", "name", "session_id", "student_id"]).rename(columns={"id": "zoom_names_id"}) - - - if __name__ == '__main__': + from collections import namedtuple from utils import create_chat_df, create_students_df + AttendanceMetaData = namedtuple('meta_data', + ['filter_modes', 'time_delta', 'start_sentence', 'not_included_zoom_users']) + chat_file_path = r"C:\Users\Inbar Shirizly\Documents\python\useful\ITC_programs\zoom_attendance_check\chat files\meeting_example_full_name.txt" excel_file_path = r"C:\Users\Inbar Shirizly\Documents\python\useful\ITC_programs\zoom_attendance_check\student_csv_examples\example_data_already_prepared.xlsx" @@ -113,7 +101,7 @@ def chat_table(self, zoom_df): chat_df = create_chat_df(f.readlines()) df_students = create_students_df(file_name=excel_file_path.split("\\")[-1], file_data=excel_file_path) - my_class = Attendance(chat_df, df_students, ['name', "id_number", "phone"], 5, "Attendance check", ["ITC", "Tech", "Challenge"]) + my_class = Attendance(chat_df, df_students, ['name', "id_number", "phone"], 1, "Attendance check", ["ITC", "Tech", "Challenge"]) a = my_class.student_status_table(1) print(a) # df_part_session = my_class._sessions[0] diff --git a/Server/Server/utils/loading_classroom_file.py b/Server/Server/parsing/loading_classroom_file.py similarity index 100% rename from Server/Server/utils/loading_classroom_file.py rename to Server/Server/parsing/loading_classroom_file.py diff --git a/Server/Server/parsing/session.py b/Server/Server/parsing/session.py new file mode 100644 index 0000000..bfc3d7a --- /dev/null +++ b/Server/Server/parsing/session.py @@ -0,0 +1,45 @@ +import pandas as pd + +class Session: + + def __init__(self, students_df, df_session_chat, meta_data): + + self._first_message_time = df_session_chat["time"].sort_values().iloc[0] + self._relevant_chat = self.get_participants_in_session(students_df, df_session_chat, meta_data) + + @ staticmethod + def get_participants_in_session(df_students, df_chat, meta_data): + """ + finds students that attendant to the session. runs over each mode which represent different way to declare that + the student attendant (for example: phone number, ID). merges this data to the csv table with the zoom name that + added it + :param df_chat: that table of the chat for the specific session + :return: df of the attendance in the session + """ + final_df = None + for mode in meta_data.filter_modes: + merged_df = pd.merge(df_students, df_chat.reset_index(), left_on=mode, right_on="message", how="left") + final_df = pd.concat([merged_df, final_df]) + + final_df.sort_values(by="time", inplace=True) + df_participated = final_df.groupby("zoom_name").first().reset_index() + df_participated["index"] = df_participated["index"].astype(int) + df_participated = df_participated.loc[:, ["id", "zoom_name", "time", "message", "index"]].set_index("index") + + filt = df_chat['zoom_name'].str.contains('|'.join(meta_data.not_included_zoom_users)) + df_relevant_chat = pd.merge(df_chat[~filt], df_participated, how="left") + + df_relevant_chat["relevant"] = df_relevant_chat["id"].apply(lambda x: 1 if x == x else 0) + df_relevant_chat["id"] = df_relevant_chat["id"].apply(lambda x: int(x) if x == x else -1) + return df_relevant_chat + + + def zoom_names_table(self, session_id): + zoom_df = self._relevant_chat.loc[:, ["zoom_name", "id"]].rename(columns={"zoom_name": "name", "id": "student_id"}) + zoom_df['session_id'] = pd.Series([session_id] * zoom_df.shape[0]) + return zoom_df.sort_values(by="student_id", ascending=False).groupby("name").first().reset_index() + + def chat_table(self, zoom_df): + relevant_chat = self._relevant_chat.drop(columns=["id"]) + chat_session_table = pd.merge(relevant_chat, zoom_df, left_on="zoom_name", right_on="name") + return chat_session_table.drop(columns=["zoom_name", "name", "session_id", "student_id"]).rename(columns={"id": "zoom_names_id"}) \ No newline at end of file diff --git a/Server/Server/utils/utils.py b/Server/Server/parsing/utils.py similarity index 100% rename from Server/Server/utils/utils.py rename to Server/Server/parsing/utils.py diff --git a/Server/Server/utils/__init__.py b/Server/Server/utils/__init__.py deleted file mode 100644 index 790df24..0000000 --- a/Server/Server/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from Server.config import ParseConfig -from Server.utils.loading_classroom_file import ParseClassFile - - -parser = ParseClassFile.from_object(ParseConfig) \ No newline at end of file diff --git a/Server/run.py b/Server/run.py index 6c193e7..3d53c53 100644 --- a/Server/run.py +++ b/Server/run.py @@ -1,8 +1,8 @@ -from Server import app, db +from server import app, db import os if __name__ == '__main__': - if not os.path.exists("/Server/site.db"): + if not os.path.exists("/server/site.db"): db.create_all() db.session.commit() app.run(debug=True) \ No newline at end of file diff --git a/Server/test.py b/Server/test.py new file mode 100644 index 0000000..b47f428 --- /dev/null +++ b/Server/test.py @@ -0,0 +1,14 @@ +import requests + +auth = { + "username": 'a', + "password": 'TEST1234', + "email": "abc@gmail.com" +} + +BASE_URL = "http://127.0.0.1:5000/api/" + +print(requests.post(BASE_URL + 'register', auth).json()) +print(requests.get(BASE_URL + 'classrooms', auth=(auth['username'], auth['password'])).json()) +print(requests.get(BASE_URL + 'classrooms')) +print(requests.get(BASE_URL + 'classrooms/1', auth=(auth['username'], auth['password'])).json())