Beispiel #1
0
    def calculate_results(self, submissions):
        score = 0
        results = []
        tips = None

        if not self.hide_results:
            tips = self.get_tips()

        for choice in self.custom_choices:
            choice_completed = True
            choice_tips_html = []
            choice_selected = choice.value in submissions

            if choice.value in self.required_choices:
                if not choice_selected:
                    choice_completed = False
            elif choice_selected and choice.value not in self.ignored_choices:
                choice_completed = False

            if choice_completed:
                score += 1

            choice_result = {
                'value': choice.value,
                'selected': choice_selected,
                'content': choice.content
            }
            # Only include tips/results in returned response if we want to display them
            if not self.hide_results:
                # choice_tips_html list is being set only when 'self.hide_results' is False, to optimize,
                # execute the loop only when 'self.hide_results' is set to False
                for tip in tips:
                    if choice.value in tip.values:
                        choice_tips_html.append(tip.render('mentoring_view').content)
                        break

                loader = ResourceLoader(__name__)
                choice_result['completed'] = choice_completed
                choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', {
                    'tips_html': choice_tips_html,
                })

            results.append(choice_result)

        status = 'incorrect' if score <= 0 else 'correct' if score >= len(results) else 'partial'

        if sub_api:
            # Send the answer as a concatenated list to the submissions API
            answer = [choice['content'] for choice in results if choice['selected']]
            sub_api.create_submission(self.student_item_key, ', '.join(answer))

        return {
            'submissions': submissions,
            'status': status,
            'choices': results,
            'message': self.message_formatted,
            'weight': self.weight,
            'score': (float(score) / len(results)) if results else 0,
        }
Beispiel #2
0
    def include_theme_files(self, fragment):
        """
        Gets theme configuration and renders theme css into fragment
        """
        theme = self.get_theme()
        if not theme or 'package' not in theme:
            return

        theme_package, theme_files = theme.get('package', None), theme.get('locations', [])
        resource_loader = ResourceLoader(theme_package)
        for theme_file in theme_files:
            fragment.add_css(resource_loader.load_unicode(theme_file))
Beispiel #3
0
    def calculate_results(self, submissions):
        score = 0
        results = []

        for choice in self.custom_choices:
            choice_completed = True
            choice_tips_html = []
            choice_selected = choice.value in submissions

            if choice.value in self.required_choices:
                if not choice_selected:
                    choice_completed = False
            elif choice_selected and choice.value not in self.ignored_choices:
                choice_completed = False
            for tip in self.get_tips():
                if choice.value in tip.values:
                    choice_tips_html.append(tip.render('mentoring_view').content)

            if choice_completed:
                score += 1

            choice_result = {
                'value': choice.value,
                'selected': choice_selected,
                }
            # Only include tips/results in returned response if we want to display them
            if not self.hide_results:
                loader = ResourceLoader(__name__)
                choice_result['completed'] = choice_completed
                choice_result['tips'] = loader.render_template('templates/html/tip_choice_group.html', {
                    'tips_html': choice_tips_html,
                    })

            results.append(choice_result)

        status = 'incorrect' if score <= 0 else 'correct' if score >= len(results) else 'partial'

        return {
            'submissions': submissions,
            'status': status,
            'choices': results,
            'message': self.message,
            'weight': self.weight,
            'score': (float(score) / len(results)) if results else 0,
        }
    def student_view(self, context):
        """
        XBlock student view of this component.

        Makes a request to `lti_launch_handler` either
        in an iframe or in a new window depending on the
        configuration of the instance of this XBlock

        Arguments:
            context (dict): XBlock context

        Returns:
            xblock.fragment.Fragment: XBlock HTML fragment
        """
        fragment = Fragment()
        loader = ResourceLoader(__name__)
        context.update(self._get_context_for_template())
        fragment.add_content(loader.render_mako_template('/templates/html/student.html', context))
        fragment.add_css(loader.load_unicode('static/css/student.css'))
        fragment.add_javascript(loader.load_unicode('static/js/xblock_lti_consumer.js'))
        fragment.initialize_js('LtiConsumerXBlock')
        return fragment
    def lti_launch_handler(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        XBlock handler for launching the LTI provider.

        Displays a form which is submitted via Javascript
        to send the LTI launch POST request to the LTI
        provider.

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request
            suffix (unicode): Request path after "lti_launch_handler/"

        Returns:
            webob.response: HTML LTI launch form
        """
        lti_consumer = LtiConsumer(self)
        lti_parameters = lti_consumer.get_signed_lti_parameters()
        loader = ResourceLoader(__name__)
        context = self._get_context_for_template()
        context.update({'lti_parameters': lti_parameters})
        template = loader.render_mako_template('/templates/html/lti_launch.html', context)
        return Response(template, content_type='text/html')
    def student_view(self, context):
        """
        XBlock student view of this component.

        Arguments:
            context (dict): XBlock context

        Returns:
            xblock.fragment.Fragment: XBlock HTML fragment
        """
        fragment = Fragment()
        loader = ResourceLoader(__name__)
        context.update(self._get_context_for_template())
        fragment.add_content(loader.render_mako_template('/templates/player.html', context))

        '''
        Note: DO NOT USE the "latest" folder in production, but specify a version
                from https://aka.ms/ampchangelog . This allows us to run a test
                pass prior to ingesting later versions.
        '''
        fragment.add_javascript(loader.load_unicode('node_modules/videojs-vtt.js/lib/vttcue.js'))

        fragment.add_css_url('//amp.azure.net/libs/amp/1.8.1/skins/amp-default/azuremediaplayer.min.css')
        fragment.add_javascript_url('//amp.azure.net/libs/amp/1.8.1/azuremediaplayer.min.js')

        fragment.add_javascript(loader.load_unicode('static/js/player.js'))

        fragment.add_css(loader.load_unicode('public/css/player.css'))

        # NOTE: The Azure Media Player JS file includes the VTT JavaScript library, so we don't
        # actually need to include our local copy of public/js/vendor/vtt.js. In fact, if we do
        # the overlay subtitles stop working

        # @TODO: Make sure all fields are well structured/formatted, if it is not correct, then
        # print out an error msg in view rather than just silently failing

        fragment.initialize_js('AzureMediaServicesBlock')
        return fragment
Beispiel #7
0
class PDFXBlock(XBlock):
    """
    PDF XBlock.
    """

    loader = ResourceLoader(__name__)
    PDF_FILEUPLOAD_MAX_SIZE = 100 * 1000 * 1000  # 100 MB

    # Icon of the XBlock. Values : [other (default), video, problem]

    icon_class = 'other'

    # Enable view as specific student

    show_in_read_only_mode = True

    # Fields

    display_name = String(
        display_name=_('Display Name'),
        default=_('PDF'),
        scope=Scope.settings,
        help=
        _('This name appears in the horizontal navigation at the top of the page.'
          ))

    pdf_file_path = String(
        display_name=_("Upload PDF file"),
        scope=Scope.settings,
    )

    pdf_file_name = String(scope=Scope.settings)

    @classmethod
    def upload_max_size(self):
        """
        returns max file size limit in system
        """
        return getattr(settings, "PDF_FILEUPLOAD_MAX_SIZE",
                       self.PDF_FILEUPLOAD_MAX_SIZE)

    @classmethod
    def file_size_over_limit(self, file_obj):
        """
        checks if file size is under limit.
        """
        file_obj.seek(0, os.SEEK_END)
        return file_obj.tell() > self.upload_max_size()

    def get_live_url(self):
        """
            Get the file url
        """
        if not self.pdf_file_path:
            return ''
        return reverse('eol/pdf:pdf_get_url',
                       kwargs={'file_path': self.pdf_file_path})

    def load_resource(self, resource_path):  # pylint: disable=no-self-use
        """
        Gets the content of a resource
        """

        resource_content = pkg_resources.resource_string(
            __name__, resource_path)
        return resource_content.decode('utf-8')

    def render_template(self, path, context=None):
        """
        Evaluate a template by resource path, applying the provided context
        """

        return self.loader.render_django_template(
            os.path.join('static/html', path),
            context=Context(context or {}),
            i18n_service=self.runtime.service(self, 'i18n'))

    def student_view(self, context=None):
        """
        The primary view of the XBlock, shown to students
        when viewing courses.
        """
        context = {
            'display_name': self.display_name,
            'url': self.get_live_url(),
        }
        html = self.render_template('pdf_view.html', context)
        frag = Fragment(html)
        frag.add_css(self.load_resource('static/css/pdf.css'))
        frag.add_javascript(self.load_resource('static/js/pdf_view.js'))
        frag.initialize_js('pdfXBlockInitView')

        self.runtime.publish(self, 'completion',
                             {'completion': 1.0})  # Set xblock completed
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """

        context = {
            'display_name': self.display_name,
            'pdf_file_name': self.pdf_file_name,
        }
        html = self.render_template('pdf_edit.html', context)

        frag = Fragment(html)
        frag.add_javascript(self.load_resource('static/js/pdf_edit.js'))
        frag.initialize_js('pdfXBlockInitEdit')
        return frag

    @XBlock.handler
    def save_pdf(self, request, suffix=''):  # pylint: disable=unused-argument
        """
        The saving handler.
        """
        self.display_name = request.params['display_name']
        response = {"result": "success", "errors": []}
        if not hasattr(request.params["pdf_file"], "file"):
            # File not uploaded
            return Response(json.dumps(response),
                            content_type="application/json",
                            charset="utf8")

        pdf_file = request.params["pdf_file"]
        sha1 = get_sha1(pdf_file.file)
        if self.file_size_over_limit(pdf_file.file):
            response["errors"].append(
                'Unable to upload file. Max size limit is {size}'.format(
                    size=self.upload_max_size()))
            return Response(json.dumps(response),
                            content_type="application/json",
                            charset="utf8")

        path = get_file_storage_path(self.location, sha1, pdf_file.file.name)
        storage = get_storage()
        storage.save(path, File(pdf_file.file))

        logger.info("Saving file: %s at path: %s", pdf_file.file.name, path)
        self.pdf_file_path = path
        self.pdf_file_name = pdf_file.file.name
        return Response(json.dumps(response),
                        content_type="application/json",
                        charset="utf8")
Beispiel #8
0
class PDFXBlock(XBlock):
    """
    PDF XBlock.
    """

    loader = ResourceLoader(__name__)

    # Icon of the XBlock. Values : [other (default), video, problem]

    icon_class = 'other'

    # Enable view as specific student

    show_in_read_only_mode = True

    # Fields

    display_name = String(
        display_name=_('Display Name'),
        default=_('PDF'),
        scope=Scope.settings,
        help=
        _('This name appears in the horizontal navigation at the top of the page.'
          ))

    url = String(
        display_name=_('PDF URL'),
        default=_('https://tutorial.math.lamar.edu/pdf/Trig_Cheat_Sheet.pdf'),
        scope=Scope.content,
        help=_('The URL for your PDF.'),
    )

    allow_download = Boolean(
        display_name=_('PDF Download Allowed'),
        default=True,
        scope=Scope.content,
        help=_('Display a download button for this PDF.'),
    )

    source_text = String(
        display_name=_('Source document button text'),
        default='',
        scope=Scope.content,
        help=_(
            'Add a download link for the source file of your PDF. '
            'Use it for example to provide the PowerPoint file used to create this PDF.'
        ))

    source_url = String(
        display_name=_('Source document URL'),
        default='',
        scope=Scope.content,
        help=_(
            'Add a download link for the source file of your PDF. '
            'Use it for example to provide the PowerPoint file used to create this PDF.'
        ))

    def load_resource(self, resource_path):  # pylint: disable=no-self-use
        """
        Gets the content of a resource
        """

        resource_content = pkg_resources.resource_string(
            __name__, resource_path)
        return resource_content.decode('utf-8')

    def render_template(self, path, context=None):
        """
        Evaluate a template by resource path, applying the provided context
        """

        return self.loader.render_django_template(
            os.path.join('static/html', path),
            context=Context(context or {}),
            i18n_service=self.runtime.service(self, 'i18n'))

    def student_view(self, context=None):
        """
        The primary view of the XBlock, shown to students
        when viewing courses.
        """

        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url,
        }
        html = self.render_template('pdf_view.html', context)

        frag = Fragment(html)
        frag.add_css(self.load_resource('static/css/pdf.css'))
        frag.add_javascript(self.load_resource('static/js/pdf_view.js'))
        frag.initialize_js('pdfXBlockInitView')
        return frag

    def studio_view(self, context=None):
        """
        The secondary view of the XBlock, shown to teachers
        when editing the XBlock.
        """

        context = {
            'display_name': self.display_name,
            'url': self.url,
            'allow_download': self.allow_download,
            'source_text': self.source_text,
            'source_url': self.source_url,
        }
        html = self.render_template('pdf_edit.html', context)

        frag = Fragment(html)
        frag.add_javascript(self.load_resource('static/js/pdf_edit.js'))
        frag.initialize_js('pdfXBlockInitEdit')
        return frag

    @XBlock.json_handler
    def save_pdf(self, data, suffix=''):  # pylint: disable=unused-argument
        """
        The saving handler.
        """
        self.display_name = data['display_name']
        self.url = data['url']
        self.allow_download = data[
            'allow_download'] == 'True'  # Basic str to translation
        self.source_text = data['source_text']
        self.source_url = data['source_url']

        return {'result': 'success'}
Beispiel #9
0
 def test_load_unicode_from_another_module(self):
     s = ResourceLoader("tests.unit.data").load_unicode(
         "simple_django_template.txt")
     self.assertEquals(s, expected_string)
Beispiel #10
0
from xblock.core import XBlock
from xblock.fields import Boolean, Dict, List, Scope, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin

from .dashboard_visual import DashboardVisualData
from .mcq import MCQBlock
from .sub_api import sub_api

# Globals ###########################################################

log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)


def _(text):
    """ A no-op to mark strings that we need to translate """
    return text


# Classes ###########################################################


class ExportMixin(object):
    """
    Used by blocks which need to provide a downloadable export.
    """
    def _get_user_full_name(self):
 def test_render_template_deprecated(self, mock_warn):
     loader = ResourceLoader(__name__)
     s = loader.render_template("data/simple_django_template.txt", example_context)
     self.assertTrue(mock_warn.called)
     self.assertEquals(s, expected_filled_template)
from xblock.fields import Scope, Boolean, Dict, Float, Integer, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin

try:
    # Used to detect if we're in the workbench so we can add Underscore.js
    from workbench.runtime import WorkbenchRuntime
except ImportError:
    WorkbenchRuntime = False  # pylint: disable=invalid-name

from .grader import Grader
from .utils import get_doc_link

loader = ResourceLoader(__name__)  # pylint: disable=invalid-name

log = logging.getLogger(__name__)  # pylint: disable=invalid-name


class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
    """
    An XBlock that allows course authors to define vector drawing exercises.
    """

    icon_class = "problem"

    # Content
    display_name = String(display_name="Title (display name)",
                          help="Title to display",
                          default="Vector Drawing",
class FreeTextResponse(
        EnforceDueDates,
        MissingDataFetcherMixin,
        StudioEditableXBlockMixin,
        XBlock,
):
    #  pylint: disable=too-many-ancestors, too-many-instance-attributes
    """
    Enables instructors to create questions with free-text responses.
    """

    loader = ResourceLoader(__name__)

    @staticmethod
    def workbench_scenarios():
        """
        Gather scenarios to be displayed in the workbench
        """
        scenarios = [
            ('Free-text Response XBlock', '''<sequence_demo>
                    <freetextresponse />
                    <freetextresponse name='My First XBlock' />
                    <freetextresponse
                        display_name="Full Credit is asdf, half is fdsa"
                        min_word_count="2"
                        max_word_count="3"
                        max_attempts="5"
                    />
                    <freetextresponse
                        display_name="Min words 2"
                        min_word_count="2"
                    />
                    <freetextresponse
                        display_name="Max Attempts 5 XBlock"
                        max_attempts="5"
                    />
                    <freetextresponse
                        display_name="Full credit is asdf, Max Attempts 3"
                        max_attempts="3"
                        min_word_count="2"
                        fullcredit_keyphrases="['asdf']"
                    />
                    <freetextresponse
                        display_name="New submitted message"
                        submitted_message="Different message"
                    />
                    <freetextresponse
                        display_name="Blank submitted message"
                        submitted_message=""
                    />
                    <freetextresponse
                        display_name="Display correctness if off"
                        display_correctness="False"
                    />
                </sequence_demo>
             '''),
        ]
        return scenarios

    display_correctness = Boolean(
        display_name=_('Mostrar ticket?'),
        help=
        _('Si se muestra o no el ticket verde al responder el nro correcto de caracteres'
          ),
        default=True,
        scope=Scope.settings,
    )
    display_other_student_responses = Boolean(
        display_name=_('Display Other Student Responses'),
        help=_('This will display other student responses to the '
               'student after they submit their response.'),
        default=False,
        scope=Scope.settings,
    )
    displayable_answers = List(
        default=[],
        scope=Scope.user_state_summary,
        help=_('System selected answers to give to students'),
    )
    display_name = String(
        display_name=_('Display Name'),
        help=_('This is the title for this question type'),
        default='',
        scope=Scope.settings,
    )
    fullcredit_keyphrases = List(
        display_name=_('Full-Credit Key Phrases'),
        help=_('This is a list of words or phrases, one of '
               'which must be present in order for the student\'s answer '
               'to receive full credit'),
        default=[],
        scope=Scope.settings,
    )
    halfcredit_keyphrases = List(
        display_name=_('Half-Credit Key Phrases'),
        help=_('This is a list of words or phrases, one of '
               'which must be present in order for the student\'s answer '
               'to receive half credit'),
        default=[],
        scope=Scope.settings,
    )
    max_attempts = Integer(
        display_name=_('Nro de Intentos'),
        help=_('Nro de veces que el estudiante puede intentar responder'),
        default=1,
        values={'min': 1},
        scope=Scope.settings,
    )
    max_word_count = Integer(
        display_name=_("Nro. máximo de caracteres."),
        help=_('Max nro de caracteres que puede tener la respuesta'),
        default=100000,
        values={'min': 1},
        scope=Scope.settings,
    )
    min_word_count = Integer(
        display_name=_("Nro. mínimo de caracteres."),
        help=_('Min nro de caracteres que puede tener la respuesta'),
        default=10,
        values={'min': 10},
        scope=Scope.settings,
    )
    prompt = String(
        display_name=_('Prompt'),
        help=_('This is the prompt students will see when '
               'asked to enter their response'),
        default='',
        scope=Scope.settings,
        multiline_editor=True,
    )
    submitted_message = String(
        display_name=_('Mensaje Respuesta bien recibida'),
        help=
        _('Este es el mensaje que se ve luego de responder con el nro correcto de caracteres'
          ),
        default=_("¡Tu respuesta ha sido recibida con éxito!"),
        scope=Scope.settings,
    )
    weight = Integer(
        display_name=_('Peso'),
        help=_('Peso del problema en una evaluacion'),
        default=1,
        values={'min': 0},
        scope=Scope.settings,
    )
    saved_message = String(
        display_name=_('Draft Received Message'),
        help=_('This is the message students will see upon '
               'submitting a draft response'),
        default=_("Tu respuesta ha sido guardada, pero aún no se envía"),
        scope=Scope.settings,
    )

    theme = String(display_name=_("Estilo"),
                   help=_("Cambiar estilo"),
                   default="SumaySigue",
                   values=["SumaySigue", "Media"],
                   scope=Scope.settings)

    count_attempts = Integer(
        default=0,
        scope=Scope.user_state,
    )
    score = Float(
        default=0.0,
        scope=Scope.user_state,
    )
    student_answer = String(
        default='',
        scope=Scope.user_state,
    )

    has_score = True

    icon_class = "problem"

    #saque todo lo que no se va a usar para que no de problemas
    editable_fields = (
        #'display_name',
        #'prompt',
        'min_word_count',
        'max_word_count',
        #'fullcredit_keyphrases',
        #'halfcredit_keyphrases',
        'submitted_message',
        #'display_other_student_responses',
        #'saved_message',
        'weight',
        'max_attempts',
        'display_correctness',
        'theme')

    def build_fragment(
        self,
        rendered_template,
        initialize_js_func,
        additional_css=[],
        additional_js=[],
    ):
        #  pylint: disable=dangerous-default-value, too-many-arguments
        """
        Creates a fragment for display.
        """
        fragment = Fragment(rendered_template)
        for item in additional_css:
            url = self.resource_string(item)
            fragment.add_css(url)
        for item in additional_js:
            url = self.resource_string(item)
            fragment.add_javascript(url)
        fragment.initialize_js(initialize_js_func)
        return fragment

    def resource_string(self, path):
        """Handy helper for getting resources from our kit."""
        data = pkg_resources.resource_string(__name__, path).decode()
        return data

    # Decorate the view in order to support multiple devices e.g. mobile
    # See: https://openedx.atlassian.net/wiki/display/MA/Course+Blocks+API
    # section 'View @supports(multi_device) decorator'
    @XBlock.supports('multi_device')
    def student_view(self, context={}):
        # pylint: disable=dangerous-default-value
        """The main view of FreeTextResponse, displayed when viewing courses.

        The main view which displays the general layout for FreeTextResponse

        Args:
            context: Not used for this view.

        Returns:
            (Fragment): The HTML Fragment for this XBlock, which determines the
            general frame of the FreeTextResponse Question.
        """
        display_other_responses = self.display_other_student_responses
        self.runtime.service(self, 'i18n')
        context.update({
            'display_name':
            self.display_name,
            'location':
            str(self.scope_ids.usage_id).split('@')[-1],
            'indicator_class':
            self._get_indicator_class(),
            'nodisplay_class':
            self._get_nodisplay_class(),
            'problem_progress':
            self._get_problem_progress(),
            'prompt':
            self.prompt,
            'student_answer':
            self.student_answer,
            'theme':
            self.theme,
            #no se por que self.is_past_due() da siempre true al testear
            'is_past_due':
            self.get_is_past_due(),
            'used_attempts_feedback':
            self._get_used_attempts_feedback(),
            'visibility_class':
            self._get_indicator_visibility_class(),
            'word_count_message':
            self._get_word_count_message(),
            'display_other_responses':
            display_other_responses,
            'other_responses':
            self.get_other_answers(),
            'image_path':
            self.runtime.local_resource_url(self, "public/images/")
        })
        template = self.loader.render_django_template(
            'templates/freetextresponse_view.html',
            context=Context(context),
            i18n_service=self.runtime.service(self, 'i18n'),
        )

        fragment = self.build_fragment(
            template,
            initialize_js_func='FreeTextResponseView',
            additional_css=[
                'public/view.css',
            ],
            additional_js=[
                'public/view.js',
            ],
        )
        return fragment

    def max_score(self):
        """
        Returns the configured number of possible points for this component.
        Arguments:
            None
        Returns:
            float: The number of possible points for this component
        """
        return self.weight

    @classmethod
    def _generate_validation_message(cls, msg):
        """
        Helper method to generate a ValidationMessage from
        the supplied string
        """
        result = ValidationMessage(ValidationMessage.ERROR, ugettext(str(msg)))
        return result

    def validate_field_data(self, validation, data):
        """
        Validates settings entered by the instructor.
        """
        if data.weight < 0:
            msg = FreeTextResponse._generate_validation_message(
                'Weight Attempts cannot be negative')
            validation.add(msg)
        if data.max_attempts < 0:
            msg = FreeTextResponse._generate_validation_message(
                'Maximum Attempts cannot be negative')
            validation.add(msg)
        if data.min_word_count < 1:
            msg = FreeTextResponse._generate_validation_message(
                'Minimum Word Count cannot be less than 1')
            validation.add(msg)
        if data.min_word_count > data.max_word_count:
            msg = FreeTextResponse._generate_validation_message(
                'Minimum Word Count cannot be greater than Max Word Count')
            validation.add(msg)
        if not data.submitted_message:
            msg = FreeTextResponse._generate_validation_message(
                'Submission Received Message cannot be blank')
            validation.add(msg)

    def _get_indicator_visibility_class(self):
        """
        Returns the visibility class for the correctness indicator html element
        """
        if self.display_correctness:
            result = ''
        else:
            result = 'hidden'
        return result

    def _get_word_count_message(self):
        """
        Returns the word count message
        """
        if len(self.student_answer.strip()) > self.max_word_count:
            result = "Debes escribir como máximo {max} caracteres.".format(
                max=self.max_word_count)
        else:
            result = ungettext("Debes escribir al menos {min} caracteres.",
                               "Debes escribir al menos {min} caracteres.",
                               self.max_word_count).format(
                                   min=self.min_word_count,
                                   max=self.max_word_count,
                               )
        return result

    def _get_invalid_word_count_message(self, ignore_attempts=False):
        """
        Returns the invalid word count message
        """
        result = ''
        if ((ignore_attempts or self.count_attempts > 0)
                and (not self._word_count_valid())):
            word_count_message = self._get_word_count_message()
            result = ugettext("{word_count_message}").format(
                word_count_message=word_count_message, )
        return result

    def _get_indicator_class(self):
        """
        Returns the class of the correctness indicator element
        """
        result = 'unanswered'
        if self.display_correctness and self._word_count_valid():
            if self._determine_credit() == Credit.zero:
                result = 'incorrect'
            else:
                result = 'correct'
        return result

    def _word_count_valid(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's answer is valid
        Jromero: Overwritten to use character count
        """
        word_count = len(self.student_answer.strip())
        result = (word_count <= self.max_word_count
                  and word_count >= self.min_word_count)
        return result

    @classmethod
    def _is_at_least_one_phrase_present(cls, phrases, answer):
        """
        Determines if at least one of the supplied phrases is
        present in the given answer
        """
        answer = answer.lower()
        matches = [phrase.lower() in answer for phrase in phrases]
        return any(matches)

    def _get_problem_progress(self):
        """
        Returns a statement of progress for the XBlock, which depends
        on the user's current score
        """
        if self.weight == 0:
            result = ''
        elif self.score == 0.0:
            result = "({})".format(
                ungettext(
                    "{weight} point possible",
                    "{weight} points possible",
                    self.weight,
                ).format(weight=self.weight, ))
        else:
            scaled_score = self.score * self.weight
            # No trailing zero and no scientific notation
            score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.')
            result = "({})".format(
                ungettext(
                    "{score_string}/{weight} point",
                    "{score_string}/{weight} points",
                    self.weight,
                ).format(
                    score_string=score_string,
                    weight=self.weight,
                ))
        return result

    def _compute_score(self):
        """
        Computes and publishes the user's core for the XBlock
        based on their answer
        """
        credit = self._determine_credit()
        self.score = credit.value
        try:
            self.runtime.publish(self, 'grade', {
                'value': self.score * self.weight,
                'max_value': self.weight
            })
        except IntegrityError:
            pass

    def _determine_credit(self):
        #  Not a standard xlbock pylint disable.
        # This is a problem with pylint 'enums and R0204 in general'
        """
        Helper Method that determines the level of credit that
        the user should earn based on their answer
        """
        result = None
        if self.student_answer == '' or not self._word_count_valid():
            result = Credit.zero
        else:
            result = Credit.full
        #No voy a ocupar keyphrases asi que sacare esto para que no de problemas
        """
        elif not self.fullcredit_keyphrases \
                and not self.halfcredit_keyphrases:
            result = Credit.full
        elif FreeTextResponse._is_at_least_one_phrase_present(
                self.fullcredit_keyphrases,
                self.student_answer
        ):
            result = Credit.full
        elif FreeTextResponse._is_at_least_one_phrase_present(
                self.halfcredit_keyphrases,
                self.student_answer
        ):
            result = Credit.half
        else:
            result = Credit.zero
        """
        return result

    def _get_used_attempts_feedback(self):
        """
        Returns the text with feedback to the user about the number of attempts
        they have used if applicable
        """
        result = ''
        if self.max_attempts > 0:
            result = ungettext(
                'Has realizado {count_attempts} de {max_attempts} intento',
                'Has realizado {count_attempts} de {max_attempts} intentos',
                self.max_attempts,
            ).format(
                count_attempts=self.count_attempts,
                max_attempts=self.max_attempts,
            )
        return result

    def _get_nodisplay_class(self):
        """
        Returns the css class for the submit button
        """
        result = ''
        if self.max_attempts > 0 and self.count_attempts >= self.max_attempts:
            result = 'nodisplay'
        return result

    def _get_submitted_message(self):
        """
        Returns the message to display in the submission-received div
        """
        result = ''
        if self._word_count_valid():
            result = self.submitted_message
        return result

    def _get_user_alert(self, ignore_attempts=False):
        """
        Returns the message to display in the user_alert div
        depending on the student answer
        """
        result = ''
        if not self._word_count_valid():
            result = self._get_invalid_word_count_message(ignore_attempts)
        return result

    def _can_submit(self):
        #no se por q el past due da True siempre en ambientes de test
        #if self.is_past_due():
        #    return False
        if self.max_attempts == 0:
            return True
        if self.count_attempts < self.max_attempts:
            return True
        return False

    def get_is_past_due(self):
        if hasattr(self, 'show_correctness'):
            return self.is_past_due()
        else:
            return False

    @XBlock.json_handler
    def submit(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's submission
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        sub_msg_error = False
        if self._can_submit():
            if (self.count_attempts +
                    1) <= self.max_attempts or self.max_attempts <= 0:
                self.student_answer = data['student_answer']
            else:
                sub_msg_error = True
            # Counting the attempts and publishing a score
            # Pero no los cuento si puse menos caracteres
            self._compute_score()
            if self._determine_credit() != Credit.zero:
                self.count_attempts += 1
            display_other_responses = self.display_other_student_responses
            if display_other_responses and data.get('can_record_response'):
                self.store_student_response()
        else:
            sub_msg_error = True

        sub_msg = self._get_submitted_message()
        if sub_msg_error:
            sub_msg = "Error: El estado de la pregunta fue modificado, por favor recargue el sitio"

        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'indicator_class': self._get_indicator_class(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': sub_msg,
            'user_alert': self._get_user_alert(ignore_attempts=True, ),
            'other_responses': self.get_other_answers(),
            'display_other_responses': self.display_other_student_responses,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    @XBlock.json_handler
    def save_reponse(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's save
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self.max_attempts == 0 or self.count_attempts < self.max_attempts:
            self.student_answer = data['student_answer']
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': '',
            'user_alert': str(self.saved_message),
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    def store_student_response(self):
        """
        Submit a student answer to the answer pool by appending the given
        answer to the end of the list.
        """
        # if the answer is wrong, do not display it
        if self.score != Credit.full.value:
            return

        student_id = self.get_student_id()
        # remove any previous answers the student submitted
        for index, response in enumerate(self.displayable_answers):
            if response['student_id'] == student_id:
                del self.displayable_answers[index]
                break

        self.displayable_answers.append({
            'student_id': student_id,
            'answer': self.student_answer,
        })

        # Want to store extra response so student can still see
        # MAX_RESPONSES answers if their answer is in the pool.
        response_index = -(MAX_RESPONSES + 1)
        self.displayable_answers = self.displayable_answers[response_index:]

    def get_other_answers(self):
        """
        Returns at most MAX_RESPONSES answers from the pool.

        Does not return answers the student had submitted.
        """
        student_id = self.get_student_id()
        display_other_responses = self.display_other_student_responses
        shouldnt_show_other_responses = not display_other_responses
        student_answer_incorrect = self._determine_credit() == Credit.zero
        if student_answer_incorrect or shouldnt_show_other_responses:
            return []
        return_list = [
            response for response in self.displayable_answers
            if response['student_id'] != student_id
        ]

        return_list = return_list[-(MAX_RESPONSES):]
        return return_list
    def handle_request(self, request):
        """
        Handler for Outcome Service requests.

        Parses and validates XML request body. Currently, only the
        replaceResultRequest action is supported.

        Example of request body from LTI provider::

        <?xml version = "1.0" encoding = "UTF-8"?>
            <imsx_POXEnvelopeRequest xmlns = "some_link (may be not required)">
              <imsx_POXHeader>
                <imsx_POXRequestHeaderInfo>
                  <imsx_version>V1.0</imsx_version>
                  <imsx_messageIdentifier>528243ba5241b</imsx_messageIdentifier>
                </imsx_POXRequestHeaderInfo>
              </imsx_POXHeader>
              <imsx_POXBody>
                <replaceResultRequest>
                  <resultRecord>
                    <sourcedGUID>
                      <sourcedId>feb-123-456-2929::28883</sourcedId>
                    </sourcedGUID>
                    <result>
                      <resultScore>
                        <language>en-us</language>
                        <textString>0.4</textString>
                      </resultScore>
                    </result>
                  </resultRecord>
                </replaceResultRequest>
              </imsx_POXBody>
            </imsx_POXEnvelopeRequest>

        See /templates/xml/outcome_service_response.xml for the response body format.

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request

        Returns:
            str: Outcome Service XML response
        """
        resource_loader = ResourceLoader(__name__)
        response_xml_template = resource_loader.load_unicode(
            '/templates/xml/outcome_service_response.xml')

        # Returns when `action` is unsupported.
        # Supported actions:
        #   - replaceResultRequest.
        unsupported_values = {
            'imsx_codeMajor': 'unsupported',
            'imsx_description':
            'Target does not support the requested operation.',
            'imsx_messageIdentifier': 'unknown',
            'response': ''
        }
        # Returns if:
        #   - past due grades are not accepted and grade is past due
        #   - score is out of range
        #   - can't parse response from TP;
        #   - can't verify OAuth signing or OAuth signing is incorrect.
        failure_values = {
            'imsx_codeMajor': 'failure',
            'imsx_description': 'The request has failed.',
            'imsx_messageIdentifier': 'unknown',
            'response': ''
        }
        request_body = request.body.decode('utf-8')

        if not self.xblock.accept_grades_past_due and self.xblock.is_past_due(
        ):
            failure_values['imsx_description'] = "Grade is past due"
            return response_xml_template.format(**failure_values)

        try:
            imsx_message_identifier, sourced_id, score, action = parse_grade_xml_body(
                request_body)
        except LtiError as ex:
            body = escape(request_body) if request_body else ''
            error_message = "Request body XML parsing error: {} {}".format(
                str(ex), body)
            log.debug("[LTI]: %s", error_message)
            failure_values['imsx_description'] = error_message
            return response_xml_template.format(**failure_values)

        # Verify OAuth signing.
        __, secret = self.xblock.lti_provider_key_secret
        try:
            verify_oauth_body_signature(request, secret,
                                        self.xblock.outcome_service_url)
        except (ValueError, LtiError) as ex:
            failure_values['imsx_messageIdentifier'] = escape(
                imsx_message_identifier)
            error_message = "OAuth verification error: " + escape(str(ex))
            failure_values['imsx_description'] = error_message
            log.debug("[LTI]: %s", error_message)
            return response_xml_template.format(**failure_values)

        real_user = self.xblock.runtime.get_real_user(
            urllib.parse.unquote(sourced_id.split(':')[-1]))
        if not real_user:  # that means we can't save to database, as we do not have real user id.
            failure_values['imsx_messageIdentifier'] = escape(
                imsx_message_identifier)
            failure_values['imsx_description'] = "User not found."
            return response_xml_template.format(**failure_values)

        if action == 'replaceResultRequest':
            self.xblock.set_user_module_score(real_user, score,
                                              self.xblock.max_score())

            values = {
                'imsx_codeMajor':
                'success',
                'imsx_description':
                'Score for {sourced_id} is now {score}'.format(
                    sourced_id=sourced_id, score=score),
                'imsx_messageIdentifier':
                escape(imsx_message_identifier),
                'response':
                '<replaceResultResponse/>'
            }
            log.debug(u"[LTI]: Grade is saved.")
            return response_xml_template.format(**values)

        unsupported_values['imsx_messageIdentifier'] = escape(
            imsx_message_identifier)
        log.debug(u"[LTI]: Incorrect action.")
        return response_xml_template.format(**unsupported_values)
Beispiel #15
0
class OppiaXBlock(XBlock):
    """
    An XBlock providing an embedded Oppia exploration.
    """
    loader = ResourceLoader(__name__)

    _EVENT_NAME_EXPLORATION_LOADED = 'oppia.exploration.loaded'
    _EVENT_NAME_EXPLORATION_COMPLETED = 'oppia.exploration.completed'
    _EVENT_NAME_STATE_TRANSITION = 'oppia.exploration.state.changed'

    display_name = String(
        help=_("Display name of the component"),
        default=_("Oppia Exploration"),
        scope=Scope.content)
    oppiaid = String(
        help=_("ID of the Oppia exploration to embed"),
        default="4",
        scope=Scope.content)
    src = String(
        help=_("Source URL of the site"),
        default="https://www.oppia.org",
        scope=Scope.content)

    def resource_string(self, path):
        """Handy helper for getting resources from our kit."""
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

    def render_template(self, path, context):
        return self.loader.render_django_template(
            os.path.join('templates', path),
            context=Context(context),
            i18n_service=self.runtime.service(self, "i18n"),
        )

    def get_translation_content(self):
        try:
            return self.resource_string('static/js/translations/{lang}/textjs.js'.format(
                lang=utils.translation.get_language(),
            ))
        except IOError:
            return self.resource_string('static/js/translations/en/textjs.js')

    def student_view(self, context=None):
        """
        The primary view of the OppiaXBlock, shown to students
        when viewing courses.
        """
        frag = Fragment(self.render_template("oppia.html", {
            'src': self.src,
            'oppiaid': self.oppiaid,
        }))
        frag.add_javascript(self.get_translation_content())
        frag.add_javascript(
            self.resource_string('static/lib/oppia-player-0.0.1-modified.js'))
        frag.add_javascript(self.resource_string("static/js/oppia.js"))
        frag.initialize_js('OppiaXBlock')
        return frag

    def author_view(self, context=None):
        """
        A view of the XBlock to show within the Studio preview. For some
        reason, the student_view() does not display, so we show a placeholder
        instead.
        """
        frag = Fragment(self.render_template("oppia_preview.html", {
            'src': self.src,
            'oppiaid': self.oppiaid,
        }))
        frag.add_javascript(self.get_translation_content())
        return frag

    def _log(self, event_name, payload):
        """
        Logger for load, state transition and completion events.
        """
        self.runtime.publish(self, event_name, payload)

    @XBlock.json_handler
    def on_exploration_loaded(self, data, suffix=''):
        """Called when an exploration has loaded."""
        self._log(self._EVENT_NAME_EXPLORATION_LOADED, {
            'exploration_id': self.oppiaid,
            'exploration_version': data['explorationVersion'],
        })

    @XBlock.json_handler
    def on_state_transition(self, data, suffix=''):
        """Called when a state transition in the exploration has occurred."""
        self._log(self._EVENT_NAME_STATE_TRANSITION, {
            'exploration_id': self.oppiaid,
            'old_state_name': data['oldStateName'],
            'new_state_name': data['newStateName'],
            'exploration_version': data['explorationVersion'],
        })

    @XBlock.json_handler
    def on_exploration_completed(self, data, suffix=''):
        """Called when the exploration has been completed."""
        self._log(self._EVENT_NAME_EXPLORATION_COMPLETED, {
            'exploration_id': self.oppiaid,
            'exploration_version': data['explorationVersion'],
        })

    def studio_view(self, context):
        """
        Create a fragment used to display the edit view in the Studio.
        """
        frag = Fragment(self.render_template("oppia_edit.html", {
            'src': self.src,
            'oppiaid': self.oppiaid or '',
            'display_name': self.display_name,
        }))

        frag.add_javascript(self.get_translation_content())
        js_str = pkg_resources.resource_string(
            __name__, "static/js/oppia_edit.js")
        frag.add_javascript(unicode(js_str))
        frag.initialize_js('OppiaXBlockEditor')

        return frag

    @XBlock.json_handler
    def studio_submit(self, data, suffix=''):
        """
        Called when submitting the form in Studio.
        """
        self.oppiaid = data.get('oppiaid')
        self.src = data.get('src')
        self.display_name = data.get('display_name')

        return {'result': 'success'}

    @staticmethod
    def workbench_scenarios():
        """A canned scenario for display in the workbench."""
        return [
            ("Oppia Embedding",
             """<vertical_demo>
                <oppia oppiaid="0" src="https://www.oppia.org"/>
                </vertical_demo>
             """),
        ]
Beispiel #16
0
 def test_load_scenarios_with_identifiers(self):
     loader = ResourceLoader(__name__)
     scenarios = loader.load_scenarios_from_path("data",
                                                 include_identifier=True)
     self.assertEquals(scenarios, expected_scenarios_with_identifiers)
Beispiel #17
0
 def test_load_scenarios(self):
     loader = ResourceLoader(__name__)
     scenarios = loader.load_scenarios_from_path("data")
     self.assertEquals(scenarios, expected_scenarios)
Beispiel #18
0
 def test_render_js_template(self):
     loader = ResourceLoader(__name__)
     s = loader.render_js_template("data/simple_django_template.txt",
                                   example_id, example_context)
     self.assertEquals(s, expected_filled_js_template)
Beispiel #19
0
 def test_render_template_deprecated(self, mock_warn):
     loader = ResourceLoader(__name__)
     s = loader.render_template("data/simple_django_template.txt",
                                example_context)
     self.assertTrue(mock_warn.called)
     self.assertEquals(s, expected_filled_template)
Beispiel #20
0
    def calculate_results(self, submissions):
        score = 0
        results = []
        tips = None

        if not self.hide_results:
            tips = self.get_tips()

        for choice in self.custom_choices:
            choice_completed = True
            choice_tips_html = []
            choice_selected = choice.value in submissions

            if choice.value in self.required_choices:
                if not choice_selected:
                    choice_completed = False
            elif choice_selected and choice.value not in self.ignored_choices:
                choice_completed = False

            if choice_completed:
                score += 1

            choice_result = {
                'value': choice.value,
                'selected': choice_selected,
                'content': choice.content
            }
            # Only include tips/results in returned response if we want to display them
            if not self.hide_results:
                # choice_tips_html list is being set only when 'self.hide_results' is False, to optimize,
                # execute the loop only when 'self.hide_results' is set to False
                for tip in tips:
                    if choice.value in tip.values:
                        choice_tips_html.append(
                            tip.render('mentoring_view').content)
                        break

                loader = ResourceLoader(__name__)
                choice_result['completed'] = choice_completed
                choice_result['tips'] = loader.render_django_template(
                    'templates/html/tip_choice_group.html', {
                        'tips_html': choice_tips_html,
                    })

            results.append(choice_result)

        status = 'incorrect' if score <= 0 else 'correct' if score >= len(
            results) else 'partial'

        if sub_api:
            # Send the answer as a concatenated list to the submissions API
            answer = [
                choice['content'] for choice in results if choice['selected']
            ]
            sub_api.create_submission(self.student_item_key, ', '.join(answer))

        return {
            'submissions': submissions,
            'status': status,
            'choices': results,
            'message': self.message_formatted,
            'weight': self.weight,
            'score': (float(score) / len(results)) if results else 0,
        }
class PlatformTourXBlock(XBlock):
    """
    Allows students to tour through the course and get familiar with the
    platform.
    """

    loader = ResourceLoader(__name__)

    display_name = String(
        display_name=('Display Name'),
        help=('The title for this component'),
        default='Platform Tour',
        scope=Scope.settings,
    )
    button_label = String(
        display_name=('Button label'),
        help=('The text that will appear on the button on which learners click'
              ' to start the Platform Tour.'),
        default='Begin Platform Tour',
        scope=Scope.settings,
    )
    intro = String(
        display_name=('Introduction text'),
        help=('The introduction that will precede the button'
              ' and explain its presence to the user'),
        default='Click the button below to learn how to navigate the platform.',
        scope=Scope.settings,
    )
    enabled_default_steps = List(
        display_name=('Choose the steps for the Platform Tour'),
        help=('List representing steps of the tour'),
        default=None,
        multiline_editor=True,
        scope=Scope.settings,
        resettable_editor=False,
    )
    custom_steps = List(
        display_name=('Custom steps for the platform tour'),
        help=('JSON dictionaries representing additional steps of the tour'),
        default=[],
        multiline_editor=True,
        scope=Scope.settings,
    )

    def get_resource_url(self, path):
        """
        Retrieve a public URL for the file path
        """
        path = os.path.join('public', path)
        resource_url = self.runtime.local_resource_url(self, path)
        return resource_url

    def build_fragment(
        self,
        rendered_template,
        initialize_js_func,
        additional_css=None,
        additional_js=None,
    ):
        """
        Build the HTML fragment, and add required static assets to it.
        """
        additional_css = additional_css or []
        additional_js = additional_js or []
        fragment = Fragment(rendered_template)
        for item in additional_css:
            url = self.get_resource_url(item)
            fragment.add_css_url(url)
        for item in additional_js:
            url = self.get_resource_url(item)
            fragment.add_javascript_url(url)
        fragment.initialize_js(initialize_js_func)
        return fragment

    def student_view(self, context=None):
        """
        The primary view of the PlatformTourXBlock, shown to students
        when viewing courses.
        """
        enabled_default_step_keys = self.enabled_default_steps
        if enabled_default_step_keys is None:
            enabled_default_step_keys = default_steps.get_default_keys()
        step_choice_dict = default_steps.get_display_steps(
            enabled_default_step_keys)
        if 'custom' in enabled_default_step_keys:
            step_choice_dict.extend(self.custom_steps)
        steps = json.dumps(step_choice_dict)

        context = context or {}
        context.update({
            'display_name': self.display_name,
            'button_label': self.button_label,
            'intro': self.intro,
            'steps': steps,
        })
        rendered_template = self.loader.render_django_template(
            'templates/platformtour.html',
            context=Context(context),
        )
        fragment = self.build_fragment(
            rendered_template,
            initialize_js_func='PlatformTourXBlock',
            additional_css=[
                'css/platformtour.css',
            ],
            additional_js=[
                'js/src/intro.js',
                'js/src/platformtour.js',
            ],
        )
        return fragment

    def studio_view(self, context=None):
        """
        Build the fragment for the edit/studio view
        Implementation is optional.
        """
        enabled_default_step_keys = self.enabled_default_steps
        if enabled_default_step_keys is None:
            enabled_default_step_keys = default_steps.get_default_keys()
        context = context or {}
        context.update({
            'display_name':
            self.display_name,
            'button_label':
            self.button_label,
            'intro':
            self.intro,
            'enabled_default_steps':
            default_steps.get_choices(enabled_default_step_keys),
            'custom_steps':
            json.dumps(self.custom_steps),
        })
        rendered_template = self.loader.render_django_template(
            'templates/platformtour_studio.html',
            context=Context(context),
        )
        fragment = self.build_fragment(
            rendered_template,
            initialize_js_func='PlatformTourStudioUI',
            additional_css=[
                'css/platformtour_studio.css',
            ],
            additional_js=[
                'js/src/platformtour_studio.js',
            ],
        )
        return fragment

    @XBlock.json_handler
    def studio_view_save(self, data, suffix=''):
        """
        Save XBlock fields
        Returns: the new field values
        """

        self.display_name = data['display_name']
        self.button_label = data['button_label']
        self.intro = data['intro']
        self.enabled_default_steps = data['enabled_default_steps']
        self.custom_steps = data['custom_steps']

        return {
            'display_name': self.display_name,
            'button_label': self.button_label,
            'intro': self.intro,
            'enabled_default_steps': self.enabled_default_steps,
            'custom_steps': self.custom_steps,
        }

    # TO-DO: change this to create the scenarios you'd like to see in the
    # workbench while developing your XBlock.
    @staticmethod
    def workbench_scenarios():
        """
        A canned scenario for display in the workbench.
        """
        return [
            ("PlatformTourXBlock", """<platformtour/>
             """),
            ("Multiple PlatformTourXBlock", """<vertical_demo>
                    <platformtour
                        display_name="Platform Tour 1"
                        button_label="Start Tour #1"
                        intro="This is the Platform Tour #1, click the button to start."
                    />
                    <platformtour
                        display_name="Platform Tour 2"
                        button_label="Start Tour #2"
                        intro="This is the Platform Tour #2, click the button to start."
                    />
                    <platformtour
                        display_name="Platform Tour 3"
                        button_label="Start Tour #3"
                        intro="This is the Platform Tour #3, click the button to start."
                    />
                </vertical_demo>
             """),
        ]
    def handle_request(self, request):
        """
        Handler for Outcome Service requests.

        Parses and validates XML request body. Currently, only the
        replaceResultRequest action is supported.

        Example of request body from LTI provider::

        <?xml version = "1.0" encoding = "UTF-8"?>
            <imsx_POXEnvelopeRequest xmlns = "some_link (may be not required)">
              <imsx_POXHeader>
                <imsx_POXRequestHeaderInfo>
                  <imsx_version>V1.0</imsx_version>
                  <imsx_messageIdentifier>528243ba5241b</imsx_messageIdentifier>
                </imsx_POXRequestHeaderInfo>
              </imsx_POXHeader>
              <imsx_POXBody>
                <replaceResultRequest>
                  <resultRecord>
                    <sourcedGUID>
                      <sourcedId>feb-123-456-2929::28883</sourcedId>
                    </sourcedGUID>
                    <result>
                      <resultScore>
                        <language>en-us</language>
                        <textString>0.4</textString>
                      </resultScore>
                    </result>
                  </resultRecord>
                </replaceResultRequest>
              </imsx_POXBody>
            </imsx_POXEnvelopeRequest>

        See /templates/xml/outcome_service_response.xml for the response body format.

        Arguments:
            request (xblock.django.request.DjangoWebobRequest): Request object for current HTTP request

        Returns:
            str: Outcome Service XML response
        """
        resource_loader = ResourceLoader(__name__)
        response_xml_template = resource_loader.load_unicode('/templates/xml/outcome_service_response.xml')

        # Returns when `action` is unsupported.
        # Supported actions:
        #   - replaceResultRequest.
        unsupported_values = {
            'imsx_codeMajor': 'unsupported',
            'imsx_description': 'Target does not support the requested operation.',
            'imsx_messageIdentifier': 'unknown',
            'response': ''
        }
        # Returns if:
        #   - past due grades are not accepted and grade is past due
        #   - score is out of range
        #   - can't parse response from TP;
        #   - can't verify OAuth signing or OAuth signing is incorrect.
        failure_values = {
            'imsx_codeMajor': 'failure',
            'imsx_description': 'The request has failed.',
            'imsx_messageIdentifier': 'unknown',
            'response': ''
        }

        if not self.xblock.accept_grades_past_due and self.xblock.is_past_due:
            failure_values['imsx_description'] = "Grade is past due"
            return response_xml_template.format(**failure_values)

        try:
            imsx_message_identifier, sourced_id, score, action = parse_grade_xml_body(request.body)
        except LtiError as ex:  # pylint: disable=no-member
            body = escape(request.body) if request.body else ''
            error_message = "Request body XML parsing error: {} {}".format(ex.message, body)
            log.debug("[LTI]: %s" + error_message)
            failure_values['imsx_description'] = error_message
            return response_xml_template.format(**failure_values)

        # Verify OAuth signing.
        __, secret = self.xblock.lti_provider_key_secret
        try:
            verify_oauth_body_signature(request, secret, self.xblock.outcome_service_url)
        except (ValueError, LtiError) as ex:
            failure_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
            error_message = "OAuth verification error: " + escape(ex.message)
            failure_values['imsx_description'] = error_message
            log.debug("[LTI]: " + error_message)
            return response_xml_template.format(**failure_values)

        real_user = self.xblock.runtime.get_real_user(urllib.unquote(sourced_id.split(':')[-1]))
        if not real_user:  # that means we can't save to database, as we do not have real user id.
            failure_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
            failure_values['imsx_description'] = "User not found."
            return response_xml_template.format(**failure_values)

        if action == 'replaceResultRequest':
            self.xblock.set_user_module_score(real_user, score, self.xblock.max_score())

            values = {
                'imsx_codeMajor': 'success',
                'imsx_description': 'Score for {sourced_id} is now {score}'.format(sourced_id=sourced_id, score=score),
                'imsx_messageIdentifier': escape(imsx_message_identifier),
                'response': '<replaceResultResponse/>'
            }
            log.debug("[LTI]: Grade is saved.")
            return response_xml_template.format(**values)

        unsupported_values['imsx_messageIdentifier'] = escape(imsx_message_identifier)
        log.debug("[LTI]: Incorrect action.")
        return response_xml_template.format(**unsupported_values)
Beispiel #23
0
""" Google Calendar XBlock implementation """
# -*- coding: utf-8 -*-
#

# Imports ###########################################################
import logging

from xblock.core import XBlock
from xblock.fields import Scope, String, Integer
from xblock.fragment import Fragment

from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader

LOG = logging.getLogger(__name__)
RESOURCE_LOADER = ResourceLoader(__name__)

# Constants ###########################################################
DEFAULT_CALENDAR_ID = "*****@*****.**"
DEFAULT_CALENDAR_URL = (
    'https://www.google.com/calendar/embed?mode=Month&src={}&showCalendars=0'.format(DEFAULT_CALENDAR_ID))
CALENDAR_TEMPLATE = "/templates/html/google_calendar.html"
CALENDAR_EDIT_TEMPLATE = "/templates/html/google_calendar_edit.html"


# Classes ###########################################################
class GoogleCalendarBlock(XBlock, PublishEventMixin):  # pylint: disable=too-many-ancestors
    """
    XBlock providing a google calendar view for a specific calendar
    """
    display_name = String(
Beispiel #24
0
 def include_theme_files(self, fragment):
     theme = self.get_theme()
     theme_package, theme_files = theme['package'], theme['locations']
     for theme_file in theme_files:
         fragment.add_css(
             ResourceLoader(theme_package).load_unicode(theme_file))
Beispiel #25
0
class MufiViewMixin(
        XBlockFragmentBuilderMixin,
        StudioEditableXBlockMixin,
):
    """
    Handle view logic for XBlock instances
    """

    loader = ResourceLoader(__name__)
    static_css = [
        'view.css',
        'library/font-awesome.min.css',
    ]
    static_js_init = 'XblockMufiView'
    # Icon of the XBlock. Values :
    #   [other (default), video, problem]
    icon_class = 'problem'

    def provide_context(self, context=None):
        """
        Build a context dictionary to render the student view
        """
        context = context or {}
        context = dict(context)
        context.update({
            'display_name': self.display_name,
            'student_answer': self.student_answer,
            'is_past_due': self.is_past_due(),
            'your_answer_label': self.your_answer_label,
            'our_answer_label': self.our_answer_label,
            'answer_string': self.answer_string,
        })
        return context

    # pylint: disable=unused-argument
    @XBlock.json_handler
    def publish_event(self, data, **kwargs):
        """
        Publish an event from the front-end
        """
        try:
            event_type = data.pop('event_type')
        except KeyError:
            return {
                'result': 'error',
                'message': 'Missing event_type in JSON data',
            }
        data['user_id'] = self.scope_ids.user_id
        data['component_id'] = self._get_unique_id()
        self.runtime.publish(self, event_type, data)
        return {'result': 'success'}

    # pylint: enable=unused-argument

    # pylint: disable=unused-argument
    @XBlock.json_handler
    def student_submit(self, data, **kwargs):
        """
        Save student answer
        """
        if self.is_past_due():
            success_value = False
        else:
            success_value = True
            self.student_answer = data['answer']
        return {
            'success': success_value,
        }

    # pylint: enable=unused-argument

    def _get_unique_id(self):
        """
        Lookup the component identifier
        """
        try:
            unique_id = self.location.name
        except AttributeError:
            # workaround for xblock workbench
            unique_id = 'workbench-workaround-id'
        return unique_id

    def studio_view(self, context=None):
        """
        Build the fragment for the edit/studio view

        Implementation is optional.
        """
        context = context or {}
        context.update({
            'display_name': self.display_name,
            'your_answer_label': self.your_answer_label,
            'our_answer_label': self.our_answer_label,
            'answer_string': self.answer_string,
        })
        template = 'edit.html'
        fragment = self.build_fragment(
            template=template,
            context=context,
            js_init='XblockMufiEdit',
            css=[
                'edit.css',
                'library/font-awesome.min.css',
            ],
            js=[
                'edit.js',
            ],
        )
        return fragment

    # pylint: disable=unused-argument
    @XBlock.json_handler
    def studio_view_save(self, data, *args, **kwargs):
        """
        Save XBlock fields

        Returns: the new field values
        """
        self.display_name = data['display_name']
        self.your_answer_label = data['your_answer_label']
        self.our_answer_label = data['our_answer_label']
        self.answer_string = data['answer_string']
        return {
            'display_name': self.display_name,
            'your_answer_label': self.your_answer_label,
            'our_answer_label': self.our_answer_label,
            'answer_string': self.answer_string,
        }
Beispiel #26
0
 def test_load_scenarios(self):
     loader = ResourceLoader(__name__)
     scenarios = loader.load_scenarios_from_path("data")
     self.assertEquals(scenarios, expected_scenarios)
class FreeTextResponseViewMixin(
        I18nXBlockMixin,
        EnforceDueDates,
        XBlockFragmentBuilderMixin,
        StudioEditableXBlockMixin,
):
    """
    Handle view logic for FreeTextResponse XBlock instances
    """

    loader = ResourceLoader(__name__)
    static_js_init = 'FreeTextResponseView'

    def provide_context(self, context=None):
        """
        Build a context dictionary to render the student view
        """
        context = context or {}
        context = dict(context)
        context.update({
            'max_word_count':
            self.max_word_count,
            'comments':
            self.comments,
            'comments_upperlimit':
            self.comments_upperlimit,
            'display_name':
            self.display_name,
            'description_required':
            self.description_required,
            'description_upperlimit':
            self.description_upperlimit,
            'description':
            self.description,
            'indicator_class':
            self._get_indicator_class(),
            'nodisplay_class':
            self._get_nodisplay_class(),
            'problem_progress':
            self._get_problem_progress(),
            'prompt':
            self.prompt,
            'student_answer':
            self.student_answer,
            'student_comments':
            self.student_comments,
            'is_past_due':
            self.is_past_due(),
            'used_attempts_feedback':
            self._get_used_attempts_feedback(),
            'visibility_class':
            self._get_indicator_visibility_class(),
            'display_other_responses':
            self.display_other_student_responses,
            'other_responses':
            self.get_other_answers(),
            'user_alert':
            '',
            'submitted_message':
            '',
        })
        return context

    def _get_indicator_class(self):
        """
        Returns the class of the correctness indicator element
        """
        result = 'unanswered'
        if self.display_correctness and self._word_count_valid(
        ) and self._word_count_valid_comments():
            if self._determine_credit() == Credit.zero:
                result = 'incorrect'
            else:
                result = 'correct'
        return result

    def _get_nodisplay_class(self):
        """
        Returns the css class for the submit button
        """
        result = ''
        if self.max_attempts > 0 and self.count_attempts >= self.max_attempts:
            result = 'nodisplay'
        return result

    def _word_count_valid_min(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's answer is empty or not
        """
        word_count = len(self.student_answer.split())
        result = word_count >= self.min_word_count
        return result

    def _word_count_valid(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's answer is valid
        """
        word_count = len(self.student_answer.split())
        result = self.max_word_count >= word_count >= self.min_word_count
        return result

    def _word_count_valid_comments(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's comments is valid
        """
        word_count = len(self.student_comments.split())
        result = self.max_word_count >= word_count >= 0
        return result

    def _determine_credit(self):
        #  Not a standard xlbock pylint disable.
        # This is a problem with pylint 'enums and R0204 in general'
        """
        Helper Method that determines the level of credit that
        the user should earn based on their answer
        """
        result = None
        if self.student_answer == '' or not self._word_count_valid():
            result = Credit.zero
        elif not self.fullcredit_keyphrases \
                and not self.halfcredit_keyphrases:
            result = Credit.full
        elif _is_at_least_one_phrase_present(self.fullcredit_keyphrases,
                                             self.student_answer):
            result = Credit.full
        elif _is_at_least_one_phrase_present(self.halfcredit_keyphrases,
                                             self.student_answer):
            result = Credit.half
        else:
            result = Credit.zero
        return result

    def _get_problem_progress(self):
        """
        Returns a statement of progress for the XBlock, which depends
        on the user's current score
        """
        if self.weight == 0:
            result = ''
        elif self.score == 0.0:
            result = "({})".format(
                self.ungettext(
                    "{weight} point possible",
                    "{weight} points possible",
                    self.weight,
                ).format(weight=self.weight, ))
        else:
            scaled_score = self.score * self.weight
            # No trailing zero and no scientific notation
            score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.')
            result = "({})".format(
                self.ungettext(
                    "{score_string}/{weight} point",
                    "{score_string}/{weight} points",
                    self.weight,
                ).format(
                    score_string=score_string,
                    weight=self.weight,
                ))
        return result

    def _get_used_attempts_feedback(self):
        """
        Returns the text with feedback to the user about the number of attempts
        they have used if applicable
        """
        result = ''
        if self.max_attempts > 0:
            result = self.ungettext(
                'You have used {count_attempts} of {max_attempts} submission',
                'You have used {count_attempts} of {max_attempts} submissions',
                self.max_attempts,
            ).format(
                count_attempts=self.count_attempts,
                max_attempts=self.max_attempts,
            )
        return result

    def _get_indicator_visibility_class(self):
        """
        Returns the visibility class for the correctness indicator html element
        """
        if self.display_correctness:
            result = ''
        else:
            result = 'hidden'
        return result

    def get_other_answers(self):
        """
        Returns at most MAX_RESPONSES answers from the pool.

        Does not return answers the student had submitted.
        """
        student_id = self.get_student_id()
        display_other_responses = self.display_other_student_responses
        shouldnt_show_other_responses = not display_other_responses
        student_answer_incorrect = self._determine_credit() == Credit.zero
        if student_answer_incorrect or shouldnt_show_other_responses:
            return []
        return_list = [
            response for response in self.displayable_answers
            if response['student_id'] != student_id
        ]
        return_list = return_list[-(MAX_RESPONSES):]
        return return_list

    @XBlock.json_handler
    def submit(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's submission
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self._can_submit():
            self.student_answer = data['student_answer']
            self.student_comments = data['student_comments']
            # Counting the attempts and publishing a score
            # even if word count is invalid.
            self.count_attempts += 1
            self._compute_score()
            display_other_responses = self.display_other_student_responses
            if display_other_responses and data.get('can_record_response'):
                self.store_student_response()
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'indicator_class': self._get_indicator_class(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': self._get_submitted_message(),
            'user_alert': self._get_user_alert(ignore_attempts=True, ),
            'other_responses': self.get_other_answers(),
            'display_other_responses': self.display_other_student_responses,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    def _get_invalid_word_count_message(self, ignore_attempts=False):
        """
        Returns the invalid word count message
        """
        result = ''
        if (not self._word_count_valid_min()):
            result = self.description_required

        elif (not self._word_count_valid()):
            result = self.description_upperlimit

        elif (not self._word_count_valid_comments()):
            result = self.comments_upperlimit
        return result

    def _get_submitted_message(self):
        """
        Returns the message to display in the submission-received div
        """
        result = ''
        if self._word_count_valid() and self._word_count_valid_comments():
            result = self.submitted_message
        return result

    def _get_user_alert(self, ignore_attempts=False):
        """
        Returns the message to display in the user_alert div
        depending on the student answer
        """
        result = ''
        if not self._word_count_valid() or not self._word_count_valid_comments(
        ):
            result = self._get_invalid_word_count_message(ignore_attempts)
        return result

    def _can_submit(self):
        """
        Determine if a user may submit a response
        """
        if self.max_attempts == 0:
            return True
        if self.count_attempts < self.max_attempts:
            return True
        return False

    def _generate_validation_message(self, text):
        """
        Helper method to generate a ValidationMessage from
        the supplied string
        """
        result = ValidationMessage(ValidationMessage.ERROR,
                                   self.ugettext(text_type(text)))
        return result

    def validate_field_data(self, validation, data):
        """
        Validates settings entered by the instructor.
        """
        if data.weight < 0:
            msg = self._generate_validation_message(
                'Weight Attempts cannot be negative')
            validation.add(msg)
        if data.max_attempts < 0:
            msg = self._generate_validation_message(
                'Maximum Attempts cannot be negative')
            validation.add(msg)
        if data.min_word_count < 1:
            msg = self._generate_validation_message(
                'Minimum Word Count cannot be less than 1')
            validation.add(msg)
        if data.min_word_count > data.max_word_count:
            msg = self._generate_validation_message(
                'Minimum Word Count cannot be greater than Max Word Count')
            validation.add(msg)
        if not data.submitted_message:
            msg = self._generate_validation_message(
                'Submission Received Message cannot be blank')
            validation.add(msg)
Beispiel #28
0
 def test_load_unicode(self):
     s = ResourceLoader(__name__).load_unicode(
         "data/simple_django_template.txt")
     self.assertEquals(s, expected_string)
class FreeTextResponseViewMixin(
        EnforceDueDates,
        XBlockFragmentBuilderMixin,
        # StudioEditableXBlockMixin,
):
    """
    Handle view logic for FreeTextResponse XBlock instances
    """

    loader = ResourceLoader(__name__)
    static_js_init = 'FreeTextResponseView'

    @property
    def course_id(self):
        return self._serialize_opaque_key(self.xmodule_runtime.course_id)  # pylint:disable=E1101

    def is_course_staff(self, user, course_id):
        """
        Returns True if the user is the course staff
        else Returns False
        """
        return auth.user_has_role(
            user, CourseStaffRole(CourseKey.from_string(course_id)))

    def provide_context(self, context=None):
        """
        Build a context dictionary to render the student view
        """
        if self.get_student_id() != "student":
            user_is_admin = self.is_course_staff(
                user_by_anonymous_id(self.get_student_id()), self.course_id)
        else:
            user_is_admin = True

        context = context or {}
        context = dict(context)
        context.update({
            'display_name':
            self.display_name,
            'indicator_class':
            self._get_indicator_class(),
            'nodisplay_class':
            self._get_nodisplay_class(),
            'problem_progress':
            self._get_problem_progress(),
            'prompt':
            self.prompt,
            'student_answer':
            self.student_answer,
            'is_past_due':
            self.is_past_due(),
            'used_attempts_feedback':
            self._get_used_attempts_feedback(),
            'visibility_class':
            self._get_indicator_visibility_class(),
            'word_count_message':
            self._get_word_count_message(),
            'display_other_responses':
            self.display_other_student_responses,
            'other_responses':
            self.get_other_answers(),
            'user_is_admin':
            user_is_admin,
            'user_alert':
            '',
            'submitted_message':
            '',
        })
        return context

    def _serialize_opaque_key(self, key):
        """
        Gracefully handle opaque keys, both before and after the transition.
        https://github.com/edx/edx-platform/wiki/Opaque-Keys
        Currently uses `to_deprecated_string()` to ensure that new keys
        are backwards-compatible with keys we store in ORA2 database models.
        Args:
            key (unicode or OpaqueKey subclass): The key to serialize.
        Returns:
            unicode
        """
        if hasattr(key, 'to_deprecated_string'):
            return key.to_deprecated_string()
        else:
            return unicode(key)

    def _get_indicator_class(self):
        """
        Returns the class of the correctness indicator element
        """
        result = 'unanswered'
        if self.display_correctness and self._word_count_valid():
            if self._determine_credit() == Credit.zero:
                result = 'incorrect'
            else:
                result = 'correct'
        return result

    def _get_nodisplay_class(self):
        """
        Returns the css class for the submit button
        """
        result = ''
        if self.max_attempts > 0 and self.count_attempts >= self.max_attempts:
            result = 'nodisplay'
        return result

    def _word_count_valid(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's answer is valid
        """
        word_count = len(self.student_answer.split())
        result = self.max_word_count >= word_count >= self.min_word_count
        return result

    def _determine_credit(self):
        #  Not a standard xlbock pylint disable.
        # This is a problem with pylint 'enums and R0204 in general'
        """
        Helper Method that determines the level of credit that
        the user should earn based on their answer
        """
        result = None
        if self.student_answer == '' or not self._word_count_valid():
            result = Credit.zero
        elif not self.fullcredit_keyphrases \
                and not self.halfcredit_keyphrases:
            result = Credit.full
        elif _is_at_least_one_phrase_present(self.fullcredit_keyphrases,
                                             self.student_answer):
            result = Credit.full
        elif _is_at_least_one_phrase_present(self.halfcredit_keyphrases,
                                             self.student_answer):
            result = Credit.half
        else:
            result = Credit.zero
        return result

    def _get_problem_progress(self):
        """
        Returns a statement of progress for the XBlock, which depends
        on the user's current score
        """
        if self.weight == 0:
            result = ''
        elif self.score == 0.0:
            result = ("{weight} point possible" + "{weight} points possible" +
                      str(self.weight)).format(weight=self.weight, )

        else:
            scaled_score = self.score * self.weight
            # No trailing zero and no scientific notation
            score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.')
            result = "({})".format((
                "{score_string}/{weight} point",
                "{score_string}/{weight} points",
                self.weight,
            ).format(
                score_string=score_string,
                weight=self.weight,
            ))
        return result

    def _get_used_attempts_feedback(self):
        """
        Returns the text with feedback to the user about the number of attempts
        they have used if applicable
        """
        result = ''
        if self.max_attempts > 0 and self.count_attempts >= self.max_attempts:
            result = 'Ваш ответ отправлен'
        return result

    def _get_indicator_visibility_class(self):
        """
        Returns the visibility class for the correctness indicator html element
        """
        if self.display_correctness:
            result = ''
        else:
            result = 'hidden'
        return result

    def _get_word_count_message(self):
        """
        Returns the word count message
        """
        result = "Слишком короткое сообщение"
        return result

    def get_other_answers(self):
        """
        Returns at most MAX_RESPONSES answers from the pool.

        Does not return answers the student had submitted.
        """
        # student_id = self.get_student_id()
        #
        # display_other_responses = self.display_other_student_responses
        # shouldnt_show_other_responses = not display_other_responses
        # student_answer_incorrect = self._determine_credit() == Credit.zero

        # if student_answer_incorrect or shouldnt_show_other_responses:
        #     return []

        return_list = self.displayable_answers
        # return_list = return_list[-(MAX_RESPONSES):]
        return return_list

    @XBlock.json_handler
    def submit(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's submission
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self._can_submit():
            self.student_answer = smart_text(data['student_answer'])
            # Counting the attempts and publishing a score
            # even if word count is invalid.
            if len(self.student_answer.replace(" ", "")) > 0:
                self.count_attempts += 1
            # self._compute_score()
            # display_other_responses = self.display_other_student_responses
            # if data.get('can_record_response'):
            self.store_student_response()
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'indicator_class': self._get_indicator_class(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': self._get_submitted_message(),
            'user_alert': self._get_user_alert(ignore_attempts=True, ),
            'other_responses': self.get_other_answers(),
            # 'display_other_responses': self.display_other_student_responses,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    @XBlock.json_handler
    def save_reponse(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's save
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self.max_attempts == 0 or self.count_attempts < self.max_attempts:
            self.student_answer = data['student_answer']
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': '',
            'user_alert': self.saved_message,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    def _get_invalid_word_count_message(self, ignore_attempts=False):
        """
        Returns the invalid word count message
        """
        result = ''
        if ((ignore_attempts or self.count_attempts > 0)
                and (not self._word_count_valid())):
            word_count_message = self._get_word_count_message()
            result = "{word_count_message}".format(
                word_count_message=word_count_message, )
        return result

    def _get_submitted_message(self):
        """
        Returns the message to display in the submission-received div
        """
        result = ''
        if self._word_count_valid():
            result = self.submitted_message
        return result

    def _get_user_alert(self, ignore_attempts=False):
        """
        Returns the message to display in the user_alert div
        depending on the student answer
        """
        result = ''
        if not self._word_count_valid():
            result = self._get_invalid_word_count_message(ignore_attempts)
        return result

    def _can_submit(self):
        """
        Determine if a user may submit a response
        """
        if self.is_past_due():
            return False
        if self.max_attempts == 0:
            return True
        if self.count_attempts < self.max_attempts:
            return True
        return False

    def _generate_validation_message(self, text):
        """
        Helper method to generate a ValidationMessage from
        the supplied string
        """
        result = ValidationMessage(ValidationMessage.ERROR, text_type(text))
        return result

    def validate_field_data(self, validation, data):
        """
        Validates settings entered by the instructor.
        """
        pass
Beispiel #30
0
def lti_embed(*,
              html_element_id,
              resource_link_id,
              user_id,
              roles,
              context_id,
              context_title,
              context_label,
              result_sourcedid,
              lti_consumer=None,
              lti_launch_url=None,
              oauth_key=None,
              oauth_secret=None,
              person_sourcedid=None,
              person_contact_email_primary=None,
              outcome_service_url=None,
              launch_presentation_locale=None,
              **custom_parameters):
    """
    Returns an HTML template with JavaScript that will launch an LTI embed

    IMPORTANT NOTE: This method uses keyword only arguments as described in PEP 3102.
    Given the large number of arguments for this method, there is a  desire to
    guarantee that developers using this method know which arguments are being set
    to which values.
    See https://www.python.org/dev/peps/pep-3102/

    This method will use the LtiConsumer1p1 class to generate an HTML form and
    JavaScript that will automatically launch the LTI embedding, but it does not
    generate any response to encapsulate this content. The caller of this method
    must render the HTML on their own.

    Note: This method uses xblockutils.resources.ResourceLoader to load the HTML
    template used. The rationale for this is that ResourceLoader is agnostic
    to XBlock code and functionality. It is recommended that this remain in use
    until LTI1.3 support is merged, or a better means of loading the template is
    made available.

    Arguments:
        html_element_id (string):  Value to use as the HTML element id in the HTML form
        resource_link_id (string):  Opaque identifier guaranteed to be unique
            for every placement of the link
        user_id (string):  Unique value identifying the user
        roles (string):  A comma separated list of role values
        context_id (string):  Opaque identifier used to uniquely identify the
            context that contains the link being launched
        context_title (string):  Plain text title of the context
        context_label (string):  Plain text label for the context
        result_sourcedid (string):  Indicates the LIS Result Identifier (if any)
            and uniquely identifies a row and column within the Tool Consumer gradebook
        lti_consumer (LtiConsumer1p1): A pre-configured LtiConsumer1p1 object
            as an alternative to providing the launch url, oauth key and oauth secret
        lti_launch_url (string):  The URL to send the LTI Launch request to
        oauth_key (string):  The OAuth consumer key
        oauth_secret (string):  The OAuth consumer secret
        person_sourcedid (string):  LIS identifier for the user account performing the launch
        person_contact_email_primary (string):  Primary contact email address of the user
        outcome_service_url (string):  URL pointing to the outcome service. This
            is required if the Tool Consumer is accepting outcomes for launches
            associated with the resource_link_id
        launch_presentation_locale (string):  Language, country and variant as
            represented using the IETF Best Practices for Tags for Identifying
            Languages (BCP-47)
        custom_parameters (dict): Contains any other keyword arguments not listed
            above. It will filter out all arguments provided that do not start with
            'custom_' and will submit the remaining arguments on the LTI Launch form

    Returns:
        unicode: HTML template with the form and JavaScript to automatically
            launch the LTI embedding
    """
    if lti_consumer is None:
        lti_consumer = LtiConsumer1p1(lti_launch_url, oauth_key, oauth_secret)
    else:
        lti_launch_url = lti_consumer.lti_launch_url

    # Set LTI parameters from kwargs
    lti_consumer.set_user_data(
        user_id,
        roles,
        result_sourcedid,
        person_sourcedid=person_sourcedid,
        person_contact_email_primary=person_contact_email_primary)
    lti_consumer.set_context_data(context_id, context_title, context_label)

    if outcome_service_url:
        lti_consumer.set_outcome_service_url(outcome_service_url)

    if launch_presentation_locale:
        lti_consumer.set_launch_presentation_locale(launch_presentation_locale)

    lti_consumer.set_custom_parameters({
        key: value
        for key, value in custom_parameters.items()
        if key.startswith('custom_')
    })

    lti_parameters = lti_consumer.generate_launch_request(resource_link_id)

    # Prepare form data
    context = {'launch_url': lti_launch_url, 'element_id': html_element_id}
    context.update({'lti_parameters': lti_parameters})

    # Render the form template and return the template
    loader = ResourceLoader(__name__)
    template = loader.render_django_template(
        '../../templates/html/lti_launch.html', context)
    return template
Beispiel #31
0
 def test_render_js_template(self):
     loader = ResourceLoader(__name__)
     s = loader.render_js_template("data/simple_django_template.txt", example_id, example_context)
     self.assertEquals(s, expected_filled_js_template)
from lms.djangoapps.teams.models import CourseTeamMembership
from django.conf import settings
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from webob.response import Response

from xblock.core import XBlock
from xblock.fields import Scope, String, Boolean, DateTime, Integer, Float
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin
from xblockutils.studio_editable import StudioEditableXBlockMixin

LOADER = ResourceLoader(__name__)
LOG = logging.getLogger(__name__)
ROCKET_CHAT_DATA = "rocket_chat_data"
CACHE_TIMEOUT = 86400


def generate_custom_fields(course, team=None, specific_team=False):

    team_name, topic = generate_team_variables(team)
    return {
        "customFields": {
            "course": course,
            "team": team_name,
            "topic": topic,
            "specificTeam": specific_team,
        }
Beispiel #33
0
 def test_load_scenarios_with_identifiers(self):
     loader = ResourceLoader(__name__)
     scenarios = loader.load_scenarios_from_path("data", include_identifier=True)
     self.assertEquals(scenarios, expected_scenarios_with_identifiers)
class FreeTextResponse(
        EnforceDueDates,
        MissingDataFetcherMixin,
        StudioEditableXBlockMixin,
        XBlock,
):
    #  pylint: disable=too-many-ancestors, too-many-instance-attributes
    """
    Enables instructors to create questions with free-text responses.
    """

    loader = ResourceLoader(__name__)

    @staticmethod
    def workbench_scenarios():
        """
        Gather scenarios to be displayed in the workbench
        """
        scenarios = [
            ('Free-text Response XBlock', '''<sequence_demo>
                    <freetextresponse />
                    <freetextresponse name='My First XBlock' />
                    <freetextresponse
                        display_name="Full Credit is asdf, half is fdsa"
                        fullcredit_keyphrases="['asdf']"
                        halfcredit_keyphrases="['fdsa']"
                        min_word_count="2"
                        max_word_count="2"
                        max_attempts="5"
                    />
                    <freetextresponse
                        display_name="Min words 2"
                        min_word_count="2"
                    />
                    <freetextresponse
                        display_name="Max Attempts 5 XBlock"
                        max_attempts="5"
                    />
                    <freetextresponse
                        display_name="Full credit is asdf, Max Attempts 3"
                        max_attempts="3"
                        min_word_count="2"
                        fullcredit_keyphrases="['asdf']"
                    />
                    <freetextresponse
                        display_name="New submitted message"
                        submitted_message="Different message"
                    />
                    <freetextresponse
                        display_name="Blank submitted message"
                        submitted_message=""
                    />
                    <freetextresponse
                        display_name="Display correctness if off"
                        display_correctness="False"
                    />
                </sequence_demo>
             '''),
        ]
        return scenarios

    display_correctness = Boolean(
        display_name=_('Display Correctness?'),
        help=_('This is a flag that indicates if the indicator '
               'icon should be displayed after a student enters '
               'their response'),
        default=True,
        scope=Scope.settings,
    )
    display_other_student_responses = Boolean(
        display_name=_('Display Other Student Responses'),
        help=_('This will display other student responses to the '
               'student after they submit their response.'),
        default=False,
        scope=Scope.settings,
    )
    displayable_answers = List(
        default=[],
        scope=Scope.user_state_summary,
        help=_('System selected answers to give to students'),
    )
    display_name = String(
        display_name=_('Display Name'),
        help=_('This is the title for this question type'),
        default='Free-text Response',
        scope=Scope.settings,
    )
    fullcredit_keyphrases = List(
        display_name=_('Full-Credit Key Phrases'),
        help=_('This is a list of words or phrases, one of '
               'which must be present in order for the student\'s answer '
               'to receive full credit'),
        default=[],
        scope=Scope.settings,
    )
    halfcredit_keyphrases = List(
        display_name=_('Half-Credit Key Phrases'),
        help=_('This is a list of words or phrases, one of '
               'which must be present in order for the student\'s answer '
               'to receive half credit'),
        default=[],
        scope=Scope.settings,
    )
    max_attempts = Integer(
        display_name=_('Maximum Number of Attempts'),
        help=_('This is the maximum number of times a '
               'student is allowed to attempt the problem'),
        default=0,
        values={'min': 1},
        scope=Scope.settings,
    )
    max_word_count = Integer(
        display_name=_('Maximum Word Count'),
        help=_('This is the maximum number of words allowed for this '
               'question'),
        default=10000,
        values={'min': 1},
        scope=Scope.settings,
    )
    min_word_count = Integer(
        display_name=_('Minimum Word Count'),
        help=_('This is the minimum number of words required '
               'for this question'),
        default=1,
        values={'min': 1},
        scope=Scope.settings,
    )
    prompt = String(
        display_name=_('Prompt'),
        help=_('This is the prompt students will see when '
               'asked to enter their response'),
        default=_('Please enter your response within this text area'),
        scope=Scope.settings,
        multiline_editor=True,
    )
    submitted_message = String(
        display_name=_('Submission Received Message'),
        help=_('This is the message students will see upon '
               'submitting their response'),
        default='Your submission has been received',
        scope=Scope.settings,
    )
    weight = Integer(
        display_name=_('Weight'),
        help=_('This assigns an integer value representing '
               'the weight of this problem'),
        default=0,
        values={'min': 1},
        scope=Scope.settings,
    )
    saved_message = String(
        display_name=_('Draft Received Message'),
        help=_('This is the message students will see upon '
               'submitting a draft response'),
        default=('Your answers have been saved but not graded. '
                 'Click "Submit" to grade them.'),
        scope=Scope.settings,
    )

    count_attempts = Integer(
        default=0,
        scope=Scope.user_state,
    )
    score = Float(
        default=0.0,
        scope=Scope.user_state,
    )
    student_answer = String(
        default='',
        scope=Scope.user_state,
    )

    has_score = True

    editable_fields = (
        'display_name',
        'prompt',
        'weight',
        'max_attempts',
        'display_correctness',
        'min_word_count',
        'max_word_count',
        'fullcredit_keyphrases',
        'halfcredit_keyphrases',
        'submitted_message',
        'display_other_student_responses',
        'saved_message',
    )

    def ungettext(self, *args, **kwargs):
        """
        XBlock aware version of `ungettext`.
        """
        return self.runtime.service(self, 'i18n').ungettext(*args, **kwargs)

    def build_fragment(
        self,
        rendered_template,
        initialize_js_func,
        additional_css=[],
        additional_js=[],
    ):
        #  pylint: disable=dangerous-default-value, too-many-arguments
        """
        Creates a fragment for display.
        """
        fragment = Fragment(rendered_template)
        for item in additional_css:
            url = self.runtime.local_resource_url(self, item)
            fragment.add_css_url(url)
        for item in additional_js:
            url = self.runtime.local_resource_url(self, item)
            fragment.add_javascript_url(url)
        fragment.initialize_js(initialize_js_func)
        return fragment

    # Decorate the view in order to support multiple devices e.g. mobile
    # See: https://openedx.atlassian.net/wiki/display/MA/Course+Blocks+API
    # section 'View @supports(multi_device) decorator'
    @XBlock.supports('multi_device')
    def student_view(self, context={}):
        # pylint: disable=dangerous-default-value
        """The main view of FreeTextResponse, displayed when viewing courses.

        The main view which displays the general layout for FreeTextResponse

        Args:
            context: Not used for this view.

        Returns:
            (Fragment): The HTML Fragment for this XBlock, which determines the
            general frame of the FreeTextResponse Question.
        """
        display_other_responses = self.display_other_student_responses
        self.runtime.service(self, 'i18n')
        context.update({
            'display_name':
            self.display_name,
            'indicator_class':
            self._get_indicator_class(),
            'nodisplay_class':
            self._get_nodisplay_class(),
            'problem_progress':
            self._get_problem_progress(),
            'prompt':
            self.prompt,
            'student_answer':
            self.student_answer,
            'is_past_due':
            self.is_past_due(),
            'used_attempts_feedback':
            self._get_used_attempts_feedback(),
            'visibility_class':
            self._get_indicator_visibility_class(),
            'word_count_message':
            self._get_word_count_message(),
            'display_other_responses':
            display_other_responses,
            'other_responses':
            self.get_other_answers(),
        })
        template = self.loader.render_django_template(
            'templates/freetextresponse_view.html',
            context=Context(context),
            i18n_service=self.runtime.service(self, 'i18n'),
        )
        fragment = self.build_fragment(
            template,
            initialize_js_func='FreeTextResponseView',
            additional_css=[
                'public/view.css',
            ],
            additional_js=[
                'public/view.js',
            ],
        )
        return fragment

    def max_score(self):
        """
        Returns the configured number of possible points for this component.
        Arguments:
            None
        Returns:
            float: The number of possible points for this component
        """
        return self.weight

    def _generate_validation_message(self, msg):
        """
        Helper method to generate a ValidationMessage from
        the supplied string
        """
        result = ValidationMessage(
            ValidationMessage.ERROR,
            self.ugettext(unicode(msg))  # pylint: disable=undefined-variable
        )
        return result

    def validate_field_data(self, validation, data):
        """
        Validates settings entered by the instructor.
        """
        if data.weight < 0:
            msg = self._generate_validation_message(
                'Weight Attempts cannot be negative')
            validation.add(msg)
        if data.max_attempts < 0:
            msg = self._generate_validation_message(
                'Maximum Attempts cannot be negative')
            validation.add(msg)
        if data.min_word_count < 1:
            msg = self._generate_validation_message(
                'Minimum Word Count cannot be less than 1')
            validation.add(msg)
        if data.min_word_count > data.max_word_count:
            msg = self._generate_validation_message(
                'Minimum Word Count cannot be greater than Max Word Count')
            validation.add(msg)
        if not data.submitted_message:
            msg = self._generate_validation_message(
                'Submission Received Message cannot be blank')
            validation.add(msg)

    def _get_indicator_visibility_class(self):
        """
        Returns the visibility class for the correctness indicator html element
        """
        if self.display_correctness:
            result = ''
        else:
            result = 'hidden'
        return result

    def _get_word_count_message(self):
        """
        Returns the word count message
        """
        result = self.ungettext(
            "Your response must be "
            "between {min} and {max} word.",
            "Your response must be "
            "between {min} and {max} words.",
            self.max_word_count,
        ).format(
            min=self.min_word_count,
            max=self.max_word_count,
        )
        return result

    def _get_invalid_word_count_message(self, ignore_attempts=False):
        """
        Returns the invalid word count message
        """
        result = ''
        if ((ignore_attempts or self.count_attempts > 0)
                and (not self._word_count_valid())):
            word_count_message = self._get_word_count_message()
            result = self.ugettext(
                "Invalid Word Count. {word_count_message}").format(
                    word_count_message=word_count_message, )
        return result

    def _get_indicator_class(self):
        """
        Returns the class of the correctness indicator element
        """
        result = 'unanswered'
        if self.display_correctness and self._word_count_valid():
            if self._determine_credit() == Credit.zero:
                result = 'incorrect'
            else:
                result = 'correct'
        return result

    def _word_count_valid(self):
        """
        Returns a boolean value indicating whether the current
        word count of the user's answer is valid
        """
        word_count = len(self.student_answer.split())
        return self.max_word_count >= word_count >= self.min_word_count

    @classmethod
    def _is_at_least_one_phrase_present(cls, phrases, answer):
        """
        Determines if at least one of the supplied phrases is
        present in the given answer
        """
        answer = answer.lower()
        matches = [phrase.lower() in answer for phrase in phrases]
        return any(matches)

    def _get_problem_progress(self):
        """
        Returns a statement of progress for the XBlock, which depends
        on the user's current score
        """
        if self.weight == 0:
            result = ''
        elif self.score == 0.0:
            result = "({})".format(
                self.ungettext(
                    "{weight} point possible",
                    "{weight} points possible",
                    self.weight,
                ).format(weight=self.weight, ))
        else:
            scaled_score = self.score * self.weight
            # No trailing zero and no scientific notation
            score_string = ('%.15f' % scaled_score).rstrip('0').rstrip('.')
            result = "({})".format(
                self.ungettext(
                    "{score_string}/{weight} point",
                    "{score_string}/{weight} points",
                    self.weight,
                ).format(
                    score_string=score_string,
                    weight=self.weight,
                ))
        return result

    def _compute_score(self):
        """
        Computes and publishes the user's core for the XBlock
        based on their answer
        """
        credit = self._determine_credit()
        self.score = credit.value
        try:
            self.runtime.publish(self, 'grade', {
                'value': self.score,
                'max_value': Credit.full.value
            })
        except IntegrityError:
            pass

    def _determine_credit(self):
        #  Not a standard xlbock pylint disable.
        # This is a problem with pylint 'enums and R0204 in general'
        """
        Helper Method that determines the level of credit that
        the user should earn based on their answer
        """
        result = None
        if self.student_answer == '' or not self._word_count_valid():
            result = Credit.zero
        elif not self.fullcredit_keyphrases \
                and not self.halfcredit_keyphrases:
            result = Credit.full
        elif FreeTextResponse._is_at_least_one_phrase_present(
                self.fullcredit_keyphrases, self.student_answer):
            result = Credit.full
        elif FreeTextResponse._is_at_least_one_phrase_present(
                self.halfcredit_keyphrases, self.student_answer):
            result = Credit.half
        else:
            result = Credit.zero
        return result

    def _get_used_attempts_feedback(self):
        """
        Returns the text with feedback to the user about the number of attempts
        they have used if applicable
        """
        result = ''
        if self.max_attempts > 0:
            result = self.ungettext(
                'You have used {count_attempts} of {max_attempts} submission',
                'You have used {count_attempts} of {max_attempts} submissions',
                self.max_attempts,
            ).format(
                count_attempts=self.count_attempts,
                max_attempts=self.max_attempts,
            )
        return result

    def _get_nodisplay_class(self):
        """
        Returns the css class for the submit button
        """
        result = ''
        if self.max_attempts > 0 and self.count_attempts >= self.max_attempts:
            result = 'nodisplay'
        return result

    def _get_submitted_message(self):
        """
        Returns the message to display in the submission-received div
        """
        result = ''
        if self._word_count_valid():
            result = self.submitted_message
        return result

    def _get_user_alert(self, ignore_attempts=False):
        """
        Returns the message to display in the user_alert div
        depending on the student answer
        """
        result = ''
        if not self._word_count_valid():
            result = self._get_invalid_word_count_message(ignore_attempts)
        return result

    def _can_submit(self):
        if self.is_past_due():
            return False
        if self.max_attempts == 0:
            return True
        if self.count_attempts < self.max_attempts:
            return True
        return False

    @XBlock.json_handler
    def submit(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's submission
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self._can_submit():
            self.student_answer = data['student_answer']
            # Counting the attempts and publishing a score
            # even if word count is invalid.
            self.count_attempts += 1
            self._compute_score()
            display_other_responses = self.display_other_student_responses
            if display_other_responses and data.get('can_record_response'):
                self.store_student_response()
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'indicator_class': self._get_indicator_class(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': self._get_submitted_message(),
            'user_alert': self._get_user_alert(ignore_attempts=True, ),
            'other_responses': self.get_other_answers(),
            'display_other_responses': self.display_other_student_responses,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    @XBlock.json_handler
    def save_reponse(self, data, suffix=''):
        # pylint: disable=unused-argument
        """
        Processes the user's save
        """
        # Fails if the UI submit/save buttons were shut
        # down on the previous sumbisson
        if self.max_attempts == 0 or self.count_attempts < self.max_attempts:
            self.student_answer = data['student_answer']
        result = {
            'status': 'success',
            'problem_progress': self._get_problem_progress(),
            'used_attempts_feedback': self._get_used_attempts_feedback(),
            'nodisplay_class': self._get_nodisplay_class(),
            'submitted_message': '',
            'user_alert': self.saved_message,
            'visibility_class': self._get_indicator_visibility_class(),
        }
        return result

    def store_student_response(self):
        """
        Submit a student answer to the answer pool by appending the given
        answer to the end of the list.
        """
        # if the answer is wrong, do not display it
        if self.score != Credit.full.value:
            return

        student_id = self.get_student_id()
        # remove any previous answers the student submitted
        for index, response in enumerate(self.displayable_answers):
            if response['student_id'] == student_id:
                del self.displayable_answers[index]
                break

        self.displayable_answers.append({
            'student_id': student_id,
            'answer': self.student_answer,
        })

        # Want to store extra response so student can still see
        # MAX_RESPONSES answers if their answer is in the pool.
        response_index = -(MAX_RESPONSES + 1)
        self.displayable_answers = self.displayable_answers[response_index:]

    def get_other_answers(self):
        """
        Returns at most MAX_RESPONSES answers from the pool.

        Does not return answers the student had submitted.
        """
        student_id = self.get_student_id()
        display_other_responses = self.display_other_student_responses
        shouldnt_show_other_responses = not display_other_responses
        student_answer_incorrect = self._determine_credit() == Credit.zero
        if student_answer_incorrect or shouldnt_show_other_responses:
            return []
        return_list = [
            response for response in self.displayable_answers
            if response['student_id'] != student_id
        ]

        return_list = return_list[-(MAX_RESPONSES):]
        return return_list