def test_course_no_events_file(self): self.mock_get_yaml_from_repo.side_effect = ( get_yaml_from_repo_no_events_file_side_effect) validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is not called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_get_repo_blob_flows_dir_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect2 validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is not called, because no flow files self.assertEqual(self.mock_validate_flow_id.call_count, 0) # validate_flow_desc is not called, because no flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 0) # check_for_page_type_changes is not called, because no flow files self.assertEqual(self.mock_check_for_page_type_changes.call_count, 0) # validate_static_page_name is called twice for two static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_get_repo_blob_media_dir_not_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect1 validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 1) expected_warn_msg = ( "Your course repository has a 'media/' directory. Linking to " "media files using 'media:' is discouraged. Use the 'repo:' " "and 'repocur:' linkng schemes instead.") self.assertIn(expected_warn_msg, self.mock_vctx_add_warning.call_args[0]) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_id.call_count, 1) # validate_flow_desc is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_desc.call_count, 1) # check_for_page_type_changes is called once, there's only 1 flow file self.assertEqual(self.mock_check_for_page_type_changes.call_count, 1) # validate_static_page_name is called twice for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_get_repo_blob_flows_dir_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect2 validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is not called, because no flow files self.assertEqual(self.mock_validate_flow_id.call_count, 0) # validate_flow_desc is not called, because no flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 0) # check_for_page_type_changes is not called, because no flow files self.assertEqual(self.mock_check_for_page_type_changes.call_count, 0) # validate_static_page_name is called twice for two static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_get_repo_blob_staticpages_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect3 validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page only self.assertEqual(self.mock_validate_staticpage_desc.call_count, 1) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, there's only 1 flow file self.assertEqual(self.mock_validate_flow_id.call_count, 1) # validate_flow_desc is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_desc.call_count, 1) # check_for_page_type_changes is called once, there's only 1 flow file self.assertEqual(self.mock_check_for_page_type_changes.call_count, 1) # validate_static_page_name is not called, no static page self.assertEqual(self.mock_validate_static_page_name.call_count, 0)
def test_get_repo_blob_staticpages_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect3 validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page only self.assertEqual(self.mock_validate_staticpage_desc.call_count, 1) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, there's only 1 flow file self.assertEqual(self.mock_validate_flow_id.call_count, 1) # validate_flow_desc is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_desc.call_count, 1) # check_for_page_type_changes is called once, there's only 1 flow file self.assertEqual(self.mock_check_for_page_type_changes.call_count, 1) # validate_static_page_name is not called, no static page self.assertEqual(self.mock_validate_static_page_name.call_count, 0)
def test_course_no_events_file(self): self.mock_get_yaml_from_repo.side_effect = ( get_yaml_from_repo_no_events_file_side_effect) validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is not called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_course_custom_events_file_does_not_exist(self): self.mock_get_yaml_from_repo.side_effect = ( get_yaml_from_repo_no_events_file_side_effect) validation.validate_course_content(self.repo, course_file, "my_events_file.yml", validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 1) expected_warn_msg = ("Your course repository does not have an events " "file named 'my_events_file.yml'.") self.assertIn(expected_warn_msg, self.mock_vctx_add_warning.call_args[0]) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is not called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_course_custom_events_file_does_not_exist(self): self.mock_get_yaml_from_repo.side_effect = ( get_yaml_from_repo_no_events_file_side_effect) validation.validate_course_content( self.repo, course_file, "my_events_file.yml", validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 1) expected_warn_msg = ( "Your course repository does not have an events " "file named 'my_events_file.yml'.") self.assertIn(expected_warn_msg, self.mock_vctx_add_warning.call_args[0]) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is not called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def check_events(pctx): if pctx.role not in [ participation_role.instructor, participation_role.teaching_assistant ]: raise PermissionDenied("only instructors and TAs may do that") invalid_datespecs = {} from course.content import InvalidDatespec, parse_date_spec def datespec_callback(location, datespec): try: parse_date_spec(pctx.course, datespec, return_now_on_error=False) except InvalidDatespec as e: invalid_datespecs.setdefault(e.datespec, []).append(location) from course.validation import validate_course_content validate_course_content(pctx.repo, pctx.course.course_file, pctx.course.events_file, pctx.course_commit_sha, datespec_callback=datespec_callback) return render_course_page( pctx, "course/invalid-datespec-list.html", { "invalid_datespecs": sorted(invalid_datespecs.iteritems()), })
def test_duplicated_grade_identifier(self): self.mock_get_yaml_from_repo_safely.side_effect = ( get_yaml_from_repo_safely_with_duplicate_grade_identifier_side_effect ) with self.assertRaises(ValidationError) as cm: validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) expected_error_msg = ("flows/flow3.yml: flow uses the same " "grade_identifier as another flow") self.assertIn(expected_error_msg, str(cm.exception)) self.assertEqual(self.mock_vctx_add_warning.call_count, 0)
def test_duplicated_grade_identifier(self): self.mock_get_yaml_from_repo_safely.side_effect = ( get_yaml_from_repo_safely_with_duplicate_grade_identifier_side_effect ) with self.assertRaises(ValidationError) as cm: validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) expected_error_msg = ("flows/flow3.yml: flow uses the same " "grade_identifier as another flow") self.assertIn(expected_error_msg, str(cm.exception)) self.assertEqual(self.mock_vctx_add_warning.call_count, 0)
def test_course_not_none(self): validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_grade_identifier_link is call twice, only 2 flow # has grade_identifier self.assertEqual(self.mock_check_grade_identifier_link.call_count, 2) # make sure validate_static_page_name was called with expected args expected_check_grade_identifier_link_call_args = { (flow2_path, self.course, flow2_id, flow2_grade_identifier), (flow3_path, self.course, flow3_id, flow3_grade_identifier) } args_set = set() for args, kwargs in self.mock_check_grade_identifier_link.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_check_grade_identifier_link_call_args, args_set) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def check_events(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("only instructors may do that") invalid_datespecs = {} from course.content import InvalidDatespec, parse_date_spec def datespec_callback(location, datespec): try: parse_date_spec(pctx.course, datespec, return_now_on_error=False) except InvalidDatespec as e: invalid_datespecs.setdefault(e.datespec, []).append(location) from course.validation import validate_course_content validate_course_content( pctx.repo, pctx.course.course_file, pctx.course.events_file, pctx.course_commit_sha, datespec_callback=datespec_callback) return render_course_page(pctx, "course/invalid-datespec-list.html", { "invalid_datespecs": sorted(invalid_datespecs.iteritems()), })
def test_course_not_none(self): validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # check_grade_identifier_link is call twice, only 2 flow # has grade_identifier self.assertEqual(self.mock_check_grade_identifier_link.call_count, 2) # make sure validate_static_page_name was called with expected args expected_check_grade_identifier_link_call_args = { (flow2_path, self.course, flow2_id, flow2_grade_identifier), (flow3_path, self.course, flow3_id, flow3_grade_identifier)} args_set = set() for args, kwargs in self.mock_check_grade_identifier_link.call_args_list: args_set.add(args[1:]) self.assertSetEqual( expected_check_grade_identifier_link_call_args, args_set) # check_for_page_type_changes is called 3 times for 3 flows self.assertEqual(self.mock_check_for_page_type_changes.call_count, 3) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def test_get_repo_blob_media_dir_not_empty(self): self.mock_get_repo_blob.side_effect = get_repo_blob_side_effect1 validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 1) expected_warn_msg = ( "Your course repository has a 'media/' directory. Linking to " "media files using 'media:' is discouraged. Use the 'repo:' " "and 'repocur:' linkng schemes instead.") self.assertIn(expected_warn_msg, self.mock_vctx_add_warning.call_args[0]) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) # validate_flow_id is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_id.call_count, 1) # validate_flow_desc is called once, there's only 1 flow file self.assertEqual(self.mock_validate_flow_desc.call_count, 1) # check_for_page_type_changes is called once, there's only 1 flow file self.assertEqual(self.mock_check_for_page_type_changes.call_count, 1) # validate_static_page_name is called twice for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2)
def run_course_update_command(request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command not in ALLOWED_COURSE_REVISIOIN_COMMANDS: raise RuntimeError(_("invalid command")) if command.startswith("fetch"): if command != "fetch": command = command[6:] client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) fetch_pack_result = client.fetch(remote_path, repo) assert isinstance(fetch_pack_result, dulwich.client.FetchPackResult) transfer_remote_refs(repo, fetch_pack_result) remote_head = fetch_pack_result.refs[b"HEAD"] if prevent_discarding_revisions: # Guard against bad scenario: # Local is not ancestor of remote, i.e. the branches have diverged. if not is_ancestor_commit(repo, repo[b"HEAD"], repo[remote_head], max_history_check_size=20) and \ repo[b"HEAD"] != repo[remote_head]: raise RuntimeError( _("internal git repo has more commits. Fetch, " "merge and push.")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content(content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message( request, messages.ERROR, _("Course content did not validate successfully: '%s' " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message( request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join("<li><i>%(location)s</i>: %(warningtext)s</li>" % { 'location': w.location, 'warningtext': w.text } for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: # pragma: no branch pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() if pctx.participation.preview_git_commit_sha is not None: pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) messages.add_message(request, messages.SUCCESS, _("Update applied. "))
def test_course_not_none_check_attributes_yml(self): # This test check_attributes_yml args access_type # is generated with course-specific pperm.access_files_for user = factories.UserFactory() # {{{ create another course with different set of participation role # permission and participation permission another_course = factories.CourseFactory(identifier="another-course") another_course_prole = ParticipationRole( course=another_course, identifier="another_course_role", name="another_course_role") another_course_prole.save() another_course_participation = factories.ParticipationFactory( course=another_course, user=user) another_course_participation.roles.set([another_course_prole]) another_course_ppm_access_files_for_roles = "another_role" ParticipationPermission( participation=another_course_participation, permission=pperm.access_files_for, argument=another_course_ppm_access_files_for_roles).save() another_course_rpm_access_files_for_roles = "another_course_everyone" ParticipationRolePermission( role=another_course_prole, permission=pperm.access_files_for, argument=another_course_rpm_access_files_for_roles).save() self.assertTrue( another_course_participation.has_permission( pperm.access_files_for, argument=another_course_ppm_access_files_for_roles)) self.assertTrue( another_course_participation.has_permission( pperm.access_files_for, argument=another_course_rpm_access_files_for_roles)) # }}} # {{{ create for default test course extra participation role # permission and participation permission this_course_prole = ParticipationRole(course=self.course, identifier="another_course_role", name="another_course_role") this_course_prole.save() this_course_participation = factories.ParticipationFactory( course=self.course, user=user) this_course_participation.roles.set([this_course_prole]) this_course_ppm_access_files_for_roles = "this_course_some_role" ParticipationPermission( participation=this_course_participation, permission=pperm.access_files_for, argument=this_course_ppm_access_files_for_roles).save() this_course_rpm_access_files_for_roles = "this_course_everyone" ParticipationRolePermission( role=this_course_prole, permission=pperm.access_files_for, argument=this_course_rpm_access_files_for_roles).save() self.assertTrue( this_course_participation.has_permission( pperm.access_files_for, argument=this_course_ppm_access_files_for_roles)) self.assertTrue( this_course_participation.has_permission( pperm.access_files_for, argument=this_course_rpm_access_files_for_roles)) # }}} validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) access_kinds = list(self.mock_check_attributes_yml.call_args[0][-1]) self.assertIn(this_course_ppm_access_files_for_roles, access_kinds) self.assertIn(this_course_rpm_access_files_for_roles, access_kinds) self.assertNotIn(another_course_ppm_access_files_for_roles, access_kinds) self.assertNotIn(another_course_rpm_access_files_for_roles, access_kinds)
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied(_("only staff may create courses")) if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) new_sha = repo["HEAD"] = remote_refs["HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content(vrepo, new_course.course_file, new_course.events_file, new_sha) del repo del vrepo new_course.valid = True new_course.active_git_commit_sha = new_sha new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message( request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. import gc gc.collect() def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message( request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, string_concat(_("Course creation failed"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: return redirect("relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })
def test_course_none(self): validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=None) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # make sure validate_staticpage_desc was called with expected args expected_validate_staticpage_desc_call_args = { (course_file, course_desc), (staticpage1_path, staticpage1_desc), (staticpage2_path, staticpage2_desc)} args_set = set() for args, kwargs in self.mock_validate_staticpage_desc.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_staticpage_desc_call_args, args_set) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) expected_check_attributes_yml_call_args_access_kinds = DEFAULT_ACCESS_KINDS self.assertEqual( self.mock_check_attributes_yml.call_args[0][-1], expected_check_attributes_yml_call_args_access_kinds) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # make sure validate_flow_id was called with expected args expected_validate_flow_id_call_args = { (flow1_location, flow1_id), (flow2_location, flow2_id), (flow3_location, flow3_id), } args_set = set() for args, kwargs in self.mock_validate_flow_id.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_flow_id_call_args, args_set) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # make sure validate_flow_desc was called with expected args expected_validate_flow_desc_call_args = { (flow1_path, flow1_no_rule_desc), (flow2_path, flow2_default_desc), (flow3_path, flow3_default_desc), } args_set = set() for args, kwargs in self.mock_validate_flow_desc.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_flow_desc_call_args, args_set) # check_grade_identifier_link is not called, because course is None self.assertEqual(self.mock_check_grade_identifier_link.call_count, 0) # check_for_page_type_changes is not called, because course is None self.assertEqual(self.mock_check_for_page_type_changes.call_count, 0) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2) # make sure validate_static_page_name was called with expected args expected_validate_static_page_name_call_args = { (staticpage1_location, staticpage1_id), (staticpage2_location, staticpage2_id)} args_set = set() for args, kwargs in self.mock_validate_static_page_name.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_static_page_name_call_args, args_set)
def update_course(pctx): if pctx.role != participation_role.instructor: raise PermissionDenied("must be instructor to update course") course = pctx.course request = pctx.request repo = pctx.repo participation = pctx.participation previewing = bool(participation is not None and participation.preview_git_commit_sha) response_form = None if request.method == "POST": form = GitUpdateForm(previewing, request.POST, request.FILES) if "fetch" in form.data: return fetch_course_updates_inner(pctx) if "end_preview" in form.data: messages.add_message(request, messages.INFO, "Preview ended.") participation.preview_git_commit_sha = None participation.save() previewing = False elif form.is_valid(): new_sha = form.cleaned_data["new_sha"].encode("utf-8") from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content( repo, course.course_file, course.events_file, new_sha) except ValidationError as e: messages.add_message(request, messages.ERROR, "Course content did not validate successfully. (%s) " "Update not applied." % str(e)) validated = False else: if not warnings: messages.add_message(request, messages.SUCCESS, "Course content validated successfully.") else: messages.add_message(request, messages.WARNING, "Course content validated OK, with warnings:" "<ul>%s</ul>" % ("".join( "<li><i>%s</i>: %s</li>" % (w.location, w.text) for w in warnings))) validated = True if validated and "update" in form.data: messages.add_message(request, messages.INFO, "Update applied. " "You may want to view the events used " "in the course content and check that they " "are recognized. " + '<p><a href="%s" class="btn btn-primary" ' 'style="margin-top:8px">' 'Check »</a></p>' % reverse("course.calendar.check_events", args=(course.identifier,))) course.active_git_commit_sha = new_sha course.valid = True course.save() response_form = form elif validated and "preview" in form.data: messages.add_message(request, messages.INFO, "Preview activated.") participation.preview_git_commit_sha = new_sha participation.save() previewing = True if response_form is None: form = GitUpdateForm(previewing, {"new_sha": repo.head()}) text_lines = [ "<b>Current git HEAD:</b> %s (%s)" % ( repo.head(), repo[repo.head()].message), "<b>Public active git SHA:</b> %s (%s)" % ( course.active_git_commit_sha, repo[course.active_git_commit_sha.encode()].message), ] if participation is not None and participation.preview_git_commit_sha: text_lines.append( "<b>Current preview git SHA:</b> %s (%s)" % ( participation.preview_git_commit_sha, repo[participation.preview_git_commit_sha.encode()].message, )) else: text_lines.append("<b>Current preview git SHA:</b> None") return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_text": "".join( "<p>%s</p>" % line for line in text_lines ), "form_description": "Update Course Revision", })
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied("only staff may create courses") if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) new_sha = repo["HEAD"] = remote_refs["HEAD"] from course.validation import validate_course_content validate_course_content( repo, new_course.course_file, new_course.events_file, new_sha) new_course.valid = True new_course.active_git_commit_sha = new_sha new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message(request, messages.INFO, "Course content validated, creation succeeded. " "You may want to view the events used " "in the course content and create them. " + '<a href="%s" class="btn btn-primary">' 'Check »</a>' % reverse("course.calendar.check_events", args=(new_course.identifier,))) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. import shutil shutil.rmtree(repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message(request, messages.ERROR, "Course creation failed: %s: %s" % ( type(e).__name__, str(e))) else: return redirect( "course.views.course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": "Set up new course", "form": form })
def run_course_update_command( request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command.startswith("fetch"): if command != "fetch": command = command[6:] if not pctx.course.git_source: raise RuntimeError(_("no git source URL specified")) client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) remote_head = remote_refs[b"HEAD"] if ( prevent_discarding_revisions and is_parent_commit(repo, repo[remote_head], repo[b"HEAD"], max_history_check_size=20)): raise RuntimeError(_("fetch would discard commits, refusing")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": messages.add_message(request, messages.INFO, _("Preview ended.")) pctx.participation.preview_git_commit_sha = None pctx.participation.save() return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content( content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message(request, messages.ERROR, _("Course content did not validate successfully. (%s) " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message(request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join( "<li><i>%(location)s</i>: %(warningtext)s</li>" % {'location': w.location, 'warningtext': w.text} for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() messages.add_message(request, messages.SUCCESS, _("Update applied. ")) else: raise RuntimeError(_("invalid command"))
def test_course_not_none_check_attributes_yml(self): # This test check_attributes_yml args access_type # is generated with course-specific pperm.access_files_for user = factories.UserFactory() # {{{ create another course with different set of participation role # permission and participation permission another_course = factories.CourseFactory(identifier="another-course") another_course_prole = ParticipationRole( course=another_course, identifier="another_course_role", name="another_course_role") another_course_prole.save() another_course_participation = factories.ParticipationFactory( course=another_course, user=user) another_course_participation.roles.set([another_course_prole]) another_course_ppm_access_files_for_roles = "another_role" ParticipationPermission( participation=another_course_participation, permission=pperm.access_files_for, argument=another_course_ppm_access_files_for_roles ).save() another_course_rpm_access_files_for_roles = "another_course_everyone" ParticipationRolePermission( role=another_course_prole, permission=pperm.access_files_for, argument=another_course_rpm_access_files_for_roles).save() self.assertTrue( another_course_participation.has_permission( pperm.access_files_for, argument=another_course_ppm_access_files_for_roles)) self.assertTrue( another_course_participation.has_permission( pperm.access_files_for, argument=another_course_rpm_access_files_for_roles)) # }}} # {{{ create for default test course extra participation role # permission and participation permission this_course_prole = ParticipationRole( course=self.course, identifier="another_course_role", name="another_course_role") this_course_prole.save() this_course_participation = factories.ParticipationFactory( course=self.course, user=user) this_course_participation.roles.set([this_course_prole]) this_course_ppm_access_files_for_roles = "this_course_some_role" ParticipationPermission( participation=this_course_participation, permission=pperm.access_files_for, argument=this_course_ppm_access_files_for_roles ).save() this_course_rpm_access_files_for_roles = "this_course_everyone" ParticipationRolePermission( role=this_course_prole, permission=pperm.access_files_for, argument=this_course_rpm_access_files_for_roles).save() self.assertTrue( this_course_participation.has_permission( pperm.access_files_for, argument=this_course_ppm_access_files_for_roles)) self.assertTrue( this_course_participation.has_permission( pperm.access_files_for, argument=this_course_rpm_access_files_for_roles)) # }}} validation.validate_course_content( self.repo, course_file, events_file, validate_sha, course=self.course) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) access_kinds = list(self.mock_check_attributes_yml.call_args[0][-1]) self.assertIn(this_course_ppm_access_files_for_roles, access_kinds) self.assertIn(this_course_rpm_access_files_for_roles, access_kinds) self.assertNotIn(another_course_ppm_access_files_for_roles, access_kinds) self.assertNotIn(another_course_rpm_access_files_for_roles, access_kinds)
def run_course_update_command(request, pctx, command, new_sha): if command.startswith("fetch_"): command = command[6:] if not pctx.course.git_source: raise RuntimeError("no git source URL specified") repo = pctx.repo client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) remote_head = remote_refs["HEAD"] if is_parent_commit(repo, repo[remote_head], repo["HEAD"], max_history_check_size=10): raise RuntimeError("fetch would discard commits, refusing") repo["HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, "Fetch successful.") new_sha = repo.head() if command == "end_preview": messages.add_message(request, messages.INFO, "Preview ended.") pctx.participation.preview_git_commit_sha = None pctx.participation.save() return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content( repo, pctx.course.course_file, pctx.course.events_file, new_sha) except ValidationError as e: messages.add_message(request, messages.ERROR, "Course content did not validate successfully. (%s) " "Update not applied." % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, "Course content validated successfully.") else: messages.add_message(request, messages.WARNING, "Course content validated OK, with warnings:" "<ul>%s</ul>" % ("".join( "<li><i>%s</i>: %s</li>" % (w.location, w.text) for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, "Preview activated.") pctx.participation.preview_git_commit_sha = new_sha pctx.participation.save() elif command == "update": pctx.course.active_git_commit_sha = new_sha pctx.course.valid = True pctx.course.save() messages.add_message(request, messages.SUCCESS, "Update applied. " "You may want to view the events used " "in the course content and check that they " "are recognized. " + '<p><a href="%s" class="btn btn-primary" ' 'style="margin-top:8px">' 'Check »</a></p>' % reverse("course.calendar.check_events", args=(pctx.course.identifier,))) else: raise RuntimeError("invalid command")
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied(_("only staff may create courses")) if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) repo = None try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) new_sha = repo[b"HEAD"] = remote_refs[b"HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content( vrepo, new_course.course_file, new_course.events_file, new_sha) del repo del vrepo new_course.active_git_commit_sha = new_sha.decode() new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message(request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. if repo is not None: # noqa repo.close() # noqa def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message(request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message(request, messages.ERROR, string_concat( _("Course creation failed"), ": %(err_type)s: %(err_str)s") % {"err_type": type(e).__name__, "err_str": str(e)}) else: return redirect( "relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })
def set_up_new_course(request): # type: (http.HttpRequest) -> http.HttpResponse if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) repo = None try: with transaction.atomic(): repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) if remote_refs is None: raise RuntimeError( _("No refs found in remote repository" " (i.e. no master branch, no HEAD). " "This looks very much like a blank repository. " "Please create course.yml in the remote " "repository before creating your course.")) transfer_remote_refs(repo, remote_refs) new_sha = repo[b"HEAD"] = remote_refs[b"HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content( # type: ignore vrepo, new_course.course_file, new_course.events_file, new_sha) del vrepo new_course.active_git_commit_sha = new_sha.decode() new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.status = participation_status.active part.save() part.roles.set([ # created by signal handler for course creation ParticipationRole.objects.get( course=new_course, identifier="instructor") ]) # }}} messages.add_message( request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. if repo is not None: # noqa repo.close() # noqa def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message( request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, string_concat(_("Course creation failed"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: return redirect("relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })
def run_course_update_command(request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command.startswith("fetch"): if command != "fetch": command = command[6:] if not pctx.course.git_source: raise RuntimeError(_("no git source URL specified")) client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) remote_head = remote_refs[b"HEAD"] if (prevent_discarding_revisions and is_parent_commit( repo, repo[remote_head], repo[b"HEAD"], max_history_check_size=20)): raise RuntimeError(_("fetch would discard commits, refusing")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content(content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message( request, messages.ERROR, _("Course content did not validate successfully. (%s) " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message( request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join("<li><i>%(location)s</i>: %(warningtext)s</li>" % { 'location': w.location, 'warningtext': w.text } for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() if pctx.participation.preview_git_commit_sha is not None: pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) messages.add_message(request, messages.SUCCESS, _("Update applied. ")) else: raise RuntimeError(_("invalid command"))
def test_course_none(self): validation.validate_course_content(self.repo, course_file, events_file, validate_sha, course=None) self.assertEqual(self.mock_vctx_add_warning.call_count, 0) # validate_staticpage_desc call to validate course_page, and 2 staticpages self.assertEqual(self.mock_validate_staticpage_desc.call_count, 3) # make sure validate_staticpage_desc was called with expected args expected_validate_staticpage_desc_call_args = { (course_file, course_desc), (staticpage1_path, staticpage1_desc), (staticpage2_path, staticpage2_desc) } args_set = set() for args, kwargs in self.mock_validate_staticpage_desc.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_staticpage_desc_call_args, args_set) # validate_calendar_desc_struct is called self.assertEqual(self.mock_validate_calendar_desc_struct.call_count, 1) # check_attributes_yml is called self.assertEqual(self.mock_check_attributes_yml.call_count, 1) expected_check_attributes_yml_call_args_access_kinds = DEFAULT_ACCESS_KINDS self.assertEqual(self.mock_check_attributes_yml.call_args[0][-1], expected_check_attributes_yml_call_args_access_kinds) # validate_flow_id is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_id.call_count, 3) # make sure validate_flow_id was called with expected args expected_validate_flow_id_call_args = { (flow1_location, flow1_id), (flow2_location, flow2_id), (flow3_location, flow3_id), } args_set = set() for args, kwargs in self.mock_validate_flow_id.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_flow_id_call_args, args_set) # validate_flow_desc is called 3 times, for 3 flow files self.assertEqual(self.mock_validate_flow_desc.call_count, 3) # make sure validate_flow_desc was called with expected args expected_validate_flow_desc_call_args = { (flow1_path, flow1_no_rule_desc), (flow2_path, flow2_default_desc), (flow3_path, flow3_default_desc), } args_set = set() for args, kwargs in self.mock_validate_flow_desc.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_flow_desc_call_args, args_set) # check_grade_identifier_link is not called, because course is None self.assertEqual(self.mock_check_grade_identifier_link.call_count, 0) # check_for_page_type_changes is not called, because course is None self.assertEqual(self.mock_check_for_page_type_changes.call_count, 0) # validate_static_page_name is called once for 2 static pages self.assertEqual(self.mock_validate_static_page_name.call_count, 2) # make sure validate_static_page_name was called with expected args expected_validate_static_page_name_call_args = { (staticpage1_location, staticpage1_id), (staticpage2_location, staticpage2_id) } args_set = set() for args, kwargs in self.mock_validate_static_page_name.call_args_list: args_set.add(args[1:]) self.assertSetEqual(expected_validate_static_page_name_call_args, args_set)