From 8c904f31a90af89dea026489d8e316e93cb85c20 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Tue, 16 Jul 2013 14:22:42 -0400 Subject: [PATCH] Move defaults from yaml templates to field definitions. This standardizes the XModule field default values to be the same as the values that are presented by studio when a component is added to a course. --- .../features/discussion-editor.py | 6 +- .../contentstore/features/html-editor.py | 2 +- .../contentstore/features/problem-editor.py | 14 +- .../contentstore/features/video-editor.py | 2 +- .../contentstore/tests/test_contentstore.py | 24 +- .../tests/test_course_settings.py | 2 - common/djangoapps/xmodule_modifiers.py | 2 +- .../lib/xmodule/xmodule/annotatable_module.py | 27 +- common/lib/xmodule/xmodule/capa_module.py | 27 +- .../xmodule/combined_open_ended_module.py | 54 +++- common/lib/xmodule/xmodule/course_module.py | 254 ++++++++++++++---- .../lib/xmodule/xmodule/discussion_module.py | 22 +- common/lib/xmodule/xmodule/html_module.py | 81 +++++- .../xmodule/xmodule/peer_grading_module.py | 9 + common/lib/xmodule/xmodule/raw_module.py | 2 +- .../xmodule/templates/about/empty.yaml | 6 +- .../xmodule/templates/about/overview.yaml | 95 ++++--- .../templates/annotatable/default.yaml | 21 +- .../templates/combinedopenended/default.yaml | 38 +-- .../xmodule/templates/course/empty.yaml | 125 +-------- .../xmodule/templates/courseinfo/empty.yaml | 6 +- .../xmodule/templates/default/empty.yaml | 6 +- .../xmodule/templates/discussion/default.yaml | 10 +- .../xmodule/templates/html/announcement.yaml | 3 +- .../xmodule/xmodule/templates/html/empty.yaml | 8 +- .../xmodule/templates/html/latex_html.yaml | 14 +- .../templates/peer_grading/default.yaml | 10 +- .../templates/problem/blank_common.yaml | 5 + .../templates/problem/circuitschematic.yaml | 111 ++++---- .../xmodule/templates/problem/empty.yaml | 12 +- .../xmodule/templates/statictab/empty.yaml | 6 +- .../xmodule/templates/video/default.yaml | 6 +- .../xmodule/templates/videoalpha/default.yaml | 12 +- .../xmodule/templates/word_cloud/default.yaml | 6 +- common/lib/xmodule/xmodule/video_module.py | 15 ++ .../lib/xmodule/xmodule/videoalpha_module.py | 16 +- .../lib/xmodule/xmodule/word_cloud_module.py | 10 +- common/lib/xmodule/xmodule/x_module.py | 126 ++++++--- lms/xmodule_namespace.py | 8 +- 39 files changed, 707 insertions(+), 496 deletions(-) create mode 100644 common/lib/xmodule/xmodule/templates/problem/blank_common.yaml diff --git a/cms/djangoapps/contentstore/features/discussion-editor.py b/cms/djangoapps/contentstore/features/discussion-editor.py index ae3da3c4582..a4a4b716687 100644 --- a/cms/djangoapps/contentstore/features/discussion-editor.py +++ b/cms/djangoapps/contentstore/features/discussion-editor.py @@ -17,9 +17,9 @@ def i_created_discussion_tag(step): def i_see_only_the_settings_and_values(step): world.verify_all_setting_entries( [ - ['Category', "Week 1", True], - ['Display Name', "Discussion Tag", True], - ['Subcategory', "Topic-Level Student-Visible Label", True] + ['Category', "Week 1", False], + ['Display Name', "Discussion Tag", False], + ['Subcategory', "Topic-Level Student-Visible Label", False] ]) diff --git a/cms/djangoapps/contentstore/features/html-editor.py b/cms/djangoapps/contentstore/features/html-editor.py index 054c0ea6420..53462ba0947 100644 --- a/cms/djangoapps/contentstore/features/html-editor.py +++ b/cms/djangoapps/contentstore/features/html-editor.py @@ -14,4 +14,4 @@ def i_created_blank_html_page(step): @step('I see only the HTML display name setting$') def i_see_only_the_html_display_name(step): - world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]]) + world.verify_all_setting_entries([['Display Name', "Blank HTML Page", False]]) diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index 5d12b23d904..15f5da95e9e 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -18,8 +18,9 @@ def i_created_blank_common_problem(step): world.create_component_instance( step, '.large-problem-icon', - 'i4x://edx/templates/problem/Blank_Common_Problem', - '.xmodule_CapaModule' + 'problem', + '.xmodule_CapaModule', + 'blank_common.yaml' ) @@ -32,11 +33,12 @@ def i_edit_and_select_settings(step): def i_see_five_settings_with_values(step): world.verify_all_setting_entries( [ - [DISPLAY_NAME, "Blank Common Problem", True], + [DISPLAY_NAME, "New problem", True], [MAXIMUM_ATTEMPTS, "", False], [PROBLEM_WEIGHT, "", False], - [RANDOMIZATION, "Never", True], - [SHOW_ANSWER, "Finished", True] + # Not sure why these are True other than via inspection + [RANDOMIZATION, "Always", True], + [SHOW_ANSWER, "Closed", True] ]) @@ -203,7 +205,7 @@ def verify_modified_display_name_with_special_chars(): def verify_unset_display_name(): - world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False) + world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'Blank Advanced Problem', False) def set_weight(weight): diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py index a6865fdd6d3..e0f76b30adb 100644 --- a/cms/djangoapps/contentstore/features/video-editor.py +++ b/cms/djangoapps/contentstore/features/video-editor.py @@ -7,7 +7,7 @@ @step('I see the correct settings and default values$') def i_see_the_correct_settings_and_values(step): world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False], - ['Display Name', 'default', True], + ['Display Name', 'Video Title', False], ['Download Track', '', False], ['Download Video', '', False], ['Show Captions', 'True', False], diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index be122fa1a45..6099b60eb19 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -135,7 +135,7 @@ def test_advanced_components_in_edit_unit(self): self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha', 'Word cloud', 'Annotation', - 'Open Ended Response', + 'Open Ended Grading', 'Peer Grading Interface']) def test_advanced_components_require_two_clicks(self): @@ -1271,6 +1271,28 @@ def test_metadata_inheritance(self): self.assertEqual(timedelta(1), new_module.lms.graceperiod) + def test_default_metadata_inheritance(self): + course = CourseFactory.create() + vertical = ItemFactory.create(parent_location=course.location) + course.children.append(vertical) + # in memory + self.assertIsNotNone(course.start) + self.assertEqual(course.start, vertical.lms.start) + self.assertEqual(course.textbooks, []) + self.assertIn('GRADER', course.grading_policy) + self.assertIn('GRADE_CUTOFFS', course.grading_policy) + self.assertGreaterEqual(len(course.checklists), 4) + + # by fetching + module_store = modulestore('direct') + fetched_course = module_store.get_item(course.location) + fetched_item = module_store.get_item(vertical.location) + self.assertIsNotNone(fetched_course.start) + self.assertEqual(course.start, fetched_course.start) + self.assertEqual(fetched_course.start, fetched_item.lms.start) + self.assertEqual(course.textbooks, fetched_course.textbooks) + # is this test too strict? i.e., it requires the dicts to be == + self.assertEqual(course.checklists, fetched_course.checklists) class TemplateTestCase(ModuleStoreTestCase): diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 21d7d69d410..6c23e682403 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -36,7 +36,6 @@ def test_virgin_fetch(self): self.assertIsNone(details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)) self.assertIsNone(details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end)) self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus)) - self.assertEqual(details.overview, "", "overview somehow initialized" + details.overview) self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) @@ -49,7 +48,6 @@ def test_encoder(self): self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_end'], "enrollment_end date somehow initialized ") self.assertIsNone(jsondetails['syllabus'], "syllabus somehow initialized") - self.assertEqual(jsondetails['overview'], "", "overview somehow initialized") self.assertIsNone(jsondetails['intro_video'], "intro_video somehow initialized") self.assertIsNone(jsondetails['effort'], "effort somehow initialized") diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py index 7a74e755914..3914892bbf0 100644 --- a/common/djangoapps/xmodule_modifiers.py +++ b/common/djangoapps/xmodule_modifiers.py @@ -120,7 +120,7 @@ def _get_html(): # doesn't like symlinks) filepath = filename data_dir = osfs.root_path.rsplit('/')[-1] - giturl = getattr(module.lms, 'giturl', '') or 'https://github.com/MITx' + giturl = module.lms.giturl or 'https://github.com/MITx' edit_link = "%s/%s/tree/master/%s" % (giturl, data_dir, filepath) else: edit_link = False diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py index e8674360c30..8b1bbc71d3e 100644 --- a/common/lib/xmodule/xmodule/annotatable_module.py +++ b/common/lib/xmodule/xmodule/annotatable_module.py @@ -6,12 +6,37 @@ from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor from xblock.core import Scope, String +import textwrap log = logging.getLogger(__name__) class AnnotatableFields(object): - data = String(help="XML data for the annotation", scope=Scope.content) + data = String(help="XML data for the annotation", scope=Scope.content, + default=textwrap.dedent( + """\ + + +

Enter your (optional) instructions for the exercise in HTML format.

+

Annotations are specified by an <annotation> tag which may may have the following attributes:

+
    +
  • title (optional). Title of the annotation. Defaults to Commentary if omitted.
  • +
  • body (required). Text of the annotation.
  • +
  • problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".
  • +
  • highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.
  • +
+
+

Add your HTML with annotation spans here.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sodales laoreet est, egestas gravida felis egestas nec. Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.

+

Nulla facilisi. Pellentesque id vestibulum libero. Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.

+
+ """)) + display_name = String( + display_name="Display Name", + help="Display name for this module", + scope=Scope.settings, + default='Annotation', + ) class AnnotatableModule(AnnotatableFields, XModule): diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index eeb8f19439d..752f0d33623 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -77,6 +77,14 @@ class CapaFields(object): """ Define the possible fields for a Capa problem """ + display_name = String( + display_name="Display Name", + help="This name appears in the horizontal navigation at the top of the page.", + scope=Scope.settings, + # it'd be nice to have a useful default but it screws up other things; so, + # use display_name_with_default for those + default="Blank Advanced Problem" + ) attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) max_attempts = Integer( @@ -94,7 +102,8 @@ class CapaFields(object): display_name="Show Answer", help=("Defines when to show the answer to the problem. " "A default value can be set in Advanced Settings."), - scope=Scope.settings, default="closed", + scope=Scope.settings, + default="closed", values=[ {"display_name": "Always", "value": "always"}, {"display_name": "Answered", "value": "answered"}, @@ -106,21 +115,24 @@ class CapaFields(object): ) force_save_button = Boolean( help="Whether to force the save button to appear on the page", - scope=Scope.settings, default=False + scope=Scope.settings, + default=False ) rerandomize = Randomization( display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. " - "This setting only applies to problems that can have randomly generated numeric values. " - "A default value can be set in Advanced Settings.", - default="always", scope=Scope.settings, values=[ + "This setting only applies to problems that can have randomly generated numeric values. " + "A default value can be set in Advanced Settings.", + default="always", + scope=Scope.settings, + values=[ {"display_name": "Always", "value": "always"}, {"display_name": "On Reset", "value": "onreset"}, {"display_name": "Never", "value": "never"}, {"display_name": "Per Student", "value": "per_student"} ] ) - data = String(help="XML data for the problem", scope=Scope.content) + data = String(help="XML data for the problem", scope=Scope.content, default="") correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={}) input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state) @@ -134,13 +146,12 @@ class CapaFields(object): values={"min": 0, "step": .1}, scope=Scope.settings ) - markdown = String(help="Markdown source of this module", scope=Scope.settings) + markdown = String(help="Markdown source of this module", default="", scope=Scope.settings) source_code = String( help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings ) - class CapaModule(CapaFields, XModule): """ An XModule implementing LonCapa format problems, implemented by way of diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 52d98f032ea..13f00bb77ef 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -9,6 +9,7 @@ from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from collections import namedtuple from .fields import Date +import textwrap log = logging.getLogger("mitx.courseware") @@ -27,6 +28,38 @@ } DEFAULT_VERSION = 1 +DEFAULT_DATA = textwrap.dedent("""\ + + + + + Category 1 + + + + + + +

Why is the sky blue?

+
+ + + + + + + Enter essay here. + This is the answer. + {"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"} + + + +
+""") class VersionInteger(Integer): @@ -85,13 +118,30 @@ class CombinedOpenEndedFields(object): scope=Scope.settings ) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) - data = String(help="XML data for the problem", scope=Scope.content) + data = String(help="XML data for the problem", scope=Scope.content, + default=DEFAULT_DATA) weight = Float( display_name="Problem Weight", help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.", scope=Scope.settings, values={"min" : 0 , "step": ".1"} ) - markdown = String(help="Markdown source of this module", scope=Scope.settings) + markdown = String( + help="Markdown source of this module", + default=textwrap.dedent("""\ + [rubric] + + Category 1 + - The response does not incorporate what is needed for a one response. + - The response is correct for category 1. + [rubric] + [prompt] +

Why is the sky blue?

+ [prompt] + [tasks] + (Self), ({1-2}AI) + [tasks] + """), + scope=Scope.settings + ) class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index d75033c8a04..ceadee1c155 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -145,16 +145,56 @@ def to_json(self, values): class CourseFields(object): - textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", scope=Scope.content) + textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", + default=[], scope=Scope.content) wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content) enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings) enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) - start = Date(help="Start time when this module is visible", scope=Scope.settings) + start = Date(help="Start time when this module is visible", + # using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the + # time of first invocation of this stmt on the server + default=datetime.fromtimestamp(0, UTC()), + scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings) advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) - grading_policy = Dict(help="Grading policy definition for this class", scope=Scope.content) + grading_policy = Dict(help="Grading policy definition for this class", + default={"GRADER": [ + { + "type": "Homework", + "min_count": 12, + "drop_count": 2, + "short_label": "HW", + "weight": 0.15 + }, + { + "type": "Lab", + "min_count": 12, + "drop_count": 2, + "weight": 0.15 + }, + { + "type": "Midterm Exam", + "short_label": "Midterm", + "min_count": 1, + "drop_count": 0, + "weight": 0.3 + }, + { + "type": "Final Exam", + "short_label": "Final", + "min_count": 1, + "drop_count": 0, + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.5 + }}, + scope=Scope.content) show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) - display_name = String(help="Display name for this module", scope=Scope.settings) + display_name = String( + help="Display name for this module", default="Empty", + display_name="Display Name", scope=Scope.settings) tabs = List(help="List of tabs to enable in this course", scope=Scope.settings) end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings) discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings) @@ -175,7 +215,125 @@ class CourseFields(object): allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False) advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings) has_children = True - checklists = List(scope=Scope.settings) + checklists = List(scope=Scope.settings, + default=[ + {"short_description" : "Getting Started With Studio", + "items" : [{"short_description": "Add Course Team Members", + "long_description": "Grant your collaborators permission to edit your course so you can work together.", + "is_checked": False, + "action_url": "ManageUsers", + "action_text": "Edit Course Team", + "action_external": False}, + {"short_description": "Set Important Dates for Your Course", + "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Details & Schedule", + "action_external": False}, + {"short_description": "Draft Your Course's Grading Policy", + "long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.", + "is_checked": False, + "action_url": "SettingsGrading", + "action_text": "Edit Grading Settings", + "action_external": False}, + {"short_description": "Explore the Other Studio Checklists", + "long_description": "Discover other available course authoring tools, and find help when you need it.", + "is_checked": False, + "action_url": "", + "action_text": "", + "action_external": False}] + }, + {"short_description" : "Draft a Rough Course Outline", + "items" : [{"short_description": "Create Your First Section and Subsection", + "long_description": "Use your course outline to build your first Section and Subsection.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Set Section Release Dates", + "long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Designate a Subsection as Graded", + "long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Reordering Course Content", + "long_description": "Use drag and drop to reorder the content in your course.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Renaming Sections", + "long_description": "Rename Sections by clicking the Section name from the Course Outline.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Deleting Course Content", + "long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}, + {"short_description": "Add an Instructor-Only Section to Your Outline", + "long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.", + "is_checked": False, + "action_url": "CourseOutline", + "action_text": "Edit Course Outline", + "action_external": False}] + }, + {"short_description" : "Explore edX's Support Tools", + "items" : [{"short_description": "Explore the Studio Help Forum", + "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", + "is_checked": False, + "action_url": "http://help.edge.edx.org/", + "action_text": "Visit Studio Help", + "action_external": True}, + {"short_description": "Enroll in edX 101", + "long_description": "Register for edX 101, edX's primer for course creation.", + "is_checked": False, + "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about", + "action_text": "Register for edX 101", + "action_external": True}, + {"short_description": "Download the Studio Documentation", + "long_description": "Download the searchable Studio reference documentation in PDF form.", + "is_checked": False, + "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", + "action_text": "Download Documentation", + "action_external": True}] + }, + {"short_description" : "Draft Your Course About Page", + "items" : [{"short_description": "Draft a Course Description", + "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Staff Bios", + "long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Course FAQs", + "long_description": "Include a short list of frequently asked questions about your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}, + {"short_description": "Add Course Prerequisites", + "long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.", + "is_checked": False, + "action_url": "SettingsDetails", + "action_text": "Edit Course Schedule & Details", + "action_external": False}] + } + ]) info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) enrollment_domain = String(help="External login method associated with user accounts allowed to register in course", @@ -220,18 +378,16 @@ def __init__(self, *args, **kwargs): self.wiki_slug = self.location.course msg = None - if self.start is None: - msg = "Course loaded without a valid start date. id = %s" % self.id - self.start = datetime.now(UTC()) - log.critical(msg) - self.system.error_tracker(msg) # NOTE: relies on the modulestore to call set_grading_policy() right after # init. (Modulestore is in charge of figuring out where to load the policy from) # NOTE (THK): This is a last-minute addition for Fall 2012 launch to dynamically # disable the syllabus content for courses that do not provide a syllabus - self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) + if self.system.resources_fs is None: + self.syllabus_present = False + else: + self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) self._grading_policy = {} self.set_grading_policy(self.grading_policy) @@ -252,42 +408,33 @@ def __init__(self, *args, **kwargs): log.error(msg) continue - def default_grading_policy(self): - """ - Return a dict which is a copy of the default grading policy - """ - return {"GRADER": [ - { - "type": "Homework", - "min_count": 12, - "drop_count": 2, - "short_label": "HW", - "weight": 0.15 - }, - { - "type": "Lab", - "min_count": 12, - "drop_count": 2, - "weight": 0.15 - }, - { - "type": "Midterm Exam", - "short_label": "Midterm", - "min_count": 1, - "drop_count": 0, - "weight": 0.3 - }, - { - "type": "Final Exam", - "short_label": "Final", - "min_count": 1, - "drop_count": 0, - "weight": 0.4 - } - ], - "GRADE_CUTOFFS": { - "Pass": 0.5 - }} + # TODO check that this is still needed here and can't be by defaults. + if self.tabs is None: + # When calling the various _tab methods, can omit the 'type':'blah' from the + # first arg, since that's only used for dispatch + tabs = [] + tabs.append({'type': 'courseware'}) + tabs.append({'type': 'course_info', 'name': 'Course Info'}) + + if self.syllabus_present: + tabs.append({'type': 'syllabus'}) + + tabs.append({'type': 'textbooks'}) + + # # If they have a discussion link specified, use that even if we feature + # # flag discussions off. Disabling that is mostly a server safety feature + # # at this point, and we don't need to worry about external sites. + if self.discussion_link: + tabs.append({'type': 'external_discussion', 'link': self.discussion_link}) + else: + tabs.append({'type': 'discussion', 'name': 'Discussion'}) + + tabs.append({'type': 'wiki', 'name': 'Wiki'}) + + if not self.hide_progress_tab: + tabs.append({'type': 'progress', 'name': 'Progress'}) + + self.tabs = tabs def set_grading_policy(self, course_policy): """ @@ -298,7 +445,13 @@ def set_grading_policy(self, course_policy): course_policy = {} # Load the global settings as a dictionary - grading_policy = self.default_grading_policy() + grading_policy = self.grading_policy + # BOY DO I HATE THIS grading_policy CODE ACROBATICS YET HERE I ADD MORE (dhm)--this fixes things persisted w/ + # defective grading policy values (but not None) + if 'GRADER' not in grading_policy: + grading_policy['GRADER'] = CourseFields.grading_policy.default['GRADER'] + if 'GRADE_CUTOFFS' not in grading_policy: + grading_policy['GRADE_CUTOFFS'] = CourseFields.grading_policy.default['GRADE_CUTOFFS'] # Override any global settings with the course settings grading_policy.update(course_policy) @@ -354,10 +507,6 @@ def from_xml(cls, xml_data, system, org=None, course=None): system.error_tracker("Unable to decode grading policy as json") policy = {} - # cdodge: import the grading policy information that is on disk and put into the - # descriptor 'definition' bucket as a dictionary so that it is persisted in the DB - instance.grading_policy = policy - # now set the current instance. set_grading_policy() will apply some inheritance rules instance.set_grading_policy(policy) @@ -661,6 +810,7 @@ def try_parse_iso_8601(text): if isinstance(self.advertised_start, basestring): return try_parse_iso_8601(self.advertised_start) elif self.advertised_start is None and self.start is None: + # TODO this is an impossible state since the init function forces start to have a value return 'TBD' else: return (self.advertised_start or self.start).strftime("%b %d, %Y") diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py index aef48218392..fac6a498e57 100644 --- a/common/lib/xmodule/xmodule/discussion_module.py +++ b/common/lib/xmodule/xmodule/discussion_module.py @@ -4,17 +4,27 @@ from xmodule.raw_module import RawDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor from xblock.core import String, Scope +from uuid import uuid4 class DiscussionFields(object): - discussion_id = String(scope=Scope.settings) + discussion_id = String(scope=Scope.settings, default="$$GUID$$") + display_name = String( + display_name="Display Name", + help="Display name for this module", + default="Discussion Tag", + scope=Scope.settings) + data = String(help="XML data for the problem", scope=Scope.content, + default="") discussion_category = String( display_name="Category", + default="Week 1", help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.", scope=Scope.settings ) discussion_target = String( display_name="Subcategory", + default="Topic-Level Student-Visible Label", help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.", scope=Scope.settings ) @@ -36,9 +46,15 @@ def get_html(self): class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor): - module_class = DiscussionModule - template_dir_name = "discussion" + def __init__(self, *args, **kwargs): + super(DiscussionDescriptor, self).__init__(*args, **kwargs) + # is this too late? i.e., will it get persisted and stay static w/ the first value + # any code references. I believe so. + if self.discussion_id == '$$GUID$$': + self.discussion_id = uuid4().hex + + module_class = DiscussionModule # The discussion XML format uses `id` and `for` attributes, # but these would overload other module attributes, so we prefix them # for actual use in the code diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 0f7e7899068..9ff2e4d9a87 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -13,12 +13,21 @@ from xmodule.stringify import stringify_children from xmodule.x_module import XModule from xmodule.xml_module import XmlDescriptor, name_to_pathname +import textwrap log = logging.getLogger("mitx.courseware") class HtmlFields(object): - data = String(help="Html contents to display for this module", scope=Scope.content) + display_name = String( + display_name="Display Name", + help="This name appears in the horizontal navigation at the top of the page.", + scope=Scope.settings, + # it'd be nice to have a useful default but it screws up other things; so, + # use display_name_with_default for those + default="Blank HTML Page" + ) + data = String(help="Html contents to display for this module", default="", scope=Scope.content) source_code = String(help="Source code for LaTeX documents. This feature is not well-supported.", scope=Scope.settings) @@ -32,7 +41,7 @@ class HtmlModule(HtmlFields, XModule): css = {'scss': [resource_string(__name__, 'css/html/display.scss')]} def get_html(self): - if self.system.anonymous_student_id: + if self.system.anonymous_student_id: return self.data.replace("%%USER_ID%%", self.system.anonymous_student_id) return self.data @@ -169,26 +178,88 @@ def definition_to_xml(self, resource_fs): elt.set("filename", relname) return elt +class AboutFields(object): + display_name = String( + help="Display name for this module", + scope=Scope.settings, + default="overview", + ) + data = String( + help="Html contents to display for this module", + default="", + scope=Scope.content + ) + +class AboutModule(AboutFields, HtmlModule): + """ + Overriding defaults but otherwise treated as HtmlModule. + """ + pass -class AboutDescriptor(HtmlDescriptor): +class AboutDescriptor(AboutFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located in order to be able to create new ones """ template_dir_name = "about" + module_class = AboutModule +class StaticTabFields(object): + """ + The overrides for Static Tabs + """ + display_name = String( + display_name="Display Name", + help="This name appears in the horizontal navigation at the top of the page.", + scope=Scope.settings, + default="Empty", + ) + data = String( + default=textwrap.dedent("""\ +

This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing.

+ """), + scope=Scope.content, + help="HTML for the additional pages" + ) + + +class StaticTabModule(StaticTabFields, HtmlModule): + """ + Supports the field overrides + """ + pass -class StaticTabDescriptor(HtmlDescriptor): +class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located in order to be able to create new ones """ template_dir_name = "statictab" + module_class = StaticTabModule + + +class CourseInfoFields(object): + """ + Field overrides + """ + data = String( + help="Html contents to display for this module", + default="
    ", + scope=Scope.content + ) + + +class CourseInfoModule(CourseInfoFields, HtmlModule): + """ + Just to support xblock field overrides + """ + pass -class CourseInfoDescriptor(HtmlDescriptor): +class CourseInfoDescriptor(CourseInfoFields, HtmlDescriptor): """ These pieces of course content are treated as HtmlModules but we need to overload where the templates are located in order to be able to create new ones """ template_dir_name = "courseinfo" + module_class = CourseInfoModule diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 7df444a8923..c88a2e1b388 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -59,6 +59,15 @@ class PeerGradingFields(object): help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.", scope=Scope.settings, values={"min": 0, "step": ".1"} ) + display_name = String( + display_name="Display Name", + help="Display name for this module", + scope=Scope.settings, + default="Peer Grading Interface" + ) + data = String(help="Html contents to display for this module", + default='', + scope=Scope.content) class PeerGradingModule(PeerGradingFields, XModule): diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py index 554be739265..4c6c719224c 100644 --- a/common/lib/xmodule/xmodule/raw_module.py +++ b/common/lib/xmodule/xmodule/raw_module.py @@ -13,7 +13,7 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor): Module that provides a raw editing view of its data and children. It requires that the definition xml is valid. """ - data = String(help="XML data for the module", scope=Scope.content) + data = String(help="XML data for the module", default="", scope=Scope.content) @classmethod def definition_from_xml(cls, xml_object, system): diff --git a/common/lib/xmodule/xmodule/templates/about/empty.yaml b/common/lib/xmodule/xmodule/templates/about/empty.yaml index fa3ed606bdd..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/about/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/about/empty.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: Empty -data: "

    This is where you can add additional information about your course.

    " -children: [] \ No newline at end of file +{} diff --git a/common/lib/xmodule/xmodule/templates/about/overview.yaml b/common/lib/xmodule/xmodule/templates/about/overview.yaml index 0031ebffafd..9b2e8955268 100644 --- a/common/lib/xmodule/xmodule/templates/about/overview.yaml +++ b/common/lib/xmodule/xmodule/templates/about/overview.yaml @@ -3,51 +3,50 @@ metadata: display_name: overview data: | -
    -

    About This Course

    -

    Include your long course description here. The long course description should contain 150-400 words.

    - -

    This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.

    -
    - -
    -

    Prerequisites

    -

    Add information about course prerequisites here.

    -
    - -
    -

    Course Staff

    -
    -
    - -
    - -

    Staff Member #1

    -

    Biography of instructor/staff member #1

    -
    - -
    -
    - -
    - -

    Staff Member #2

    -

    Biography of instructor/staff member #2

    -
    -
    - -
    -
    -

    Frequently Asked Questions

    -
    -

    Do I need to buy a textbook?

    -

    No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.

    -
    - -
    -

    Question #2

    -

    Your answer would be displayed here.

    -
    -
    -
    -children: [] +
    +

    About This Course

    +

    Include your long course description here. The long course description should contain 150-400 words.

    + +

    This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.

    +
    + +
    +

    Prerequisites

    +

    Add information about course prerequisites here.

    +
    + +
    +

    Course Staff

    +
    +
    + +
    + +

    Staff Member #1

    +

    Biography of instructor/staff member #1

    +
    + +
    +
    + +
    + +

    Staff Member #2

    +

    Biography of instructor/staff member #2

    +
    +
    + +
    +
    +

    Frequently Asked Questions

    +
    +

    Do I need to buy a textbook?

    +

    No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.

    +
    + +
    +

    Question #2

    +

    Your answer would be displayed here.

    +
    +
    +
    diff --git a/common/lib/xmodule/xmodule/templates/annotatable/default.yaml b/common/lib/xmodule/xmodule/templates/annotatable/default.yaml index 31dd489fb4e..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/annotatable/default.yaml +++ b/common/lib/xmodule/xmodule/templates/annotatable/default.yaml @@ -1,20 +1 @@ ---- -metadata: - display_name: 'Annotation' -data: | - - -

    Enter your (optional) instructions for the exercise in HTML format.

    -

    Annotations are specified by an <annotation> tag which may may have the following attributes:

    -
      -
    • title (optional). Title of the annotation. Defaults to Commentary if omitted.
    • -
    • body (required). Text of the annotation.
    • -
    • problem (optional). Numeric index of the problem associated with this annotation. This is a zero-based index, so the first problem on the page would have problem="0".
    • -
    • highlight (optional). Possible values: yellow, red, orange, green, blue, or purple. Defaults to yellow if this attribute is omitted.
    • -
    -
    -

    Add your HTML with annotation spans here.

    -

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sodales laoreet est, egestas gravida felis egestas nec. Aenean at volutpat erat. Cras commodo viverra nibh in aliquam.

    -

    Nulla facilisi. Pellentesque id vestibulum libero. Suspendisse potenti. Morbi scelerisque nisi vitae felis dictum mattis. Nam sit amet magna elit. Nullam volutpat cursus est, sit amet sagittis odio vulputate et. Curabitur euismod, orci in vulputate imperdiet, augue lorem tempor purus, id aliquet augue turpis a est. Aenean a sagittis libero. Praesent fringilla pretium magna, non condimentum risus elementum nec. Pellentesque faucibus elementum pharetra. Pellentesque vitae metus eros.

    -
    -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml index f7d639ebfb0..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml +++ b/common/lib/xmodule/xmodule/templates/combinedopenended/default.yaml @@ -1,37 +1 @@ ---- -metadata: - display_name: Open Ended Response - markdown: "" -data: | - - - - - Category 1 - - - - - - -

    Why is the sky blue?

    -
    - - - - - - - Enter essay here. - This is the answer. - {"grader_settings" : "peer_grading.conf", "problem_id" : "700x/Demo"} - - - -
    - -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/course/empty.yaml b/common/lib/xmodule/xmodule/templates/course/empty.yaml index 89f1bfcf215..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/course/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/course/empty.yaml @@ -1,124 +1 @@ ---- -metadata: - display_name: Empty - start: 2020-10-10T10:00 - checklists: [ - {"short_description" : "Getting Started With Studio", - "items" : [{"short_description": "Add Course Team Members", - "long_description": "Grant your collaborators permission to edit your course so you can work together.", - "is_checked": false, - "action_url": "ManageUsers", - "action_text": "Edit Course Team", - "action_external": false}, - {"short_description": "Set Important Dates for Your Course", - "long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.", - "is_checked": false, - "action_url": "SettingsDetails", - "action_text": "Edit Course Details & Schedule", - "action_external": false}, - {"short_description": "Draft Your Course's Grading Policy", - "long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.", - "is_checked": false, - "action_url": "SettingsGrading", - "action_text": "Edit Grading Settings", - "action_external": false}, - {"short_description": "Explore the Other Studio Checklists", - "long_description": "Discover other available course authoring tools, and find help when you need it.", - "is_checked": false, - "action_url": "", - "action_text": "", - "action_external": false}] - }, - {"short_description" : "Draft a Rough Course Outline", - "items" : [{"short_description": "Create Your First Section and Subsection", - "long_description": "Use your course outline to build your first Section and Subsection.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Set Section Release Dates", - "long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Designate a Subsection as Graded", - "long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Reordering Course Content", - "long_description": "Use drag and drop to reorder the content in your course.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Renaming Sections", - "long_description": "Rename Sections by clicking the Section name from the Course Outline.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Deleting Course Content", - "long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}, - {"short_description": "Add an Instructor-Only Section to Your Outline", - "long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.", - "is_checked": false, - "action_url": "CourseOutline", - "action_text": "Edit Course Outline", - "action_external": false}] - }, - {"short_description" : "Explore edX's Support Tools", - "items" : [{"short_description": "Explore the Studio Help Forum", - "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", - "is_checked": false, - "action_url": "http://help.edge.edx.org/", - "action_text": "Visit Studio Help", - "action_external": true}, - {"short_description": "Enroll in edX 101", - "long_description": "Register for edX 101, edX's primer for course creation.", - "is_checked": false, - "action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about", - "action_text": "Register for edX 101", - "action_external": true}, - {"short_description": "Download the Studio Documentation", - "long_description": "Download the searchable Studio reference documentation in PDF form.", - "is_checked": false, - "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", - "action_text": "Download Documentation", - "action_external": true}] - }, - {"short_description" : "Draft Your Course About Page", - "items" : [{"short_description": "Draft a Course Description", - "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.", - "is_checked": false, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": false}, - {"short_description": "Add Staff Bios", - "long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.", - "is_checked": false, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": false}, - {"short_description": "Add Course FAQs", - "long_description": "Include a short list of frequently asked questions about your course.", - "is_checked": false, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": false}, - {"short_description": "Add Course Prerequisites", - "long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.", - "is_checked": false, - "action_url": "SettingsDetails", - "action_text": "Edit Course Schedule & Details", - "action_external": false}] - } - ] -data: { 'textbooks' : [ ], 'wiki_slug' : null } -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml index c6958ed8875..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/courseinfo/empty.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: Empty -data: "
      " -children: [] \ No newline at end of file +{} diff --git a/common/lib/xmodule/xmodule/templates/default/empty.yaml b/common/lib/xmodule/xmodule/templates/default/empty.yaml index a2fb2b58329..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/default/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/default/empty.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: Empty -data: "" -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/discussion/default.yaml b/common/lib/xmodule/xmodule/templates/discussion/default.yaml index 049e34b3e71..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/discussion/default.yaml +++ b/common/lib/xmodule/xmodule/templates/discussion/default.yaml @@ -1,9 +1 @@ ---- -metadata: - display_name: Discussion Tag - for: Topic-Level Student-Visible Label - id: $$GUID$$ - discussion_category: Week 1 -data: | - -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/html/announcement.yaml b/common/lib/xmodule/xmodule/templates/html/announcement.yaml index 82fe8fbc03b..30a8ccb41e2 100644 --- a/common/lib/xmodule/xmodule/templates/html/announcement.yaml +++ b/common/lib/xmodule/xmodule/templates/html/announcement.yaml @@ -1,7 +1,6 @@ --- metadata: - display_name: Announcement - + display_name: Announcement data: |
      1. diff --git a/common/lib/xmodule/xmodule/templates/html/empty.yaml b/common/lib/xmodule/xmodule/templates/html/empty.yaml index 40b005af28c..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/html/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/html/empty.yaml @@ -1,7 +1 @@ ---- -metadata: - display_name: Blank HTML Page - -data: | - -children: [] \ No newline at end of file +{} diff --git a/common/lib/xmodule/xmodule/templates/html/latex_html.yaml b/common/lib/xmodule/xmodule/templates/html/latex_html.yaml index ff92f2aeadf..ba5c4b5c063 100644 --- a/common/lib/xmodule/xmodule/templates/html/latex_html.yaml +++ b/common/lib/xmodule/xmodule/templates/html/latex_html.yaml @@ -1,16 +1,16 @@ --- metadata: display_name: E-text Written in LaTeX - source_code: | - \subsection{Example of E-text in LaTeX} +source_code: | + \subsection{Example of E-text in LaTeX} - It is very convenient to write complex equations in LaTeX. + It is very convenient to write complex equations in LaTeX. - \begin{equation} - x = \frac{-b\pm\sqrt{b^2-4*a*c}}{2a} - \end{equation} + \begin{equation} + x = \frac{-b\pm\sqrt{b^2-4*a*c}}{2a} + \end{equation} - Seize the moment. + Seize the moment. data: | diff --git a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml b/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml index 5d88a18ad86..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml +++ b/common/lib/xmodule/xmodule/templates/peer_grading/default.yaml @@ -1,9 +1 @@ ---- -metadata: - display_name: Peer Grading Interface - max_grade: 1 -data: | - - - -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/problem/blank_common.yaml b/common/lib/xmodule/xmodule/templates/problem/blank_common.yaml new file mode 100644 index 00000000000..3dcac29aba3 --- /dev/null +++ b/common/lib/xmodule/xmodule/templates/problem/blank_common.yaml @@ -0,0 +1,5 @@ +--- +metadata: + display_name: Blank Common Problem + markdown: "" +data: "" diff --git a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml index 56f802a6a36..3b051f2ba83 100644 --- a/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/circuitschematic.yaml @@ -5,59 +5,58 @@ metadata: rerandomize: never showanswer: finished data: | - - Please make a voltage divider that splits the provided voltage evenly. - - -
        - -
        - - dc_value = "dc analysis not found" - for response in submission[0]: - if response[0] == 'dc': - for node in response[1:]: - dc_value = node['output'] - - if dc_value == .5: - correct = ['correct'] - else: - correct = ['incorrect'] - -
        - -

        Make a high pass filter

        -
        - -
        - - ac_values = None - for response in submission[0]: - if response[0] == 'ac': - for node in response[1:]: - ac_values = node['NodeA'] - print "the ac analysis value:", ac_values - if ac_values == None: - correct = ['incorrect'] - elif ac_values[0][1] < ac_values[1][1]: - correct = ['correct'] - else: - correct = ['incorrect'] - -
        - - -
        -

        Explanation

        -

        A voltage divider that evenly divides the input voltage can be formed with two identically valued resistors, with the sampled voltage taken in between the two.

        -

        -

        A simple high-pass filter without any further constaints can be formed by simply putting a resister in series with a capacitor. The actual values of the components do not really matter in order to meet the constraints of the problem.

        -

        -
        -
        -
        -children: [] + + Please make a voltage divider that splits the provided voltage evenly. + + +
        + +
        + + dc_value = "dc analysis not found" + for response in submission[0]: + if response[0] == 'dc': + for node in response[1:]: + dc_value = node['output'] + + if dc_value == .5: + correct = ['correct'] + else: + correct = ['incorrect'] + +
        + +

        Make a high pass filter

        +
        + +
        + + ac_values = None + for response in submission[0]: + if response[0] == 'ac': + for node in response[1:]: + ac_values = node['NodeA'] + print "the ac analysis value:", ac_values + if ac_values == None: + correct = ['incorrect'] + elif ac_values[0][1] < ac_values[1][1]: + correct = ['correct'] + else: + correct = ['incorrect'] + +
        + + +
        +

        Explanation

        +

        A voltage divider that evenly divides the input voltage can be formed with two identically valued resistors, with the sampled voltage taken in between the two.

        +

        +

        A simple high-pass filter without any further constaints can be formed by simply putting a resister in series with a capacitor. The actual values of the components do not really matter in order to meet the constraints of the problem.

        +

        +
        +
        +
        diff --git a/common/lib/xmodule/xmodule/templates/problem/empty.yaml b/common/lib/xmodule/xmodule/templates/problem/empty.yaml index 97a2aef423a..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/problem/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/empty.yaml @@ -1,11 +1 @@ ---- -metadata: - display_name: Blank Common Problem - rerandomize: never - showanswer: finished - markdown: "" -data: | - - - -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml index 410e1496c26..9e26dfeeb6e 100644 --- a/common/lib/xmodule/xmodule/templates/statictab/empty.yaml +++ b/common/lib/xmodule/xmodule/templates/statictab/empty.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: Empty -data: "

        This is where you can add additional pages to your courseware. Click the 'edit' button to begin editing.

        " -children: [] \ No newline at end of file +{} \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/templates/video/default.yaml b/common/lib/xmodule/xmodule/templates/video/default.yaml index 048e7396c71..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/video/default.yaml +++ b/common/lib/xmodule/xmodule/templates/video/default.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: default -data: "" -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index 1c25b272a38..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,11 +1 @@ ---- -metadata: - display_name: Video Alpha - version: 1 -data: | - - - - - -children: [] +{} diff --git a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml index 53e9eeaae46..0967ef424bc 100644 --- a/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml +++ b/common/lib/xmodule/xmodule/templates/word_cloud/default.yaml @@ -1,5 +1 @@ ---- -metadata: - display_name: Word cloud -data: {} -children: [] +{} diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index 3c6203107d1..ebff888f344 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -21,6 +21,17 @@ class VideoFields(object): """Fields for `VideoModule` and `VideoDescriptor`.""" + display_name = String( + display_name="Display Name", + help="This name appears in the horizontal navigation at the top of the page.", + scope=Scope.settings, + # it'd be nice to have a useful default but it screws up other things; so, + # use display_name_with_default for those + default="Video Title" + ) + data = String(help="XML data for the problem", + default='', + scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True) youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM") @@ -129,6 +140,10 @@ def _parse_video_xml(video, xml_data): display_name = xml.get('display_name') if display_name: video.display_name = display_name + elif video.url_name is not None: + # copies the logic of display_name_with_default in order that studio created videos will have an + # initial non guid name + video.display_name = video.url_name.replace('_', ' ') youtube = xml.get('youtube') if youtube: diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 3b5b90e6743..33945c33fc9 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -28,15 +28,27 @@ import datetime import time +import textwrap log = logging.getLogger(__name__) class VideoAlphaFields(object): """Fields for `VideoAlphaModule` and `VideoAlphaDescriptor`.""" - data = String(help="XML data for the problem", scope=Scope.content) + data = String(help="XML data for the problem", + default=textwrap.dedent('''\ + + + + + '''), + scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) - display_name = String(help="Display name for this module", scope=Scope.settings) + display_name = String( + display_name="Display Name", help="Display name for this module", + default="Video Alpha", + scope=Scope.settings + ) class VideoAlphaModule(VideoAlphaFields, XModule): diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index a7f3f927957..004e6ed3206 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -14,7 +14,7 @@ from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.x_module import XModule -from xblock.core import Scope, Dict, Boolean, List, Integer +from xblock.core import Scope, Dict, Boolean, List, Integer, String log = logging.getLogger(__name__) @@ -31,6 +31,12 @@ def pretty_bool(value): class WordCloudFields(object): """XFields for word cloud.""" + display_name = String( + display_name="Display Name", + help="Display name for this module", + scope=Scope.settings, + default="Word cloud" + ) num_inputs = Integer( display_name="Inputs", help="Number of text boxes available for students to input words/sentences.", @@ -234,7 +240,7 @@ def get_html(self): return self.content -class WordCloudDescriptor(MetadataOnlyEditingDescriptor, RawDescriptor, WordCloudFields): +class WordCloudDescriptor(WordCloudFields, MetadataOnlyEditingDescriptor, RawDescriptor): """Descriptor for WordCloud Xmodule.""" module_class = WordCloudModule template_dir_name = 'word_cloud' diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 0f5bbf4f2eb..aee8e261713 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -7,8 +7,8 @@ from collections import namedtuple from pkg_resources import resource_listdir, resource_string, resource_isdir -from xmodule.modulestore import Location -from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore import inheritance, Location +from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError from xblock.core import XBlock, Scope, String, Integer, Float, ModelType @@ -101,6 +101,8 @@ class XModuleFields(object): display_name="Display Name", help="This name appears in the horizontal navigation at the top of the page.", scope=Scope.settings, + # it'd be nice to have a useful default but it screws up other things; so, + # use display_name_with_default for those default=None ) @@ -113,6 +115,14 @@ class XModuleFields(object): scope=Scope.content, default=Location(None), ) + # Please note that in order to be compatible with XBlocks more generally, + # the LMS and CMS shouldn't be using this field. It's only for internal + # consumption by the XModules themselves + category = String( + display_name="xmodule category", + help="This is the category id for the XModule. It's for internal use only", + scope=Scope.content, + ) class XModule(XModuleFields, HTMLSnippet, XBlock): @@ -148,8 +158,16 @@ def __init__(self, runtime, descriptor, model_data): self._model_data = model_data self.system = runtime self.descriptor = descriptor - self.url_name = self.location.name - self.category = self.location.category + # LMS tests don't require descriptor but really it's required + if descriptor: + self.url_name = descriptor.url_name + # don't need to set category as it will automatically get from descriptor + elif isinstance(self.location, Location): + self.url_name = self.location.name + if not hasattr(self, 'category'): + self.category = self.location.category + else: + raise InsufficientSpecificationError() self._loaded_children = None @property @@ -290,36 +308,67 @@ def policy_key(location): class ResourceTemplates(object): + """ + Gets the templates associated w/ a containing cls. The cls must have a 'template_dir_name' attribute. + It finds the templates as directly in this directory under 'templates'. + """ @classmethod def templates(cls): """ - Returns a list of Template objects that describe possible templates that can be used - to create a module of this type. - If no templates are provided, there will be no way to create a module of - this type + Returns a list of dictionary field: value objects that describe possible templates that can be used + to seed a module of this type. Expects a class attribute template_dir_name that defines the directory inside the 'templates' resource directory to pull templates from """ templates = [] - dirname = os.path.join('templates', cls.template_dir_name) - if not resource_isdir(__name__, dirname): - log.warning("No resource directory {dir} found when loading {cls_name} templates".format( - dir=dirname, - cls_name=cls.__name__, - )) - return [] - - for template_file in resource_listdir(__name__, dirname): - if not template_file.endswith('.yaml'): - log.warning("Skipping unknown template file %s" % template_file) - continue - template_content = resource_string(__name__, os.path.join(dirname, template_file)) - template = yaml.safe_load(template_content) - templates.append(Template(**template)) + dirname = cls.get_template_dir() + if dirname is not None: + for template_file in resource_listdir(__name__, dirname): + if not template_file.endswith('.yaml'): + log.warning("Skipping unknown template file %s", template_file) + continue + template_content = resource_string(__name__, os.path.join(dirname, template_file)) + template = yaml.safe_load(template_content) + template['template_id'] = template_file + templates.append(template) return templates + @classmethod + def get_template_dir(cls): + if getattr(cls, 'template_dir_name', None): + dirname = os.path.join('templates', getattr(cls, 'template_dir_name')) + if not resource_isdir(__name__, dirname): + log.warning("No resource directory {dir} found when loading {cls_name} templates".format( + dir=dirname, + cls_name=cls.__name__, + )) + return None + else: + return dirname + else: + return None + + @classmethod + def get_template(cls, template_id): + """ + Get a single template by the given id (which is the file name identifying it w/in the class's + template_dir_name) + + """ + dirname = cls.get_template_dir() + if dirname is not None: + try: + template_content = resource_string(__name__, os.path.join(dirname, template_id)) + except IOError: + return None + template = yaml.safe_load(template_content) + template['template_id'] = template_id + return template + else: + return None + class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): """ @@ -346,9 +395,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): # be equal equality_attributes = ('_model_data', 'location') - # Name of resource directory to load templates from - template_dir_name = "default" - # Class level variable # True if this descriptor always requires recalculation of grades, for @@ -386,8 +432,12 @@ def __init__(self, *args, **kwargs): """ super(XModuleDescriptor, self).__init__(*args, **kwargs) self.system = self.runtime - self.url_name = self.location.name - self.category = self.location.category + if isinstance(self.location, Location): + self.url_name = self.location.name + if not hasattr(self, 'category'): + self.category = self.location.category + else: + raise InsufficientSpecificationError() self._child_instances = None @property @@ -419,11 +469,14 @@ def get_children(self): if self._child_instances is None: self._child_instances = [] for child_loc in self.children: - try: - child = self.system.load_item(child_loc) - except ItemNotFoundError: - log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc)) - continue + if isinstance(child_loc, XModuleDescriptor): + child = child_loc + else: + try: + child = self.system.load_item(child_loc) + except ItemNotFoundError: + log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc)) + continue self._child_instances.append(child) return self._child_instances @@ -591,6 +644,13 @@ def get_sample_state(self): """ return [('{}', '{}')] + @property + def xblock_kvs(self): + """ + Use w/ caution. Really intended for use by the persistence layer. + """ + return self._model_data._kvs + # =============================== BUILTIN METHODS ========================== def __eq__(self, other): eq = (self.__class__ == other.__class__ and diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py index aaef0b76db4..a78e27e5af7 100644 --- a/lms/xmodule_namespace.py +++ b/lms/xmodule_namespace.py @@ -3,6 +3,8 @@ """ from xblock.core import Namespace, Boolean, Scope, String, Float from xmodule.fields import Date, Timedelta +from datetime import datetime +from pytz import UTC class LmsNamespace(Namespace): @@ -25,7 +27,11 @@ class LmsNamespace(Namespace): scope=Scope.settings, ) - start = Date(help="Start time when this module is visible", scope=Scope.settings) + start = Date( + help="Start time when this module is visible", + default=datetime.fromtimestamp(0, UTC), + scope=Scope.settings + ) due = Date(help="Date that this problem is due by", scope=Scope.settings) source_file = String(help="source file name (eg for latex)", scope=Scope.settings) giturl = String(help="url root for course data git repository", scope=Scope.settings)