def _new_exchange_object(self, cls, course_id, assignment_id, student_id): assert issubclass(cls, Exchange) cls.cache = str(self.cache_dir) coursedir = CourseDirectory() coursedir.root = str(self.course_dir) coursedir.course_id = course_id coursedir.assignment_id = assignment_id obj = cls(coursedir=coursedir) obj.username = student_id return obj
def test_list_acknowledges_multi_marker_feature_flag(plugin_config, tmpdir, monkeypatch): plugin_config.CourseDirectory.course_id = "no_course" plugin_config.ExchangeList.inbound = True plugin = ExchangeList(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ("assignments?course_id=no_course") assert "method" not in kwargs or kwargs.get("method").lower() == "get" return type( "Request", (object,), { "status_code": 200, "json": ( lambda: { "success": True, "value": [ { "assignment_id": "assign_1_1", "student_id": 1, "course_id": "no_course", "status": "released", "path": "", "notebooks": [ { "notebook_id": "assignment-0.6", "has_exchange_feedback": False, "feedback_updated": False, "feedback_timestamp": None, } ], "timestamp": "2020-01-01 00:00:00.0 00:00", } ], } ), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start() assert plugin.coursedir.submitted_directory == "collected" monkeypatch.setenv("NAAS_FEATURE_MULTI_MARKERS", "True") plugin = ExchangeList(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start() assert plugin.coursedir.submitted_directory == "submitted"
def test_fetch_assignment_methods_init_dest(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = course_id plugin_config.CourseDirectory.assignment_id = ass_1_2 plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) # we're good if the dir doesn't exist plugin.init_dest() assert re.search(rf"{ass_1_2}$", plugin.dest_path) assert os.path.isdir(plugin.dest_path) # we're good if the dir exists and is empty plugin.init_dest() assert re.search(rf"{ass_1_2}$", plugin.dest_path) # we're good if the dir exists, and has something OTHER than an ipynb file in it with open(f"{plugin.dest_path}/random.txt", "w") as fp: fp.write("Hello world") plugin.init_dest() assert re.search(rf"{ass_1_2}$", plugin.dest_path) # FAILS if the dir exists AND there's an ipynb file in it with open(f"{plugin.dest_path}/random.ipynb", "w") as fp: fp.write("Hello world") with pytest.raises(ExchangeError) as e_info: plugin.init_dest() assert ( f"You already have notebook documents in directory: {plugin_config.CourseDirectory.assignment_id}. Please remove them before fetching again" # noqa: E501 in str(e_info.value)) shutil.rmtree(plugin.dest_path)
def submit_assignment(self, course_id, assignment_id): with self.get_assignment_dir_config() as config: try: config = self.load_config() config.CourseDirectory.course_id = course_id config.CourseDirectory.assignment_id = assignment_id coursedir = CourseDirectory(config=config) authenticator = Authenticator(config=config) submit = ExchangeFactory(config=config).Submit( coursedir=coursedir, authenticator=authenticator, config=config) retval = submit.start() hashcode = 'Exchange not set up for hashcode' timestamp = 'Exchange not set up for timestamp' if retval and len(retval) == 2: hashcode, timestamp = retval except: self.log.error(traceback.format_exc()) retvalue = {"success": False, "value": traceback.format_exc()} else: retvalue = { "success": True, "hashcode": hashcode, "timestamp": timestamp } self.log.info(retvalue) return retvalue
def check_for_noauth_jupyterhub_formgraders(self, config): try: get_jupyterhub_user() except JupyterhubEnvironmentError: # Not running on JupyterHub. raise gen.Return([]) # We are running on JupyterHub, so maybe there's a formgrader # service. Check if we have a course id and if so guess the path to the # formgrader. coursedir = CourseDirectory(config=config) if not coursedir.course_id: raise gen.Return([]) url = self.get_base_url() + "/services/" + coursedir.course_id + "/formgrader" auth = get_jupyterhub_authorization() http_client = AsyncHTTPClient() try: yield http_client.fetch(url, headers=auth) except: self.log.error("Formgrader not available at URL: %s", url) raise gen.Return([]) courses = [{ 'course_id': coursedir.course_id, 'url': url, 'kind': 'jupyterhub' }] raise gen.Return(courses)
def test_fetch_feedback_methods(plugin_config, tmpdir): plugin_config.Exchange.assignment_dir = str( tmpdir.mkdir("feedback_test").realpath() ) plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = assignment_id plugin = ExchangeFetchFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config ) plugin.init_src() with pytest.raises(AttributeError) as e_info: foo = plugin.src_path assert ( str(e_info.value) == "'ExchangeFetchFeedback' object has no attribute 'src_path'" ) plugin.init_dest() print(f"plugin.dest_path:{plugin.dest_path}") assert re.search( r"test_fetch_feedback_methods0/feedback_test/assign_1/feedback$", plugin.dest_path, ) assert os.path.isdir(plugin.dest_path)
def test_release_assignment_methods_the_rest(plugin_config, tmpdir, caplog): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str(tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" plugin = ExchangeReleaseAssignment(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) os.makedirs( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "release.ipynb"), ) with open( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") plugin.init_src() with pytest.raises(AttributeError) as e_info: plugin.dest_path assert str(e_info.value) == "'ExchangeReleaseAssignment' object has no attribute 'dest_path'" file = plugin.tar_source() assert len(file) > 1000 plugin.get_notebooks() assert plugin.notebooks == ["release"]
def test_fetch_assignment_handles_500_failure(plugin_config): http_error = "blown op" plugin_config.CourseDirectory.course_id = course_id plugin_config.CourseDirectory.assignment_id = ass_1_2 plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) try: def api_request(*args, **kwargs): return type( "Response", (object, ), { "status_code": 500, "headers": { "content-type": "application/gzip" }, "content": http_error, }, ) with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: plugin.start() assert ( str(e_info.value) == f"Error failing to fetch assignment {ass_1_2} on course {course_id}: status code 500: error {http_error}" # noqa: E501 ) finally: shutil.rmtree(plugin.dest_path)
def test_fetch_feedback_dir_created(plugin_config, tmpdir): plugin_config.Exchange.assignment_dir = str( tmpdir.mkdir("feedback_test").realpath() ) plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = assignment_id assert not os.path.isdir( os.path.join(plugin_config.Exchange.assignment_dir, student_id, "feedback") ) plugin = ExchangeFetchFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config ) def api_request(*args, **kwargs): assert args[0] == (f"feedback?course_id=no_course&assignment_id=assign_1") return type( "Response", (object,), { "status_code": 200, "headers": {"content-type": "text/json"}, "json": lambda: {"success": True, "feedback": []}, }, ) with patch.object(Exchange, "api_request", side_effect=api_request): called = plugin.start() assert os.path.isdir( os.path.join( plugin_config.Exchange.assignment_dir, assignment_id, "feedback" ) )
class CourseDirectory(LoggingConfigurable): _instance = None def initialize(self): app = NbGrader() app.load_config_file() self.coursedir = NbGraderCourseDirectory() self.coursedir.config = app.config self.log.info('Loaded nbassignment coursedir') def __new__(cls): if not cls._instance: print('Creating new instance') cls._instance = super(CourseDirectory, cls).__new__(cls) cls._instance.initialize() return cls._instance def format_path(self, nbgrader_step: str, student_id: str, assignment_id: str, escape: bool = False) -> str: return self.coursedir.format_path(nbgrader_step=nbgrader_step, student_id=student_id, assignment_id=assignment_id, escape=escape)
def test_release_assignment_several_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str(tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join( plugin_config.CourseDirectory.release_directory, "assign_1", "release1.ipynb", ), ) with open( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") copyfile( notebook1_filename, os.path.join( plugin_config.CourseDirectory.release_directory, "assign_1", "release1.ipynb", ), ) copyfile( notebook2_filename, os.path.join( plugin_config.CourseDirectory.release_directory, "assign_1", "release2.ipynb", ), ) plugin = ExchangeReleaseAssignment(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ("assignment?course_id=no_course&assignment_id=assign_1") assert kwargs.get("method").lower() == "post" assert kwargs.get("data").get("notebooks") == ["release1", "release2"] assert "assignment" in kwargs.get("files") assert "assignment.tar.gz" == kwargs.get("files").get("assignment")[0] assert len(kwargs.get("files").get("assignment")[1]) > 0 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_list_normal(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" plugin = ExchangeList(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ("assignments?course_id=no_course") assert "method" not in kwargs or kwargs.get("method").lower() == "get" return type( "Request", (object,), { "status_code": 200, "json": ( lambda: { "success": True, "value": [ { "assignment_id": "assign_1_1", "student_id": 1, "course_id": "no_course", "status": "released", "path": "", "notebooks": [ { "notebook_id": "assignment-0.6", "has_exchange_feedback": False, "feedback_updated": False, "feedback_timestamp": None, } ], "timestamp": "2020-01-01 00:00:00.0 00:00", } ], } ), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): called = plugin.start() assert called == [ { "assignment_id": "assign_1_1", "course_id": "no_course", "student_id": 1, "status": "released", "notebooks": [ { "notebook_id": "assignment-0.6", "has_exchange_feedback": False, "feedback_updated": False, "feedback_timestamp": None, } ], "path": "", "timestamp": "2020-01-01 00:00:00.0 00:00", } ]
def test_release_assignment_methods_init_src(plugin_config, tmpdir, caplog): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.source_directory = str( tmpdir.mkdir(source_dir).realpath()) plugin_config.CourseDirectory.release_directory = str( tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" plugin = ExchangeReleaseAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) # No release file, no source file with pytest.raises(ExchangeError) as e_info: plugin.init_src() assert "Assignment not found at:" in str(e_info.value) # No release, source file exists os.makedirs( os.path.join(plugin_config.CourseDirectory.source_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join(plugin_config.CourseDirectory.source_directory, "assign_1", "release.ipynb"), ) with pytest.raises(ExchangeError) as e_info: plugin.init_src() assert re.match( r"Assignment found in '.+' but not '.+', run `nbgrader assign` first.", str(e_info.value), ) # release file exists os.makedirs( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "release.ipynb"), ) with open( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") plugin.init_src() assert re.search( r"test_release_assignment_method0/release_test/./assign_1$", plugin.src_path) assert os.path.isdir(plugin.src_path)
def test_release_oversize_blocked(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "release.ipynb"), ) with open( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") plugin = ExchangeReleaseAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) # Set the max-buffer-size to 50 bytes plugin.max_buffer_size = 50 def api_request(*args, **kwargs): assert args[0] == (f"assignment?course_id=no_course" f"&assignment_id=assign_1") assert kwargs.get("method").lower() == "post" assert kwargs.get("data").get("notebooks") == ["release"] assert "assignment" in kwargs.get("files") assert "assignment.tar.gz" == kwargs.get("files").get("assignment")[0] assert len(kwargs.get("files").get("assignment")[1]) > 0 return type( "Request", (object, ), { "status_code": 200, "json": (lambda: { "success": True }) }, ) with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: plugin.start() assert ( str(e_info.value) == "Assignment assign_1 not released. The contents of your assignment are too large:\nYou may have data files, temporary files, and/or working files that should not be included - try deleting them." )
def test_release_assignment_methods_init_dest(plugin_config, tmpdir, caplog): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str(tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" plugin = ExchangeReleaseAssignment(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) with pytest.raises(ExchangeError) as e_info: plugin.init_dest() assert str(e_info.value) == "No course id specified. Re-run with --course flag."
def test_fetch_feedback_fetch_several_normal(plugin_config, tmpdir): plugin_config.Exchange.assignment_dir = str( tmpdir.mkdir("feedback_test").realpath() ) plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = assignment_id plugin = ExchangeFetchFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config ) def api_request(*args, **kwargs): assert args[0] == (f"feedback?course_id=no_course&assignment_id=assign_1") assert "method" not in kwargs or kwargs.get("method").lower() == "get" return type( "Response", (object,), { "status_code": 200, "headers": {"content-type": "text/json"}, "json": lambda: { "success": True, "feedback": [ { "filename": "test_feedback1.html", "content": feedback_file, "timestamp": "2020-01-01 00:00:01 00:00", }, { "filename": "test_feedback2.html", "content": feedback_file, "timestamp": "2020-01-01 00:00:00 00:00", }, ], }, }, ) with patch.object(Exchange, "api_request", side_effect=api_request): called = plugin.start() assert os.path.exists( os.path.join( plugin.dest_path, "2020-01-01 00:00:01 00:00", "test_feedback1.html" ) ) assert os.path.exists( os.path.join( plugin.dest_path, "2020-01-01 00:00:00 00:00", "test_feedback2.html" ) )
def list_released_assignments(self, course_id=None): with self.get_assignment_dir_config() as config: try: if course_id: config.CourseDirectory.course_id = course_id coursedir = CourseDirectory(config=config) authenticator = Authenticator(config=config) lister = ExchangeFactory(config=config).List( coursedir=coursedir, authenticator=authenticator, config=config) assignments = lister.start() except Exception as e: self.log.error(traceback.format_exc()) if isinstance(e, ExchangeError): retvalue = { "success": False, "value": """The exchange directory does not exist and could not be created. The "release" and "collect" functionality will not be available. Please see the documentation on http://nbgrader.readthedocs.io/en/stable/user_guide/managing_assignment_files.html#setting-up-the-exchange for instructions. """ } else: retvalue = { "success": False, "value": traceback.format_exc() } else: for assignment in assignments: if assignment['status'] == 'fetched': assignment['path'] = os.path.relpath( assignment['path'], self.parent.notebook_dir) for notebook in assignment['notebooks']: notebook['path'] = os.path.relpath( notebook['path'], self.parent.notebook_dir) retvalue = { "success": True, "value": sorted(assignments, key=lambda x: (x['course_id'], x['assignment_id'])) } return retvalue
def test_fetch_assignment_methods_rest(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = course_id plugin_config.CourseDirectory.assignment_id = ass_1_2 plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) plugin.init_src() assert re.search(rf"{course_id}/{ass_1_2}/assignment.tar.gz$", plugin.src_path) plugin.init_dest() try: def api_request(*args, **kwargs): tar_file = io.BytesIO() with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: tar_handle.add(notebook1_filename, arcname=os.path.basename(notebook1_filename)) tar_file.seek(0) assert args[0] == ( f"assignment?course_id={course_id}&assignment_id={ass_1_2}") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/gzip" }, "content": tar_file.read(), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.download() assert os.path.exists( os.path.join(plugin.src_path, "assignment-0.6.ipynb")) shutil.rmtree(plugin.dest_path) # do_copy includes a download() plugin.do_copy(plugin.src_path, plugin.dest_path) assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb")) finally: shutil.rmtree(plugin.dest_path)
def test_release_assignment_fail(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.release_directory = str( tmpdir.mkdir(release_dir).realpath()) plugin_config.CourseDirectory.assignment_id = "assign_1" os.makedirs( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1"), exist_ok=True, ) copyfile( notebook1_filename, os.path.join( plugin_config.CourseDirectory.release_directory, "assign_1", "feedback.ipynb", ), ) with open( os.path.join(plugin_config.CourseDirectory.release_directory, "assign_1", "timestamp.txt"), "w", ) as fp: fp.write("2020-01-01 00:00:00.0 UTC") plugin = ExchangeReleaseAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): return type( "Request", (object, ), { "status_code": 200, "json": (lambda: { "success": False, "note": "failure note" }), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: plugin.start() assert str(e_info.value) == "failure note"
def test_list_delete(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = "assign_1_1" plugin_config.ExchangeList.remove = True plugin = ExchangeList(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ( "assignment?course_id=no_course&assignment_id=assign_1_1") assert "method" not in kwargs or kwargs.get( "method").lower() == "delete" return type("Request", (object, ), {"status_code": 200}) with patch.object(Exchange, "api_request", side_effect=api_request): called = plugin.start()
def test_fetch_folder_exists_with_ipynb(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = course_id plugin_config.CourseDirectory.assignment_id = ass_1_3 plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) os.makedirs(ass_1_3) with open("assign_1_3/decoy.ipynb", "w") as f: f.write(" ") try: def api_request(*args, **kwargs): tar_file = io.BytesIO() with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: tar_handle.add(notebook1_filename, arcname=os.path.basename(notebook1_filename)) tar_handle.add(notebook2_filename, arcname=os.path.basename(notebook2_filename)) tar_file.seek(0) assert args[0] == ( f"assignment?course_id={course_id}&assignment_id=assign_1_3") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/gzip" }, "content": tar_file.read(), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): with pytest.raises(ExchangeError) as e_info: plugin.start() assert ( str(e_info.value) == "You already have notebook documents in directory: assign_1_3. Please remove them before fetching again" # noqa: E501 ) finally: shutil.rmtree(plugin.dest_path)
def test_fetch_folder_exists_with_other_file(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = course_id plugin_config.CourseDirectory.assignment_id = ass_1_3 plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) os.makedirs(ass_1_3) with open("assign_1_3/decoy.txt", "w") as f: f.write(" ") try: def api_request(*args, **kwargs): tar_file = io.BytesIO() with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: tar_handle.add(notebook1_filename, arcname=os.path.basename(notebook1_filename)) tar_handle.add(notebook2_filename, arcname=os.path.basename(notebook2_filename)) tar_file.seek(0) assert args[0] == ( f"assignment?course_id={course_id}&assignment_id=assign_1_3") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/gzip" }, "content": tar_file.read(), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb")) assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6-2.ipynb")) finally: shutil.rmtree(plugin.dest_path)
def list_submitted_assignments(self, course_id=None): with self.get_assignment_dir_config() as config: try: config.ExchangeList.cached = True if course_id: config.CourseDirectory.course_id = course_id coursedir = CourseDirectory(config=config) authenticator = Authenticator(config=config) lister = ExchangeFactory(config=config).List( coursedir=coursedir, authenticator=authenticator, config=config) assignments = lister.start() except Exception as e: self.log.error(traceback.format_exc()) if isinstance(e, ExchangeError): retvalue = { "success": False, "value": """The exchange directory does not exist and could not be created. The "release" and "collect" functionality will not be available. Please see the documentation on http://nbgrader.readthedocs.io/en/stable/user_guide/managing_assignment_files.html#setting-up-the-exchange for instructions. """ } else: retvalue = { "success": False, "value": traceback.format_exc() } else: for assignment in assignments: assignment["submissions"] = sorted( assignment["submissions"], key=lambda x: x["timestamp"]) assignments = sorted(assignments, key=lambda x: x["assignment_id"]) retvalue = {"success": True, "value": assignments} return retvalue
def get_nbgrader_api(notebook_dir: str, course_id: str = None) -> NbGraderAPI: """Returns an instance of the NbraderAPI based on the notebook directory and course_id. Args: notebook_dir ([str]): the notebook directory path and filename. course_id ([str], optional): The course _id, defaults to None. Returns: NbGraderAPI: and instance of the nbrader API. """ with get_assignment_dir_config(notebook_dir) as config: if course_id: config.CourseDirectory.course_id = course_id coursedir = CourseDirectory(config=config) authenticator = Authenticator(config=config) api = NbGraderAPI(coursedir, authenticator) return api
def test_fetch_assignment_fetch_normal_with_path_includes_course( plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = "assign_1_2" plugin_config.Exchange.path_includes_course = True plugin = ExchangeFetchAssignment( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) try: def api_request(*args, **kwargs): tar_file = io.BytesIO() with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: tar_handle.add(notebook1_filename, arcname=os.path.basename(notebook1_filename)) tar_file.seek(0) assert args[0] == ( f"assignment?course_id=no_course&assignment_id=assign_1_2") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/x-tar" }, "content": tar_file.read(), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): plugin.start() assert os.path.exists( os.path.join(plugin.dest_path, "assignment-0.6.ipynb")) finally: shutil.rmtree(plugin.dest_path)
def submit_assignment(self, course_id, assignment_id): with self.get_assignment_dir_config() as config: try: config = self.load_config() config.CourseDirectory.course_id = course_id config.CourseDirectory.assignment_id = assignment_id coursedir = CourseDirectory(config=config) authenticator = Authenticator(config=config) submit = ExchangeFactory(config=config).Submit( coursedir=coursedir, authenticator=authenticator, config=config) submit.start() except: self.log.error(traceback.format_exc()) retvalue = {"success": False, "value": traceback.format_exc()} else: retvalue = {"success": True} return retvalue
def test_release_feedback_methods(plugin_config, tmpdir): plugin_config.CourseDirectory.root = "/" plugin_config.CourseDirectory.feedback_directory = str( tmpdir.mkdir("feedback_test").realpath()) plugin_config.CourseDirectory.assignment_id = assignment_id plugin = ExchangeReleaseFeedback( coursedir=CourseDirectory(config=plugin_config), config=plugin_config) plugin.init_src() print(f"asserting plugin.src_path: {plugin.src_path}") assert re.search( r"test_release_feedback_methods0/feedback_test/\*/assign_1$", plugin.src_path) plugin.coursedir.student_id = student_id plugin.init_src() assert re.search( r"test_release_feedback_methods0/feedback_test/1/assign_1$", plugin.src_path) with pytest.raises(ExchangeError) as e_info: plugin.init_dest() assert str(e_info.value ) == "No course id specified. Re-run with --course flag."
def test_list_feedback_available_with_path_includes_course( plugin_config, tmpdir): try: course_code = "no_course" assignment_id = "assign_1_1" plugin_config.CourseDirectory.course_id = course_code plugin_config.CourseDirectory.assignment_id = assignment_id plugin_config.ExchangeList.inbound = True plugin_config.Exchange.path_includes_course = True my_feedback_dir = f"{course_code}/{assignment_id}/feedback/2020-01-01 00:02:00.2 00:00" os.makedirs(my_feedback_dir, exist_ok=True) copyfile( feedback1_filename, os.path.join( my_feedback_dir, basename(feedback1_filename), ), ) plugin = ExchangeList(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) def api_request(*args, **kwargs): assert args[0] == ("assignments?course_id=no_course") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Request", (object, ), { "status_code": 200, "json": (lambda: { "success": True, "value": [ { "assignment_id": assignment_id, "student_id": 1, "course_id": course_code, "status": "submitted", "path": "", "notebooks": [{ "notebook_id": root_notebook_name, "has_exchange_feedback": True, "feedback_updated": False, "feedback_timestamp": "2020-01-01 00:02:00.2 00:00", }], "timestamp": "2020-01-01 00:00:00.2 00:00", }, ], }), }, ) with patch.object(Exchange, "api_request", side_effect=api_request): called = plugin.start() assert called == [{ "assignment_id": assignment_id, "course_id": course_code, "student_id": 1, "status": "submitted", "submissions": [{ "assignment_id": assignment_id, "course_id": course_code, "path": "", "status": "submitted", "student_id": 1, "notebooks": [{ "feedback_timestamp": "2020-01-01 00:02:00.2 00:00", "has_exchange_feedback": True, "has_local_feedback": True, "local_feedback_path": f"{course_code}/{assignment_id}/feedback/2020-01-01%2000%3A02%3A00.2%2000%3A00/{root_notebook_name}.html", # noqa: E501 "feedback_updated": False, "notebook_id": root_notebook_name, }], "timestamp": "2020-01-01 00:00:00.2 00:00", "feedback_updated": False, "has_exchange_feedback": True, "has_local_feedback": True, "local_feedback_path": f"{course_code}/{assignment_id}/feedback/2020-01-01%2000%3A02%3A00.2%2000%3A00", # noqa: E501 }], }] finally: shutil.rmtree(course_code)
def test_collect_normal_several_full_name_none(plugin_config, tmpdir): plugin_config.CourseDirectory.course_id = "no_course" plugin_config.CourseDirectory.assignment_id = ass_1_1 plugin_config.CourseDirectory.submitted_directory = str( tmpdir.mkdir("submitted").realpath()) plugin = ExchangeCollect(coursedir=CourseDirectory(config=plugin_config), config=plugin_config) collections = False collection = False gradebook_called = False student_ids = ["1", "2"] collection_number = 0 def gradebook_update(*args, **kwargs): nonlocal gradebook_called, collection_number gradebook_called = True assert kwargs.get("first_name") == "" assert kwargs.get("last_name") == "" assert args[0] == student_ids[collection_number] collection_number += 1 def api_request(*args, **kwargs): nonlocal collections, collection tar_file = io.BytesIO() if "collections" in args[0]: collections = True assert args[0] == ( f"collections?course_id=no_course&assignment_id={ass_1_1}") assert "method" not in kwargs or kwargs.get( "method").lower() == "get" return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/x-tar" }, "json": lambda: { "success": True, "value": [ { "student_id": student_ids[0], "full_name": None, "path": f"/submitted/no_course/{ass_1_1}/1/", "timestamp": "2020-01-01 00:00:00.0 UTC", }, { "student_id": student_ids[1], "full_name": None, "path": f"/submitted/no_course/{ass_1_1}/2/", "timestamp": "2020-01-01 00:00:00.1 UTC", }, ], }, }, ) else: num = "2" if collection else "1" assert args[0] == ( f"collection?course_id=no_course&assignment_id={ass_1_1}&path=%2Fsubmitted%2Fno_course%2F{ass_1_1}%2F{num}%2F" ) collection = True assert "method" not in kwargs or kwargs.get( "method").lower() == "get" with tarfile.open(fileobj=tar_file, mode="w:gz") as tar_handle: tar_handle.add(notebook1_filename, arcname=os.path.basename(notebook1_filename)) tar_file.seek(0) return type( "Response", (object, ), { "status_code": 200, "headers": { "content-type": "application/x-tar" }, "content": tar_file.read(), }, ) with patch.object(Exchange, "api_request", side_effect=api_request), patch.object( Gradebook, "update_or_create_student", side_effect=gradebook_update): plugin.start() assert gradebook_called assert collection_number == 2 assert collections and collection
def initialize(self): app = NbGrader() app.load_config_file() self.coursedir = NbGraderCourseDirectory() self.coursedir.config = app.config self.log.info('Loaded nbassignment coursedir')