from api_teams import ApiTeams # pylint: disable=relative-import from api_rocket_chat import ApiRocketChat # pylint: disable=relative-import from django.conf import settings from django.contrib.auth.models import User 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__) @XBlock.wants("user") # pylint: disable=too-many-ancestors, too-many-instance-attributes @XBlock.wants("settings") class RocketChatXBlock(XBlock, XBlockWithSettingsMixin, StudioEditableXBlockMixin): """ This class allows to embed a chat window inside a unit course and set the necessary variables to config the rocketChat enviroment """ display_name = String( display_name="Display Name", scope=Scope.settings, default="Rocket Chat" )
# You should have received a copy of the GNU Affero General Public License # along with this program in a file in the toplevel directory called # "AGPLv3". If not, see <http://www.gnu.org/licenses/>. # from xblock.fields import String from xblockutils.base_test import SeleniumBaseTest, SeleniumXBlockTest from xblockutils.resources import ResourceLoader # Studio adds a url_name property to each XBlock but Workbench doesn't. # Since we rely on it, we need to mock url_name support so it can be set via XML and # accessed like a normal field. from problem_builder import MentoringBlock MentoringBlock.url_name = String() loader = ResourceLoader(__name__) CORRECT, INCORRECT, PARTIAL = "correct", "incorrect", "partially-correct" class PopupCheckMixin(object): """ Code used by MentoringBaseTest and MentoringAssessmentBaseTest """ def popup_check(self, mentoring, item_feedbacks, prefix='', do_submit=True): submit = mentoring.find_element_by_css_selector('.submit input.input-main') for index, expected_feedback in enumerate(item_feedbacks): choice_wrapper = mentoring.find_elements_by_css_selector(prefix + " .choice")[index] if do_submit:
import logging import markdown2 import pkg_resources from django.conf import settings as django_settings from xblock.core import XBlock from xblock.fields import Scope, String from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.settings import XBlockWithSettingsMixin from xblockutils.studio_editable import StudioEditableXBlockMixin, loader from .utils import _ log = logging.getLogger(__name__) # pylint: disable=invalid-name xblock_loader = ResourceLoader(__name__) # pylint: disable=invalid-name SETTINGS_KEY = 'markdown' DEFAULT_EXTRAS = [ "code-friendly", "fenced-code-blocks", "footnotes", "tables", "use-file-vars" ] DEFAULT_SETTINGS = {"extras": DEFAULT_EXTRAS, "safe_mode": True} def get_xblock_settings(): """Extract xblock settings.""" try: xblock_settings = django_settings.XBLOCK_SETTINGS settings = xblock_settings.get(SETTINGS_KEY, DEFAULT_SETTINGS) except AttributeError:
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))
""" Google Calendar XBlock implementation """ # -*- coding: utf-8 -*- # # Imports ########################################################### import logging from django import utils 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&hl=en-us' .format(DEFAULT_CALENDAR_ID)) CALENDAR_TEMPLATE = "/templates/html/google_calendar.html" CALENDAR_EDIT_TEMPLATE = "/templates/html/google_calendar_edit.html" def _(text): """ Dummy ugettext. """ return text
class OpenClassroomXBlock(XBlock): """ An XBlock providing an embedded Open Classroom lesson. """ loader = ResourceLoader(__name__) _EVENT_NAME_EXPLORATION_LOADED = 'openclassroom.exploration.loaded' _EVENT_NAME_EXPLORATION_COMPLETED = 'openclassroom.exploration.completed' _EVENT_NAME_STATE_TRANSITION = 'openclassroom.exploration.state.changed' display_name = String(help=_("Display name of the component"), default=_("Open Classroom lesson"), scope=Scope.content) openclassroomid = String( help=_("ID of the Open Classroom lesson to embed"), default="2DB88aOgiXgD", scope=Scope.content) src = String(help=_("Source URL of the site"), default="https://lessons.openclassroom.edu.vn", 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), ) 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 OpenClassroomXBlock, shown to students when viewing courses. """ frag = Fragment( self.render_template("openclassroom.html", { 'src': self.src, 'openclassroomid': self.openclassroomid, })) frag.add_javascript(self.get_translation_content()) frag.add_javascript( self.resource_string('static/js/openclassroom_player.js')) frag.add_javascript(self.resource_string("static/js/openclassroom.js")) frag.initialize_js('OpenClassroomXBlock') 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("openclassroom_preview.html", { 'src': self.src, 'openclassroomid': self.openclassroomid, })) 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.openclassroomid, '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.openclassroomid, '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.openclassroomid, '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( "openclassroom_edit.html", { 'src': self.src, 'openclassroomid': self.openclassroomid or '', 'display_name': self.display_name, })) frag.add_javascript(self.get_translation_content()) js_str = pkg_resources.resource_string( __name__, "static/js/openclassroom_edit.js") frag.add_javascript(unicode(js_str)) frag.initialize_js('OpenClassroomXBlockEditor') return frag @XBlock.json_handler def studio_submit(self, data, suffix=''): """ Called when submitting the form in Studio. """ self.openclassroomid = data.get('openclassroomid') 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 [ ("Open Classroom Embedding", """<vertical_demo> <openclassroom openclassroomid="0" src="https://lessons.openclassroom.edu.vn"/> </vertical_demo> """), ]
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({ '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_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(): 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 = "({})".format( self.ngettext( "{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.ngettext( "{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.ngettext( '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_word_count_message(self): """ Returns the word count message """ result = self.ngettext( "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_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'] # 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 _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.gettext( "Invalid Word Count. {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, self.gettext(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)
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")
def test_load_scenarios(self): loader = ResourceLoader(__name__) scenarios = loader.load_scenarios_from_path("data") self.assertEquals(scenarios, expected_scenarios)
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_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 test_load_unicode_from_another_module(self): s = ResourceLoader("tests.unit.data").load_unicode( "simple_django_template.txt") self.assertEquals(s, expected_string)
def test_load_unicode(self): s = ResourceLoader(__name__).load_unicode( "data/simple_django_template.txt") self.assertEquals(s, expected_string)
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)
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