From 85fb7b8ad29e24d6969daf690e005f4fbdee8e43 Mon Sep 17 00:00:00 2001 From: Sanford Student Date: Wed, 11 Jan 2017 14:39:10 -0500 Subject: [PATCH 01/10] add index for modified to course and subsection grades --- lms/djangoapps/grades/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index e95cda16e0..e53ca6f674 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -253,6 +253,14 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel): # * Course staff can see all grades for a course using (course_id,) ('course_id', 'user_id', 'usage_key'), ] + # Allows querying in the following ways: + # (modified): find all the grades updated within a certain timespan + # (modified, course_id): find all the grades updated within a timespan for a certain course + # (modified, course_id, usage_key): find all the grades updated within a timespan for a subsection + # in a course + index_together = [ + ('modified', 'course_id', 'usage_key') + ] # primary key will need to be large for this table id = UnsignedBigIntAutoField(primary_key=True) # pylint: disable=invalid-name @@ -502,11 +510,14 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel): # (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint # (user_id) for course dashboard; explicitly declared as an index below # (passed_timestamp, course_id) for tracking when users first earned a passing grade. + # (modified): find all the grades updated within a certain timespan + # (modified, course_id): find all the grades updated within a certain timespan for a course unique_together = [ ('course_id', 'user_id'), ] index_together = [ ('passed_timestamp', 'course_id'), + ('modified', 'course_id') ] # primary key will need to be large for this table From 434d4038078b9c919dd536836a0856971d3343fc Mon Sep 17 00:00:00 2001 From: Sanford Student Date: Wed, 11 Jan 2017 15:23:21 -0500 Subject: [PATCH 02/10] forgot migration --- .../migrations/0009_auto_20170111_1507.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lms/djangoapps/grades/migrations/0009_auto_20170111_1507.py diff --git a/lms/djangoapps/grades/migrations/0009_auto_20170111_1507.py b/lms/djangoapps/grades/migrations/0009_auto_20170111_1507.py new file mode 100644 index 0000000000..d71c45a811 --- /dev/null +++ b/lms/djangoapps/grades/migrations/0009_auto_20170111_1507.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('grades', '0008_persistentsubsectiongrade_first_attempted'), + ] + + operations = [ + migrations.AlterIndexTogether( + name='persistentcoursegrade', + index_together=set([('passed_timestamp', 'course_id'), ('modified', 'course_id')]), + ), + migrations.AlterIndexTogether( + name='persistentsubsectiongrade', + index_together=set([('modified', 'course_id', 'usage_key')]), + ), + ] From 4e6745f3c14039121dc8c352c9e3ef2e54bf8bbf Mon Sep 17 00:00:00 2001 From: Kevin Falcone Date: Wed, 11 Jan 2017 16:45:03 -0500 Subject: [PATCH 03/10] These tasks tend to be very short but can queue behind grade reports Here are the 3 tasks over the last 30 days with average duration runtimes in seconds: lms.djangoapps.instructor_task.tasks.calculate_students_features_csv 37.794393 lms.djangoapps.instructor_task.tasks.calculate_may_enroll_csv 0.030303 lms.djangoapps.instructor_task.tasks.export_ora2_data 67.629921 All but ora2 seem fine to pass through the default queue, which has 3 workers per physical machine. --- lms/djangoapps/instructor_task/tasks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py index 5a24516701..7ad7cf2a60 100644 --- a/lms/djangoapps/instructor_task/tasks.py +++ b/lms/djangoapps/instructor_task/tasks.py @@ -193,7 +193,7 @@ def calculate_problem_grade_report(entry_id, xmodule_instance_args): return run_main_task(entry_id, task_fn, action_name) -@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable +@task(base=BaseInstructorTask) # pylint: disable=not-callable def calculate_students_features_csv(entry_id, xmodule_instance_args): """ Compute student profile information for a course and upload the @@ -252,7 +252,7 @@ def proctored_exam_results_csv(entry_id, xmodule_instance_args): return run_main_task(entry_id, task_fn, action_name) -@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable +@task(base=BaseInstructorTask) # pylint: disable=not-callable def calculate_may_enroll_csv(entry_id, xmodule_instance_args): """ Compute information about invited students who have not enrolled @@ -293,7 +293,7 @@ def cohort_students(entry_id, xmodule_instance_args): return run_main_task(entry_id, task_fn, action_name) -@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable +@task(base=BaseInstructorTask) # pylint: disable=not-callable def export_ora2_data(entry_id, xmodule_instance_args): """ Generate a CSV of ora2 responses and push it to S3. From e1e47793dff29bdb6e6d63ece52f722ced9db43e Mon Sep 17 00:00:00 2001 From: Gregory Martin Date: Tue, 10 Jan 2017 13:25:28 -0500 Subject: [PATCH 04/10] change help strings --- .../lib/xmodule/xmodule/annotatable_module.py | 2 +- common/lib/xmodule/xmodule/capa_base.py | 2 +- .../lib/xmodule/xmodule/conditional_module.py | 2 +- common/lib/xmodule/xmodule/html_module.py | 6 ++-- .../xmodule/xmodule/imageannotation_module.py | 2 +- .../xmodule/xmodule/library_content_module.py | 2 +- .../xmodule/xmodule/library_root_xblock.py | 2 +- common/lib/xmodule/xmodule/lti_module.py | 2 +- common/lib/xmodule/xmodule/poll_module.py | 35 +++++++++++++++---- .../lib/xmodule/xmodule/split_test_module.py | 2 +- .../xmodule/xmodule/tests/test_xml_module.py | 8 +++-- .../xmodule/xmodule/textannotation_module.py | 2 +- .../xmodule/video_module/video_xfields.py | 2 +- .../xmodule/xmodule/videoannotation_module.py | 2 +- .../lib/xmodule/xmodule/word_cloud_module.py | 2 +- common/lib/xmodule/xmodule/x_module.py | 9 +++-- .../xblock_discussion/xblock_discussion.py | 2 +- 17 files changed, 58 insertions(+), 26 deletions(-) diff --git a/common/lib/xmodule/xmodule/annotatable_module.py b/common/lib/xmodule/xmodule/annotatable_module.py index 338a5fe067..36428d2acb 100644 --- a/common/lib/xmodule/xmodule/annotatable_module.py +++ b/common/lib/xmodule/xmodule/annotatable_module.py @@ -39,7 +39,7 @@ class AnnotatableFields(object): ) display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The display name for this component."), scope=Scope.settings, default=_('Annotation'), ) diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py index 783687fc8e..d02627af30 100644 --- a/common/lib/xmodule/xmodule/capa_base.py +++ b/common/lib/xmodule/xmodule/capa_base.py @@ -92,7 +92,7 @@ class CapaFields(object): """ display_name = String( display_name=_("Display Name"), - help=_("This name appears in the horizontal navigation at the top of the page."), + help=_("The display name for this component."), 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 diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index a2fa824079..0c7fc1e9d2 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -27,7 +27,7 @@ class ConditionalFields(object): has_children = True display_name = String( display_name=_("Display Name"), - help=_("This name appears in the horizontal navigation at the top of the page."), + help=_("The display name for this component."), scope=Scope.settings, default=_('Conditional') ) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 903606a5fa..fc310b70d2 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -38,7 +38,7 @@ class HtmlBlock(object): """ display_name = String( display_name=_("Display Name"), - help=_("This name appears in the horizontal navigation at the top of the page."), + help=_("The display name for this component."), 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 @@ -329,7 +329,7 @@ class HtmlDescriptor(HtmlBlock, XmlDescriptor, EditingDescriptor): # pylint: di class AboutFields(object): display_name = String( - help=_("Display name for this module"), + help=_("The display name for this component."), scope=Scope.settings, default="overview", ) @@ -364,7 +364,7 @@ class StaticTabFields(object): """ display_name = String( display_name=_("Display Name"), - help=_("This name appears in the horizontal navigation at the top of the page."), + help=_("The display name for this component."), scope=Scope.settings, default="Empty", ) diff --git a/common/lib/xmodule/xmodule/imageannotation_module.py b/common/lib/xmodule/xmodule/imageannotation_module.py index e9d68ebac1..c9b361b84a 100644 --- a/common/lib/xmodule/xmodule/imageannotation_module.py +++ b/common/lib/xmodule/xmodule/imageannotation_module.py @@ -45,7 +45,7 @@ class AnnotatableFields(object): """)) display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The display name for this component."), scope=Scope.settings, default=_('Image Annotation'), ) diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py index ae46a775b5..cd06b49ec4 100644 --- a/common/lib/xmodule/xmodule/library_content_module.py +++ b/common/lib/xmodule/xmodule/library_content_module.py @@ -60,7 +60,7 @@ class LibraryContentFields(object): # to locate input elements - keep synchronized display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The display name for this component."), default="Randomized Content Block", scope=Scope.settings, ) diff --git a/common/lib/xmodule/xmodule/library_root_xblock.py b/common/lib/xmodule/xmodule/library_root_xblock.py index 6c9466d889..f546f2f14a 100644 --- a/common/lib/xmodule/xmodule/library_root_xblock.py +++ b/common/lib/xmodule/xmodule/library_root_xblock.py @@ -25,7 +25,7 @@ class LibraryRoot(XBlock): resources_dir = None display_name = String( - help=_("Enter the name of the library as it should appear in Studio."), + help=_("The display name for this component."), default="Library", display_name=_("Library Display Name"), scope=Scope.settings diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index df02c6ce9c..84153f621c 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -109,7 +109,7 @@ class LTIFields(object): display_name = String( display_name=_("Display Name"), help=_( - "Enter the name that students see for this component. " + "The display name for this component. " "Analytics reports may also use the display name to identify this component." ), scope=Scope.settings, diff --git a/common/lib/xmodule/xmodule/poll_module.py b/common/lib/xmodule/xmodule/poll_module.py index 9b86d01ab2..a39cd34a9a 100644 --- a/common/lib/xmodule/xmodule/poll_module.py +++ b/common/lib/xmodule/xmodule/poll_module.py @@ -23,20 +23,43 @@ from xmodule.xml_module import XmlDescriptor from xblock.fields import Scope, String, Dict, Boolean, List log = logging.getLogger(__name__) +_ = lambda text: text class PollFields(object): # Name of poll to use in links to this poll - display_name = String(help="Display name for this module", scope=Scope.settings) + display_name = String( + help=_("The display name for this component."), + scope=Scope.settings + ) - voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False) - poll_answer = String(help="Student answer", scope=Scope.user_state, default='') - poll_answers = Dict(help="Poll answers from all students", scope=Scope.user_state_summary) + voted = Boolean( + help=_("Whether this student has voted on the poll"), + scope=Scope.user_state, + default=False + ) + poll_answer = String( + help=_("Student answer"), + scope=Scope.user_state, + default='' + ) + poll_answers = Dict( + help=_("Poll answers from all students"), + scope=Scope.user_state_summary + ) # List of answers, in the form {'id': 'some id', 'text': 'the answer text'} - answers = List(help="Poll answers from xml", scope=Scope.content, default=[]) + answers = List( + help=_("Poll answers from xml"), + scope=Scope.content, + default=[] + ) - question = String(help="Poll question", scope=Scope.content, default='') + question = String( + help=_("Poll question"), + scope=Scope.content, + default='' + ) class PollModule(PollFields, XModule): diff --git a/common/lib/xmodule/xmodule/split_test_module.py b/common/lib/xmodule/xmodule/split_test_module.py index eb127297f8..144460590e 100644 --- a/common/lib/xmodule/xmodule/split_test_module.py +++ b/common/lib/xmodule/xmodule/split_test_module.py @@ -58,7 +58,7 @@ class SplitTestFields(object): display_name = String( display_name=_("Display Name"), - help=_("This name is used for organizing your course content, but is not shown to students."), + help=_("The display name for this component. (Not shown to learners)"), scope=Scope.settings, default=_("Content Experiment") ) diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py index dcc0f2310d..290d5b1000 100644 --- a/common/lib/xmodule/xmodule/tests/test_xml_module.py +++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py @@ -37,8 +37,12 @@ class TestFields(object): # Will not be returned by editable_metadata_fields because is not Scope.settings. student_answers = Dict(scope=Scope.user_state) # Will be returned, and can override the inherited value from XModule. - display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name', - help='local help') + display_name = String( + scope=Scope.settings, + default='local default', + display_name='Local Display Name', + help='local help' + ) # Used for testing select type, effect of to_json method string_select = CrazyJsonString( scope=Scope.settings, diff --git a/common/lib/xmodule/xmodule/textannotation_module.py b/common/lib/xmodule/xmodule/textannotation_module.py index dfcae252be..beff2b8454 100644 --- a/common/lib/xmodule/xmodule/textannotation_module.py +++ b/common/lib/xmodule/xmodule/textannotation_module.py @@ -34,7 +34,7 @@ class AnnotatableFields(object): """)) display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The display name for this component."), scope=Scope.settings, default=_('Text Annotation'), ) diff --git a/common/lib/xmodule/xmodule/video_module/video_xfields.py b/common/lib/xmodule/xmodule/video_module/video_xfields.py index a24fe56206..cec0036391 100644 --- a/common/lib/xmodule/xmodule/video_module/video_xfields.py +++ b/common/lib/xmodule/xmodule/video_module/video_xfields.py @@ -14,7 +14,7 @@ _ = lambda text: text class VideoFields(object): """Fields for `VideoModule` and `VideoDescriptor`.""" display_name = String( - help=_("The name students see. This name appears in the course ribbon and as a header for the video."), + help=_("The display name for this component."), display_name=_("Component Display Name"), default="Video", scope=Scope.settings diff --git a/common/lib/xmodule/xmodule/videoannotation_module.py b/common/lib/xmodule/xmodule/videoannotation_module.py index 945d676594..30ad2bfdf7 100644 --- a/common/lib/xmodule/xmodule/videoannotation_module.py +++ b/common/lib/xmodule/xmodule/videoannotation_module.py @@ -34,7 +34,7 @@ class AnnotatableFields(object): """)) display_name = String( display_name=_("Display Name"), - help=_("Display name for this module"), + help=_("The display name for this component."), scope=Scope.settings, default=_('Video Annotation'), ) diff --git a/common/lib/xmodule/xmodule/word_cloud_module.py b/common/lib/xmodule/xmodule/word_cloud_module.py index 6e12843efb..e80c556d8f 100644 --- a/common/lib/xmodule/xmodule/word_cloud_module.py +++ b/common/lib/xmodule/xmodule/word_cloud_module.py @@ -38,7 +38,7 @@ class WordCloudFields(object): """XFields for word cloud.""" display_name = String( display_name=_("Display Name"), - help=_("The label for this word cloud on the course page."), + help=_("The display name for this component."), scope=Scope.settings, default="Word cloud" ) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 1a3df2e956..7e707f4884 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -66,6 +66,11 @@ STUDIO_VIEW = 'studio_view' PREVIEW_VIEWS = [STUDENT_VIEW, AUTHOR_VIEW] +# Make '_' a no-op so we can scrape strings. Using lambda instead of +# `django.utils.translation.ugettext_noop` because Django cannot be imported in this file +_ = lambda text: text + + class OpaqueKeyReader(IdReader): """ IdReader for :class:`DefinitionKey` and :class:`UsageKey`s. @@ -256,8 +261,8 @@ class XModuleFields(object): Common fields for XModules. """ display_name = String( - display_name="Display Name", - help="This name appears in the horizontal navigation at the top of the page.", + display_name=_("Display Name"), + help=_("The display name for this component."), 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 diff --git a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py index 14f83f8c81..44341eef53 100644 --- a/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py +++ b/openedx/core/lib/xblock_builtin/xblock_discussion/xblock_discussion.py @@ -39,7 +39,7 @@ class DiscussionXBlock(XBlock, StudioEditableXBlockMixin, XmlParserMixin): discussion_id = String(scope=Scope.settings, default=UNIQUE_ID) display_name = String( display_name=_("Display Name"), - help=_("Display name for this component"), + help=_("The display name for this component."), default="Discussion", scope=Scope.settings ) From 6845c64977bf9c57614475ca3608c3ab49c91e9f Mon Sep 17 00:00:00 2001 From: Eric Fischer Date: Tue, 27 Dec 2016 16:56:11 -0500 Subject: [PATCH 05/10] Hide after course end Hijacks the existing hide_after_due field to be repected as "hide after course end" for self-paced courses. Includes tests. TNL-6108 --- cms/djangoapps/contentstore/views/item.py | 1 + .../js/views/modals/course_outline_modals.js | 5 +- .../js/content-visibility-editor.underscore | 16 ++- cms/templates/js/course-outline.underscore | 7 +- common/lib/xmodule/xmodule/seq_module.py | 32 ++--- .../xmodule/xmodule/tests/test_sequence.py | 133 +++++++++--------- .../test/acceptance/pages/lms/courseware.py | 4 +- .../transformers/hidden_content.py | 14 +- lms/djangoapps/courseware/tests/test_views.py | 2 +- lms/templates/hidden_content.html | 49 ++++--- 10 files changed, 151 insertions(+), 112 deletions(-) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index df448141fd..f1789eeff8 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -981,6 +981,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F "release_date": release_date, "visibility_state": visibility_state, "has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock), + "self_paced": is_self_paced(course), "start": xblock.fields['start'].to_json(xblock.start), "graded": xblock.graded, "due_date": get_default_time_display(xblock.due), diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js index 281c4243d3..8bfe529220 100644 --- a/cms/static/js/views/modals/course_outline_modals.js +++ b/cms/static/js/views/modals/course_outline_modals.js @@ -712,7 +712,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview', return $.extend( {}, AbstractVisibilityEditor.prototype.getContext.call(this), - {hide_after_due: this.modelVisibility() === 'hide_after_due'} + { + hide_after_due: this.modelVisibility() === 'hide_after_due', + self_paced: this.model.get('self_paced') === true + } ); } }); diff --git a/cms/templates/js/content-visibility-editor.underscore b/cms/templates/js/content-visibility-editor.underscore index e80830ecd0..e66ae634c8 100644 --- a/cms/templates/js/content-visibility-editor.underscore +++ b/cms/templates/js/content-visibility-editor.underscore @@ -12,9 +12,19 @@
  • -

    <%- gettext('After the subsection\'s due date has passed, learners can no longer access its content. The subsection remains included in grade calculations.') %>

    +

    + <% if (self_paced) { %> + <%- gettext('After the course\'s end date has passed, learners can no longer access subsection content. The subsection remains included in grade calculations.') %> + <% } else { %> + <%- gettext('After the subsection\'s due date has passed, learners can no longer access its content. The subsection remains included in grade calculations.') %> + <% } %> +

  • -

    - <% if (self_paced) { %> - <%- gettext('After the course\'s end date has passed, learners can no longer access subsection content. The subsection remains included in grade calculations.') %> - <% } else { %> - <%- gettext('After the subsection\'s due date has passed, learners can no longer access its content. The subsection remains included in grade calculations.') %> - <% } %> -

    +

    <%- gettext('After the subsection\'s due date has passed, learners can no longer access its content. The subsection remains included in grade calculations.') %>