diff --git a/course/flow.py b/course/flow.py index e57807ce40db252327541ba6f21d7626108a72e3..f8d4aae25fd69e09343af849d80689b51ece411a 100644 --- a/course/flow.py +++ b/course/flow.py @@ -617,11 +617,12 @@ def get_session_answered_page_data( flow_session, # type: FlowSession answer_visits # type: List[Optional[FlowPageVisit]] ): - # type: (...) -> Tuple[List[FlowPageData], List[FlowPageData]] + # type: (...) -> Tuple[List[FlowPageData], List[FlowPageData], bool] all_page_data = get_all_page_data(flow_session) answered_page_data_list = [] # type: List[FlowPageData] unanswered_page_data_list = [] # type: List[FlowPageData] + is_interactive_flow = False # type: bool for i, page_data in enumerate(all_page_data): assert i == page_data.ordinal @@ -634,12 +635,14 @@ def get_session_answered_page_data( page = instantiate_flow_page_with_ctx(fctx, page_data) if page.expects_answer(): - if answer_data is None: - unanswered_page_data_list.append(page_data) - else: - answered_page_data_list.append(page_data) + is_interactive_flow = True + if not page.is_optional_page: + if answer_data is None: + unanswered_page_data_list.append(page_data) + else: + answered_page_data_list.append(page_data) - return (answered_page_data_list, unanswered_page_data_list) + return (answered_page_data_list, unanswered_page_data_list, is_interactive_flow) class GradeInfo(object): @@ -661,6 +664,10 @@ class GradeInfo(object): partially_correct_count, # type: int incorrect_count, # type: int unknown_count, # type: int + optional_fully_correct_count=0, # type: int + optional_partially_correct_count=0, # type: int + optional_incorrect_count=0, # type: int + optional_unknown_count=0, # type: int ): # type: (...) -> None self.points = points @@ -671,6 +678,10 @@ class GradeInfo(object): self.partially_correct_count = partially_correct_count self.incorrect_count = incorrect_count self.unknown_count = unknown_count + self.optional_fully_correct_count = optional_fully_correct_count + self.optional_partially_correct_count = optional_partially_correct_count + self.optional_incorrect_count = optional_incorrect_count + self.optional_unknown_count = optional_unknown_count # Rounding to larger than 100% will break the percent bars on the # flow results page. @@ -739,6 +750,32 @@ class GradeInfo(object): """Only to be used for visualization purposes.""" return self.FULL_PERCENT*self.unknown_count/self.total_count() + def optional_total_count(self): + return (self.optional_fully_correct_count + + self.optional_partially_correct_count + + self.optional_incorrect_count + + self.optional_unknown_count) + + def optional_fully_correct_percent(self): + """Only to be used for visualization purposes.""" + return self.FULL_PERCENT * self.optional_fully_correct_count\ + / self.optional_total_count() + + def optional_partially_correct_percent(self): + """Only to be used for visualization purposes.""" + return self.FULL_PERCENT * self.optional_partially_correct_count\ + / self.optional_total_count() + + def optional_incorrect_percent(self): + """Only to be used for visualization purposes.""" + return self.FULL_PERCENT * self.optional_incorrect_count\ + / self.optional_total_count() + + def optional_unknown_percent(self): + """Only to be used for visualization purposes.""" + return self.FULL_PERCENT * self.optional_unknown_count\ + / self.optional_total_count() + # }}} @@ -766,6 +803,11 @@ def gather_grade_info( incorrect_count = 0 unknown_count = 0 + optional_fully_correct_count = 0 + optional_partially_correct_count = 0 + optional_incorrect_count = 0 + optional_unknown_count = 0 + for i, page_data in enumerate(all_page_data): page = instantiate_flow_page_with_ctx(fctx, page_data) @@ -789,29 +831,42 @@ def gather_grade_info( feedback = get_feedback_for_grade(grade) - max_points += grade.max_points + if page.is_optional_page: + if feedback is None or feedback.correctness is None: + optional_unknown_count += 1 + continue - if feedback is None or feedback.correctness is None: - unknown_count += 1 - points = None - continue + if feedback.correctness == 1: + optional_fully_correct_count += 1 + elif feedback.correctness == 0: + optional_incorrect_count += 1 + else: + optional_partially_correct_count += 1 - max_reachable_points += grade.max_points + else: + max_points += grade.max_points - page_points = grade.max_points*feedback.correctness + if feedback is None or feedback.correctness is None: + unknown_count += 1 + points = None + continue - if points is not None: - points += page_points + max_reachable_points += grade.max_points - provisional_points += page_points + page_points = grade.max_points*feedback.correctness - if grade.max_points > 0: - if feedback.correctness == 1: - fully_correct_count += 1 - elif feedback.correctness == 0: - incorrect_count += 1 - else: - partially_correct_count += 1 + if points is not None: + points += page_points + + provisional_points += page_points + + if grade.max_points > 0: + if feedback.correctness == 1: + fully_correct_count += 1 + elif feedback.correctness == 0: + incorrect_count += 1 + else: + partially_correct_count += 1 # {{{ adjust max_points if requested @@ -841,7 +896,12 @@ def gather_grade_info( fully_correct_count=fully_correct_count, partially_correct_count=partially_correct_count, incorrect_count=incorrect_count, - unknown_count=unknown_count) + unknown_count=unknown_count, + + optional_fully_correct_count=optional_fully_correct_count, + optional_partially_correct_count=optional_partially_correct_count, + optional_incorrect_count=optional_incorrect_count, + optional_unknown_count=optional_unknown_count) @transaction.atomic @@ -1959,6 +2019,10 @@ def view_flow_page(pctx, flow_session_id, ordinal): args["max_points"] = fpctx.page.max_points(fpctx.page_data) args["page_expect_answer_and_gradable"] = True + if fpctx.page.is_optional_page: + assert not getattr(args, "max_points", None) + args["is_optional_page"] = True + return render_course_page( pctx, "course/flow-page.html", args, allow_instant_flow_requests=False) @@ -2419,15 +2483,10 @@ def finish_flow_session_view(pctx, flow_session_id): answer_visits = assemble_answer_visits(flow_session) # type: List[Optional[FlowPageVisit]] # noqa - (answered_page_data_list, unanswered_page_data_list) =\ + (answered_page_data_list, unanswered_page_data_list, is_interactive_flow) =\ get_session_answered_page_data( fctx, flow_session, answer_visits) - answered_count = len(answered_page_data_list) - unanswered_count = len(unanswered_page_data_list) - - is_interactive_flow = bool(answered_count + unanswered_count) # type: bool - if flow_permission.view not in access_rule.permissions: raise PermissionDenied() @@ -2557,6 +2616,11 @@ def finish_flow_session_view(pctx, flow_session_id): else: # confirm ending flow + answered_count = len(answered_page_data_list) + unanswered_count = len(unanswered_page_data_list) + required_count = answered_count + unanswered_count + session_may_generate_grade = ( + grading_rule.generates_grade and required_count) return render_finish_response( "course/flow-confirm-completion.html", last_page_nr=flow_session.page_count-1, @@ -2564,7 +2628,8 @@ def finish_flow_session_view(pctx, flow_session_id): answered_count=answered_count, unanswered_count=unanswered_count, unanswered_page_data_list=unanswered_page_data_list, - total_count=answered_count+unanswered_count) + required_count=required_count, + session_may_generate_grade=session_may_generate_grade) # }}} diff --git a/course/page/base.py b/course/page/base.py index a5742323b4a021056f0d78d8c01d9e101de7b2e8..d813dbc89fe0c8af99baf4a64402016a69abd2e4 100644 --- a/course/page/base.py +++ b/course/page/base.py @@ -337,6 +337,7 @@ class PageBase(object): # }}} self.page_desc = page_desc + self.is_optional_page = getattr(page_desc, "is_optional_page", False) else: from warnings import warn @@ -366,6 +367,7 @@ class PageBase(object): return ( ("access_rules", Struct), + ("is_optional_page", bool), ) def get_modified_permissions_for_page(self, permissions): @@ -731,6 +733,16 @@ class PageBaseWithTitle(PageBase): class PageBaseWithValue(PageBase): + def __init__(self, vctx, location, page_desc): + super(PageBaseWithValue, self).__init__(vctx, location, page_desc) + + if vctx is not None: + if hasattr(page_desc, "value") and self.is_optional_page: + raise ValidationError( + location, + _("Attribute 'value' should be removed when " + "'is_optional_page' is True.")) + def allowed_attrs(self): return super(PageBaseWithValue, self).allowed_attrs() + ( ("value", (int, float)), @@ -740,6 +752,8 @@ class PageBaseWithValue(PageBase): return True def max_points(self, page_data): + if self.is_optional_page: + return 0 return getattr(self.page_desc, "value", 1) diff --git a/course/page/choice.py b/course/page/choice.py index b25ddaf42369021abcb2dbef77537cacba209bd9..7db289b2f718de4be09d8b048f21afe948085709 100644 --- a/course/page/choice.py +++ b/course/page/choice.py @@ -193,6 +193,10 @@ class ChoiceQuestion(ChoiceQuestionBase): ``ChoiceQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| @@ -328,6 +332,10 @@ class MultipleChoiceQuestion(ChoiceQuestionBase): ``MultipleChoiceQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| @@ -568,6 +576,10 @@ class SurveyChoiceQuestion(PageBaseWithTitle): ``ChoiceQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| diff --git a/course/page/code.py b/course/page/code.py index 44c2173128a686c3c19d6ec12ab2326310712573..b00355ff25ad42f32d918a3b5d3d4bfcd7b804bf 100644 --- a/course/page/code.py +++ b/course/page/code.py @@ -312,6 +312,10 @@ class PythonCodeQuestion(PageBaseWithTitle, PageBaseWithValue): ``PythonCodeQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| diff --git a/course/page/inline.py b/course/page/inline.py index d1fb92b393e229bd4c910bfcf1c37a38650e36bb..d912f752650955195d81fde114b7fc45e5f3ec3b 100644 --- a/course/page/inline.py +++ b/course/page/inline.py @@ -521,6 +521,10 @@ class InlineMultiQuestion(TextQuestionBase, PageBaseWithValue): ``InlineMultiQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| diff --git a/course/page/text.py b/course/page/text.py index 33b964d6e29272ad4b5e2fc83059adec32c1b5fd..cb08c1b82cff344b940a996e73034baa5e560849 100644 --- a/course/page/text.py +++ b/course/page/text.py @@ -632,6 +632,10 @@ class TextQuestionBase(PageBaseWithTitle): ``TextQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| @@ -751,6 +755,10 @@ class SurveyTextQuestion(TextQuestionBase): ``TextQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| @@ -810,6 +818,10 @@ class TextQuestion(TextQuestionBase, PageBaseWithValue): ``TextQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| @@ -986,6 +998,10 @@ class HumanGradedTextQuestion(TextQuestionBase, PageBaseWithValue, ``HumanGradedTextQuestion`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| diff --git a/course/page/upload.py b/course/page/upload.py index d4c066ae1bd595bbaaa40c2d0cb204634c129b40..22e018d2717304d82524ee004c66921666026955 100644 --- a/course/page/upload.py +++ b/course/page/upload.py @@ -97,6 +97,10 @@ class FileUploadQuestion(PageBaseWithTitle, PageBaseWithValue, ``Page`` + .. attribute:: is_optional_page + + |is-optional-page-attr| + .. attribute:: access_rules |access-rules-page-attr| diff --git a/course/templates/course/flow-completion-grade.html b/course/templates/course/flow-completion-grade.html index 2cd4734bf639febbc6f43550b7bb9b67adb034d7..84a131a57d8420ecad15fcb67df7c8b882234628 100644 --- a/course/templates/course/flow-completion-grade.html +++ b/course/templates/course/flow-completion-grade.html @@ -8,74 +8,99 @@ {% block content %} - {% if grade_info != None and grade_info.total_count %} -

{% trans "Results" %}: {{flow_desc.title}}

-
-

- {# Translators: the following 5 blocks of literals make a sentence. PartI #} - {% trans "You have" %} - {% if grade_info.max_points != grade_info.max_reachable_points %} - {# Translators: PartII #} - {% trans "(so far)" %} - {% endif %} - {# Translators: PartIII #} - {% blocktrans trimmed with provisional_points=grade_info.provisional_points|floatformat max_points=grade_info.max_points|floatformat %} - achieved {{ provisional_points }} out of - {{ max_points }} - points. - {% endblocktrans %} - {% if grade_info.max_points != grade_info.max_reachable_points %} - {# Translators: PartIV #} - {% blocktrans trimmed %} - (Some questions are not graded yet, so your grade will likely - change.) - {% endblocktrans %} - {% else %} - {# Translators: PartV #} - {% blocktrans trimmed with points_percent=grade_info.points_percent|floatformat %} - (That's {{ points_percent }}%.) - {% endblocktrans %} - {% endif %} -

- - {% if grade_info.total_points_percent < 100.001 %} - {# otherwise we'll have trouble drawing the bar #} - -
-
-
-
-
- {% endif %} - -

- {% blocktrans trimmed with fully_correct_count=grade_info.fully_correct_count partially_correct_count=grade_info.partially_correct_count incorrect_count=grade_info.incorrect_count %} - You have answered {{ fully_correct_count }} questions - correctly, {{ partially_correct_count }} questions - partially correctly, and {{ incorrect_count }} questions - incorrectly. - {% endblocktrans %} - {% if grade_info.unknown_count %} - {% blocktrans trimmed with unknown_count=grade_info.unknown_count %} - The grade for {{ unknown_count }} questions is not yet known. - {% endblocktrans %} - {% endif %} -

- -
-
-
-
-
-
-
+ {% if grade_info != None %} + {% if grade_info.total_count or grade_info.optional_total_count %} +

{% trans "Results" %}: {{flow_desc.title}}

+
+ {% if grade_info.total_count %} +

+ {# Translators: the following 5 blocks of literals make a sentence. PartI #} + {% trans "You have" %} + {% if grade_info.max_points != grade_info.max_reachable_points %} + {# Translators: PartII #} + {% trans "(so far)" %} + {% endif %} + {# Translators: PartIII #} + {% blocktrans trimmed with provisional_points=grade_info.provisional_points|floatformat max_points=grade_info.max_points|floatformat %} + achieved {{ provisional_points }} out of + {{ max_points }} + points. + {% endblocktrans %} + {% if grade_info.max_points != grade_info.max_reachable_points %} + {# Translators: PartIV #} + {% blocktrans trimmed %} + (Some questions are not graded yet, so your grade will likely + change.) + {% endblocktrans %} + {% else %} + {# Translators: PartV #} + {% blocktrans trimmed with points_percent=grade_info.points_percent|floatformat %} + (That's {{ points_percent }}%.) + {% endblocktrans %} + {% endif %} +

+ {% if grade_info.total_points_percent < 100.001 %} + {# otherwise we'll have trouble drawing the bar #} +
+
+
+
+
+ {% endif %} +

+ {% blocktrans trimmed with fully_correct_count=grade_info.fully_correct_count partially_correct_count=grade_info.partially_correct_count incorrect_count=grade_info.incorrect_count %} + You have answered {{ fully_correct_count }} grading questions + correctly, {{ partially_correct_count }} questions + partially correctly, and {{ incorrect_count }} questions + incorrectly. + {% endblocktrans %} + {% if grade_info.unknown_count %} + {% blocktrans trimmed with unknown_count=grade_info.unknown_count %} + The grade for {{ unknown_count }} questions is not yet known. + {% endblocktrans %} + {% endif %} +

+
+
+
+
+
+
+ {% endif %} + {% if grade_info.optional_total_count %} +

+ {% blocktrans trimmed with total_count=grade_info.optional_total_count fully_correct_count=grade_info.optional_fully_correct_count partially_correct_count=grade_info.optional_partially_correct_count incorrect_count=grade_info.optional_incorrect_count %} + There are {{ total_count }} optional questions (not for grading). + You have answered {{ fully_correct_count }} correctly, + {{ partially_correct_count }} partially correctly, + and {{ incorrect_count }} incorrectly. + {% endblocktrans %} + {% if grade_info.optional_unknown_count %} + {% blocktrans trimmed with unknown_count=grade_info.optional_unknown_count %} + The correctness for {{ unknown_count }} optional questions is not yet known. + {% endblocktrans %} + {% endif %} +

+
+
+
+
+
+
+ {% endif %} +
+ {% endif %} {% endif %} {{ completion_text|safe }} diff --git a/course/templates/course/flow-confirm-completion.html b/course/templates/course/flow-confirm-completion.html index 44c34912c8ed7fdd169fdf945a6ac701e25d5c34..f2913372dc3d6b801ab0949dc49b6ba73971e871 100644 --- a/course/templates/course/flow-confirm-completion.html +++ b/course/templates/course/flow-confirm-completion.html @@ -28,12 +28,12 @@ {% endif %} - {% if total_count %} + {% if required_count %}

{% blocktrans trimmed %} - There were {{total_count}} questions. + There were {{required_count}} grading questions. {% endblocktrans %} - {% if answered_count == total_count %} + {% if answered_count == required_count %} {% blocktrans trimmed %} You have provided an answer for all of them. {% endblocktrans %} @@ -48,8 +48,10 @@ {% trans "If you choose to end your session, the following things will happen:" %}

diff --git a/course/templates/course/flow-page.html b/course/templates/course/flow-page.html index 3c78a656dcdead5143b46f76221d7bbe8f2d8a33..1e4e5ba5685f0a2010c27b0b52dd11f94ebe90b4 100644 --- a/course/templates/course/flow-page.html +++ b/course/templates/course/flow-page.html @@ -317,6 +317,10 @@ {{ max_points }} points {% endblocktrans %}
+ {% elif is_optional_page %} +
+ {% trans "Optional question" %} +
{% endif %} {# }}} #} diff --git a/doc/flow.rst b/doc/flow.rst index fa8aecf088217258be23b3404db5d44a2872957b..5a5b25cd2a69806b65e6d8e0a5548aba6f6d632d 100644 --- a/doc/flow.rst +++ b/doc/flow.rst @@ -668,6 +668,16 @@ The following page types are predefined: An integer or a floating point number, representing the point value of the question. +.. |is-optional-page-attr| replace:: + + Optional. A Boolean value indicating whether the page is an optional page + which does not require answer for fully completion of the flow. + If ``true``, :attr:`value` should not present. Defaults to ``false`` if not present. + Note that ``is_optional_page: true`` differs from ``value: 0`` in that finishing flows + with unanswered page(s) with the latter will be warned of "unanswered question(s)", + while with the former won't. When using not-for-grading page(s) to collect + answers from students, it's to better use ``value: 0``. + .. |text-widget-page-attr| replace:: Optional.