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, }
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))
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
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")
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'}
def test_load_unicode_from_another_module(self): s = ResourceLoader("tests.unit.data").load_unicode( "simple_django_template.txt") self.assertEquals(s, expected_string)
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)
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> """), ]
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)
def test_load_scenarios(self): loader = ResourceLoader(__name__) scenarios = loader.load_scenarios_from_path("data") self.assertEquals(scenarios, expected_scenarios)
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)
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)
""" 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(
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))
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, }
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)
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
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
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, }
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