def copy_files(self): if self.coursedir.student_id_exclude: exclude_students = set( self.coursedir.student_id_exclude.split(',')) else: exclude_students = set() html_files = glob.glob(os.path.join(self.src_path, "*.html")) for html_file in html_files: if 'hashcode' in html_file: self.log.debug('Skipping hashcode info') continue regexp = re.escape(os.path.sep).join([ self.coursedir.format_path(self.coursedir.feedback_directory, "(?P<student_id>.*)", self.coursedir.assignment_id, escape=True), "(?P<notebook_id>.*).html" ]) m = re.match(regexp, html_file) if m is None: msg = "Could not match '%s' with regexp '%s'" % (html_file, regexp) self.log.error(msg) continue gd = m.groupdict() student_id = gd['student_id'] notebook_id = gd['notebook_id'] if student_id in exclude_students: self.log.debug("Skipping student '{}'".format(student_id)) continue feedback_dir = os.path.split(html_file)[0] submission_dir = self.coursedir.format_path( self.coursedir.submitted_directory, student_id, self.coursedir.assignment_id) timestamp = open(os.path.join(feedback_dir, 'timestamp.txt')).read() nbfile = os.path.join(submission_dir, "{}.ipynb".format(notebook_id)) unique_key = make_unique_key(self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, student_id, timestamp) self.log.debug("Unique key is: {}".format(unique_key)) checksum = notebook_hash(nbfile, unique_key) dest = os.path.join(self.dest_path, "{}.html".format(checksum)) self.log.info( "Releasing feedback for student '{}' on assignment '{}/{}/{}' ({})" .format(student_id, self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, timestamp)) shutil.copy(html_file, dest) self.log.info("Feedback released to: {}".format(dest))
def test_feedback_post_authenticated_with_correct_params_student_submitter( app, clear_database): assignment_id = "assign_a" course_id = "course_2" notebook = "notebook" student = user_kiz_student timestamp = datetime.datetime.utcnow().isoformat(" ") checksum = notebook_hash( feedback_filename, make_unique_key(course_id, assignment_id, notebook, student["name"], timestamp), ) # XXX: Doing this in a separate function doesn't work for some reason (Exchange doesn't get called) kwargs = {"data": {"notebooks": [notebook]}} with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_instructor): r = yield async_requests.post( app.url + f"/assignment?course_id={course_id}&assignment_id={assignment_id}", files=files, **kwargs, ) with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.get( app.url + f"/assignment?course_id={course_id}&assignment_id={assignment_id}") with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.post( app.url + f"/submission?course_id={course_id}&assignment_id={assignment_id}", files=files, ) url = (f"/feedback?assignment_id={assignment_id}" f"&course_id={course_id}" f"¬ebook={notebook}" f"&student={student['name']}" f"×tamp={timestamp}" f"&checksum={checksum}") with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.post(app.url + url, files=feedbacks) response_data = r.json() assert response_data["success"] is False assert response_data[ "note"] == f"User not an instructor to course {course_id}"
def copy_files(self): """ Overried copy_files and add personalized-feedback generation """ if self.coursedir.student_id_exclude: exclude_students = set( self.coursedir.student_id_exclude.split(",")) else: exclude_students = set() html_files = glob.glob(os.path.join(self.src_path, "*.html")) for html_file in html_files: if "hashcode" in html_file: self.log.debug("Skipping hashcode info") continue regexp = re.escape(os.path.sep).join([ self.coursedir.format_path( self.coursedir.feedback_directory, "(?P<student_id>.*)", self.coursedir.assignment_id, escape=True, ), "(?P<notebook_id>.*).html", ]) m = re.match(regexp, html_file) if m is None: msg = "Could not match '%s' with regexp '%s'" % (html_file, regexp) self.log.error(msg) continue gd = m.groupdict() student_id = gd["student_id"] notebook_id = gd["notebook_id"] if student_id in exclude_students: self.log.debug("Skipping student '{}'".format(student_id)) continue feedback_dir = os.path.split(html_file)[0] submission_dir = self.coursedir.format_path( self.coursedir.submitted_directory, student_id, self.coursedir.assignment_id, ) if self.personalized_feedback: dest = os.path.join(self.dest_path, student_id, self.coursedir.assignment_id) # u+rwx, g+wx, o+wx self.ensure_directory( dest, (S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IXOTH | S_IROTH | ((S_IRGRP | S_IWGRP | S_ISGID) if self.coursedir.groupshared else 0)), ) dest = os.path.join(dest, notebook_id + ".html") self.log.info( "Releasing feedback for student '{}' on assignment '{}/{}/{}' " .format( student_id, self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, )) else: timestamp = open(os.path.join(feedback_dir, "timestamp.txt")).read() nbfile = os.path.join(submission_dir, "{}.ipynb".format(notebook_id)) unique_key = make_unique_key( self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, student_id, timestamp, ) self.log.debug("Unique key is: {}".format(unique_key)) checksum = notebook_hash(nbfile, unique_key) dest = os.path.join(self.dest_path, "{}.html".format(checksum)) self.log.info( "Releasing feedback for student '{}' on assignment '{}/{}/{}' ({})" .format( student_id, self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, timestamp, )) shutil.copy(html_file, dest) self.log.info("Feedback released to: {}".format(dest))
def parse_assignments(self): if self.coursedir.student_id: courses = self.authenticator.get_student_courses( self.coursedir.student_id) else: courses = None assignments = [] for path in self.assignments: info = self.parse_assignment(path) if courses is not None and info['course_id'] not in courses: continue if self.path_includes_course: assignment_dir = os.path.join(self.assignment_dir, info['course_id'], info['assignment_id']) else: assignment_dir = os.path.join(self.assignment_dir, info['assignment_id']) if self.inbound or self.cached: info['status'] = 'submitted' info['path'] = path elif os.path.exists(assignment_dir): info['status'] = 'fetched' info['path'] = os.path.abspath(assignment_dir) else: info['status'] = 'released' info['path'] = path if self.remove: info['status'] = 'removed' notebooks = sorted(glob.glob(os.path.join(info['path'], '*.ipynb'))) if not notebooks: self.log.warning("No notebooks found in {}".format( info['path'])) info['notebooks'] = [] for notebook in notebooks: nb_info = { 'notebook_id': os.path.splitext(os.path.split(notebook)[1])[0], 'path': os.path.abspath(notebook) } if info['status'] != 'submitted': info['notebooks'].append(nb_info) continue nb_info['has_local_feedback'] = False nb_info['has_exchange_feedback'] = False nb_info['local_feedback_path'] = None nb_info['feedback_updated'] = False # Check whether feedback has been fetched already. local_feedback_dir = os.path.join(assignment_dir, 'feedback', info['timestamp']) local_feedback_path = os.path.join( local_feedback_dir, '{0}.html'.format(nb_info['notebook_id'])) has_local_feedback = os.path.isfile(local_feedback_path) if has_local_feedback: local_feedback_checksum = _checksum(local_feedback_path) else: local_feedback_checksum = None # Also look to see if there is feedback available to fetch. unique_key = make_unique_key(info['course_id'], info['assignment_id'], nb_info['notebook_id'], info['student_id'], info['timestamp']) self.log.debug("Unique key is: {}".format(unique_key)) nb_hash = notebook_hash(notebook, unique_key) exchange_feedback_path = os.path.join( self.root, info['course_id'], 'feedback', '{0}.html'.format(nb_hash)) has_exchange_feedback = os.path.isfile(exchange_feedback_path) if not has_exchange_feedback: # Try looking for legacy feedback. nb_hash = notebook_hash(notebook) exchange_feedback_path = os.path.join( self.root, info['course_id'], 'feedback', '{0}.html'.format(nb_hash)) has_exchange_feedback = os.path.isfile( exchange_feedback_path) if has_exchange_feedback: exchange_feedback_checksum = _checksum( exchange_feedback_path) else: exchange_feedback_checksum = None nb_info['has_local_feedback'] = has_local_feedback nb_info['has_exchange_feedback'] = has_exchange_feedback if has_local_feedback: nb_info['local_feedback_path'] = local_feedback_path if has_local_feedback and has_exchange_feedback: nb_info[ 'feedback_updated'] = exchange_feedback_checksum != local_feedback_checksum info['notebooks'].append(nb_info) if info['status'] == 'submitted': if info['notebooks']: # has_local_feedback = all([nb['has_local_feedback'] for nb in info['notebooks']]) # has_exchange_feedback = all([nb['has_exchange_feedback'] for nb in info['notebooks']]) has_local_feedback = any( [nb['has_local_feedback'] for nb in info['notebooks']]) has_exchange_feedback = any([ nb['has_exchange_feedback'] for nb in info['notebooks'] ]) feedback_updated = any( [nb['feedback_updated'] for nb in info['notebooks']]) else: has_local_feedback = False has_exchange_feedback = False feedback_updated = False info['has_local_feedback'] = has_local_feedback info['has_exchange_feedback'] = has_exchange_feedback info['feedback_updated'] = feedback_updated if has_local_feedback: info['local_feedback_path'] = os.path.join( assignment_dir, 'feedback', info['timestamp']) else: info['local_feedback_path'] = None assignments.append(info) # partition the assignments into groups for course/student/assignment if self.inbound or self.cached: _get_key = lambda info: (info['course_id'], info['student_id'], info['assignment_id']) _match_key = lambda info, key: (info['course_id'] == key[ 0] and info['student_id'] == key[1] and info['assignment_id'] == key[2]) assignment_keys = sorted( list(set([_get_key(info) for info in assignments]))) assignment_submissions = [] for key in assignment_keys: submissions = [x for x in assignments if _match_key(x, key)] submissions = sorted(submissions, key=lambda x: x['timestamp']) info = { 'course_id': key[0], 'student_id': key[1], 'assignment_id': key[2], 'status': submissions[0]['status'], 'submissions': submissions } assignment_submissions.append(info) assignments = assignment_submissions return assignments
def parse_assignments(self): if self.coursedir.student_id: courses = self.authenticator.get_student_courses( self.coursedir.student_id) else: courses = None assignments = [] released_assignments = [] for path in self.assignments: info = self.parse_assignment(path) # if grader and the assignment is already known as released assignment, skip looking if (self.personalized_outbound and self.grader and info["assignment_id"] in released_assignments): self.log.debug( "Grader role and personalized-outbound are enabled, and the assignment is known to be released already" ) continue if courses is not None and info["course_id"] not in courses: continue if self.path_includes_course: assignment_dir = os.path.join(self.assignment_dir, info["course_id"], info["assignment_id"]) else: assignment_dir = os.path.join(self.assignment_dir, info["assignment_id"]) if self.inbound or self.cached: info["status"] = "submitted" info["path"] = path elif os.path.exists(assignment_dir): info["status"] = "fetched" info["path"] = os.path.abspath(assignment_dir) else: info["status"] = "released" info["path"] = path # update released assignments if self.personalized_outbound and self.grader: released_assignments.append(info["assignment_id"]) if self.remove: info["status"] = "removed" notebooks = sorted(glob.glob(os.path.join(info["path"], "*.ipynb"))) if not notebooks: self.log.warning("No notebooks found in {}".format( info["path"])) info["notebooks"] = [] for notebook in notebooks: nb_info = { "notebook_id": os.path.splitext(os.path.split(notebook)[1])[0], "path": os.path.abspath(notebook), } if info["status"] != "submitted": info["notebooks"].append(nb_info) continue nb_info["has_local_feedback"] = False nb_info["has_exchange_feedback"] = False nb_info["local_feedback_path"] = None nb_info["feedback_updated"] = False # Check whether feedback has been fetched already. local_feedback_dir = os.path.join(assignment_dir, "feedback", info["timestamp"]) local_feedback_path = os.path.join( local_feedback_dir, "{0}.html".format(nb_info["notebook_id"])) has_local_feedback = os.path.isfile(local_feedback_path) if has_local_feedback: local_feedback_checksum = _checksum(local_feedback_path) else: local_feedback_checksum = None # Also look to see if there is feedback available to fetch. # and check whether personalized-feedback is enabled if self.personalized_feedback: exchange_feedback_path = os.path.join( self.root, info["course_id"], self.feedback_directory, info["student_id"], info["assignment_id"], "{0}.html".format(nb_info["notebook_id"]), ) else: unique_key = make_unique_key( info["course_id"], info["assignment_id"], nb_info["notebook_id"], info["student_id"], info["timestamp"], ) self.log.debug("Unique key is: {}".format(unique_key)) nb_hash = notebook_hash(notebook, unique_key) exchange_feedback_path = os.path.join( self.root, info["course_id"], self.feedback_directory, "{0}.html".format(nb_hash), ) has_exchange_feedback = os.path.isfile(exchange_feedback_path) if not has_exchange_feedback: # Try looking for legacy feedback. nb_hash = notebook_hash(notebook) exchange_feedback_path = os.path.join( self.root, info["course_id"], "feedback", "{0}.html".format(nb_hash), ) has_exchange_feedback = os.path.isfile( exchange_feedback_path) if has_exchange_feedback: exchange_feedback_checksum = _checksum( exchange_feedback_path) else: exchange_feedback_checksum = None nb_info["has_local_feedback"] = has_local_feedback nb_info["has_exchange_feedback"] = has_exchange_feedback if has_local_feedback: nb_info["local_feedback_path"] = local_feedback_path if has_local_feedback and has_exchange_feedback: nb_info["feedback_updated"] = (exchange_feedback_checksum != local_feedback_checksum) info["notebooks"].append(nb_info) if info["status"] == "submitted": if info["notebooks"]: has_local_feedback = all( [nb["has_local_feedback"] for nb in info["notebooks"]]) has_exchange_feedback = all([ nb["has_exchange_feedback"] for nb in info["notebooks"] ]) feedback_updated = any( [nb["feedback_updated"] for nb in info["notebooks"]]) else: has_local_feedback = False has_exchange_feedback = False feedback_updated = False info["has_local_feedback"] = has_local_feedback info["has_exchange_feedback"] = has_exchange_feedback info["feedback_updated"] = feedback_updated if has_local_feedback: info["local_feedback_path"] = os.path.join( assignment_dir, "feedback", info["timestamp"]) else: info["local_feedback_path"] = None assignments.append(info) # partition the assignments into groups for course/student/assignment if self.inbound or self.cached: assignment_keys = sorted( list(set([_get_key(info) for info in assignments]))) assignment_submissions = [] for key in assignment_keys: submissions = [x for x in assignments if _match_key(x, key)] submissions = sorted(submissions, key=lambda x: x["timestamp"]) info = { "course_id": key[0], "student_id": key[1], "assignment_id": key[2], "status": submissions[0]["status"], "submissions": submissions, } assignment_submissions.append(info) assignments = assignment_submissions return assignments
def test_feedback_get_correct_assignment_across_courses(app, clear_database): pass # set up the situation assignment_id = "assign_a" course_1 = "course_1" course_2 = "course_2" notebook = "notebook" student = user_kiz_student timestamp = datetime.datetime.utcnow().isoformat(" ") checksum = notebook_hash( feedback_filename, make_unique_key(course_2, assignment_id, notebook, student["name"], timestamp), ) # XXX: Doing this in a separate function doesn't work for some reason (Exchange doesn't get called) kwargs = {"data": {"notebooks": [notebook]}} # release assignment on course 1 & 2 with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_instructor): r = yield async_requests.post( app.url + f"/assignment?course_id={course_1}&assignment_id={assignment_id}", files=files, **kwargs, ) with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_instructor): r = yield async_requests.post( app.url + f"/assignment?course_id={course_2}&assignment_id={assignment_id}", files=files, **kwargs, ) # Now fetch & submit on course 2 as student with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.get( app.url + f"/assignment?course_id={course_2}&assignment_id={assignment_id}") with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.post( app.url + f"/submission?course_id={course_2}&assignment_id={assignment_id}", files=files, ) # Instructor releases for course 2 url = (f"/feedback?assignment_id={assignment_id}" f"&course_id={course_2}" f"¬ebook={notebook}" f"&student={student['name']}" f"×tamp={timestamp}" f"&checksum={checksum}") with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_instructor): r = yield async_requests.post(app.url + url, files=feedbacks) url = f"/feedback?assignment_id={assignment_id}&course_id={course_1}" with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.get(app.url + url) assert r.status_code == 404 url = f"/feedback?assignment_id={assignment_id}&course_id={course_2}" with patch.object(BaseHandler, "get_current_user", return_value=user_kiz_student): r = yield async_requests.get(app.url + url) assert r.status_code == 200 response_data = r.json() assert response_data["success"] is True assert len(response_data["feedback"]) >= 1 assert response_data["feedback"][0].get( "content") == feedback_base64.decode("utf-8")
def init_src(self): if self.coursedir.course_id == "": self.fail("No course id specified. Re-run with --course flag.") self.course_path = os.path.join(self.root, self.coursedir.course_id) self.outbound_path = os.path.join(self.course_path, self.feedback_directory) self.src_path = os.path.join(self.outbound_path) self.cache_path = os.path.join(self.cache, self.coursedir.course_id) if self.coursedir.student_id != "*": # An explicit student id has been specified on the command line; we use it as student_id if "*" in self.coursedir.student_id or "+" in self.coursedir.student_id: self.fail( "The student ID should contain no '*' nor '+'; got {}". format(self.coursedir.student_id)) student_id = self.coursedir.student_id else: student_id = get_username() if not os.path.isdir(self.src_path): self._assignment_not_found(self.src_path, os.path.join(self.outbound_path, "*")) if not check_mode(self.src_path, execute=True): self.fail( "You don't have execute permissions for the directory: {}". format(self.src_path)) assignment_id = (self.coursedir.assignment_id if self.coursedir.assignment_id else "*") pattern = os.path.join(self.cache_path, "*+{}+*".format(assignment_id)) self.log.debug( "Looking for submissions with pattern: {}".format(pattern)) self.feedback_files = [] submissions = [os.path.split(x)[-1] for x in glob.glob(pattern)] for submission in submissions: _, assignment_id, timestamp = submission.split("/")[-1].split("+") self.log.debug( "Looking for feedback for '{}/{}' submitted at {}".format( self.coursedir.course_id, assignment_id, timestamp)) pattern = os.path.join(self.cache_path, submission, "*.ipynb") notebooks = glob.glob(pattern) for notebook in notebooks: notebook_id = os.path.splitext(os.path.split(notebook)[-1])[0] # Check if personalized_feedback is used if self.personalized_feedback: feedbackpath = os.path.join( self.outbound_path, student_id, assignment_id, "{}.html".format(notebook_id), ) self.log.debug("Feedback file: ", feedbackpath) if os.path.exists(feedbackpath): self.feedback_files.append( (notebook_id, timestamp, feedbackpath)) self.log.info( "Found feedback for '{}/{}/{}' submitted at {}". format( self.coursedir.course_id, assignment_id, notebook_id, timestamp, )) continue # If we reached here, then there's no feedback available self.log.warning( "Could not find feedback for '{}/{}/{}' submitted at {}" .format( self.coursedir.course_id, assignment_id, notebook_id, timestamp, )) else: unique_key = make_unique_key( self.coursedir.course_id, assignment_id, notebook_id, student_id, timestamp, ) # Look for the feedback using new-style of feedback self.log.debug("Unique key is: {}".format(unique_key)) nb_hash = notebook_hash(notebook, unique_key) feedbackpath = os.path.join(self.outbound_path, "{0}.html".format(nb_hash)) if os.path.exists(feedbackpath): self.feedback_files.append( (notebook_id, timestamp, feedbackpath)) self.log.info( "Found feedback for '{}/{}/{}' submitted at {}". format( self.coursedir.course_id, assignment_id, notebook_id, timestamp, )) continue # If it doesn't exist, try the legacy hashing nb_hash = notebook_hash(notebook) feedbackpath = os.path.join(self.outbound_path, "{0}.html".format(nb_hash)) if os.path.exists(feedbackpath): self.feedback_files.append( (notebook_id, timestamp, feedbackpath)) self.log.warning( "Found legacy feedback for '{}/{}/{}' submitted at {}" .format( self.coursedir.course_id, assignment_id, notebook_id, timestamp, )) continue # If we reached here, then there's no feedback available self.log.warning( "Could not find feedback for '{}/{}/{}' submitted at {}" .format( self.coursedir.course_id, assignment_id, notebook_id, timestamp, ))
def copy_files(self): if self.coursedir.student_id_exclude: exclude_students = set( self.coursedir.student_id_exclude.split(",")) else: exclude_students = set() html_files = glob.glob(os.path.join(self.src_path, "*.html")) for html_file in html_files: regexp = re.escape(os.path.sep).join([ os.path.normpath( self.coursedir.format_path( self.coursedir.feedback_directory, "(?P<student_id>.*)", self.coursedir.assignment_id, escape=True, )), "(?P<notebook_id>.*).html", ]) m = re.match(regexp, html_file) if m is None: msg = "Could not match '%s' with regexp '%s'" % (html_file, regexp) self.log.error(msg) continue gd = m.groupdict() student_id = gd["student_id"] notebook_id = gd["notebook_id"] if student_id in exclude_students: self.log.debug("Skipping student '{}'".format(student_id)) continue feedback_dir = os.path.split(html_file)[0] submission_dir = self.coursedir.format_path( self.coursedir.submitted_directory, student_id, self.coursedir.assignment_id, ) timestamp = open(os.path.join(feedback_dir, "timestamp.txt")).read().strip() nbfile = os.path.join(submission_dir, "{}.ipynb".format(notebook_id)) unique_key = make_unique_key( self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, student_id, timestamp, ) self.log.debug("Unique key is: {}".format(unique_key)) checksum = notebook_hash(nbfile, unique_key) timestamp = parser.parse(timestamp).strftime( self.timestamp_format).strip() self.log.info( "Releasing feedback for student '{}' on assignment '{}/{}/{}' ({})" .format( student_id, self.coursedir.course_id, self.coursedir.assignment_id, notebook_id, timestamp, )) self.upload( html_file, self.coursedir.assignment_id, student_id, notebook_id, timestamp, checksum, )
def test_release_feedback_fetch_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.feedback_directory = str( tmpdir.mkdir("feedback_test").realpath()) plugin_config.CourseDirectory.submitted_directory = str( tmpdir.mkdir("submitted_test").realpath()) plugin_config.CourseDirectory.assignment_id = assignment_id os.makedirs( os.path.join(plugin_config.CourseDirectory.feedback_directory, student_id, assignment_id), exist_ok=True, ) os.makedirs( os.path.join(plugin_config.CourseDirectory.submitted_directory, student_id, assignment_id), exist_ok=True, ) feedback_filename_uploaded = os.path.join( plugin_config.CourseDirectory.feedback_directory, student_id, assignment_id, "feedback.html", ) copyfile(feedback1_filename, feedback_filename_uploaded) copyfile( notebook1_filename, os.path.join( plugin_config.CourseDirectory.submitted_directory, student_id, assignment_id, "feedback.ipynb", ), ) with open( os.path.join( plugin_config.CourseDirectory.feedback_directory, student_id, assignment_id, "timestamp.txt", ), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") unique_key = make_unique_key("no_course", assignment_id, "feedback", student_id, "2020-01-01 00:00:00.0 UTC") checksum = notebook_hash( os.path.join( plugin_config.CourseDirectory.submitted_directory, student_id, assignment_id, "feedback.ipynb", ), unique_key, ) plugin = ExchangeReleaseFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ("feedback?course_id=no_course" f"&assignment_id={assignment_id}" "¬ebook=feedback" f"&student={student_id}" "×tamp=2020-01-01+00%3A00%3A00.000000+UTC" "&checksum=" + checksum) assert kwargs.get("method").lower() == "post" assert "feedback" in kwargs.get("files") assert ("feedback.html", open(feedback_filename_uploaded).read() ) == kwargs.get("files").get("feedback") return type( "Request", (object, ), { "status_code": 200, "json": (lambda: { "success": True }) }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start()
def test_release_feedback_fetch_several_normal(plugin_config, tmpdir): # set up the submitted & feeback directories feedback_directory = str(tmpdir.mkdir("feedback_test").realpath()) submitted_directory = str(tmpdir.mkdir("submitted_test").realpath()) plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.feedback_directory = feedback_directory plugin_config.CourseDirectory.submitted_directory = submitted_directory plugin_config.CourseDirectory.assignment_id = assignment_id os.makedirs(os.path.join(feedback_directory, student_id, assignment_id), exist_ok=True) os.makedirs(os.path.join(submitted_directory, student_id, assignment_id), exist_ok=True) # Scenario: # two pieces of feedback for two notebooks, for the same submission # * submitted_directory is where the student .ipynb file was collected into # * feedback_directory is where the generated .html feeback was written to feedback1_filename_uploaded = os.path.join(feedback_directory, student_id, assignment_id, "feedback1.html") copyfile(feedback1_filename, feedback1_filename_uploaded) copyfile( notebook1_filename, os.path.join(submitted_directory, student_id, assignment_id, "feedback1.ipynb"), ) feedback2_filename_uploaded = os.path.join(feedback_directory, student_id, assignment_id, "feedback2.html") copyfile(feedback2_filename, feedback2_filename_uploaded) copyfile( notebook2_filename, os.path.join(submitted_directory, student_id, assignment_id, "feedback2.ipynb"), ) # ...... don't forget the timestamp for the submission with open( os.path.join(feedback_directory, student_id, assignment_id, "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:01:00.0 UTC") # this makes the unique key & checksums for the submission unique_key1 = make_unique_key("no_course", assignment_id, "feedback1", student_id, "2020-01-01 00:01:00.0 UTC") checksum1 = notebook_hash( os.path.join(submitted_directory, student_id, assignment_id, "feedback1.ipynb"), unique_key1, ) unique_key2 = make_unique_key("no_course", assignment_id, "feedback2", student_id, "2020-01-01 00:01:00.0 UTC") checksum2 = notebook_hash( os.path.join(submitted_directory, student_id, assignment_id, "feedback2.ipynb"), unique_key2, ) plugin = ExchangeReleaseFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) seen_feedback1 = False seen_feedback2 = False def api_request(*args, **kwargs): nonlocal seen_feedback1, seen_feedback2 if "feedback1" in args[0]: assert seen_feedback1 is False seen_feedback1 = True assert args[0] == ("feedback?course_id=no_course" f"&assignment_id={assignment_id}" "¬ebook=feedback1" f"&student={student_id}" "×tamp=2020-01-01+00%3A01%3A00.000000+UTC" "&checksum=" + checksum1) assert kwargs.get("method").lower() == "post" assert "feedback" in kwargs.get("files") assert ( "feedback.html", open(feedback1_filename_uploaded).read(), ) == kwargs.get("files").get("feedback") elif "feedback2" in args[0]: assert seen_feedback2 is False seen_feedback2 = True assert args[0] == ("feedback?course_id=no_course" f"&assignment_id={assignment_id}" "¬ebook=feedback2" f"&student={student_id}" "×tamp=2020-01-01+00%3A01%3A00.000000+UTC" "&checksum=" + checksum2) assert kwargs.get("method").lower() == "post" assert "feedback" in kwargs.get("files") assert ( "feedback.html", open(feedback2_filename_uploaded).read(), ) == kwargs.get("files").get("feedback") else: assert False return type( "Request", (object, ), { "status_code": 200, "json": (lambda: { "success": True }) }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start() assert seen_feedback1 and seen_feedback2