def course_strange(db): orm_thing = Course.find_by_code(db, code="Strange", org_id=1) if not orm_thing: orm_thing = Course(org_id=1, course_code="Strange", course_title="Damnation Alley") db.add(orm_thing) db.commit() return orm_thing
def course_charm(db): orm_thing = Course.find_by_code(db, code="WEIRD", org_id=1) if not orm_thing: orm_thing = Course(org_id=1, course_code="WEIRD", course_title="Fable of a Failed Race") db.add(orm_thing) db.commit() return orm_thing
def test_multiple_courses(db, course_quirk, course_strange, course_charm): courses = Course.find_by_org(db, 1) assert len(courses) == 2 courses = Course.find_by_org(db, 2) assert len(courses) == 1 courses = Course.find_by_org(db, 3) assert len(courses) == 0
def course_quirk(db): orm_thing = Course.find_by_code(db, code="quirk", org_id=2) if not orm_thing: orm_thing = Course(org_id=2, course_code="quirk", course_title="Spirit of the Age") db.add(orm_thing) db.commit() return orm_thing
def delete(self): [course_code, assignment_code, purge] = self.get_params(["course_id", "assignment_id", "purge"]) self.log.debug( f"Called DELETE /assignment with arguments: course {course_code}, assignment {assignment_code}, and purge {purge}" # noqa: E501 ) if not (course_code and assignment_code): note = "Unreleasing 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 course_code not 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 "instructor" not in map(str.casefold, this_user["courses"][course_code]): note = f"User not an instructor to course {course_code}" self.log.info(note) self.finish({"success": False, "note": note}) return note = f"Assignment '{assignment_code}' on course '{course_code}' marked as unreleased" with scoped_session() as session: course = Course.find_by_code(db=session, code=course_code, org_id=this_user["org_id"], log=self.log) assignment = AssignmentModel.find_by_code(db=session, code=assignment_code, course_id=course.id) if not assignment: note = f"Missing assignment for {assignment_code} and {course_code}, cannot delete" self.log.info(note) self.finish({"success": False, "note": note}) return # Set assignment to inactive assignment.active = False # Delete the associated notebook for notebook in assignment.notebooks: session.delete(notebook) # If we have the purge parameter, we actually delete the data # The various 'cascade on delete' settings should clear all the sub-tables if purge: session.delete(assignment) note = f"Assignment '{assignment_code}' on course '{course_code}' deleted and purged from the database" self.log.info(f"{note} by user {this_user['id']} ") self.finish({"success": True, "note": note})
def test_course_basic_requirements(db, user_kaylee): orm_course = Course( org_id=user_kaylee.org_id, # course_code = "Strange", ) db.add(orm_course) with pytest.raises(IntegrityError): db.commit() db.rollback() orm_course = Course( # org_id=user_kaylee.org_id, course_code="Strange", ) db.add(orm_course) with pytest.raises(IntegrityError): db.commit() db.rollback() orm_course = Course( org_id=user_kaylee.org_id, course_code="Strange", ) db.add(orm_course) db.commit() assert orm_course.course_code == "Strange" orm_course.course_title = "Damnation Alley" db.commit()
def test_course_params(db, course_strange): # confirm named arguments work even when reversed found_by_code = Course.find_by_code(org_id=course_strange.org_id, db=db, code=course_strange.course_code) assert found_by_code.course_code == course_strange.course_code # confirm that putting the positional values the wrong way round will [probably] fail with pytest.raises(ValueError): found_by_code = Course.find_by_code(db, course_strange.org_id, course_strange.course_code) # test for unbexpected param with pytest.raises(TypeError): Course.find_by_pk(primary_key=course_strange.id, db=db) with pytest.raises(TypeError): found_by_code = Course.find_by_code(course_code=course_strange.course_code, org_id=course_strange.org_id, db=db) with pytest.raises(TypeError): found_by_code = Course.find_by_code(code=course_strange.course_code, id=course_strange.org_id, db=db) with pytest.raises(TypeError): Course.find_by_org(id=course_strange.org_id, db=db)
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 get(self): models = [] [course_code] = self.get_params(["course_id"]) if not course_code: note = f"Assigment call requires a course id" self.log.info(note) self.finish({"success": False, "note": note, "value": []}) return # Who is my user? this_user = self.nbex_user self.log.debug(f"User: {this_user.get('name')}") # For what course do we want to see the assignments? self.log.debug(f"Course: {course_code}") # Is our user subscribed to this course? if course_code not in this_user["courses"]: note = f"User not subscribed to course {course_code}" self.log.info(note) self.finish({"success": False, "note": note, "value": []}) return # Find the course being referred to with scoped_session() as session: course = Course.find_by_code( db=session, code=course_code, org_id=this_user["org_id"], log=self.log ) if not course: note = f"Course {course_code} does not exist" self.log.info(note) self.finish({"success": False, "note": note, "value": []}) return assignments = AssignmentModel.find_for_course( db=session, course_id=course.id, log=self.log ) for assignment in assignments: self.log.debug(f"==========") self.log.debug(f"Assignment: {assignment}") for action in assignment.actions: # For every action that is not "released" checked if the user id matches if ( action.action != AssignmentActions.released and this_user.get("id") != action.user_id ): self.log.debug( f"ormuser: {this_user.get('id')} - actionUser {action.user_id}" ) self.log.debug("Action does not belong to user, skip action") continue notebooks = [] for notebook in assignment.notebooks: feedback_available = False feedback_timestamp = None if action.action == AssignmentActions.submitted: feedback = Feedback.find_notebook_for_student( db=session, notebook_id=notebook.id, student_id=this_user.get("id"), log=self.log, ) if feedback: feedback_available = bool(feedback) feedback_timestamp = feedback.timestamp.strftime( "%Y-%m-%d %H:%M:%S.%f %Z" ) notebooks.append( { "notebook_id": notebook.name, "has_exchange_feedback": feedback_available, "feedback_updated": False, # TODO: needs a real value "feedback_timestamp": feedback_timestamp, } ) models.append( { "assignment_id": assignment.assignment_code, "student_id": action.user_id, "course_id": assignment.course.course_code, "status": action.action.value, # currently called 'action' in our db "path": action.location, "notebooks": notebooks, "timestamp": action.timestamp.strftime( "%Y-%m-%d %H:%M:%S.%f %Z" ), } ) self.log.debug(f"Assignments: {models}") self.finish({"success": True, "value": models})
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"})
def get(self): # def get(self, course_code, assignment_code=None): [course_code, assignment_code] = self.get_params(["course_id", "assignment_id"]) if not (course_code and assignment_code): note = "Assigment call requires both 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 # Find the course being referred to with scoped_session() as session: course = Course.find_by_code( db=session, code=course_code, org_id=this_user["org_id"], log=self.log ) if course is None: note = f"Course {course_code} does not exist" self.log.info(note) self.finish({"success": False, "note": note}) return # needs a proper 'fail' here note = "" self.log.debug(f"Course:{course_code} assignment:{assignment_code}") # The location for the data-object is actually held in the 'released' action for the given assignment # We want the last one... assignment = AssignmentModel.find_by_code( db=session, code=assignment_code, course_id=course.id, action=AssignmentActions.released.value, ) if assignment is None: note = f"Assignment {assignment_code} does not exist" self.log.info(note) self.finish({"success": False, "note": note}) return # needs a proper 'fail' here self._headers = httputil.HTTPHeaders( { "Content-Type": "application/gzip", "Date": httputil.format_timestamp(time.time()), } ) data = b"" release_file = None action = Action.find_most_recent_action( db=session, assignment_id=assignment.id, action=AssignmentActions.released, log=self.log, ) release_file = action.location if release_file: try: with open(release_file, "r+b") as handle: data = handle.read() except Exception as e: # TODO: exception handling self.log.warning(f"Error: {e}") # TODO: improve error message self.log.info(f"Unable to open file") # error 500?? raise Exception self.log.info( f"Adding action {AssignmentActions.fetched.value} for user {this_user['id']} against assignment {assignment.id}" ) action = Action( user_id=this_user["id"], assignment_id=assignment.id, action=AssignmentActions.fetched, location=release_file, ) session.add(action) self.log.info("record of fetch action committed") self.finish(data) else: self.log.info("no release file found") raise Exception
def nbex_user(self): hub_user = self.get_current_user() hub_username = hub_user.get("name") full_name = hub_user.get("full_name") current_course = hub_user.get("course_id") current_role = hub_user.get("course_role") course_title = hub_user.get("course_title", "no_title") org_id = hub_user.get("org_id", 1) if not (current_course and current_role): return self.org_id = org_id with scoped_session() as session: user = User.find_by_name(db=session, name=hub_username, log=self.log) if user is None: self.log.debug( f"New user details: name:{hub_username}, org_id:{org_id}") user = User(name=hub_username, org_id=org_id) session.add(user) if user.full_name != full_name: user.full_name = full_name course = Course.find_by_code(db=session, code=current_course, org_id=org_id, log=self.log) if course is None: self.log.debug( f"New course details: code:{current_course}, org_id:{org_id}" ) course = Course(org_id=org_id, course_code=current_course) if course_title: self.log.debug(f"Adding title {course_title}") course.course_title = course_title session.add(course) # Check to see if we have a subscription (for this course) self.log.debug( f"Looking for subscription for: user:{user.id}, course:{course.id}, role:{current_role}" ) subscription = Subscription.find_by_set(db=session, user_id=user.id, course_id=course.id, role=current_role) if subscription is None: self.log.debug( f"New subscription details: user:{user.id}, course:{course.id}, role:{current_role}" ) subscription = Subscription(user_id=user.id, course_id=course.id, role=current_role) session.add(subscription) courses = {} for subscription in user.courses: if not subscription.course.course_code in courses: courses[subscription.course.course_code] = {} courses[subscription.course.course_code][subscription.role] = 1 model = { "kind": "user", "id": user.id, "name": user.name, "org_id": user.org_id, "current_course": current_course, "current_role": current_role, "courses": courses, } return model
def test_course(db, course_strange): assert course_strange.course_code == "Strange" assert course_strange.course_title == "Damnation Alley" with pytest.raises(TypeError): found_by_pk = Course.find_by_pk() with pytest.raises(TypeError): found_by_pk = Course.find_by_pk(db) with pytest.raises(ValueError): found_by_pk = Course.find_by_pk(db, None) with pytest.raises(TypeError): found_by_pk = Course.find_by_pk(db, "abc") found_by_pk = Course.find_by_pk(db, course_strange.id) assert found_by_pk.id == course_strange.id found_by_pk = Course.find_by_pk(db, course_strange.id + 10) assert found_by_pk is None with pytest.raises(TypeError): found_by_code = Course.find_by_code(db, course_strange.course_code) with pytest.raises(ValueError): found_by_code = Course.find_by_code(db, None, course_strange.org_id) with pytest.raises(ValueError): found_by_code = Course.find_by_code(db, course_strange.course_code, None) found_by_code = Course.find_by_code(db, course_strange.course_code, course_strange.org_id) assert found_by_code.course_code == course_strange.course_code assert str(found_by_code) == f"Course/{course_strange.course_code} {course_strange.course_title}" # in real code, org_id is probably a string, so lets confirm that works found_by_code = Course.find_by_code(db, course_strange.course_code, "1") assert found_by_code.course_code == course_strange.course_code found_by_code = Course.find_by_code(db, "SANE", course_strange.org_id) assert found_by_code is None found_by_code = Course.find_by_code(db, course_strange.course_code, course_strange.org_id + 10) assert found_by_code is None
def get(self): models = [] [course_code, assignment_code, user_id] = self.get_params(["course_id", "assignment_id", "user_id"]) if not (course_code and assignment_code): note = "Collections call requires both a course code and an assignment code" self.log.info(note) self.finish({"success": False, "note": note}) return # Who is my user? this_user = self.nbex_user self.log.debug(f"User: {this_user.get('name')}") # For what course do we want to see the assignments? self.log.debug(f"Course: {course_code}") # Is our user subscribed to this course? if course_code not 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 # Find the course being referred to with scoped_session() as session: course = Course.find_by_code(db=session, code=course_code, org_id=this_user["org_id"], log=self.log) if not course: note = f"Course {course_code} does not exist" self.log.info(note) self.finish({"success": False, "note": note}) return assignment = AssignmentModel.find_by_code( db=session, course_id=course.id, log=self.log, code=assignment_code, action=AssignmentActions.submitted.value, ) if not assignment: note = f"Assignment {assignment_code} does not exist" self.log.info(note) self.finish({"success": True, "value": []}) return self.log.debug(f"Assignment: {assignment}") filters = [ Action.assignment_id == assignment.id, Action.action == AssignmentActions.submitted.value, ] if user_id: student = session.query(User).filter( User.name == user_id).first() filters.append(Action.user_id == student.id) actions = session.query(Action).filter(*filters) for action in actions: models.append({ "student_id": action.user.name, "full_name": action.user.full_name, "assignment_id": assignment.assignment_code, "course_id": assignment.course.course_code, "status": action.action.value, # currently called 'action' in our db "path": action.location, # 'name' in db, 'notebook_id' id nbgrader "notebooks": [{ "notebook_id": x.name } for x in assignment.notebooks], "timestamp": action.timestamp.strftime("%Y-%m-%d %H:%M:%S.%f %Z"), }) self.log.debug(f"Assignments: {models}") self.finish({"success": True, "value": models})
def get(self): [course_code, assignment_code, path] = self.get_params(["course_id", "assignment_id", "path"]) if not (course_code and assignment_code and path): note = "Collection call requires a course code, an assignment code, and a path" self.log.info(note) self.finish({"success": False, "note": note}) return # Who is my user? this_user = self.nbex_user self.log.debug(f"User: {this_user.get('name')}") # For what course do we want to see the assignments? self.log.debug(f"Course: {course_code}") # Is our user subscribed to this course? if course_code not in this_user["courses"]: note = f"User not subscribed to course {course_code}" self.log.info(note) self.finish({"success": False, "note": note}) return self.log.info(f"user: {this_user}") 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 # Find the course being referred to with scoped_session() as session: course = Course.find_by_code(db=session, code=course_code, org_id=this_user["org_id"], log=self.log) if not course: note = f"Course {course_code} does not exist" self.log.info(note) self.finish({"success": False, "note": note}) return # We need to key off the assignment, but we're actually looking # for the action with a action and a specific path assignments = AssignmentModel.find_for_course( db=session, course_id=course.id, log=self.log, action=AssignmentActions.submitted.value, path=path, ) self.set_header("Content-Type", "application/gzip") # I do not want to assume there will just be one. for assignment in assignments: self.log.debug(f"Assignment: {assignment}") try: with open(path, "r+b") as handle: data = handle.read() except Exception as e: # TODO: exception handling self.log.warning( f"Error: {e}") # TODO: improve error message # error 500?? raise Exception self.log.info( f"Adding action {AssignmentActions.collected.value} for user {this_user['id']} against assignment {assignment.id}" # noqa: E501 ) action = Action( user_id=this_user["id"], assignment_id=assignment.id, action=AssignmentActions.collected, location=path, ) session.add(action) self.finish(data) return
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 nbex_user(self): hub_user = self.get_current_user() hub_username = hub_user.get("name") full_name = hub_user.get("full_name") current_course = hub_user.get("course_id") current_role = hub_user.get("course_role") course_title = hub_user.get("course_title", "no_title") org_id = hub_user.get("org_id", 1) # Raising an error appears to have no detrimental affect when running. if not (current_course and current_role): note = f"Both current_course ('{current_course}') and current_role ('{current_role}') must have values. User was '{hub_username}'" # noqa: E501 self.log.info(note) raise ValueError(note) self.org_id = org_id with scoped_session() as session: user = User.find_by_name(db=session, name=hub_username, log=self.log) if user is None: self.log.debug( f"New user details: name:{hub_username}, org_id:{org_id}") user = User(name=hub_username, org_id=org_id) session.add(user) if user.full_name != full_name: user.full_name = full_name course = Course.find_by_code(db=session, code=current_course, org_id=org_id, log=self.log) if course is None: self.log.debug( f"New course details: code:{current_course}, org_id:{org_id}" ) course = Course(org_id=org_id, course_code=current_course) if course_title: self.log.debug(f"Adding title {course_title}") course.course_title = course_title session.add(course) # Check to see if we have a subscription (for this course) self.log.debug( f"Looking for subscription for: user:{user.id}, course:{course.id}, role:{current_role}" ) subscription = Subscription.find_by_set(db=session, user_id=user.id, course_id=course.id, role=current_role) if subscription is None: self.log.debug( f"New subscription details: user:{user.id}, course:{course.id}, role:{current_role}" ) subscription = Subscription(user_id=user.id, course_id=course.id, role=current_role) session.add(subscription) courses = {} for subscription in user.courses: if subscription.course.course_code not in courses: courses[subscription.course.course_code] = {} courses[subscription.course.course_code][subscription.role] = 1 model = { "kind": "user", "id": user.id, "name": user.name, "org_id": user.org_id, "current_course": current_course, "current_role": current_role, "courses": courses, } return model
def post(self): 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 files in your submission and try again." 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 /submission with arguments: course {course_code} and assignment {assignment_code}" ) if not (course_code and assignment_code): note = f"Submission call requires both 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 # 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 = Assignment.find_by_code(db=session, code=assignment_code, course_id=course.id) if assignment is None: note = f"User not fetched assignment {assignment_code}" self.log.info(note) self.finish({"success": False, "note": note}) return # storage is dynamically in $path/submitted/$course_code/$assignment_code/$username/<timestamp>/ # Note - this means that a user can submit multiple times, and we have all copies release_file = "/".join([ self.base_storage_location, str(this_user["org_id"]), AssignmentActions.submitted.value, course_code, assignment_code, this_user["name"], 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 web.HTTPError(418) # 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 need this, but it's good to double-check if os.path.getsize(release_file) > self.max_buffer_size: os.remove(release_file) note = "File upload oversize, and rejected. Please reduce the files in your submission and try again." self.log.info(note) self.finish({"success": False, "note": note}) return # now commit the assignment, and get it back to find the id assignment = Assignment.find_by_code(db=session, code=assignment_code, course_id=course.id) # Record the action. # Note we record the path to the files. self.log.info( f"Adding action {AssignmentActions.submitted.value} for user {this_user['id']} against assignment {assignment.id}" ) action = Action( user_id=this_user["id"], assignment_id=assignment.id, action=AssignmentActions.submitted, location=release_file, ) session.add(action) self.finish({"success": True, "note": "Submitted"})