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)
Пример #4
0
    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)
Пример #5
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_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)
Пример #7
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)
Пример #8
0
    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)
Пример #10
0
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()),
        })
Пример #11
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)
Пример #12
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)
Пример #13
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)
Пример #14
0
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()),
        })
Пример #15
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)
Пример #16
0
    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)
Пример #17
0
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. "))
Пример #18
0
    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)
Пример #19
0
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
    })
Пример #20
0
    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)
Пример #21
0
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 &raquo;</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",
    })
Пример #22
0
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 &raquo;</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
        })
Пример #23
0
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"))
Пример #24
0
    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)
Пример #25
0
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 &raquo;</a></p>'
                % reverse("course.calendar.check_events",
                    args=(pctx.course.identifier,)))

    else:
        raise RuntimeError("invalid command")
Пример #26
0
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 &raquo;</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
        })
Пример #27
0
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
        })
Пример #28
0
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
    })
Пример #29
0
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"))
Пример #30
0
    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)