From 1a49afb48924aef00f7e0926ec141118bd2ef66b Mon Sep 17 00:00:00 2001 From: Edith Coates Date: Tue, 22 Aug 2023 10:22:09 -0700 Subject: [PATCH 1/3] Initial restructuring --- app/models.py | 19 ---------- app/routes.py | 40 +++++++++++--------- app/selectors/__init__.py | 0 app/selectors/assignment.py | 20 ++++++++++ app/selectors/datatype.py | 5 +++ app/selectors/question.py | 8 ++++ app/services/__init__.py | 0 app/services/assignment.py | 74 +++++++++++++++++++++++++++++++++++++ app/services/question.py | 0 app/services/student.py | 12 ++++++ 10 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 app/selectors/__init__.py create mode 100644 app/selectors/assignment.py create mode 100644 app/selectors/datatype.py create mode 100644 app/selectors/question.py create mode 100644 app/services/__init__.py create mode 100644 app/services/assignment.py create mode 100644 app/services/question.py create mode 100644 app/services/student.py diff --git a/app/models.py b/app/models.py index bff84cb..639b100 100644 --- a/app/models.py +++ b/app/models.py @@ -15,25 +15,6 @@ class Assignment(db.Model): responses = db.relationship('Response', backref='assignment', lazy=True, cascade='all,delete') submissions = db.relationship('Submission', backref='assignment', lazy=True, cascade='all,delete') - def load_submissions(self): - student_ids = [int(os.path.basename(s)) for s in glob(os.path.join('submissions',self.folder_name,'*'))] - for student_id in student_ids: - student = Student.query.get(student_id) - if not student: - student = Student(id=student_id) - db.session.add(student) - submission = Submission(assignment_id=self.id,student_id=student_id,grade=0,feedback='') - db.session.add(submission) - response_files = [os.path.basename(r) for r in glob(os.path.join('submissions',self.folder_name,str(student_id),'*'))] - for response_file in response_files: - response_file_split = response_file.split('.') - var_name, extension = response_file_split[0], response_file_split[-1] - var_name = var_name.lower() - datatype = Datatype.query.filter_by(extension=extension).first() - response = Response(assignment_id=self.id,student_id=student_id,datatype_id=datatype.id,var_name=var_name) - db.session.add(response) - db.session.commit() - def create_response(self,var_name,vars,expression,extension): fun = eval(expression) for submission in self.submissions: diff --git a/app/routes.py b/app/routes.py index d4e18d2..7fa47dc 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,6 +2,9 @@ from app.models import Assignment, Question, Batch, Response, BatchResponse, Datatype, Student, Submission from flask import render_template, jsonify, request, url_for, redirect +from .services.assignment import AssignmentService +from .selectors.assignment import get_all_assignments, get_assignment, get_assignment_vars + @app.route('/') def index(): return render_template('index.html') @@ -9,28 +12,30 @@ def index(): @app.route('/assignments', methods=['GET','POST']) def assignments(): if request.method == 'GET': - assignments = Assignment.query.all() - return jsonify([assignment.to_dict() for assignment in assignments]) + assignments = get_all_assignments() + return jsonify(assignments) + elif request.method == 'POST': - assignment = Assignment(name=request.json['name'],folder_name=request.json['folder_name']) - db.session.add(assignment) - db.session.commit() - assignment.load_submissions() - return jsonify(assignment.to_dict()) + name = request.json["name"] + folder_name = request.json["folder_name"] + service = AssignmentService( + name=name, folder_name=folder_name + ) + new_assignment = service.create() + service.load_submissions() + + return jsonify(new_assignment) @app.route('/assignments/', methods=['GET','DELETE']) def assignment(assignment_id): if request.method == 'GET': - assignment = Assignment.query.get_or_404(assignment_id) - return jsonify(assignment.to_dict()) + assignment = get_assignment(assignment_id) + return jsonify(assignment) + if request.method == 'DELETE': - assignment = Assignment.query.get(assignment_id) - if assignment: - db.session.delete(assignment) - db.session.commit() - return ('',204) - else: - return ('',204) + service = AssignmentService(assignment_id=assignment_id) + service.delete() + return ('',204) @app.route('/assignments//grades') def grades(assignment_id): @@ -40,8 +45,7 @@ def grades(assignment_id): @app.route('/assignments//vars') def vars(assignment_id): - responses = Response.query.filter_by(assignment_id=assignment_id).all() - vars_list = sorted(list(set([response.var_name.lower() for response in responses]))) + vars_list = get_assignment_vars(assignment_id) return jsonify({'vars': vars_list}) @app.route('/assignments//questions', methods=['GET','POST']) diff --git a/app/selectors/__init__.py b/app/selectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/selectors/assignment.py b/app/selectors/assignment.py new file mode 100644 index 0000000..659454b --- /dev/null +++ b/app/selectors/assignment.py @@ -0,0 +1,20 @@ +from typing import List + +from ..models import Assignment, Response + + +def get_all_assignments() -> List[dict]: + assignments = Assignment.query.all() + return [instance.to_dict() for instance in assignments] + + +def get_assignment(assignment_id: int) -> dict: + assignment = Assignment.query.get_or_404(assignment_id) + return assignment.to_dict() + + +def get_assignment_vars(assignment_id: int) -> List[str]: + responses = Response.query.filter_by(assignment_id=assignment_id).all() + response_list = list(set([response.var_name.lower() for response in responses])) + response_list.sort() + return response_list diff --git a/app/selectors/datatype.py b/app/selectors/datatype.py new file mode 100644 index 0000000..a1f1eab --- /dev/null +++ b/app/selectors/datatype.py @@ -0,0 +1,5 @@ +from ..models import Datatype + + +def get_datatype_from_extension(extension: str) -> Datatype: + return Datatype.query.filter_by(extension=extension).first() diff --git a/app/selectors/question.py b/app/selectors/question.py new file mode 100644 index 0000000..9796467 --- /dev/null +++ b/app/selectors/question.py @@ -0,0 +1,8 @@ +from typing import List + +from ..models import Question + + +def get_questions_of_assignment(assignment_id: int) -> List[dict]: + questions = Question.query.filter_by(assignment_id=assignment_id).all() + return [q.to_dict() for q in questions] diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/assignment.py b/app/services/assignment.py new file mode 100644 index 0000000..5c4c2f9 --- /dev/null +++ b/app/services/assignment.py @@ -0,0 +1,74 @@ +from pathlib import Path + +from app import app, db +from ..models import ( + Assignment, + Student, + Submission, + Response, +) +from ..selectors.datatype import get_datatype_from_extension + + +class AssignmentService: + def __init__(self, + *, + name: str = None, + folder_name: str = None, + assignment_id: int = None + ): + self.name = name + self.folder_name = name + self.id = assignment_id + + def create(self) -> dict: + new_model = Assignment( + name=self.name, folder_name=self.folder_name + ) + db.session.add(new_model) + db.session.commit() + self.id = new_model.id + return new_model.to_dict() + + + def load_submissions(self): + assignment_dir = Path("submissions") / self.folder_name + student_dirs = [d for d in assignment_dir.iterdir() if d.is_dir()] + + for s_dir in student_dirs: + s_id = int(s_dir.stem) + student = Student.query.get(s_id) + if not student: + student = Student(id=s_id) + db.session.add(student) + + submission = Submission( + assignment_id=self.id, + student_id=s_id, + grade=0, + feedback="", + ) + db.session.add(submission) + + for response_file in s_dir.iterdir(): + filename_split = response_file.name.split(".") + var_name, extension = filename_split[0], filename_split[-1] + var_name = var_name.lower() + + datatype = get_datatype_from_extension(extension) + + response = Response( + assignment_id=self.id, + student_id=s_id, + datatype_id=datatype.id, + var_name=var_name + ) + db.session.add(response) + + db.session.commit() + + def delete(self): + model = Assignment.query.get(self.id) + if model: + db.session.delete(model) + db.session.commit() diff --git a/app/services/question.py b/app/services/question.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/student.py b/app/services/student.py new file mode 100644 index 0000000..92e69a1 --- /dev/null +++ b/app/services/student.py @@ -0,0 +1,12 @@ +from app import app, db +from ..models import Student + + +class StudentService: + def __init__(self, s_id: int): + self.s_id = s_id + + def create(self): + student = Student(id=self.s_id) + db.session.add(student) + db.session.commit() \ No newline at end of file From 09d05a7f56d99e8beb654bcfc1d53c72dc091a79 Mon Sep 17 00:00:00 2001 From: Edith Coates Date: Tue, 22 Aug 2023 17:01:39 -0700 Subject: [PATCH 2/3] More service + selector methods --- app/models.py | 37 ++++--------------------- app/routes.py | 46 +++++++++++++++---------------- app/selectors/question.py | 5 ++++ app/selectors/response.py | 15 ++++++++++ app/services/assignment.py | 56 ++++++++++++++++++++++++++++++++++++-- app/services/batch.py | 34 +++++++++++++++++++++++ app/services/question.py | 48 ++++++++++++++++++++++++++++++++ app/services/response.py | 32 ++++++++++++++++++++++ 8 files changed, 216 insertions(+), 57 deletions(-) create mode 100644 app/selectors/response.py create mode 100644 app/services/batch.py create mode 100644 app/services/response.py diff --git a/app/models.py b/app/models.py index 639b100..90d2f3f 100644 --- a/app/models.py +++ b/app/models.py @@ -5,6 +5,7 @@ import pandas as pd import importlib import json +from pathlib import Path class Assignment(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -15,36 +16,6 @@ class Assignment(db.Model): responses = db.relationship('Response', backref='assignment', lazy=True, cascade='all,delete') submissions = db.relationship('Submission', backref='assignment', lazy=True, cascade='all,delete') - def create_response(self,var_name,vars,expression,extension): - fun = eval(expression) - for submission in self.submissions: - student_id = submission.student_id - filename = os.path.join('submissions',self.folder_name,str(student_id),var_name + '.' + extension) - student_responses = [] - for var in [v.lower() for v in vars]: - response = Response.query.filter_by(assignment_id=self.id,student_id=student_id,var_name=var).first() - if response: - student_responses.append(response.get_data()) - else: - student_responses.append(None) - try: - value = fun(student_responses) - print(value) - except: - text_datatype = Datatype.query.filter_by(extension='txt').first() - filename = os.path.join('submissions',self.folder_name,str(student_id),var_name + '.txt') - f = open(filename,'w') - f.write('Error') - f.close() - new_response = Response(assignment_id=self.id,student_id=student_id,datatype_id=text_datatype.id,var_name=var_name.lower()) - db.session.add(new_response) - db.session.commit() - continue - np.savetxt(filename,value,fmt='%.5f',delimiter=',') - this_datatype = Datatype.query.filter_by(extension=extension).first() - new_response = Response(assignment_id=self.id,student_id=student_id,datatype_id=this_datatype.id,var_name=var_name.lower()) - db.session.add(new_response) - db.session.commit() def total_points(self): points = [question.max_grade for question in self.questions] @@ -328,7 +299,11 @@ class Response(db.Model): assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False) def get_fullfile(self): - return os.path.join('submissions',self.assignment.folder_name,str(self.student_id),self.var_name + '.' + self.datatype.extension) + submission_folder = Path("submissions/") + assignment_folder = Assignment.query.get(self.assignment_id).folder_name + student_folder = str(self.student_id) + response_file = f"{self.var_name}.{self.datatype.extension}" + return submission_folder / assignment_folder / student_folder / response_file def get_data(self): dtype = self.datatype.name diff --git a/app/routes.py b/app/routes.py index 7fa47dc..d665292 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,7 +3,9 @@ from flask import render_template, jsonify, request, url_for, redirect from .services.assignment import AssignmentService +from .services.question import QuestionService from .selectors.assignment import get_all_assignments, get_assignment, get_assignment_vars +from .selectors.question import get_questions_of_assignment, get_question @app.route('/') def index(): @@ -51,33 +53,31 @@ def vars(assignment_id): @app.route('/assignments//questions', methods=['GET','POST']) def questions(assignment_id): if request.method == 'GET': - questions = Question.query.filter_by(assignment_id=assignment_id).all() - return jsonify([question.to_dict() for question in questions]) + questions = get_questions_of_assignment(assignment_id) + return jsonify(questions) elif request.method == 'POST': - question = Question(name=request.json['name'], - var_name=request.json['var_name'].lower(), - alt_var_name=request.json['alt_var_name'].lower(), - max_grade=request.json['max_grade'] if request.json['max_grade'] != '' else 1, - tolerance=request.json['tolerance'] if request.json['tolerance'] != '' else 0.0001, - preprocessing=request.json['preprocessing'], - assignment_id=assignment_id) - db.session.add(question) - db.session.commit() - return jsonify(question.to_dict()) + data = request.json + service = QuestionService( + name=data["name"], + var_name=data["var_name"].lower(), + alt_var_name=data["alt_var_name"].lower(), + max_grade=data["max_grade"], + tolerance=data["tolerance"], + preprocessing=data["preprocessing"], + assignment_id=assignment_id, + ) + question = service.create() + return jsonify(question) @app.route('/assignments//questions/', methods=['GET','DELETE']) def question(assignment_id,question_id): if request.method == 'GET': - question = Question.query.get_or_404(question_id) - return jsonify(question.to_dict()) + question = get_question(question) + return jsonify(question) if request.method == 'DELETE': - question = Question.query.get(question_id) - if question: - db.session.delete(question) - db.session.commit() - return ('',204) - else: - return ('',204) + service = QuestionService(question_id=question_id) + service.delete() + return ('',204) @app.route('/assignments//response', methods=['POST']) def create_response(assignment_id): @@ -85,8 +85,8 @@ def create_response(assignment_id): vars = request.form['vars'].split(',') expression = request.form['expression'] extension = request.form['extension'] - assignment = Assignment.query.get_or_404(assignment_id) - assignment.create_response(var_name,vars,expression,extension) + service = AssignmentService(assignment_id=assignment_id) + service.create_responses(var_name, vars, expression, extension) return ('',200) @app.route('/assignments//questions//batches', methods=['GET']) diff --git a/app/selectors/question.py b/app/selectors/question.py index 9796467..741152a 100644 --- a/app/selectors/question.py +++ b/app/selectors/question.py @@ -6,3 +6,8 @@ def get_questions_of_assignment(assignment_id: int) -> List[dict]: questions = Question.query.filter_by(assignment_id=assignment_id).all() return [q.to_dict() for q in questions] + + +def get_question(question_id: int) -> dict: + question = Question.query.get_or_404(question_id) + return question.to_dict() diff --git a/app/selectors/response.py b/app/selectors/response.py new file mode 100644 index 0000000..247c0bb --- /dev/null +++ b/app/selectors/response.py @@ -0,0 +1,15 @@ +from app import app, db +from ..models import Response + + +def get_response( + assignment_id: int, + student_id: int, + var_name: str, +) -> Response: + response = Response.query.filter_by( + assignment_id=assignment_id, + student_id=student_id, + var_name=var_name, + ).first() + return response diff --git a/app/services/assignment.py b/app/services/assignment.py index 5c4c2f9..3cbabbe 100644 --- a/app/services/assignment.py +++ b/app/services/assignment.py @@ -1,4 +1,6 @@ from pathlib import Path +from typing import List +import numpy as np from app import app, db from ..models import ( @@ -7,18 +9,21 @@ Submission, Response, ) +from ..services.response import ResponseService from ..selectors.datatype import get_datatype_from_extension +from ..selectors.response import get_response class AssignmentService: - def __init__(self, + def __init__( + self, *, name: str = None, folder_name: str = None, assignment_id: int = None ): self.name = name - self.folder_name = name + self.folder_name = folder_name self.id = assignment_id def create(self) -> dict: @@ -27,7 +32,7 @@ def create(self) -> dict: ) db.session.add(new_model) db.session.commit() - self.id = new_model.id + self.id = new_model.id # TODO: assignment id init kwarg is ignored (assumed to be none for now) return new_model.to_dict() @@ -72,3 +77,48 @@ def delete(self): if model: db.session.delete(model) db.session.commit() + + def create_responses( + self, + var_name: str, + vars: List[str], + expression: str, + extension: str, + ): + func = eval(expression) + submissions = Assignment.query.get(self.id).submissions + for submission in submissions: + student_id = submission.student_id + student_responses = [] + for var in [v.lower() for v in vars]: + response = get_response( + self.id, student_id, var + ) + if response: + student_responses.append(response.get_data()) + + try: + value = func(student_responses) + print(value) + except Exception as e: + error_response_service = ResponseService( + var_name=var_name.lower(), + assignment_id=self.id, + extension="txt", + student_id=student_id, + ) + error_response = error_response_service.create() + filepath = error_response.get_fullfile() + with open(filepath, "w") as f: + f.write(f"{e}") + continue + + new_response_service = ResponseService( + var_name=var_name.lower(), + assignment_id=self.id, + student_id=student_id, + extension=extension, + ) + new_response = new_response_service.create() + filepath = new_response.get_fullfile() + np.savetxt(filepath, value, fmt="%.5f", delimiter="") diff --git a/app/services/batch.py b/app/services/batch.py new file mode 100644 index 0000000..f6424e7 --- /dev/null +++ b/app/services/batch.py @@ -0,0 +1,34 @@ +from app import app, db +from ..models import Batch, Response + + +class BatchService: + def __init__( + self, + *, + grade: int = None, + comments: str = None, + datatype_id: int = None, + next_id: int = None, + previous_id: int = None, + question_id: int = None, + ): + self.grade = grade + self.comments = comments + self.datatype_id = datatype_id + self.next_id = next_id + self.previous_id = previous_id + self.question_id = question_id + + def create(self) -> dict: + new_batch = Batch( + grade=self.grade, + comments=self.comments, + datatype_id=self.datatype_id, + question_id=self.question_id, + next_id=self.question_id, + previous_id=self.previous_id, + ) + db.session.add(new_batch) + db.session.commit() + return new_batch.to_dict() diff --git a/app/services/question.py b/app/services/question.py index e69de29..0ebcb71 100644 --- a/app/services/question.py +++ b/app/services/question.py @@ -0,0 +1,48 @@ +from app import app, db +from ..models import Question + + +class QuestionService: + def __init__( + self, + *, + name: str = None, + var_name: str = None, + alt_var_name: str = None, + max_grade: int = None, + tolerance: float = None, + preprocessing: str = None, + assignment_id: int = None, + question_id: int = None, + ): + self.name = name + self.var_name = var_name + self.alt_var_name = alt_var_name + self.max_grade = max_grade + self.tolerance = tolerance + self.preprocessing = preprocessing + self.assignment_id = assignment_id + self.id = question_id + + def create(self) -> dict: + max_grade = self.max_grade or 1 + tolerance = self.tolerance or 0.0001 + + question = Question( + name=self.name, + var_name=self.var_name, + alt_var_name=self.alt_var_name, + max_grade=max_grade, + tolerance=tolerance, + preprocessing = self.preprocessing, + assignment_id=self.assignment_id, + ) + db.session.add(question) + db.session.commit() + return question.to_dict() + + def delete(self): + question = Question.query.get(self.id) + if question: + db.session.delete(question) + db.session.commit() diff --git a/app/services/response.py b/app/services/response.py new file mode 100644 index 0000000..7a5ad64 --- /dev/null +++ b/app/services/response.py @@ -0,0 +1,32 @@ +from pathlib import Path +from app import app, db +from ..models import Response, Assignment +from ..selectors.datatype import get_datatype_from_extension + + +class ResponseService: + def __init__( + self, + *, + var_name: str = None, + assignment_id: int = None, + extension = str, + student_id: int = None, + ): + self.var_name = var_name.lower() + self.assignment_id = assignment_id + self.extension = extension + self.student_id = student_id + + def create(self) -> Response: + datatype = get_datatype_from_extension(self.extension) + new_response = Response( + assignment_id=self.assignment_id, + student_id=self.student_id, + datatype_id=datatype.id, + var_name=self.var_name, + ) + db.session.add(new_response) + db.session.commit() + return new_response + \ No newline at end of file From 8a48ede5c1b4476508eda00734221c63bcf3d135 Mon Sep 17 00:00:00 2001 From: Edith Coates Date: Wed, 23 Aug 2023 13:25:10 -0700 Subject: [PATCH 3/3] New models module, more service classes --- app/models.py | 359 ----------------------------------- app/models/__init__.py | 8 + app/models/assignment.py | 28 +++ app/models/batch.py | 54 ++++++ app/models/batch_response.py | 9 + app/models/datatype.py | 10 + app/models/question.py | 41 ++++ app/models/response.py | 52 +++++ app/models/student.py | 6 + app/models/submission.py | 9 + app/routes.py | 37 ++-- app/selectors/batch.py | 12 ++ app/selectors/datatype.py | 4 + app/selectors/submission.py | 7 + app/services/batch.py | 80 +++++++- app/services/grades.py | 69 +++++++ app/services/question.py | 80 +++++++- 17 files changed, 489 insertions(+), 376 deletions(-) delete mode 100644 app/models.py create mode 100644 app/models/__init__.py create mode 100644 app/models/assignment.py create mode 100644 app/models/batch.py create mode 100644 app/models/batch_response.py create mode 100644 app/models/datatype.py create mode 100644 app/models/question.py create mode 100644 app/models/response.py create mode 100644 app/models/student.py create mode 100644 app/models/submission.py create mode 100644 app/selectors/batch.py create mode 100644 app/selectors/submission.py create mode 100644 app/services/grades.py diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 90d2f3f..0000000 --- a/app/models.py +++ /dev/null @@ -1,359 +0,0 @@ -from app import db -import os -from glob import glob -import numpy as np -import pandas as pd -import importlib -import json -from pathlib import Path - -class Assignment(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(80), unique=True, nullable=False) - folder_name = db.Column(db.String(80), unique=True, nullable=False) - - questions = db.relationship('Question', backref='assignment', lazy=True, cascade='all,delete') - responses = db.relationship('Response', backref='assignment', lazy=True, cascade='all,delete') - submissions = db.relationship('Submission', backref='assignment', lazy=True, cascade='all,delete') - - - def total_points(self): - points = [question.max_grade for question in self.questions] - return sum(points) - - def total_submissions(self): - return len(self.submissions) - - def total_questions(self): - return len(self.questions) - - def save_grades(self): - grades_folder = 'grades' - os.makedirs(grades_folder,exist_ok=True) - assignment_feedback_folder = os.path.join('feedback',self.folder_name) - os.makedirs(assignment_feedback_folder,exist_ok=True) - old_feedback = glob(os.path.join(assignment_feedback_folder,'*.txt')) - for f in old_feedback: - os.remove(f) - - q1 = BatchResponse.query.join('batch','question').options(db.joinedload('batch').joinedload('question')) - q2 = q1.filter_by(assignment_id=self.id) - q3 = q2.join('response','student').options(db.joinedload('response').joinedload('student')) - - df = pd.read_sql(q3.statement,db.engine) - columns = ['student_id','grade','comments','name','status'] - df = df[columns] - df.columns = ['Student ID','Grade','Comments','Question','Status'] - - q4 = Submission.query.filter_by(assignment_id=self.id) - submissions = pd.read_sql(q4.statement,db.engine) - submissions = submissions[['student_id']] - submissions.columns = ['Student ID'] - - grades = df.pivot(index='Student ID',columns='Question',values='Grade').fillna(0) - grades['Total'] = grades.sum(axis=1) - - grades = pd.merge(grades,submissions,left_index=True,right_on='Student ID',how='outer').fillna(0).set_index('Student ID') - grades.to_csv(os.path.join(grades_folder,self.folder_name) + '.csv') - - comments = df.pivot(index='Student ID',columns='Question',values='Comments').fillna('') - comments = pd.merge(comments,submissions,left_index=True,right_on='Student ID',how='outer').fillna('').set_index('Student ID') - - statuses = df.pivot(index='Student ID',columns='Question',values='Status').fillna('Did not find a response for this question.') - statuses = pd.merge(statuses,submissions,left_index=True,right_on='Student ID',how='outer').fillna('Did not find a response for this question.').set_index('Student ID') - - for student in grades.index: - filename = os.path.join(assignment_feedback_folder,str(student) + '.txt') - f = open(filename,'w') - feedback = '{}\nStudent ID: {}\n'.format(self.name,student) - for question in comments.columns: - comment = comments.loc[student,question] - status = statuses.loc[student,question] - grade = grades.loc[student,question] - max_grade = Question.query.filter_by(assignment_id=self.id).filter_by(name=question).first().max_grade - feedback += '\n{0}\nGrade: {1}/{2}\nStatus: {3}\nComments: {4}\n'.format(question,grade,float(max_grade),status,comment) - f.write(feedback) - f.close() - - - def to_dict(self): - return {'id': self.id, - 'name': self.name, - 'folder_name': self.folder_name, - 'total_points': self.total_points(), - 'total_questions': self.total_questions(), - 'total_submissions': self.total_submissions()} - - -class Question(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(80), nullable=False) - var_name = db.Column(db.String(80), nullable=False) - alt_var_name = db.Column(db.String(80), nullable=False) - max_grade = db.Column(db.Integer, default=0) - tolerance = db.Column(db.Float, default=0.001) - preprocessing = db.Column(db.String(280)) - - assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False) - batches = db.relationship('Batch', backref='question', lazy=True, cascade='all,delete') - - def get_preprocessing(self): - f = open('preprocessing_module.py','w') - f.write(self.preprocessing) - f.close() - import preprocessing_module - importlib.reload(preprocessing_module) - os.remove('preprocessing_module.py') - return preprocessing_module.fun - - def delete_batches(self): - for batch in self.batches: - db.session.delete(batch) - db.session.commit() - - def create_batches(self): - if self.preprocessing: - fun = self.get_preprocessing() - else: - fun = None - submissions = Submission.query.filter_by(assignment_id=self.assignment_id) - for submission in submissions: - status = 'Response loaded successfully.' - response = Response.query.filter_by(assignment_id=self.assignment_id, - student_id=submission.student_id, - var_name=self.var_name).first() - - if not response: - alt_vars = self.alt_var_name.lower().split(',') - for alt_var in alt_vars: - response = Response.query.filter_by(assignment_id=self.assignment_id, - student_id=submission.student_id, - var_name=alt_var).first() - if response: - status = 'Response loaded successfully. But variable name is misspelled!' - break - - if not response: - continue - - batched = False - for batch in self.batches: - if batch.compare(response,preprocessing=fun): - this_batch = batch - batched = True - continue - if not batched: - this_batch = Batch(grade=0, - comments='', - datatype_id=response.datatype_id, - question_id=self.id, - next_id=0, - previous_id=0) - db.session.add(this_batch) - db.session.commit() - batch_response = BatchResponse(response_id=response.id,batch_id=this_batch.id,status=status) - db.session.add(batch_response) - db.session.commit() - - batch_list = sorted(list(self.batches), key=lambda b: b.to_dict()['total_batch_responses'], reverse=True) - for n,batch in enumerate(batch_list): - next_batch_index = (n + 1) % len(batch_list) - batch.next_id = batch_list[next_batch_index].id - previous_batch_index = (n - 1) % len(batch_list) - batch.previous_id = batch_list[previous_batch_index].id - db.session.add(batch) - db.session.commit() - - def total_batches(self): - return len(self.batches) - - def total_responses(self): - return sum([len(batch.batch_responses) for batch in self.batches]) - - def to_dict(self): - return {'id': self.id, - 'name': self.name, - 'var_name': self.var_name, - 'alt_var_name': self.alt_var_name, - 'max_grade': self.max_grade, - 'tolerance': self.tolerance, - 'assignment_id': self.assignment_id, - 'total_batches': self.total_batches(), - 'total_responses': self.total_responses()} - - -class BatchResponse(db.Model): - response_id = db.Column(db.Integer, db.ForeignKey('response.id'), primary_key=True) - batch_id = db.Column(db.Integer, db.ForeignKey('batch.id'), primary_key=True) - status = db.Column(db.String(140)) - - response = db.relationship('Response', backref='batch_responses', lazy=True) - -class Batch(db.Model): - id = db.Column(db.Integer, primary_key=True) - grade = db.Column(db.Integer) - comments = db.Column(db.String(280)) - next_id = db.Column(db.Integer) - previous_id = db.Column(db.Integer) - - question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False) - datatype_id = db.Column(db.Integer, db.ForeignKey('datatype.id'), nullable=False) - batch_responses = db.relationship('BatchResponse', backref='batch', lazy=True, cascade='all,delete') - - def compare(self,response,preprocessing=None): - if self.datatype.id != response.datatype.id: - return False - response_data = response.get_data() - batch_data = self.get_data() - if preprocessing: - try: - response_data = preprocessing(response.student_id,response_data) - batch_data = preprocessing(self.batch_responses[0].response.student_id,batch_data) - except: - print('Preprocessing failed ... ') - pass - dtype = self.datatype.name - if dtype in ['text','symbolic','logical']: - return batch_data == response_data - elif dtype == 'numeric': - return np.array_equal(batch_data.shape,response_data.shape) and np.allclose(batch_data,response_data,atol=self.question.tolerance) - elif dtype == 'figure': - diffs = np.zeros([len(batch_data),len(response_data)]) - for sss in range(0, len(batch_data)): - for aaa in range(0, len(response_data)): - if response_data[aaa].size > 2 and batch_data[sss].size > 2: - x_response = response_data[aaa][:,0] - y_response = response_data[aaa][:,1] - x_batch = batch_data[sss][:,0] - y_batch = batch_data[sss][:,1] - L_response = np.sum(np.sqrt(np.diff(x_response)**2 + np.diff(y_response)**2)) - L_batch = np.sum(np.sqrt(np.diff(x_batch)**2 + np.diff(y_batch)**2)) - #if np.abs(L_response - L_batch)/L_batch > 0.01: - # diffs[sss,aaa] = 999 - # continue - t_response = np.concatenate([[0],np.cumsum(np.sqrt(np.diff(x_response)**2 + np.diff(y_response)**2))]) - t_batch = np.concatenate([[0],np.cumsum(np.sqrt(np.diff(x_batch)**2 + np.diff(y_batch)**2))]) - x_interp = np.interp(t_response, t_batch, x_batch) - x_diff = np.max(np.abs(x_interp - x_response)) - y_interp = np.interp(t_response, t_batch, y_batch) - y_diff = np.max(np.abs(y_interp - y_response)) - diffs[sss,aaa] = np.max([x_diff,y_diff]) - elif response_data[aaa].size == 2 and batch_data[sss].size == 2: - diffs[sss,aaa] = np.max(np.abs(response_data[aaa] - batch_data[sss])) - else: - diffs[sss,aaa] = 999 - diffs = diffs < self.question.tolerance - if np.all(diffs.sum(axis=0)) and np.all(diffs.sum(axis=1)): - return True - else: - return False - else: - return False - - def total_responses(self): - return len(self.batch_responses) - - def get_fullfile(self): - return self.batch_responses[0].response.get_fullfile() - - def get_data(self): - return self.batch_responses[0].response.get_data() - - def to_dict(self): - datatype = Datatype.query.get(self.datatype_id).name - if self.question.preprocessing: - fun = self.question.get_preprocessing() - try: - data = fun(self.batch_responses[0].response.student_id,self.get_data()) - except: - data = self.get_data() - else: - data = self.get_data() - if datatype == 'figure': - dataJSON = [] - for line in data: - if line.size == 2: - dataJSON.append(line.tolist()) - else: - dataJSON.append({'x': line[:,0].tolist(),'y': line[:,1].tolist()}) - else: - dataJSON = str(data) - return {'id': self.id, - 'grade': self.grade, - 'comments': self.comments, - 'question_id': self.question_id, - 'assignment_id': self.question.assignment.id, - 'datatype': datatype, - 'total_batch_responses': self.total_responses(), - 'total_question_responses': self.question.total_responses(), - 'next_id': self.next_id, - 'previous_id': self.previous_id, - 'data': dataJSON} - -class Response(db.Model): - id = db.Column(db.Integer, primary_key=True) - var_name = db.Column(db.String(80), nullable=False) - - student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False) - datatype_id = db.Column(db.Integer, db.ForeignKey('datatype.id'), nullable=False) - assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False) - - def get_fullfile(self): - submission_folder = Path("submissions/") - assignment_folder = Assignment.query.get(self.assignment_id).folder_name - student_folder = str(self.student_id) - response_file = f"{self.var_name}.{self.datatype.extension}" - return submission_folder / assignment_folder / student_folder / response_file - - def get_data(self): - dtype = self.datatype.name - filename = self.get_fullfile() - if dtype in ['numeric','logical']: - f = open(filename,'r') - data = f.read() - f.close() - if 'i' in data: - data = data.replace('i','j') - dtype = complex - else: - dtype = float - tmp = os.path.join('app','tmp.txt') - file = open(tmp,'w') - file.write(data) - file.close() - data = np.loadtxt(tmp,delimiter=',',ndmin=2,dtype=dtype) - if data.size == 1: - data = data.flat[0] - os.remove(tmp) - elif dtype == 'figure': - f = open(filename,'r') - data = f.read() - f.close() - data = json.loads(data) - Lines = data['Lines'] - data = [np.array(Lines[n],dtype=float) for n in range(0,len(Lines))] - else: - f = open(filename) - data = f.read() - f.close() - return data - -class Datatype(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(80), nullable=False) - extension = db.Column(db.String(10), nullable=False) - - batches = db.relationship('Batch', backref='datatype', lazy=True) - responses = db.relationship('Response', backref='datatype', lazy=True) - -class Student(db.Model): - id = db.Column(db.Integer, primary_key=True) - responses = db.relationship('Response', backref='student', lazy=True) - -class Submission(db.Model): - grade = db.Column(db.Integer) - feedback = db.Column(db.String(280)) - - assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), primary_key=True) - student_id = db.Column(db.Integer, db.ForeignKey('student.id'), primary_key=True) - student = db.relationship('Student', backref='submissions', lazy=True) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..908fb79 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,8 @@ +from .assignment import Assignment +from .question import Question +from .batch_response import BatchResponse +from .batch import Batch +from .response import Response +from .datatype import Datatype +from .student import Student +from .submission import Submission \ No newline at end of file diff --git a/app/models/assignment.py b/app/models/assignment.py new file mode 100644 index 0000000..dbb0354 --- /dev/null +++ b/app/models/assignment.py @@ -0,0 +1,28 @@ +from app import db + +class Assignment(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=True, nullable=False) + folder_name = db.Column(db.String(80), unique=True, nullable=False) + + questions = db.relationship('Question', backref='assignment', lazy=True, cascade='all,delete') + responses = db.relationship('Response', backref='assignment', lazy=True, cascade='all,delete') + submissions = db.relationship('Submission', backref='assignment', lazy=True, cascade='all,delete') + + def total_points(self): + points = [question.max_grade for question in self.questions] + return sum(points) + + def total_submissions(self): + return len(self.submissions) + + def total_questions(self): + return len(self.questions) + + def to_dict(self): + return {'id': self.id, + 'name': self.name, + 'folder_name': self.folder_name, + 'total_points': self.total_points(), + 'total_questions': self.total_questions(), + 'total_submissions': self.total_submissions()} diff --git a/app/models/batch.py b/app/models/batch.py new file mode 100644 index 0000000..e0ec687 --- /dev/null +++ b/app/models/batch.py @@ -0,0 +1,54 @@ +from app import db + +class Batch(db.Model): + id = db.Column(db.Integer, primary_key=True) + grade = db.Column(db.Integer) + comments = db.Column(db.String(280)) + next_id = db.Column(db.Integer) + previous_id = db.Column(db.Integer) + + question_id = db.Column(db.Integer, db.ForeignKey('question.id'), nullable=False) + datatype_id = db.Column(db.Integer, db.ForeignKey('datatype.id'), nullable=False) + batch_responses = db.relationship('BatchResponse', backref='batch', lazy=True, cascade='all,delete') + + def total_responses(self): + return len(self.batch_responses) + + def get_fullfile(self): + return self.batch_responses[0].response.get_fullfile() + + def get_data(self): + return self.batch_responses[0].response.get_data() + + def to_dict(self): + from ..selectors.datatype import get_datatype_name + + datatype = get_datatype_name(self.datatype_id) + if self.question.preprocessing: + fun = self.question.get_preprocessing() + try: + data = fun(self.batch_responses[0].response.student_id,self.get_data()) + except: + data = self.get_data() + else: + data = self.get_data() + if datatype == 'figure': + dataJSON = [] + for line in data: + if line.size == 2: + dataJSON.append(line.tolist()) + else: + dataJSON.append({'x': line[:,0].tolist(),'y': line[:,1].tolist()}) + else: + dataJSON = str(data) + return {'id': self.id, + 'grade': self.grade, + 'comments': self.comments, + 'question_id': self.question_id, + 'assignment_id': self.question.assignment.id, + 'datatype': datatype, + 'total_batch_responses': self.total_responses(), + 'total_question_responses': self.question.total_responses(), + 'next_id': self.next_id, + 'previous_id': self.previous_id, + 'data': dataJSON} diff --git a/app/models/batch_response.py b/app/models/batch_response.py new file mode 100644 index 0000000..2a323db --- /dev/null +++ b/app/models/batch_response.py @@ -0,0 +1,9 @@ +from app import db + + +class BatchResponse(db.Model): + response_id = db.Column(db.Integer, db.ForeignKey('response.id'), primary_key=True) + batch_id = db.Column(db.Integer, db.ForeignKey('batch.id'), primary_key=True) + status = db.Column(db.String(140)) + + response = db.relationship('Response', backref='batch_responses', lazy=True) diff --git a/app/models/datatype.py b/app/models/datatype.py new file mode 100644 index 0000000..308239c --- /dev/null +++ b/app/models/datatype.py @@ -0,0 +1,10 @@ +from app import db + + +class Datatype(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), nullable=False) + extension = db.Column(db.String(10), nullable=False) + + batches = db.relationship('Batch', backref='datatype', lazy=True) + responses = db.relationship('Response', backref='datatype', lazy=True) diff --git a/app/models/question.py b/app/models/question.py new file mode 100644 index 0000000..30d54b4 --- /dev/null +++ b/app/models/question.py @@ -0,0 +1,41 @@ +from app import db +import importlib + + +class Question(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), nullable=False) + var_name = db.Column(db.String(80), nullable=False) + alt_var_name = db.Column(db.String(80), nullable=False) + max_grade = db.Column(db.Integer, default=0) + tolerance = db.Column(db.Float, default=0.001) + preprocessing = db.Column(db.String(280)) + + assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False) + batches = db.relationship('Batch', backref='question', lazy=True, cascade='all,delete') + + def get_preprocessing(self): + f = open('preprocessing_module.py','w') + f.write(self.preprocessing) + f.close() + import preprocessing_module + importlib.reload(preprocessing_module) + os.remove('preprocessing_module.py') + return preprocessing_module.fun + + def total_batches(self): + return len(self.batches) + + def total_responses(self): + return sum([len(batch.batch_responses) for batch in self.batches]) + + def to_dict(self): + return {'id': self.id, + 'name': self.name, + 'var_name': self.var_name, + 'alt_var_name': self.alt_var_name, + 'max_grade': self.max_grade, + 'tolerance': self.tolerance, + 'assignment_id': self.assignment_id, + 'total_batches': self.total_batches(), + 'total_responses': self.total_responses()} diff --git a/app/models/response.py b/app/models/response.py new file mode 100644 index 0000000..6fb4dfb --- /dev/null +++ b/app/models/response.py @@ -0,0 +1,52 @@ +from pathlib import Path +import numpy as np +from app import db + + +class Response(db.Model): + id = db.Column(db.Integer, primary_key=True) + var_name = db.Column(db.String(80), nullable=False) + + student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False) + datatype_id = db.Column(db.Integer, db.ForeignKey('datatype.id'), nullable=False) + assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False) + + def get_fullfile(self): + submission_folder = Path("submissions/") + assignment_folder = Assignment.query.get(self.assignment_id).folder_name + student_folder = str(self.student_id) + response_file = f"{self.var_name}.{self.datatype.extension}" + return submission_folder / assignment_folder / student_folder / response_file + + def get_data(self): + dtype = self.datatype.name + filename = self.get_fullfile() + if dtype in ['numeric','logical']: + f = open(filename,'r') + data = f.read() + f.close() + if 'i' in data: + data = data.replace('i','j') + dtype = complex + else: + dtype = float + tmp = Path("app") / "tmp.txt" + file = open(tmp,'w') + file.write(data) + file.close() + data = np.loadtxt(tmp,delimiter=',',ndmin=2,dtype=dtype) + if data.size == 1: + data = data.flat[0] + os.remove(tmp) + elif dtype == 'figure': + f = open(filename,'r') + data = f.read() + f.close() + data = json.loads(data) + Lines = data['Lines'] + data = [np.array(Lines[n],dtype=float) for n in range(0,len(Lines))] + else: + f = open(filename) + data = f.read() + f.close() + return data diff --git a/app/models/student.py b/app/models/student.py new file mode 100644 index 0000000..3e5add7 --- /dev/null +++ b/app/models/student.py @@ -0,0 +1,6 @@ +from app import db + + +class Student(db.Model): + id = db.Column(db.Integer, primary_key=True) + responses = db.relationship('Response', backref='student', lazy=True) diff --git a/app/models/submission.py b/app/models/submission.py new file mode 100644 index 0000000..323ee9f --- /dev/null +++ b/app/models/submission.py @@ -0,0 +1,9 @@ +from app import db + +class Submission(db.Model): + grade = db.Column(db.Integer) + feedback = db.Column(db.String(280)) + + assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), primary_key=True) + student_id = db.Column(db.Integer, db.ForeignKey('student.id'), primary_key=True) + student = db.relationship('Student', backref='submissions', lazy=True) diff --git a/app/routes.py b/app/routes.py index d665292..c2700ac 100644 --- a/app/routes.py +++ b/app/routes.py @@ -4,6 +4,9 @@ from .services.assignment import AssignmentService from .services.question import QuestionService +from .services.grades import GradesService +from .services.batch import BatchService + from .selectors.assignment import get_all_assignments, get_assignment, get_assignment_vars from .selectors.question import get_questions_of_assignment, get_question @@ -42,7 +45,12 @@ def assignment(assignment_id): @app.route('/assignments//grades') def grades(assignment_id): assignment = Assignment.query.get_or_404(assignment_id) - assignment.save_grades() + service = GradesService( + assignment_id=assignment.id, + assignment_name=assignment.name, + root_folder=assignment.folder_name, + ) + service.save() return ('',200) @app.route('/assignments//vars') @@ -72,7 +80,7 @@ def questions(assignment_id): @app.route('/assignments//questions/', methods=['GET','DELETE']) def question(assignment_id,question_id): if request.method == 'GET': - question = get_question(question) + question = get_question(question_id) return jsonify(question) if request.method == 'DELETE': service = QuestionService(question_id=question_id) @@ -93,13 +101,10 @@ def create_response(assignment_id): def batch(assignment_id,question_id): create = request.args.get('create') if request.method == 'GET' and create == 'true': - question = Question.query.get_or_404(question_id) - question.delete_batches() - question.create_batches() - batch_list = sorted([batch.to_dict() for batch in question.batches], - key=lambda b: b['total_batch_responses'], - reverse=True) - return jsonify(batch_list) + service = QuestionService(question_id=question_id) + service.delete_batches() + batches = service.create_batches() + return jsonify(batches) elif request.method == 'GET' and create == 'false': question = Question.query.get_or_404(question_id) return jsonify([batch.to_dict() for batch in question.batches]) @@ -110,9 +115,11 @@ def grade(assignment_id,question_id,batch_id): batch = Batch.query.get_or_404(batch_id) return jsonify(batch.to_dict()) elif request.method == 'PUT': - batch = Batch.query.get_or_404(batch_id) - batch.grade = int(request.json['grade']) - batch.comments = request.json['comments'] - db.session.add(batch) - db.session.commit() - return jsonify(batch.to_dict()) \ No newline at end of file + data = request.json + service = BatchService( + batch_id=batch_id, + grade=int(data["grade"]), + comments=data["comments"] + ) + graded_batch = service.grade() + return jsonify(graded_batch) \ No newline at end of file diff --git a/app/selectors/batch.py b/app/selectors/batch.py new file mode 100644 index 0000000..83eef1b --- /dev/null +++ b/app/selectors/batch.py @@ -0,0 +1,12 @@ +from typing import List +from app import app, db +from ..models import Batch, Question + + +def get_batches_by_question(question_id: int) -> List[Batch]: + batches = Question.query.get(question_id).batches + return sorted( + list(batches), + key=lambda b: b.total_responses(), + reverse=True, + ) diff --git a/app/selectors/datatype.py b/app/selectors/datatype.py index a1f1eab..5821b95 100644 --- a/app/selectors/datatype.py +++ b/app/selectors/datatype.py @@ -3,3 +3,7 @@ def get_datatype_from_extension(extension: str) -> Datatype: return Datatype.query.filter_by(extension=extension).first() + + +def get_datatype_name(datatype_id: int) -> str: + return Datatype.query.get(datatype_id).name diff --git a/app/selectors/submission.py b/app/selectors/submission.py new file mode 100644 index 0000000..d01e285 --- /dev/null +++ b/app/selectors/submission.py @@ -0,0 +1,7 @@ +from typing import List +from app import app, db +from ..models import Submission + + +def get_submissions_for_assignment(assignment_id: int) -> List[Submission]: + return Submission.query.filter_by(assignment_id=assignment_id) diff --git a/app/services/batch.py b/app/services/batch.py index f6424e7..e5c2fb4 100644 --- a/app/services/batch.py +++ b/app/services/batch.py @@ -1,11 +1,14 @@ +from typing import Callable, Any +import numpy as np from app import app, db -from ..models import Batch, Response +from ..models import Batch, Response, BatchResponse class BatchService: def __init__( self, *, + batch_id: int = None, grade: int = None, comments: str = None, datatype_id: int = None, @@ -19,6 +22,7 @@ def __init__( self.next_id = next_id self.previous_id = previous_id self.question_id = question_id + self.id = batch_id def create(self) -> dict: new_batch = Batch( @@ -31,4 +35,78 @@ def create(self) -> dict: ) db.session.add(new_batch) db.session.commit() + self.id = new_batch.id return new_batch.to_dict() + + def compare( + self, + response: Response, + preprocessing: Callable[[int, str], Any] = None + ) -> bool: + model = Batch.query.get(self.id) + if model.datatyoe.id != response.datatype.id: + return False + + response_data = response.get_data() + batch_data = model.get_data() + if preprocessing: + try: + response_data = preprocessing( + response.student_id, response_data + ) + batch_data = preprocessing( + model.batch_response[0].response.student_id, + batch_data, + ) + except Exception as e: + print(f"Preprocessing failed: {e}") + + dtype = model.datatype.name + if dtype in ["text", "symbolic", "logical"]: + return batch_data == response_data + + elif dtype == "numeric": + return ( + np.array_equal(batch_data.shape, response_data.shape) + and np.allclose(batch_data, response_data, atol=model.question.tolerance) + ) + elif dtype == 'figure': + diffs = np.zeros([len(batch_data),len(response_data)]) + for sss in range(0, len(batch_data)): + for aaa in range(0, len(response_data)): + if response_data[aaa].size > 2 and batch_data[sss].size > 2: + x_response = response_data[aaa][:,0] + y_response = response_data[aaa][:,1] + x_batch = batch_data[sss][:,0] + y_batch = batch_data[sss][:,1] + L_response = np.sum(np.sqrt(np.diff(x_response)**2 + np.diff(y_response)**2)) + L_batch = np.sum(np.sqrt(np.diff(x_batch)**2 + np.diff(y_batch)**2)) + #if np.abs(L_response - L_batch)/L_batch > 0.01: + # diffs[sss,aaa] = 999 + # continue + t_response = np.concatenate([[0],np.cumsum(np.sqrt(np.diff(x_response)**2 + np.diff(y_response)**2))]) + t_batch = np.concatenate([[0],np.cumsum(np.sqrt(np.diff(x_batch)**2 + np.diff(y_batch)**2))]) + x_interp = np.interp(t_response, t_batch, x_batch) + x_diff = np.max(np.abs(x_interp - x_response)) + y_interp = np.interp(t_response, t_batch, y_batch) + y_diff = np.max(np.abs(y_interp - y_response)) + diffs[sss,aaa] = np.max([x_diff,y_diff]) + elif response_data[aaa].size == 2 and batch_data[sss].size == 2: + diffs[sss,aaa] = np.max(np.abs(response_data[aaa] - batch_data[sss])) + else: + diffs[sss,aaa] = 999 + diffs = diffs < model.question.tolerance + if np.all(diffs.sum(axis=0)) and np.all(diffs.sum(axis=1)): + return True + else: + return False + else: + return False + + def grade(self) -> dict: + model = Batch.query.get_or_404(self.id) + model.grade = self.grade + model.comments = self.comments + db.session.add(batch) + db.session.commit() + return model.to_dict() diff --git a/app/services/grades.py b/app/services/grades.py new file mode 100644 index 0000000..fbf63c0 --- /dev/null +++ b/app/services/grades.py @@ -0,0 +1,69 @@ +from pathlib import Path +import pandas as pd +from app import app, db +from ..models import BatchResponse, Submission + + +class GradesService: + def __init__( + self, + *, + assignment_id: int = None, + assignment_name: str = None, + root_folder: Path = None, + ): + self.assignment_id = assignment_id, + self.assignment_name = assignment_name + self.root_folder = root_folder + + def save(self): + grades_folder = Path("grades") + grades_folder.mkdir(exist_ok=True) + + feedback_folder = Path("feedback") + feedback_folder.mkdir(exist_ok=True) + assignment_feedback_folder = feedback_folder / self.assignment_name + assignment_feedback_folder.mkdir(exist_ok=True) + + old_feedback = assignment_feedback_folder.glob("*.txt") + for f in old_feedback: + f.unlink() + + q1 = BatchResponse.query.join('batch','question').options(db.joinedload('batch').joinedload('question')) + q2 = q1.filter_by(assignment_id=self.assignment_id) + q3 = q2.join('response','student').options(db.joinedload('response').joinedload('student')) + + df = pd.read_sql(q3.statement,db.engine) + columns = ['student_id','grade','comments','name','status'] + df = df[columns] + df.columns = ['Student ID','Grade','Comments','Question','Status'] + + q4 = Submission.query.filter_by(assignment_id=self.assignment_id) + submissions = pd.read_sql(q4.statement,db.engine) + submissions = submissions[['student_id']] + submissions.columns = ['Student ID'] + + grades = df.pivot(index='Student ID',columns='Question',values='Grade').fillna(0) + grades['Total'] = grades.sum(axis=1) + + grades = pd.merge(grades,submissions,left_index=True,right_on='Student ID',how='outer').fillna(0).set_index('Student ID') + grades.to_csv(os.path.join(grades_folder,self.root_folder) + '.csv') + + comments = df.pivot(index='Student ID',columns='Question',values='Comments').fillna('') + comments = pd.merge(comments,submissions,left_index=True,right_on='Student ID',how='outer').fillna('').set_index('Student ID') + + statuses = df.pivot(index='Student ID',columns='Question',values='Status').fillna('Did not find a response for this question.') + statuses = pd.merge(statuses,submissions,left_index=True,right_on='Student ID',how='outer').fillna('Did not find a response for this question.').set_index('Student ID') + + for student in grades.index: + filename = assignment_feedback_folder / f"{student}.txt" + f = open(filename,'w') + feedback = '{}\nStudent ID: {}\n'.format(self.assignment_name,student) + for question in comments.columns: + comment = comments.loc[student,question] + status = statuses.loc[student,question] + grade = grades.loc[student,question] + max_grade = Question.query.filter_by(assignment_id=self.assignment_id).filter_by(name=question).first().max_grade + feedback += '\n{0}\nGrade: {1}/{2}\nStatus: {3}\nComments: {4}\n'.format(question,grade,float(max_grade),status,comment) + f.write(feedback) + f.close() diff --git a/app/services/question.py b/app/services/question.py index 0ebcb71..94ec806 100644 --- a/app/services/question.py +++ b/app/services/question.py @@ -1,5 +1,10 @@ +from typing import List from app import app, db -from ..models import Question +from ..models import Question, BatchResponse +from ..services.batch import BatchService +from ..selectors.submission import get_submissions_for_assignment +from ..selectors.response import get_response +from ..selectors.batch import get_batches_by_question class QuestionService: @@ -46,3 +51,76 @@ def delete(self): if question: db.session.delete(question) db.session.commit() + + def delete_batches(self): + question = Question.query.get(self.id) + if question: + for batch in question.batches: + db.session.delete(batch) + db.session.commit() + + def create_batches(self) -> List[dict]: + question = Question.query.get(self.id) + if question.preprocessing: + fun = question.get_preprocessing() + else: + fun = None + + submissions = get_submissions_for_assignment(self.assignment_id) + for submission in submissions: + status = "Response loaded successfully." + response = get_response( + self.assignment_id, + submission.student_id, + var_name=self.var_name + ) + + if not response: + alt_vars = self.alt_var_name.lower().split(",") + for alt_var in alt_vars: + response = get_response( + self.assignment_id, + submission.student_id, + var_name=self.var_name, + ) + if response: + status = "Response loaded successfully. But variable name is misspelled!" + break + + if not response: + continue + + batched = False + for batch in question.batches: + service = BatchService(batch_id=batch.id) + if service.compare(response, preprocessing=fun): + batched = True + continue + if not batched: + new_batch_service = BatchService( + grade=0, + comments="", + datatype_id=response.datatype.id, + question_id=self.id, + next_id=0, + previous_id=0 + ) + new_batch_service.create() + batch_response = BatchResponse( + response_id=response.id, + batch_id=new_batch_service.id, + status=status, + ) + db.session.add(batch_response) + db.session.commit() + + batches = get_batches_by_question(self.id) + for n, batch in enumerate(batches): + next_batch_index = (n + 1) % len(batches) + batch.next_id = batches[next_batch_index].id + previous_batch_index = (n - 1) % len(batches) + batch.previous_id = batches[previous_batch_index].id + db.session.add(batch) + db.session.commit() + + return [b.to_dict() for b in batches]