diff --git a/course/content.py b/course/content.py index fb3fcb93a420680b6f7fcc0fdb9943d7fe04d91c..d5f7db03e29c6725309a8c77ebb66553b2eecb1b 100644 --- a/course/content.py +++ b/course/content.py @@ -78,6 +78,8 @@ class ChunkRulesDesc(Struct): if_before = None # type: Datespec if_after = None # type: Datespec if_in_facility = None # type: Text + if_has_participation_tags_any = None # type: List[Text] + if_has_participation_tags_all = None # type: List[Text] roles = None # type: List[Text] start = None # type: Datespec end = None # type: Datespec @@ -108,6 +110,8 @@ class FlowSessionStartRuleDesc(Struct): if_after = None # type: Date_ish if_before = None # type: Date_ish if_has_role = None # type: list + if_has_participation_tags_any = None # type: List[Text] + if_has_participation_tags_all = None # type: List[Text] if_in_facility = None # type: Text if_has_in_progress_session = None # type: bool if_has_session_tagged = None # type: Optional[Text] @@ -127,6 +131,8 @@ class FlowSessionAccessRuleDesc(Struct): if_before = None # type: Date_ish if_started_before = None # type: Date_ish if_has_role = None # type: List[Text] + if_has_participation_tags_any = None # type: List[Text] + if_has_participation_tags_all = None # type: List[Text] if_in_facility = None # type: Text if_has_tag = None # type: Optional[Text] if_in_progress = None # type: bool diff --git a/course/utils.py b/course/utils.py index 122af4fde23dbf7905dffe5a3ab1949caaee306d..efb26c95de9e8beaf20a2a4c94c3e616e0a803d4 100644 --- a/course/utils.py +++ b/course/utils.py @@ -172,6 +172,26 @@ def _eval_generic_conditions( if all(role not in rule.if_has_role for role in roles): return False + participation_tags_any_set = ( + set(getattr(rule, "if_has_participation_tags_any", []))) + participation_tags_all_set = ( + set(getattr(rule, "if_has_participation_tags_all", []))) + + if participation_tags_any_set or participation_tags_all_set: + if not participation: + return False + ptag_set = set(participation.tags.all().values_list("name", flat=True)) + if not ptag_set: + return False + if (participation_tags_any_set + and + not participation_tags_any_set & ptag_set): + return False + if (participation_tags_all_set + and + not participation_tags_all_set <= ptag_set): + return False + if (hasattr(rule, "if_signed_in_with_matching_exam_ticket") and rule.if_signed_in_with_matching_exam_ticket): if login_exam_ticket is None: diff --git a/course/validation.py b/course/validation.py index f3b51b1c8f20edcdb6d8c52b6063bc750887863a..6010911d34152ddcc35bb51df9f873ee5f74dc69 100644 --- a/course/validation.py +++ b/course/validation.py @@ -115,6 +115,33 @@ def validate_facility(vctx, location, facility): }) +def validate_participationtag(vctx, location, participationtag): + # type: (ValidationContext, Text, Text) -> None + + if vctx.course is not None: + from pytools import memoize_in + + @memoize_in(vctx, "available_participation_tags") + def get_ptag_list(vctx): + # type: (ValidationContext) -> List[str] + from course.models import ParticipationTag + return list( + ParticipationTag.objects.filter(course=vctx.course) + .values_list('name', flat=True)) + + ptag_list = get_ptag_list(vctx) + if participationtag not in ptag_list: + vctx.add_warning( + location, + _( + "Name of participation tag not recognized: '%(ptag_name)s'. " + "Known participation tag names: '%(known_ptag_names)s'") + % { + "ptag_name": participationtag, + "known_ptag_names": ", ".join(ptag_list), + }) + + def validate_struct( vctx, # type: ValidationContext location, # type: Text @@ -285,6 +312,8 @@ def validate_chunk_rule(vctx, location, chunk_rule): ("if_before", datespec_types), ("if_in_facility", str), ("if_has_role", list), + ("if_has_participation_tags_any", list), + ("if_has_participation_tags_all", list), ("start", datespec_types), ("end", datespec_types), @@ -303,6 +332,14 @@ def validate_chunk_rule(vctx, location, chunk_rule): for role in chunk_rule.if_has_role: validate_role(vctx, location, role) + if hasattr(chunk_rule, "if_has_participation_tags_any"): + for ptag in chunk_rule.if_has_participation_tags_any: + validate_participationtag(vctx, location, ptag) + + if hasattr(chunk_rule, "if_has_participation_tags_all"): + for ptag in chunk_rule.if_has_participation_tags_all: + validate_participationtag(vctx, location, ptag) + if hasattr(chunk_rule, "if_in_facility"): validate_facility(vctx, location, chunk_rule.if_in_facility) @@ -523,6 +560,8 @@ def validate_session_start_rule(vctx, location, nrule, tags): ("if_after", datespec_types), ("if_before", datespec_types), ("if_has_role", list), + ("if_has_participation_tags_any", list), + ("if_has_participation_tags_all", list), ("if_in_facility", str), ("if_has_in_progress_session", bool), ("if_has_session_tagged", (six.string_types, type(None))), @@ -548,6 +587,14 @@ def validate_session_start_rule(vctx, location, nrule, tags): "%s, role %d" % (location, j+1), role) + if hasattr(nrule, "if_has_participation_tags_any"): + for ptag in nrule.if_has_participation_tags_any: + validate_participationtag(vctx, location, ptag) + + if hasattr(nrule, "if_has_participation_tags_all"): + for ptag in nrule.if_has_participation_tags_all: + validate_participationtag(vctx, location, ptag) + if hasattr(nrule, "if_in_facility"): validate_facility(vctx, location, nrule.if_in_facility) @@ -608,6 +655,8 @@ def validate_session_access_rule(vctx, location, arule, tags): ("if_before", datespec_types), ("if_started_before", datespec_types), ("if_has_role", list), + ("if_has_participation_tags_any", list), + ("if_has_participation_tags_all", list), ("if_in_facility", str), ("if_has_tag", (six.string_types, type(None))), ("if_in_progress", bool), @@ -633,6 +682,14 @@ def validate_session_access_rule(vctx, location, arule, tags): "%s, role %d" % (location, j+1), role) + if hasattr(arule, "if_has_participation_tags_any"): + for ptag in arule.if_has_participation_tags_any: + validate_participationtag(vctx, location, ptag) + + if hasattr(arule, "if_has_participation_tags_all"): + for ptag in arule.if_has_participation_tags_all: + validate_participationtag(vctx, location, ptag) + if hasattr(arule, "if_in_facility"): validate_facility(vctx, location, arule.if_in_facility) @@ -690,6 +747,8 @@ def validate_session_grading_rule( ], allowed_attrs=[ ("if_has_role", list), + ("if_has_participation_tags_any", list), + ("if_has_participation_tags_all", list), ("if_has_tag", (six.string_types, type(None))), ("if_started_before", datespec_types), ("if_completed_before", datespec_types), @@ -742,6 +801,14 @@ def validate_session_grading_rule( role) has_conditionals = True + if hasattr(grule, "if_has_participation_tags_any"): + for ptag in grule.if_has_participation_tags_any: + validate_participationtag(vctx, location, ptag) + + if hasattr(grule, "if_has_participation_tags_all"): + for ptag in grule.if_has_participation_tags_all: + validate_participationtag(vctx, location, ptag) + if hasattr(grule, "if_has_tag"): if not (grule.if_has_tag is None or grule.if_has_tag in tags): raise ValidationError( diff --git a/doc/content.rst b/doc/content.rst index 8c5ae9d278fb17d4773ff5dfdb241c921ff44e49..610028a3903fbfe77aa91e335549e3d3ac8ca489 100644 --- a/doc/content.rst +++ b/doc/content.rst @@ -470,34 +470,44 @@ Here's an example: .. attribute:: weight - (required) An integer indicating how far up the page the block + (Required) An integer indicating how far up the page the block will be shown. Blocks with identical weight retain the order in which they are given in the course information file. .. attribute:: if_after - A :ref:`datespec ` that determines a date/time after which this rule + (Optional) A :ref:`datespec ` that determines a date/time after which this rule applies. .. attribute:: if_before - A :ref:`datespec ` that determines a date/time before which this rule + (Optional) A :ref:`datespec ` that determines a date/time before which this rule applies. .. attribute:: if_has_role - A list of a subset of ``[unenrolled, ta, student, instructor]``. + (Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``. + + .. attribute:: if_has_participation_tags_any + + (Optional) A list of participation tags. Rule applies when the + participation has at least one tag in this list. + + .. attribute:: if_has_participation_tags_all + + (Optional) A list of participation tags. Rule applies if only the + participation's tags include all items in this list. .. attribute:: if_in_facility - Name of a facility known to the RELATE web page. This rule allows + (Optional) Name of a facility known to the RELATE web page. This rule allows (for example) showing chunks based on whether a user is physically located in a computer-based testing center (which RELATE can recognize based on IP ranges). .. attribute:: shown - A boolean (``true`` or ``false``) indicating whether the chunk + (Optional) A boolean (``true`` or ``false``) indicating whether the chunk should be shown. diff --git a/doc/flow.rst b/doc/flow.rst index 5a5b25cd2a69806b65e6d8e0a5548aba6f6d632d..abfcbd24807b040ef4a9e6beae7adb14fa3600ed 100644 --- a/doc/flow.rst +++ b/doc/flow.rst @@ -304,6 +304,16 @@ Rules for starting new sessions (Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``. + .. attribute:: if_has_participation_tags_any + + (Optional) A list of participation tags. Rule applies when the + participation has at least one tag in this list. + + .. attribute:: if_has_participation_tags_all + + (Optional) A list of participation tags. Rule applies if only the + participation's tags include all items in this list. + .. attribute:: if_in_facility (Optional) Name of a facility known to the RELATE web page. This rule allows @@ -389,6 +399,16 @@ Rules about accessing and interacting with a flow (Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``. + .. attribute:: if_has_participation_tags_any + + (Optional) A list of participation tags. Rule applies when the + participation has at least one tag in this list. + + .. attribute:: if_has_participation_tags_all + + (Optional) A list of participation tags. Rule applies if only the + participation's tags include all items in this list. + .. attribute:: if_in_facility (Optional) Name of a facility known to the RELATE web page. This rule allows @@ -465,6 +485,16 @@ Determining how final (overall) grades of flows are computed (Optional) A list of a subset of ``[unenrolled, ta, student, instructor]``. + .. attribute:: if_has_participation_tags_any + + (Optional) A list of participation tags. Rule applies when the + participation has at least one tag in this list. + + .. attribute:: if_has_participation_tags_all + + (Optional) A list of participation tags. Rule applies if only the + participation's tags include all items in this list. + .. attribute:: if_started_before (Optional) A :ref:`datespec `. Rule applies if the session was started before