def test_notebook_find_by_name(db, assignment_tree): orm_notebook = Notebook( name="Exam 2", assignment_id=assignment_tree.id, ) db.add(orm_notebook) db.commit() with pytest.raises(TypeError): found_by_name = Notebook.find_by_name() with pytest.raises(TypeError): found_by_name = Notebook.find_by_name(db) with pytest.raises(TypeError): found_by_name = Notebook.find_by_name(db, None) with pytest.raises(TypeError): found_by_name = Notebook.find_by_name(db, "abc") with pytest.raises(TypeError): found_by_name = Notebook.find_by_name(db, "abc", "foo") found_by_name = Notebook.find_by_name(db, "Exam 2", assignment_tree.id) assert found_by_name.id == orm_notebook.id found_by_name = Notebook.find_by_name(db, "Exam 3", assignment_tree.id) assert found_by_name is None found_by_name = Notebook.find_by_name(db, assignment_id=assignment_tree.id, name="Exam 2") assert found_by_name.id == orm_notebook.id
def test_feedback_find_all_for_student(db, assignment_tree, user_johaannes): # previous subscriptions, actions, feedback, and notebooks still in the db Notebook.find_by_name(db, "Exam 2", assignment_tree.id) with pytest.raises(TypeError): Feedback.find_all_for_student() with pytest.raises(TypeError): Feedback.find_all_for_student(db) with pytest.raises(TypeError): Feedback.find_all_for_student(db, "Johannes") with pytest.raises(TypeError): Feedback.find_all_for_student(db, user_johaannes.id, "tree 1")
def test_feedback_find_all_for_student_again(db, assignment_tree, user_johaannes, user_kaylee): notebook = Notebook.find_by_name(db, "Exam 2", assignment_tree.id) released = Action.find_most_recent_action(db, assignment_tree.id, AssignmentActions.fetched) orm_feedback = Feedback( notebook_id=notebook.id, instructor_id=user_kaylee.id, student_id=user_johaannes.id, location=released.location, checksum="234567890abcdef1", timestamp=released.timestamp, ) db.add(orm_feedback) db.commit() # Note this this swaps instructor & user, so should *not* be included orm_feedback_2 = Feedback( notebook_id=notebook.id, instructor_id=user_johaannes.id, student_id=user_kaylee.id, location=released.location, checksum="34567890abcdef12", timestamp=released.timestamp, ) db.add(orm_feedback_2) db.commit() feedback = Feedback.find_all_for_student(db, user_johaannes.id, assignment_tree.id) assert len(feedback) == 2
def test_feedback_find_notebook_for_student(db, assignment_tree, user_johaannes): # previous subscriptions, actions, feedback, and notebooks still in the db notebook = Notebook.find_by_name(db, "Exam 2", assignment_tree.id) with pytest.raises(TypeError): feedback = Feedback.find_notebook_for_student() with pytest.raises(TypeError): feedback = Feedback.find_notebook_for_student(db) with pytest.raises(TypeError): feedback = Feedback.find_notebook_for_student(db, "Exam 2") with pytest.raises(TypeError): feedback = Feedback.find_notebook_for_student(db, notebook.id, "Kaylee") feedback = Feedback.find_notebook_for_student(db, notebook.id, user_johaannes.id) assert feedback.notebook_id == notebook.id feedback = Feedback.find_notebook_for_student(db, student_id=user_johaannes.id, notebook_id=notebook.id) assert feedback.notebook_id == notebook.id
def test_feedback_base_mathods_and_find_by_pk(db, assignment_tree, user_kaylee, user_johaannes): # previous subscriptions & notebooks still in the db notebook = Notebook.find_by_name(db, "Exam 2", assignment_tree.id) released = Action.find_most_recent_action(db, assignment_tree.id, AssignmentActions.released) orm_feedback = Feedback( notebook_id=notebook.id, instructor_id=user_kaylee.id, student_id=user_johaannes.id, location=released.location, checksum="1234567890abcdef", timestamp=released.timestamp, ) db.add(orm_feedback) db.commit() with pytest.raises(TypeError): found_by_pk = Feedback.find_by_pk() with pytest.raises(TypeError): found_by_pk = Feedback.find_by_pk(db) with pytest.raises(ValueError): found_by_pk = Feedback.find_by_pk(db, None) with pytest.raises(TypeError): found_by_pk = Feedback.find_by_pk(db, "abc") found_by_pk = Feedback.find_by_pk(db, orm_feedback.id) assert found_by_pk.id == orm_feedback.id assert ( str(found_by_pk) == f"Feedback<Notebook-{found_by_pk.notebook_id}/Student-{found_by_pk.student_id}/{found_by_pk.checksum}>" ) assert found_by_pk.notebook_id == notebook.id assert found_by_pk.instructor_id == user_kaylee.id assert found_by_pk.student_id == user_johaannes.id # relationships assert found_by_pk.notebook.name == notebook.name assert found_by_pk.instructor.name == user_kaylee.name assert found_by_pk.student.name == user_johaannes.name found_by_pk = Feedback.find_by_pk(db, orm_feedback.id + 10) assert found_by_pk is None
def test_notebook_base_mathods_and_find_by_pk(db, assignment_tree): # name is required orm_notebook = Notebook( # name="Test 1", assignment_id=assignment_tree.id, ) db.add(orm_notebook) with pytest.raises(IntegrityError): db.commit() db.rollback() orm_notebook = Notebook( name="Test 1", assignment_id=assignment_tree.id, ) db.add(orm_notebook) db.commit() assert orm_notebook.name == "Test 1" assert orm_notebook.assignment.assignment_code == assignment_tree.assignment_code with pytest.raises(TypeError): found_by_pk = Notebook.find_by_pk() with pytest.raises(TypeError): found_by_pk = Notebook.find_by_pk(db) with pytest.raises(ValueError): found_by_pk = Notebook.find_by_pk(db, None) with pytest.raises(TypeError): found_by_pk = Notebook.find_by_pk(db, "abc") found_by_pk = Notebook.find_by_pk(db, orm_notebook.id) assert found_by_pk.id == orm_notebook.id # # relationships assert found_by_pk.assignment.id == assignment_tree.id found_by_pk = Notebook.find_by_pk(db, orm_notebook.id + 10) assert found_by_pk is None
def test_notebook_find_all(db, assignment_tree): # previous notebooks still in the db with pytest.raises(TypeError): found_all_for_assignment = Notebook.find_all_for_assignment() with pytest.raises(TypeError): found_all_for_assignment = Notebook.find_all_for_assignment(db) with pytest.raises(TypeError): found_all_for_assignment = Notebook.find_all_for_assignment(db, None) with pytest.raises(TypeError): found_all_for_assignment = Notebook.find_all_for_assignment(db, "abc") found_all_for_assignment = Notebook.find_all_for_assignment(db, assignment_tree.id) assert len(found_all_for_assignment) == 2 found_all_for_assignment = Notebook.find_all_for_assignment(assignment_id=assignment_tree.id, db=db) assert len(found_all_for_assignment) == 2
def get(self): [course_id, assignment_id] = self.get_params(["course_id", "assignment_id"]) if not assignment_id or not course_id: note = "Feedback call requires an assignment id and a course id" self.log.info(note) self.finish({"success": False, "note": note}) return self.log.debug( f"checking for feedback for {assignment_id} on {course_id}") this_user = self.nbex_user with scoped_session() as session: course = Course.find_by_code(db=session, code=course_id, org_id=this_user["org_id"], log=self.log) if not course: note = f"Course {course_id} not found" self.log.info(note) # self.finish({"success": False, "note": note, "value": []}) # return raise web.HTTPError(404, note) assignment = AssignmentModel.find_by_code(db=session, code=assignment_id, course_id=course.id, log=self.log) if not assignment: note = f"Assignment {assignment_id} for Course {course_id} not found" self.log.info(note) # self.finish({"success": False, "note": note, "value": []}) # return raise web.HTTPError(404, note) student = User.find_by_name(db=session, name=this_user["name"], log=self.log) res = Feedback.find_all_for_student( db=session, student_id=student.id, assignment_id=assignment.id, log=self.log, ) feedbacks = [] for r in res: f = {} notebook = Notebook.find_by_pk(db=session, pk=r.notebook_id, log=self.log) if notebook is not None: feedback_name = "{0}.html".format(notebook.name) else: feedback_name = os.path.basename(r.location) with open(r.location, "r+b") as fp: f["content"] = base64.b64encode(fp.read()).decode("utf-8") f["filename"] = feedback_name # This matches self.timestamp_format f["timestamp"] = r.timestamp.strftime( "%Y-%m-%d %H:%M:%S.%f %Z") f["checksum"] = r.checksum feedbacks.append(f) # Add action action = Action( user_id=this_user["id"], assignment_id=assignment.id, action=AssignmentActions.feedback_fetched, location=r.location, ) session.add(action) self.finish({"success": True, "feedback": feedbacks})
def post(self): """ This endpoint accepts feedback files for a notebook. It requires a notebook id, student id, feedback timestamp and a checksum. The endpoint return {'success': true} for all successful feedback releases. """ [ course_id, assignment_id, notebook_id, student_id, timestamp, checksum, ] = self.get_params([ "course_id", "assignment_id", "notebook", "student", "timestamp", "checksum", ]) if not (course_id and assignment_id and notebook_id and student_id and timestamp and checksum): note = "Feedback call requires a course id, assignment id, notebook name, student id, checksum and timestamp." self.log.debug(note) self.finish({"success": False, "note": note}) return this_user = self.nbex_user if course_id not in this_user["courses"]: note = f"User not subscribed to course {course_id}" self.log.info(note) self.finish({"success": False, "note": note}) return if ("instructor" != this_user["current_role"].casefold() ): # we may need to revisit this note = f"User not an instructor to course {course_id}" self.log.info(note) self.finish({"success": False, "note": note}) return with scoped_session() as session: # Start building feedback object course = Course.find_by_code(db=session, code=course_id, org_id=this_user["org_id"], log=self.log) if not course: self.log.info( f"Could not find requested resource course {course_id}") raise web.HTTPError( 404, f"Could not find requested resource course {course_id}") assignment = AssignmentModel.find_by_code( db=session, code=assignment_id, course_id=course.id, action=AssignmentActions.released.value, ) if not assignment: note = f"Could not find requested resource assignment {assignment_id}" self.log.info(note) raise web.HTTPError(404, note) notebook = Notebook.find_by_name(db=session, name=notebook_id, assignment_id=assignment.id, log=self.log) if not notebook: note = f"Could not find requested resource notebook {notebook_id}" self.log.info(note) raise web.HTTPError(404, note) student = User.find_by_name(db=session, name=student_id, log=self.log) if not student: note = f"Could not find requested resource student {student_id}" self.log.info(note) raise web.HTTPError(404, note) # # raise Exception(f"{res}") # self.log.info(f"Notebook: {notebook}") # self.log.info(f"Student: {student}") # self.log.info(f"Instructor: {this_user}") # TODO: check access. Is the user an instructor on the course to which the notebook belongs # Check whether there is an HTML file attached to the request if not self.request.files: self.log.warning(f"Error: No file supplied in upload" ) # TODO: improve error message raise web.HTTPError(412) # precondition failed try: # Grab the file file_info = self.request.files["feedback"][0] filename, content_type = ( file_info["filename"], file_info["content_type"], ) note = f"Received file {filename}, of type {content_type}" self.log.info(note) fbfile = tempfile.NamedTemporaryFile() fbfile.write(file_info["body"]) fbfile.seek(0) except Exception as e: # Could not grab the feedback file self.log.error(f"Error: {e}") raise web.HTTPError(412) # TODO: should we check the checksum? # unique_key = make_unique_key( # course_id, # assignment_id, # notebook_id, # student_id, # str(timestamp).strip(), # ) # check_checksum = notebook_hash(fbfile.name, unique_key) # # if check_checksum != checksum: # self.log.info(f"Checksum {checksum} does not match {check_checksum}") # raise web.HTTPError(403, f"Checksum {checksum} does not match {check_checksum}") # TODO: What is file of the original notebook we are getting the feedback for? # assignment_dir = "collected/student_id/assignment_name" # nbfile = os.path.join(assignment_dir, "{}.ipynb".format(notebook.name)) # calc_checksum = notebook_hash(nbfile.name, unique_key) # if calc_checksum != checksum: # self.log.info(f"Mismatched checksums {calc_checksum} and {checksum}.") # raise web.HTTPError(412) location = "/".join([ self.base_storage_location, str(this_user["org_id"]), "feedback", notebook.assignment.course.course_code, notebook.assignment.assignment_code, str(int(time.time())), ]) # This should be abstracted, so it can be overloaded to store in other manners (eg AWS) feedback_file = location + "/" + checksum + ".html" try: # Ensure the directory exists os.makedirs(os.path.dirname(feedback_file), exist_ok=True) with open(feedback_file, "w+b") as handle: handle.write(file_info["body"]) except Exception as e: self.log.error(f"Could not save file. \n {e}") raise web.HTTPError(500) feedback = Feedback( notebook_id=notebook.id, checksum=checksum, location=feedback_file, student_id=student.id, instructor_id=this_user.get("id"), timestamp=parser.parse(timestamp), ) session.add(feedback) # Add action action = Action( user_id=this_user["id"], assignment_id=notebook.assignment.id, action=AssignmentActions.feedback_released, location=feedback_file, ) session.add(action) self.finish({"success": True, "note": "Feedback released"})
def post(self): # Do a content-length check, before we go any further if "Content-Length" in self.request.headers and int( self.request.headers["Content-Length"] ) > int(self.max_buffer_size): note = "File upload oversize, and rejected. Please reduce the contents of the assignment, re-generate, and re-release" self.log.info(note) self.finish({"success": False, "note": note}) return [course_code, assignment_code] = self.get_params(["course_id", "assignment_id"]) self.log.debug( f"Called POST /assignment with arguments: course {course_code} and assignment {assignment_code}" ) if not (course_code and assignment_code): note = f"Posting an Assigment requires a course code and an assignment code" self.log.info(note) self.finish({"success": False, "note": note}) return this_user = self.nbex_user if not course_code in this_user["courses"]: note = f"User not subscribed to course {course_code}" self.log.info(note) self.finish({"success": False, "note": note}) return if ( not "instructor" == this_user["current_role"].casefold() ): # we may need to revisit this note = f"User not an instructor to course {course_code}" self.log.info(note) self.finish({"success": False, "note": note}) return # The course will exist: the user object creates it if it doesn't exist # - and we know the user is subscribed to the course as an instructor (above) with scoped_session() as session: course = Course.find_by_code( db=session, code=course_code, org_id=this_user["org_id"], log=self.log ) # We need to find this assignment, or make a new one. assignment = AssignmentModel.find_by_code( db=session, code=assignment_code, course_id=course.id ) if assignment is None: # Look for inactive assignments assignment = AssignmentModel.find_by_code( db=session, code=assignment_code, course_id=course.id, active=False ) if assignment is None: self.log.info( f"New Assignment details: assignment_code:{assignment_code}, course_id:{course.id}" ) # defaults active assignment = AssignmentModel( assignment_code=assignment_code, course_id=course.id ) session.add(assignment) # deliberately no commit: we need to be able to roll-back if there's no data! # Set assignment to active assignment.active = True # storage is dynamically in $path/release/$course_code/$assignment_code/<timestamp>/ # Note - this means we can have multiple versions of the same release on the system release_file = "/".join( [ self.base_storage_location, str(this_user["org_id"]), AssignmentActions.released.value, course_code, assignment_code, str(int(time.time())), ] ) if not self.request.files: self.log.warning( f"Error: No file supplies in upload" ) # TODO: improve error message raise web.HTTPError(412) # precondition failed try: # Write the uploaded file to the desired location file_info = self.request.files["assignment"][0] filename, content_type = ( file_info["filename"], file_info["content_type"], ) note = f"Received file {filename}, of type {content_type}" self.log.info(note) extn = os.path.splitext(filename)[1] cname = str(uuid.uuid4()) + extn # store to disk. # This should be abstracted, so it can be overloaded to store in other manners (eg AWS) release_file = release_file + "/" + cname # Ensure the directory exists os.makedirs(os.path.dirname(release_file), exist_ok=True) with open(release_file, "w+b") as handle: handle.write(file_info["body"]) except Exception as e: # TODO: exception handling self.log.warning(f"Error: {e}") # TODO: improve error message self.log.info(f"Upload failed") # error 500?? raise Exception # Check the file exists on disk if not ( os.path.exists(release_file) and os.access(release_file, os.R_OK) and os.path.getsize(release_file) > 0 ): note = "File upload failed." self.log.info(note) self.finish({"success": False, "note": note}) return # We shouldn't get here, but a double-check is good if os.path.getsize(release_file) > self.max_buffer_size: os.remove(release_file) note = "File upload oversize, and rejected. Please reduce the contents of the assignment, re-generate, and re-release" self.log.info(note) self.finish({"success": False, "note": note}) return # now commit the assignment, and get it back to find the id assignment = AssignmentModel.find_by_code( db=session, code=assignment_code, course_id=course.id ) # Record the notebooks associated with this assignment notebooks = self.get_arguments("notebooks") for notebook in notebooks: self.log.debug(f"Adding notebook {notebook}") new_notebook = Notebook(name=notebook) assignment.notebooks.append(new_notebook) # Record the action. # Note we record the path to the files. self.log.info( f"Adding action {AssignmentActions.released.value} for user {this_user['id']} against assignment {assignment.id}" ) action = Action( user_id=this_user["id"], assignment_id=assignment.id, action=AssignmentActions.released, location=release_file, ) session.add(action) self.finish({"success": True, "note": "Released"})