class SplitTestFields(object):
    """Fields needed for split test module"""
    has_children = True

    # All available user partitions (with value and display name). This is updated each time
    # editable_metadata_fields is called.
    user_partition_values = []
    # Default value used for user_partition_id
    no_partition_selected = {'display_name': _("Not Selected"), 'value': -1}

    @staticmethod
    def build_partition_values(all_user_partitions, selected_user_partition):
        """
        This helper method builds up the user_partition values that will
        be passed to the Studio editor
        """
        SplitTestFields.user_partition_values = []
        # Add "No selection" value if there is not a valid selected user partition.
        if not selected_user_partition:
            SplitTestFields.user_partition_values.append(SplitTestFields.no_partition_selected)
        for user_partition in all_user_partitions:
            SplitTestFields.user_partition_values.append({"display_name": user_partition.name, "value": user_partition.id})
        return SplitTestFields.user_partition_values

    display_name = String(
        display_name=_("Display Name"),
        help=_("This name is used for organizing your course content, but is not shown to students."),
        scope=Scope.settings,
        default=_("Content Experiment")
    )

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(
        help=_("The list of group configurations for partitioning students in content experiments."),
        default=[],
        scope=Scope.settings
    )

    user_partition_id = Integer(
        help=_("The configuration defines how users are grouped for this content experiment. Caution: Changing the group configuration of a student-visible experiment will impact the experiment data."),
        scope=Scope.content,
        display_name=_("Group Configuration"),
        default=no_partition_selected["value"],
        values=lambda: SplitTestFields.user_partition_values  # Will be populated before the Studio editor is shown.
    )

    # group_id is an int
    # child is a serialized UsageId (aka Location).  This child
    # location needs to actually match one of the children of this
    # Block.  (expected invariant that we'll need to test, and handle
    # authoring tools that mess this up)

    # TODO: is there a way to add some validation around this, to
    # be run on course load or in studio or ....

    group_id_to_child = ReferenceValueDict(
        help=_("Which child module students in a particular group_id should see"),
        scope=Scope.content
    )
Beispiel #2
0
class SplitTestFields:
    """Fields needed for split test module"""
    has_children = True

    # Default value used for user_partition_id
    no_partition_selected = {'display_name': _("Not Selected"), 'value': -1}

    display_name = String(
        display_name=_("Display Name"),
        help=_("The display name for this component. (Not shown to learners)"),
        scope=Scope.settings,
        default=_("Content Experiment"))

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(help=_(
        "The list of group configurations for partitioning students in content experiments."
    ),
                                        default=[],
                                        scope=Scope.settings)

    user_partition_id = Integer(
        help=
        _("The configuration defines how users are grouped for this content experiment. Caution: Changing the group configuration of a student-visible experiment will impact the experiment data."
          ),  # lint-amnesty, pylint: disable=line-too-long
        scope=Scope.content,
        display_name=_("Group Configuration"),
        default=no_partition_selected["value"],
        values=lambda: user_partition_values.
        values  # Will be populated before the Studio editor is shown.
    )

    # group_id is an int
    # child is a serialized UsageId (aka Location).  This child
    # location needs to actually match one of the children of this
    # Block.  (expected invariant that we'll need to test, and handle
    # authoring tools that mess this up)
    group_id_to_child = ReferenceValueDict(help=_(
        "Which child module students in a particular group_id should see"),
                                           scope=Scope.content)
Beispiel #3
0
class LmsBlockMixin(XBlockMixin):
    """
    Mixin that defines fields common to all blocks used in the LMS
    """
    hide_from_toc = Boolean(
        help=_("Whether to display this module in the table of contents"),
        default=False,
        scope=Scope.settings)
    format = String(
        # Translators: "TOC" stands for "Table of Contents"
        help=_("What format this module is in (used for deciding which "
               "grader to apply, and what to show in the TOC)"),
        scope=Scope.settings,
    )
    chrome = String(
        display_name=_("Course Chrome"),
        # Translators: DO NOT translate the words in quotes here, they are
        # specific words for the acceptable values.
        help=_(
            "Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
            "\"chromeless\" -- to not use tabs or the accordion; \n"
            "\"tabs\" -- to use tabs only; \n"
            "\"accordion\" -- to use the accordion only; or \n"
            "\"tabs,accordion\" -- to use tabs and the accordion."),
        scope=Scope.settings,
        default=None,
    )
    default_tab = String(
        display_name=_("Default Tab"),
        help=
        _("Enter the tab that is selected in the XBlock. If not set, the Course tab is selected."
          ),
        scope=Scope.settings,
        default=None,
    )
    source_file = String(display_name=_("LaTeX Source File Name"),
                         help=_("Enter the source file name for LaTeX."),
                         scope=Scope.settings,
                         deprecated=True)
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    group_access = GroupAccessDict(
        help=
        _("A dictionary that maps which groups can be shown this block. The keys "
          "are group configuration ids and the values are a list of group IDs. "
          "If there is no key for a group configuration or if the set of group IDs "
          "is empty then the block is considered visible to all. Note that this "
          "field is ignored if the block is visible_to_staff_only."),
        default={},
        scope=Scope.settings,
    )

    @lazy
    def merged_group_access(self):
        """
        This computes access to a block's group_access rules in the context of its position
        within the courseware structure, in the form of a lazily-computed attribute.
        Each block's group_access rule is merged recursively with its parent's, guaranteeing
        that any rule in a parent block will be enforced on descendants, even if a descendant
        also defined its own access rules.  The return value is always a dict, with the same
        structure as that of the group_access field.

        When merging access rules results in a case where all groups are denied access in a
        user partition (which effectively denies access to that block for all students),
        the special value False will be returned for that user partition key.
        """
        parent = self.get_parent()
        if not parent:
            return self.group_access or {}

        merged_access = parent.merged_group_access.copy()
        if self.group_access is not None:
            for partition_id, group_ids in self.group_access.items():
                if group_ids:  # skip if the "local" group_access for this partition is None or empty.
                    if partition_id in merged_access:
                        if merged_access[partition_id] is False:
                            # special case - means somewhere up the hierarchy, merged access rules have eliminated
                            # all group_ids from this partition, so there's no possible intersection.
                            continue
                        # otherwise, if the parent defines group access rules for this partition,
                        # intersect with the local ones.
                        merged_access[partition_id] = list(
                            set(merged_access[partition_id]).intersection(
                                group_ids)) or False
                    else:
                        # add the group access rules for this partition to the merged set of rules.
                        merged_access[partition_id] = group_ids
        return merged_access

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(help=_(
        "The list of group configurations for partitioning students in content experiments."
    ),
                                        default=[],
                                        scope=Scope.settings)

    def _get_user_partition(self, user_partition_id):
        """
        Returns the user partition with the specified id. Note that this method can return
        an inactive user partition. Raises `NoSuchUserPartitionError` if the lookup fails.
        """
        for user_partition in self.runtime.service(
                self, 'partitions').course_partitions:
            if user_partition.id == user_partition_id:
                return user_partition

        raise NoSuchUserPartitionError(
            "could not find a UserPartition with ID [{}]".format(
                user_partition_id))

    def validate(self):
        """
        Validates the state of this xblock instance.
        """
        _ = self.runtime.service(self, "i18n").ugettext
        validation = super(LmsBlockMixin, self).validate()
        has_invalid_user_partitions = False
        has_invalid_groups = False
        for user_partition_id, group_ids in self.group_access.iteritems():
            try:
                user_partition = self._get_user_partition(user_partition_id)
            except NoSuchUserPartitionError:
                has_invalid_user_partitions = True
            else:
                # Skip the validation check if the partition has been disabled
                if user_partition.active:
                    for group_id in group_ids:
                        try:
                            user_partition.get_group(group_id)
                        except NoSuchUserPartitionGroupError:
                            has_invalid_groups = True

        if has_invalid_user_partitions:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  INVALID_USER_PARTITION_VALIDATION))
        if has_invalid_groups:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  INVALID_USER_PARTITION_GROUP_VALIDATION))
        return validation
Beispiel #4
0
class LmsBlockMixin(XBlockMixin):
    """
    Mixin that defines fields common to all blocks used in the LMS
    """
    hide_from_toc = Boolean(
        help=_("Whether to display this module in the table of contents"),
        default=False,
        scope=Scope.settings)
    format = String(
        # Translators: "TOC" stands for "Table of Contents"
        help=_("What format this module is in (used for deciding which "
               "grader to apply, and what to show in the TOC)"),
        scope=Scope.settings,
    )
    chrome = String(
        display_name=_("Course Chrome"),
        # Translators: DO NOT translate the words in quotes here, they are
        # specific words for the acceptable values.
        help=_(
            "Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
            "\"chromeless\" -- to not use tabs or the accordion; \n"
            "\"tabs\" -- to use tabs only; \n"
            "\"accordion\" -- to use the accordion only; or \n"
            "\"tabs,accordion\" -- to use tabs and the accordion."),
        scope=Scope.settings,
        default=None,
    )
    default_tab = String(
        display_name=_("Default Tab"),
        help=
        _("Enter the tab that is selected in the XBlock. If not set, the Course tab is selected."
          ),
        scope=Scope.settings,
        default=None,
    )
    source_file = String(display_name=_("LaTeX Source File Name"),
                         help=_("Enter the source file name for LaTeX."),
                         scope=Scope.settings,
                         deprecated=True)
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    group_access = GroupAccessDict(
        help=
        _("A dictionary that maps which groups can be shown this block. The keys "
          "are group configuration ids and the values are a list of group IDs. "
          "If there is no key for a group configuration or if the set of group IDs "
          "is empty then the block is considered visible to all. Note that this "
          "field is ignored if the block is visible_to_staff_only."),
        default={},
        scope=Scope.settings,
    )

    @lazy
    def merged_group_access(self):
        """
        This computes access to a block's group_access rules in the context of its position
        within the courseware structure, in the form of a lazily-computed attribute.
        Each block's group_access rule is merged recursively with its parent's, guaranteeing
        that any rule in a parent block will be enforced on descendants, even if a descendant
        also defined its own access rules.  The return value is always a dict, with the same
        structure as that of the group_access field.

        When merging access rules results in a case where all groups are denied access in a
        user partition (which effectively denies access to that block for all students),
        the special value False will be returned for that user partition key.
        """
        parent = self.get_parent()
        if not parent:
            return self.group_access or {}

        merged_access = parent.merged_group_access.copy()
        if self.group_access is not None:
            for partition_id, group_ids in self.group_access.items():  # pylint: disable=no-member
                if group_ids:  # skip if the "local" group_access for this partition is None or empty.
                    if partition_id in merged_access:
                        if merged_access[partition_id] is False:
                            # special case - means somewhere up the hierarchy, merged access rules have eliminated
                            # all group_ids from this partition, so there's no possible intersection.
                            continue
                        # otherwise, if the parent defines group access rules for this partition,
                        # intersect with the local ones.
                        merged_access[partition_id] = list(
                            set(merged_access[partition_id]).intersection(
                                group_ids)) or False
                    else:
                        # add the group access rules for this partition to the merged set of rules.
                        merged_access[partition_id] = group_ids
        return merged_access

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(help=_(
        "The list of group configurations for partitioning students in content experiments."
    ),
                                        default=[],
                                        scope=Scope.settings)

    def _get_user_partition(self, user_partition_id):
        """
        Returns the user partition with the specified id. Note that this method can return
        an inactive user partition. Raises `NoSuchUserPartitionError` if the lookup fails.
        """
        for user_partition in self.runtime.service(
                self, 'partitions').course_partitions:
            if user_partition.id == user_partition_id:
                return user_partition

        raise NoSuchUserPartitionError(
            u"could not find a UserPartition with ID [{}]".format(
                user_partition_id))

    def _has_nonsensical_access_settings(self):
        """
        Checks if a block's group access settings do not make sense.

        By nonsensical access settings, we mean a component's access
        settings which contradict its parent's access in that they
        restrict access to the component to a group that already
        will not be able to see that content.
        Note:  This contradiction can occur when a component
        restricts access to the same partition but a different group
        than its parent, or when there is a parent access
        restriction but the component attempts to allow access to
        all learners.

        Returns:
            bool: True if the block's access settings contradict its
            parent's access settings.
        """
        parent = self.get_parent()
        if not parent:
            return False

        parent_group_access = parent.group_access
        component_group_access = self.group_access

        for user_partition_id, parent_group_ids in six.iteritems(
                parent_group_access):
            component_group_ids = component_group_access.get(user_partition_id)  # pylint: disable=no-member
            if component_group_ids:
                return parent_group_ids and not set(
                    component_group_ids).issubset(set(parent_group_ids))
            else:
                return not component_group_access
        return False

    def validate(self):
        """
        Validates the state of this xblock instance.
        """
        _ = self.runtime.service(self, "i18n").ugettext
        validation = super(LmsBlockMixin, self).validate()
        has_invalid_user_partitions = False
        has_invalid_groups = False
        block_is_unit = is_unit(self)

        for user_partition_id, group_ids in six.iteritems(self.group_access):
            try:
                user_partition = self._get_user_partition(user_partition_id)
            except NoSuchUserPartitionError:
                has_invalid_user_partitions = True
            else:
                # Skip the validation check if the partition has been disabled
                if user_partition.active:
                    for group_id in group_ids:
                        try:
                            user_partition.get_group(group_id)
                        except NoSuchUserPartitionGroupError:
                            has_invalid_groups = True

        if has_invalid_user_partitions:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    (INVALID_USER_PARTITION_VALIDATION_UNIT if block_is_unit
                     else INVALID_USER_PARTITION_VALIDATION_COMPONENT)))

        if has_invalid_groups:
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    (INVALID_USER_PARTITION_GROUP_VALIDATION_UNIT
                     if block_is_unit else
                     INVALID_USER_PARTITION_GROUP_VALIDATION_COMPONENT)))

        if self._has_nonsensical_access_settings():
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  NONSENSICAL_ACCESS_RESTRICTION))

        return validation

    @XBlock.json_handler
    def publish_completion(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        Publish completion data from the front end.
        """
        completion_service = self.runtime.service(self, 'completion')
        if completion_service is None:
            raise JsonHandlerError(500, u"No completion service found")
        elif not completion_service.completion_tracking_enabled():
            raise JsonHandlerError(
                404,
                u"Completion tracking is not enabled and API calls are unexpected"
            )
        if not completion_service.can_mark_block_complete_on_view(self):
            raise JsonHandlerError(
                400, u"Block not configured for completion on view.")
        self.runtime.publish(self, "completion", data)
        return {'result': 'ok'}
Beispiel #5
0
class LmsBlockMixin(XBlockMixin):
    """
    Mixin that defines fields common to all blocks used in the LMS
    """
    hide_from_toc = Boolean(
        help="Whether to display this module in the table of contents",
        default=False,
        scope=Scope.settings)
    format = String(
        help="What format this module is in (used for deciding which "
        "grader to apply, and what to show in the TOC)",
        scope=Scope.settings,
    )
    chrome = String(
        display_name=_("Courseware Chrome"),
        help=_(
            "Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
            "\"chromeless\" -- to not use tabs or the accordion; \n"
            "\"tabs\" -- to use tabs only; \n"
            "\"accordion\" -- to use the accordion only; or \n"
            "\"tabs,accordion\" -- to use tabs and the accordion."),
        scope=Scope.settings,
        default=None,
    )
    default_tab = String(
        display_name=_("Default Tab"),
        help=
        _("Enter the tab that is selected in the XBlock. If not set, the Courseware tab is selected."
          ),
        scope=Scope.settings,
        default=None,
    )
    source_file = String(display_name=_("LaTeX Source File Name"),
                         help=_("Enter the source file name for LaTeX."),
                         scope=Scope.settings,
                         deprecated=True)
    ispublic = Boolean(
        display_name=_("Course Is Public"),
        help=
        _("Enter true or false. If true, the course is open to the public. If false, the course is open only to admins."
          ),
        scope=Scope.settings)
    visible_to_staff_only = Boolean(
        help=
        _("If true, can be seen only by course staff, regardless of start date."
          ),
        default=False,
        scope=Scope.settings,
    )
    group_access = Dict(
        help=
        "A dictionary that maps which groups can be shown this block. The keys "
        "are group configuration ids and the values are a list of group IDs. "
        "If there is no key for a group configuration or if the list of group IDs "
        "is empty then the block is considered visible to all. Note that this "
        "field is ignored if the block is visible_to_staff_only.",
        default={},
        scope=Scope.settings,
    )

    # Specified here so we can see what the value set at the course-level is.
    user_partitions = UserPartitionList(help=_(
        "The list of group configurations for partitioning students in content experiments."
    ),
                                        default=[],
                                        scope=Scope.settings)

    def _get_user_partition(self, user_partition_id):
        """
        Returns the user partition with the specified id, or None if there is no such partition.
        """
        for user_partition in self.user_partitions:
            if user_partition.id == user_partition_id:
                return user_partition

        return None

    def is_visible_to_group(self, user_partition, group):
        """
        Returns true if this xblock should be shown to a user in the specified user partition group.
        This method returns true if one of the following is true:
         - the xblock has no group_access dictionary specified
         - if the dictionary has no key for the user partition's id
         - if the value for the user partition's id is an empty list
         - if the value for the user partition's id contains the specified group's id
        """
        if not self.group_access:
            return True
        group_ids = self.group_access.get(user_partition.id, [])
        if len(group_ids) == 0:
            return True
        return group.id in group_ids

    def validate(self):
        """
        Validates the state of this xblock instance.
        """
        _ = self.runtime.service(self, "i18n").ugettext  # pylint: disable=redefined-outer-name
        validation = super(LmsBlockMixin, self).validate()
        for user_partition_id, group_ids in self.group_access.iteritems():
            user_partition = self._get_user_partition(user_partition_id)
            if not user_partition:
                validation.add(
                    ValidationMessage(
                        ValidationMessage.ERROR,
                        _(u"This xblock refers to a deleted or invalid content group configuration."
                          )))
            else:
                for group_id in group_ids:
                    group = user_partition.get_group(group_id)
                    if not group:
                        validation.add(
                            ValidationMessage(
                                ValidationMessage.ERROR,
                                _(u"This xblock refers to a deleted or invalid content group."
                                  )))

        return validation