class AdaptiveTestXBlock(XBlock): """ An adaptive-learning testing xblock. This Xblock allows instructors to selected one of many avlaiable tests (currently Kolb and Dominancia Cerebral) and provide an output of the student's learning style via a survey. Improvements to this Xblock include Course Modification (see TODOs). """ # Scopes. Persistent variables # See scopes definition for user_state (per user) and user_state_summary (global), among others. testNumber = Integer( default=0, scope=Scope.user_state_summary, help="Test number (0: Not avaliable, 1: Kolb, 2: Dominancia", ) # TestResult contains object: { result: string } testResult = JSONField( default="", scope=Scope.user_state, help="String identifying student learning style, according to test", ) # TestResults[] contains per item: # { test: number, result: object, user_id: string, user_full_name: string } testResults = JSONField( default=[], scope=Scope.user_state_summary, help="Array containing student information and results", ) testSolved = Boolean( default=False, scope=Scope.user_state, help="Flag if the user already solved the test", ) 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 student_view(self, context=None): """ The primary view of the StudentAdaptiveTestXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/student_adaptive_test.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.add_javascript( self.resource_string("static/js/src/jquery-1.12.4.js")) frag.add_javascript(self.resource_string("static/js/src/jquery-ui.js")) frag.add_javascript( self.resource_string("static/js/src/student_adaptive_test.js")) frag.initialize_js('StudentAdaptiveTestXBlock') return frag def studio_view(self, context=None): """ The primary view of the StudioAdaptiveTestXBlock, shown to students when viewing courses. """ if len(self.testResults) > 0: html = self.resource_string("static/html/studio_analytics.html") frag = Fragment(html.format(self=self)) frag.add_javascript( self.resource_string("static/js/src/studio_analytics.js")) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.initialize_js('StudioAnalyticsXBlock') # Notice else: html = self.resource_string( "static/html/studio_adaptive_test.html") frag = Fragment(html.format(self=self)) frag.add_javascript( self.resource_string("static/js/src/studio_adaptive_test.js")) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.initialize_js('StudioAdaptiveTestXBlock') # Notice return frag @XBlock.json_handler def select_test(self, data, suffix=''): """ Instructor's selected test handler. JS returned data is saved into global testNumber """ self.testNumber = data return True @XBlock.json_handler def load_test(self, data, suffix=''): """ Handler that returns the test currently used """ # Returns results in case they exist if self.testSolved: return {'test': self.testNumber, 'test_result': self.testResult} else: return {'test': self.testNumber} @XBlock.json_handler def submit_test(self, data, suffix=''): """ An example handler, which increments the data. """ collectedTest = data user_test_result = {} # Something should be modified in the course # EDXCUT: https://github.com/mitodl/edxcut showed to be an option. # Testing was unabled to use it correctly. # TODO: Take collectedTest and make modifications into the course content user_test_result["result"] = collectedTest user_test_result["test"] = self.testNumber user_test_result['user_id'] = self.scope_ids.user_id user_service = self.runtime.service(self, 'user') xb_user = user_service.get_current_user() user_test_result['user_full_name'] = xb_user.full_name self.testResults.append(user_test_result) self.testResult = collectedTest self.testSolved = True return True @XBlock.json_handler def load_analytics(self, data, suffix=''): """ An example handler, which increments the data. """ return self.testResults # Workbench scenarios. Ignore, unless you know how to use them. @staticmethod def workbench_scenarios(): """A canned scenario for display in the workbench.""" return [ ("AdaptiveTestXBlock", """<adaptive_test/> """), ]
class DSPXBlock(XBlock): graded = True has_score = True icon_class = 'problem' display_name = String(display_name=u'Отображаемое название', default=u"Лабораторная работа", scope=Scope.settings) lab_list = JSONField( display_name=u'Список лабораторных работ', scope=Scope.settings, default=[ { "id": "lab_1", "title": u"Лабораторная 1. Исследование цифровых фильтров с конечной импульсной характеристикой", }, { "id": "lab_2", "title": u"Лабораторная 2. Цифровой спектральный анализ", }, { "id": "lab_3", "title": u"Лабораторная 3. Цифровой согласованный фильтр", }, { "id": "lab_4", "title": u"Лабораторная 4. Исследование рекурсивных цифровых фильтров", }, { "id": "lab_5", "title": u"Лабораторная 5. Исследование рекурсивных цифровых фильтров", }, { "id": "lab_7", "title": u"Лабораторная 7. Цифровые модуляторы и демодуляторы", }, ]) max_attempts = Integer(display_name=u"Максимальное количество попыток", help=u"", default=None, scope=Scope.settings) attempts = Integer(display_name=u"Количество использованных попыток", help=u"", default=0, scope=Scope.user_state) weight = Integer(display_name=u"Максимальное количество баллов", help=(u"Максимальное количество баллов", u"которое может получить студент."), default=10, scope=Scope.settings) score = Float(display_name=u"Текущее количество баллов студента", default=None, scope=Scope.user_state) current_lab = String(display_name=u"ID текущей лаборатории", help=u"ID текущей лаборатории", default="lab_1", scope=Scope.settings) lab_settings = JSONField( default={ "array_tolerance": 0.01, "number_tolerance": 0.5, "show_reset_button": False, }, scope=Scope.settings, help=u'Настройки лабораторной работы', ) lab_source_data = JSONField( default={}, scope=Scope.user_state, help=u'Начальные данные лабораторной для студента', ) student_state = JSONField( default={}, scope=Scope.user_state, help=u'Ответ студента', ) correct_answer = JSONField( default={}, scope=Scope.user_state, help=u'Правильный ответ', ) def is_course_staff(self): """ Проверка, является ли пользователь автором курса. """ return getattr(self.xmodule_runtime, 'user_is_staff', False) def is_instructor(self): """ Проверка, является ли пользователь инструктором. """ return self.xmodule_runtime.get_user_role() == 'instructor' def past_due(self): """ Проверка, истекла ли дата для выполнения задания. """ due = get_extended_due_date(self) if due is not None: if _now() > due: return False def answer_opportunity(self): """ Возможность ответа (если количество сделанное попыток меньше заданного). """ if self.max_attempts is not None and self.max_attempts <= self.attempts: return False else: return True 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 student_view(self, context=None): context = self.lab_context() fragment = self.load_lab_static(self.current_lab, context) fragment.initialize_js('DSPXBlock', context) return fragment @XBlock.json_handler def student_submit(self, data, suffix=''): self.student_state["answer"] = data result = {} try: if not self.answer_opportunity(): raise Exception('Maximum number of attempts exceeded') if self.current_lab == "lab_1": result = lab_1_check_answer(data, self.lab_source_data, self.lab_settings) elif self.current_lab == "lab_2": result = lab_2_check_answer(data, self.lab_source_data, self.lab_settings, self.correct_answer) elif self.current_lab == "lab_3": result = lab_3_check_answer(data, self.lab_source_data, self.lab_settings, self.correct_answer) elif self.current_lab == "lab_4": result = lab_4_check_answer(data, self.lab_source_data, self.lab_settings) elif self.current_lab == "lab_5": result = lab_5_check_answer(data, self.lab_source_data, self.lab_settings) # elif self.current_lab == "lab_6": # result = lab_6_check_answer(data, self.lab_source_data, self.lab_settings) elif self.current_lab == "lab_7": result = lab_7_check_answer(data, self.lab_source_data, self.lab_settings, self.correct_answer) else: raise Exception('Hiding bugs lol') self.score = round(self.weight * result["score"], 1) self.student_state["score"] = self.score self.student_state["correctness"] = result["correctness"] if result["score"] == 1: self.student_state["is_success"] = "success" elif result["score"] == 0: self.student_state["is_success"] = "error" else: self.student_state["is_success"] = "partially" self.runtime.publish(self, 'grade', { 'value': float(self.score), 'max_value': float(self.weight) }) self.student_state["weight"] = self.weight self.student_state["max_attempts"] = self.max_attempts self.attempts += 1 self.student_state["attempts"] = self.attempts return Response(json_body=self.student_state) except Exception as e: ex = dict() ex["exception"] = str(e) # возможно, трейсбэк следует показывать только сотрудникам # if self.is_course_staff() is True or self.is_instructor() is True: trace = traceback.extract_tb(sys.exc_info()[2]) output = "Traceback is:\n" for (file, linenumber, affected, line) in trace: output += "> Error at function {}\n".format(affected) output += " At: {}:{}\n".format(file, linenumber) output += " Source: {}\n".format(line) output += "> Exception: {}\n".format(e) ex["traceback"] = output return Response(json.dumps(ex), status=500) @XBlock.json_handler def lab_1_get_graphics(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_1_get_graphics(data, self.lab_source_data) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.handler def lab_2_get_graphics_1(self, data, suffix=''): try: graphics = lab_2_get_graphics_1(self.lab_source_data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.handler def lab_2_get_graphics_2(self, data, suffix=''): try: graphics = lab_2_get_graphics_2(self.lab_source_data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.handler def lab_2_get_graphics_3(self, data, suffix=''): try: graphics = lab_2_get_graphics_3(self.lab_source_data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def lab_3_get_graphic_1(self, data, suffix=''): self.student_state["answer"] = data try: graphic = lab_3_get_graphic_1(data, self.lab_source_data, self.correct_answer) return Response(json_body={"graphic": graphic}) except: return Response('Error!', 500) @XBlock.handler def lab_3_get_graphic_2(self, request, suffix=''): self.student_state["answer"] = json.loads(request.body) reload = False is_signal = "" try: if 'reload' in request.GET: reload = True if 'is_signal' in request.GET: is_signal = request.GET["is_signal"] self.correct_answer, self.student_state, graphic = lab_3_get_graphic_2( self.correct_answer, self.student_state, self.lab_source_data, reload, is_signal) return Response( json_body={ "graphic": graphic, "student_state": self.student_state, "correct_answer": self.correct_answer }) except: return Response('Error!', 500) @XBlock.json_handler def lab_3_get_graphic_3(self, data, suffix=''): self.student_state["answer"] = data try: graphic = lab_3_get_graphic_3(data, self.lab_source_data) return Response(json_body={"graphic": graphic}) except: return Response('Error!', 500) @XBlock.handler def lab_3_reset_task(self, data, suffix=''): self.student_state["state"]["Ku_j"] = 1 self.student_state["state"]["Ku_i"] = 1 self.student_state["state"]["Ku_done"] = False self.student_state["state"]["there_is_signal_count"] = 0 self.student_state["state"]["there_is_no_signal_count"] = 0 self.student_state["state"]["there_is_signal_states"] = [{}] * len( self.lab_source_data["s"]) _, self.student_state, graphic = lab_3_get_graphic_2( self.correct_answer, self.student_state, self.lab_source_data, True) return Response(json_body={ "graphic": graphic, "student_state": self.student_state }) @XBlock.json_handler def lab_4_get_graphics(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_4_get_graphics(data, self.lab_source_data) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def lab_5_get_graphic_1(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_5_get_graphic_1(data, self.lab_source_data) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def lab_5_get_graphic_2(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_5_get_graphic_2(data, self.lab_source_data) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.handler def lab_7_get_graphic_1(self, data, suffix=''): try: graphics = lab_7_get_graphic_1(self.lab_source_data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def lab_7_get_graphic_2(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_7_get_graphic_2(data) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.handler def lab_7_get_graphic_3(self, data, suffix=''): try: graphics = lab_7_get_graphic_3(self.lab_source_data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def lab_7_get_graphic_4(self, data, suffix=''): self.student_state["answer"] = data try: graphics = lab_7_get_graphic_4(data, self.correct_answer) return Response(json_body={"graphics": graphics}) except: return Response('Error!', 500) @XBlock.json_handler def save_answer(self, data, suffix=''): self.student_state["answer"] = data return Response(json_body={"success": "success"}) @XBlock.handler def reset_task(self, data, suffix=''): if self.lab_settings["show_reset_button"]: self.attempts = 0 self.score = None self.correct_answer = {} self.student_state = {} self.lab_source_data = {} return Response(json_body={"success": "success"}) else: return Response('Error!', 500) def get_general_context(self): general_context = { "current_lab": self.current_lab, "display_name": self.display_name, "weight": self.weight, "score": self.score, "max_attempts": self.max_attempts, "attempts": self.attempts, "student_state": self.student_state, "show_reset_button": self.lab_settings["show_reset_button"], } if self.answer_opportunity(): general_context["answer_opportunity"] = True if self.is_course_staff() is True or self.is_instructor() is True: general_context['is_course_staff'] = True return general_context def lab_context(self): if not self.lab_source_data or self.lab_source_data[ "lab_id"] != self.current_lab: self.student_state = {} self.attempts = 0 self.score = None if self.current_lab == "lab_1": self.lab_source_data = lab_1_get_source_data() elif self.current_lab == "lab_2": self.lab_source_data, self.correct_answer = lab_2_get_source_data( ) elif self.current_lab == "lab_3": state = dict() self.lab_source_data, self.correct_answer = lab_3_get_source_data( self.correct_answer) state["Ku_j"] = 1 state["Ku_i"] = 1 state["Ku_done"] = False state["there_is_signal_count"] = 0 state["there_is_no_signal_count"] = 0 state["y2_s2"] = None state["there_is_signal_states"] = [{}] * len( self.lab_source_data["s"]) self.correct_answer["s"] = [None] * len( self.lab_source_data["s"]) self.student_state["state"] = state elif self.current_lab == "lab_4": self.lab_source_data = lab_4_get_source_data() elif self.current_lab == "lab_5": self.lab_source_data = lab_5_get_source_data() # elif self.current_lab == "lab_6": # self.lab_source_data = lab_6_get_source_data() elif self.current_lab == "lab_7": self.lab_source_data, self.correct_answer = lab_7_get_source_data( ) context = merge_two_dicts(self.get_general_context(), self.lab_source_data) return context def load_lab_static(self, lab_id, context): frag = Fragment() frag.add_content( render_template("static/{}/{}.html".format(lab_id, lab_id), context)) frag.add_css( self.resource_string("static/{}/{}.css".format(lab_id, lab_id))) frag.add_css(self.resource_string("static/css/dsp.css")) frag.add_javascript( self.resource_string("static/{}/{}.js".format(lab_id, lab_id))) frag.add_javascript(self.resource_string("static/js/src/dsp.js")) return frag def studio_view(self, context=None): context = { "display_name": self.display_name, "current_lab": self.current_lab, "lab_list": self.lab_list, "weight": self.weight, "max_attempts": self.max_attempts, "number_tolerance": self.lab_settings["number_tolerance"], "array_tolerance": self.lab_settings["array_tolerance"], "show_reset_button": self.lab_settings["show_reset_button"], } fragment = Fragment() fragment.add_content( render_template("static/html/dsp_studio.html", context)) js_urls = ("static/js/src/dsp_studio.js", ) css_urls = ("static/css/dsp_studio.css", ) load_resources(js_urls, css_urls, fragment) fragment.initialize_js('DSPXBlock') return fragment @XBlock.json_handler def studio_submit(self, data, suffix=''): self.display_name = data.get('display_name') self.current_lab = data.get('current_lab') self.weight = int(float(data.get('weight'))) try: self.max_attempts = int(round(float(data.get('max_attempts')))) if self.max_attempts == 0: raise Exception('Zero attempts is not allowed') except: self.max_attempts = None self.lab_settings["array_tolerance"] = float( data.get('array_tolerance')) self.lab_settings["number_tolerance"] = float( data.get('number_tolerance')) self.lab_settings["show_reset_button"] = True if data.get( 'show_reset_button') == 'true' else False return {'result': 'success'} # 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 [ ("DSPXBlock", """<dsp/> """), ("Multiple DSPXBlock", """<vertical_demo> <dsp/> <dsp/> <dsp/> </vertical_demo> """), ]
class RecommenderXBlock(HelperXBlock): """ This XBlock will show a set of recommended resources which may be helpful to students solving a given problem. The resources are provided and edited by students; they can also vote for useful resources and flag problematic ones. """ seen = Boolean( help= "Has the student interacted with the XBlock before? Used to show optional tutorial.", default=False, scope=Scope.user_info) version = String( help= "The version of this RecommenderXBlock. Used to simplify migrations.", default="recommender.v1.0", scope=Scope.content) intro_enabled = Boolean( help= "Should we show the users a short usage tutorial the first time they see the XBlock?", default=True, scope=Scope.content) # A dict of default recommendations supplied by the instructors to # seed the list with before students add new recommendations. # Also, useful for testing. # Usage: default_recommendations[index] = { # "id": (String) A unique ID. The ID is currently derived from # the URL, but this has changed and may change again # "title": (String) a 1-3 sentence summary description of a resource # "upvotes" : (Integer) number of upvotes, # "downvotes" : (Integer) number of downvotes, # "url" : (String) link to resource, # "description" : (String) the url of a resource's screenshot. # 'screenshot' would be a better name, but would # require a cumbersome data migration. # "descriptionText" : (String) a potentially longer overview of the resource } # we use url as key (index) of resource default_recommendations = JSONField( help= "Dict of instructor-supplied help resources to seed the resource list with.", default={}, scope=Scope.content) # A dict of recommendations provided by students. # Usage: the same as default_recommendations recommendations = JSONField(help="Current set of recommended resources", default={}, scope=Scope.user_state_summary) # A list of recommendations removed by course staff. This is used to filter out # cheats, give-aways, spam, etc. # Usage: the same as default_recommendations plus # removed_recommendations[index]['reason'] = (String) the reason why # course staff remove this resource removed_recommendations = Dict(help="Dict of removed resources", default={}, scope=Scope.user_state_summary) # A list of endorsed recommendations' ids -- the recommendations the course # staff marked as particularly helpful. # Usage: endorsed_recommendation_ids[index] = (String) id of a # endorsed resource endorsed_recommendation_ids = List(help="List of endorsed resources' ID", default=[], scope=Scope.user_state_summary) # A list of reasons why the resources were endorsed. # Usage: endorsed_recommendation_reasons[index] = (String) the reason # why the resource (id = endorsed_recommendation_ids[index]) is endorsed endorsed_recommendation_reasons = List( help="List of reasons why the resources are endorsed", default=[], scope=Scope.user_state_summary) # A dict of problematic recommendations which are flagged by users for review # by instructors. Used to remove spam, etc. # Usage: flagged_accum_resources[userId] = { # "problematic resource id": (String) reason why the resource is # flagged as problematic by that user } flagged_accum_resources = Dict( help= "Dict of potentially problematic resources which were flagged by users", default={}, scope=Scope.user_state_summary) # A list of recommendations' ids which a particular user upvoted, so users # cannot vote twice # Usage: upvoted_ids[index] = (String) id of a resource which was # upvoted by the current user upvoted_ids = List(help="List of resources' ids which user upvoted", default=[], scope=Scope.user_state) # A list of recommendations' ids which user downvoted, so users cannot vote twice. # Usage: downvoted_ids[index] = (String) id of a resource which was # downvoted by the current user downvoted_ids = List(help="List of resources' ids which user downvoted", default=[], scope=Scope.user_state) # A list of problematic recommendations' ids which user flagged. # Usage: flagged_ids[index] = (String) id of a problematic resource which # was flagged by the current user flagged_ids = List( help="List of problematic resources' ids which the user flagged", default=[], scope=Scope.user_state) # A list of reasons why the resources corresponding to those in flagged_ids were flagged # Usage: flagged_reasons[index] = (String) reason why the resource # 'flagged_ids[index]' was flagged by the current user as problematic flagged_reasons = List( help="List of reasons why the corresponding resources were flagged", default=[], scope=Scope.user_state) # The file system we used to store uploaded screenshots fs = Filesystem(help="File system for screenshots", scope=Scope.user_state_summary) client_configuration = Dict(help="Dict of customizable settings", default={ 'disable_dev_ux': True, 'entries_per_page': 5, 'page_span': 2 }, scope=Scope.content) # the dictionary keys for storing the content of a recommendation resource_content_fields = [ 'url', 'title', 'description', 'descriptionText' ] def _get_onetime_url(self, filename): """ Return one time url for uploaded screenshot We benchmarked this as less than 8ms on a sandbox machine. """ if filename.startswith('fs://'): return str( self.fs.get_url(filename.replace('fs://', ''), 1000 * 60 * 60 * 10)) else: return filename def _error_handler(self, error_msg, event, resource_id=None): """ Generate an error dictionary if something unexpected happens, such as a user upvoting a resource which no longer exists. We both log to this to the event logs, and return to the browser. """ result = {'error': error_msg} if resource_id is not None: result['id'] = resource_id tracker.emit(event, result) raise JsonHandlerError(400, result['error']) def _check_redundant_resource(self, resource_id, event_name, result): """ Check whether the submitted resource is redundant. If true, raise an exception and return a HTTP status code for the error. """ # check url for redundancy if resource_id in self.recommendations: result['error'] = self.ugettext( 'The resource you are attempting to provide already exists') for field in self.resource_content_fields: result['dup_' + field] = self.recommendations[resource_id][field] result['dup_id'] = self.recommendations[resource_id]['id'] tracker.emit(event_name, result) raise JsonHandlerError(409, result['error']) def _check_removed_resource(self, resource_id, event_name, result): """ Check whether the submitted resource is removed. If true, raise an exception and return a HTTP status code for the error. """ if resource_id in self.removed_recommendations: result['error'] = self.ugettext( 'The resource you are attempting to ' 'provide has been disallowed by the staff. ' 'Reason: ' + self.removed_recommendations[resource_id]['reason']) for field in self.resource_content_fields: result[ 'dup_' + field] = self.removed_recommendations[resource_id][field] result['dup_id'] = self.removed_recommendations[resource_id]['id'] tracker.emit(event_name, result) raise JsonHandlerError(405, result['error']) def _validate_resource(self, data_id, event): """ Validate whether the resource exists in the database. If not, generate the error message, and return to the browser for a given event, otherwise, return the stemmed id. """ resource_id = stem_url(data_id) if resource_id not in self.recommendations: msg = self.ugettext('The selected resource does not exist') self._error_handler(msg, event, resource_id) return resource_id def _check_upload_file(self, request, file_types, file_type_error_msg, event, file_size_threshold): """ Check the type and size of uploaded file. If the file type is unexpected or the size exceeds the threshold, log the error and return to browser, otherwise, return None. """ # Check invalid file types file_type_error = False file_type = [ ft for ft in file_types if any( str(request.POST['file'].file).lower().endswith(ext) for ext in file_types[ft]['extension']) ] # Check extension if not file_type: file_type_error = True else: file_type = file_type[0] # Check mimetypes if request.POST['file'].file.content_type not in file_types[ file_type]['mimetypes']: file_type_error = True else: if 'magic' in file_types[file_type]: # Check magic number headers = file_types[file_type]['magic'] if request.POST['file'].file.read( len(headers[0]) / 2).encode('hex') not in headers: file_type_error = True request.POST['file'].file.seek(0) if file_type_error: response = Response() tracker.emit(event, {'uploadedFileName': 'FILE_TYPE_ERROR'}) response.status = 415 response.body = json.dumps({'error': file_type_error_msg}) response.headers['Content-Type'] = 'application/json' return response # Check whether file size exceeds threshold (30MB) if request.POST['file'].file.size > file_size_threshold: response = Response() tracker.emit(event, {'uploadedFileName': 'FILE_SIZE_ERROR'}) response.status = 413 response.body = json.dumps({ 'error': self.ugettext('Size of uploaded file exceeds threshold') }) response.headers['Content-Type'] = 'application/json' return response return file_type def _raise_pyfs_error(self, event): """ Log and return an error if the pyfs is not properly set. """ response = Response() error = self.ugettext('The configuration of pyfs is not properly set') tracker.emit(event, {'uploadedFileName': 'IMPROPER_FS_SETUP'}) response.status = 404 response.body = json.dumps({'error': error}) response.headers['Content-Type'] = 'application/json' return response def _init_template_lookup(self): """ Initialize template_lookup by adding mappings between strings and urls. """ global template_lookup template_lookup = TemplateLookup() template_lookup.put_string( "recommenderstudio.html", self.resource_string("static/html/recommenderstudio.html")) template_lookup.put_string( "recommender.html", self.resource_string("static/html/recommender.html")) template_lookup.put_string( "resourcebox.html", self.resource_string("static/html/resourcebox.html")) def get_client_configuration(self): """ Return the parameters for client-side configuration settings. Returns: disable_dev_ux: feature flag for any new UX under development which should not appear in prod entries_per_page: the number of resources in each page page_span: page range in pagination control intro: whether to take users through a short usage tutorial the first time they see the RecommenderXBlock is_user_staff: whether the user is staff """ result = self.client_configuration.copy() result['is_user_staff'] = self.get_user_is_staff() result['intro'] = not self.seen and self.intro_enabled if not self.seen: # Mark the user who interacted with the XBlock first time as seen, # in order not to show the usage tutorial in future. self.seen = True tracker.emit('get_client_configuration', result) return result @XBlock.json_handler def set_client_configuration(self, data, _suffix=''): # pylint: disable=unused-argument """ Set the parameters for student-view, client side configurations. Args: data: dict in JSON format. Keys in data: disable_dev_ux: feature flag for any new UX under development which should not appear in prod entries_per_page: the number of resources in each page page_span: page range in pagination control intro_enable: Should we show the users a short usage tutorial the first time they see the XBlock? """ self.intro_enabled = data['intro_enable'] for key in ['disable_dev_ux', 'page_span', 'entries_per_page']: self.client_configuration[key] = data[key] tracker.emit('set_client_configuration', data) return {} @XBlock.json_handler def handle_vote(self, data, _suffix=''): # pylint: disable=unused-argument """ Add/Subtract a vote to a resource entry. Args: data: dict in JSON format data['id']: the ID of the resouce which was upvoted/downvoted data['event']: recommender_upvote or recommender_downvote Returns: result: dict in JSON format result['error']: error message generated if the process fails result['oldVotes']: original # of votes result['newVotes']: votes after this action result['toggle']: boolean indicator for whether the resource was switched from downvoted to upvoted """ resource_id = self._validate_resource(data['id'], data['event']) result = {} result['id'] = resource_id is_event_upvote = (data['event'] == 'recommender_upvote') result['oldVotes'] = (self.recommendations[resource_id]['upvotes'] - self.recommendations[resource_id]['downvotes']) upvoting_existing_upvote = is_event_upvote and resource_id in self.upvoted_ids downvoting_existing_downvote = not is_event_upvote and resource_id in self.downvoted_ids if upvoting_existing_upvote: # While the user is trying to upvote a resource which has been # upvoted, we restore the resource to unvoted self.upvoted_ids.remove(resource_id) self.recommendations[resource_id]['upvotes'] -= 1 elif downvoting_existing_downvote: # While the user is trying to downvote a resource which has # been downvoted, we restore the resource to unvoted self.downvoted_ids.remove(resource_id) self.recommendations[resource_id]['downvotes'] -= 1 elif is_event_upvote: # New upvote if resource_id in self.downvoted_ids: self.downvoted_ids.remove(resource_id) self.recommendations[resource_id]['downvotes'] -= 1 result['toggle'] = True self.upvoted_ids.append(resource_id) self.recommendations[resource_id]['upvotes'] += 1 else: # New downvote if resource_id in self.upvoted_ids: self.upvoted_ids.remove(resource_id) self.recommendations[resource_id]['upvotes'] -= 1 result['toggle'] = True self.downvoted_ids.append(resource_id) self.recommendations[resource_id]['downvotes'] += 1 result['newVotes'] = (self.recommendations[resource_id]['upvotes'] - self.recommendations[resource_id]['downvotes']) tracker.emit(data['event'], result) return result @XBlock.handler def upload_screenshot(self, request, _suffix=''): # pylint: disable=unused-argument """ Upload a screenshot for an entry of resource as a preview (typically to S3 or filesystem). Args: request: HTTP POST request request.POST['file'].file: the file to be uploaded Returns: response: HTTP response response.body (response.responseText): name of the uploaded file We validate that this is a valid JPG, GIF, or PNG by checking magic number, mimetype, and extension all correspond. We also limit to 30MB. We save the file under its MD5 hash to (1) avoid name conflicts, (2) avoid race conditions and (3) save space. """ # Check invalid file types image_types = { 'jpeg': { 'extension': [".jpeg", ".jpg"], 'mimetypes': ['image/jpeg', 'image/pjpeg'], 'magic': ["ffd8"] }, 'png': { 'extension': [".png"], 'mimetypes': ['image/png'], 'magic': ["89504e470d0a1a0a"] }, 'gif': { 'extension': [".gif"], 'mimetypes': ['image/gif'], 'magic': ["474946383961", "474946383761"] } } file_type_error_msg = 'Please upload an image in GIF/JPG/PNG' result = self._check_upload_file(request, image_types, file_type_error_msg, 'upload_screenshot', 31457280) if isinstance(result, Response): return result try: content = request.POST['file'].file.read() file_id = hashlib.md5(content).hexdigest() file_name = (file_id + '.' + result) fhwrite = self.fs.open(file_name, "wb") fhwrite.write(content) fhwrite.close() except IOError: return self._raise_pyfs_error('upload_screenshot') response = Response() response.body = json.dumps({'file_name': str("fs://" + file_name)}) response.headers['Content-Type'] = 'application/json' tracker.emit('upload_screenshot', {'uploadedFileName': response.body}) response.status = 200 return response @XBlock.json_handler def add_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Add a new resource entry. Args: data: dict in JSON format data[resource_content_field]: the resource to be added. Dictionary of description, etc. as defined above Returns: result: dict in JSON format result['error']: error message generated if the addition fails result[resource_content_field]: the content of the added resource """ # Construct new resource result = {} for field in self.resource_content_fields: result[field] = data[field] resource_id = stem_url(data['url']) self._check_redundant_resource(resource_id, 'add_resource', result) self._check_removed_resource(resource_id, 'add_resource', result) result['id'] = resource_id result['upvotes'] = 0 result['downvotes'] = 0 self.recommendations[resource_id] = dict(result) tracker.emit('add_resource', result) result["description"] = self._get_onetime_url(result["description"]) return result @XBlock.json_handler def edit_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Edit an entry of existing resource. Args: data: dict in JSON format data['id']: the ID of the edited resouce data[resource_content_field]: the content of the resource to be edited Returns: result: dict in JSON format result['error']: the error message generated when the edit fails result[old_resource_content_field]: the content of the resource before edited result[resource_content_field]: the content of the resource after edited """ resource_id = self._validate_resource(data['id'], 'edit_resource') result = {} result['id'] = resource_id result['old_id'] = resource_id for field in self.resource_content_fields: result['old_' + field] = self.recommendations[resource_id][field] # If the content in resource is unchanged (i.e., data[field] is # empty), return and log the content stored in the database # (self.recommendations), otherwise, return and log the edited # one (data[field]) if data[field] == "": result[field] = self.recommendations[resource_id][field] else: result[field] = data[field] ## Handle resource ID changes edited_resource_id = stem_url(data['url']) if edited_resource_id != resource_id: self._check_redundant_resource(edited_resource_id, 'edit_resource', result) self._check_removed_resource(edited_resource_id, 'edit_resource', result) self.recommendations[edited_resource_id] = deepcopy( self.recommendations[resource_id]) self.recommendations[edited_resource_id]['id'] = edited_resource_id result['id'] = edited_resource_id del self.recommendations[resource_id] # Handle all other changes for field in data: if field == 'id': continue if data[field] == "": continue self.recommendations[edited_resource_id][field] = data[field] tracker.emit('edit_resource', result) result["description"] = self._get_onetime_url(result["description"]) return result @XBlock.json_handler def flag_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Flag (or unflag) an entry of problematic resource and give the reason. This shows in a list for staff to review. Args: data: dict in JSON format data['id']: the ID of the problematic resouce data['isProblematic']: the boolean indicator for whether the resource is being flagged or unflagged. Only flagging works. data['reason']: the reason why the user believes the resource is problematic Returns: result: dict in JSON format result['reason']: the new reason result['oldReason']: the old reason result['id']: the ID of the problematic resouce result['isProblematic']: the boolean indicator for whether the resource is now flagged """ result = {} result['id'] = data['id'] result['isProblematic'] = data['isProblematic'] result['reason'] = data['reason'] user_id = self.get_user_id() # If already flagged, update the reason for the flag if data['isProblematic']: # If already flagged, update the reason if data['id'] in self.flagged_ids: result['oldReason'] = self.flagged_reasons[ self.flagged_ids.index(data['id'])] self.flagged_reasons[self.flagged_ids.index( data['id'])] = data['reason'] # Otherwise, flag it. else: self.flagged_ids.append(data['id']) self.flagged_reasons.append(data['reason']) if user_id not in self.flagged_accum_resources: self.flagged_accum_resources[user_id] = {} self.flagged_accum_resources[user_id][data['id']] = data['reason'] # Unflag resource. Currently unsupported. else: if data['id'] in self.flagged_ids: result['oldReason'] = self.flagged_reasons[ self.flagged_ids.index(data['id'])] result['reason'] = '' idx = self.flagged_ids.index(data['id']) del self.flagged_ids[idx] del self.flagged_reasons[idx] del self.flagged_accum_resources[user_id][data['id']] tracker.emit('flag_resource', result) return result @XBlock.json_handler def endorse_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Endorse an entry of resource. This shows the students the resource has the staff seal of approval. Args: data: dict in JSON format data['id']: the ID of the resouce to be endorsed Returns: result: dict in JSON format result['error']: the error message generated when the endorsement fails result['id']: the ID of the resouce to be endorsed result['status']: endorse the resource or undo it """ # Auth+auth if not self.get_user_is_staff(): msg = self.ugettext('Endorse resource without permission') self._error_handler(msg, 'endorse_resource') resource_id = self._validate_resource(data['id'], 'endorse_resource') result = {} result['id'] = resource_id # Unendorse previously endorsed resource if resource_id in self.endorsed_recommendation_ids: result['status'] = 'undo endorsement' endorsed_index = self.endorsed_recommendation_ids.index( resource_id) del self.endorsed_recommendation_ids[endorsed_index] del self.endorsed_recommendation_reasons[endorsed_index] # Endorse new resource else: result['reason'] = data['reason'] result['status'] = 'endorsement' self.endorsed_recommendation_ids.append(resource_id) self.endorsed_recommendation_reasons.append(data['reason']) tracker.emit('endorse_resource', result) return result @XBlock.json_handler def remove_resource(self, data, _suffix=''): """ Remove an entry of resource. This removes it from the student view, and prevents students from being able to add it back. Args: data: dict in JSON format data['id']: the ID of the resouce to be removed data['reason']: the reason why the resouce was removed Returns: result: dict in JSON format result['error']: the error message generated when the removal fails result['recommendation']: (Dict) the removed resource result['recommendation']['reason']: the reason why the resouce was removed """ # Auth+auth if not self.get_user_is_staff(): msg = self.ugettext( "You don't have the permission to remove this resource") self._error_handler(msg, 'remove_resource') resource_id = self._validate_resource(data['id'], 'remove_resource') # Grab a copy of the resource for the removed list # (swli: I reorganized the code a bit. First copy, then delete. This is more fault-tolerant) result = {} result['id'] = resource_id removed_resource = deepcopy(self.recommendations[resource_id]) removed_resource['reason'] = data['reason'] # Add it to removed resources and remove it from main resource list. self.removed_recommendations[resource_id] = removed_resource del self.recommendations[resource_id] # And return result['recommendation'] = removed_resource tracker.emit('remove_resource', result) return result @XBlock.json_handler def export_resources(self, _data, _suffix): # pylint: disable=unused-argument """ Export all resources from the Recommender. This is intentionally not limited to staff members (community contributions do not belong to the course staff). Sensitive information is exported *is* limited (flagged resources, and in the future, PII if any). """ result = {} result['export'] = { 'recommendations': self.recommendations, 'removed_recommendations': self.removed_recommendations, 'endorsed_recommendation_ids': self.endorsed_recommendation_ids, 'endorsed_recommendation_reasons': self.endorsed_recommendation_reasons, } if self.get_user_is_staff(): result['export'][ 'flagged_accum_resources'] = self.flagged_accum_resources tracker.emit('export_resources', result) return result @XBlock.handler def import_resources(self, request, _suffix=''): """ Import resources into the recommender. """ response = Response() response.headers['Content-Type'] = 'application/json' if not self.get_user_is_staff(): response.status = 403 response.body = json.dumps( {'error': self.ugettext('Only staff can import resources')}) tracker.emit('import_resources', {'Status': 'NOT_A_STAFF'}) return response # Check invalid file types file_types = { 'json': { 'extension': [".json"], 'mimetypes': ['application/json', 'text/json', 'text/x-json'] } } file_type_error_msg = self.ugettext( 'Please submit the JSON file obtained with the download resources button' ) result = self._check_upload_file(request, file_types, file_type_error_msg, 'import_resources', 31457280) if isinstance(result, Response): return result try: data = json.load(request.POST['file'].file) self.flagged_accum_resources = data['flagged_accum_resources'] self.endorsed_recommendation_reasons = data[ 'endorsed_recommendation_reasons'] self.endorsed_recommendation_ids = data[ 'endorsed_recommendation_ids'] if 'removed_recommendations' in data: self.removed_recommendations = data_structure_upgrade( data['removed_recommendations']) data['removed_recommendations'] = self.removed_recommendations self.recommendations = data_structure_upgrade( data['recommendations']) data['recommendations'] = self.recommendations tracker.emit('import_resources', { 'Status': 'SUCCESS', 'data': data }) response.body = json.dumps(data, sort_keys=True) response.status = 200 return response except (ValueError, KeyError): response.status = 415 response.body = json.dumps({ 'error': self.ugettext( 'Please submit the JSON file obtained with the download resources button' ) }) tracker.emit('import_resources', {'Status': 'FILE_FORMAT_ERROR'}) return response except IOError: return self._raise_pyfs_error('import_resources') @XBlock.json_handler def accum_flagged_resource(self, _data, _suffix=''): # pylint: disable=unused-argument """ Accumulate the flagged resource ids and reasons from all students """ if not self.get_user_is_staff(): msg = self.ugettext( 'Tried to access flagged resources without staff permission') self._error_handler(msg, 'accum_flagged_resource') result = {'flagged_resources': {}} for _, flagged_accum_resource_map in self.flagged_accum_resources.iteritems( ): for resource_id in flagged_accum_resource_map: if resource_id in self.removed_recommendations: continue if resource_id not in result['flagged_resources']: result['flagged_resources'][resource_id] = [] if flagged_accum_resource_map[resource_id] != '': result['flagged_resources'][resource_id].append( flagged_accum_resource_map[resource_id]) tracker.emit('accum_flagged_resource', result) return result def student_view(self, _context=None): # pylint: disable=unused-argument """ The primary view of the RecommenderXBlock, shown to students when viewing courses. """ self.recommendations = (data_structure_upgrade(self.recommendations) or data_structure_upgrade( self.default_recommendations) or {}) # Transition between two versions. In the previous version, there is # no endorsed_recommendation_reasons. Thus, we add empty reasons to # make the length of the two lists equal # # TODO: Go through old lists of resources in course, and remove this # code. The migration should be done. while len(self.endorsed_recommendation_ids) > len( self.endorsed_recommendation_reasons): self.endorsed_recommendation_reasons.append('') global template_lookup if not template_lookup: self._init_template_lookup() # Ideally, we'd estimate score based on votes, such that items with # 1 vote have a sensible ranking (rather than a perfect rating) # We pre-generate URLs for all resources. We benchmarked doing this # for 44 URLs, and the time per URL was about 8ms. The 44 URLs were # all of the images added by students over several problem sets. If # load continues to be as-is, pre-generation is not a performance # issue. If students make substantially more resources, we may want # to paginate, and generate in sets of 5-20 URLs per load. resources = [{ 'id': r['id'], 'title': r['title'], "votes": r['upvotes'] - r['downvotes'], 'url': r['url'], 'description': self._get_onetime_url(r['description']), 'descriptionText': r['descriptionText'] } for r in self.recommendations.values()] resources = sorted(resources, key=lambda r: r['votes'], reverse=True) frag = Fragment( template_lookup.get_template("recommender.html").render( resources=resources, upvoted_ids=self.upvoted_ids, downvoted_ids=self.downvoted_ids, endorsed_recommendation_ids=self.endorsed_recommendation_ids, endorsed_recommendation_reasons=self. endorsed_recommendation_reasons, flagged_ids=self.flagged_ids, flagged_reasons=self.flagged_reasons)) frag.add_css_url( "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css" ) frag.add_javascript_url( "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js") frag.add_javascript_url( '//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.8.1/mustache.min.js' ) frag.add_javascript_url( '//cdnjs.cloudflare.com/ajax/libs/intro.js/0.5.0/intro.min.js') frag.add_css(self.resource_string("static/css/tooltipster.css")) frag.add_css(self.resource_string("static/css/recommender.css")) frag.add_css(self.resource_string("static/css/introjs.css")) frag.add_javascript( self.resource_string("static/js/src/jquery.tooltipster.min.js")) frag.add_javascript(self.resource_string("static/js/src/cats.js")) frag.add_javascript( self.resource_string("static/js/src/recommender.js")) frag.initialize_js('RecommenderXBlock', self.get_client_configuration()) return frag def studio_view(self, _context=None): # pylint: disable=unused-argument """ The primary view of the RecommenderXBlock in studio. This is shown to course staff when editing a course in studio. """ global template_lookup if not template_lookup: self._init_template_lookup() frag = Fragment( template_lookup.get_template("recommenderstudio.html").render()) frag.add_css( pkg_resources.resource_string(__name__, "static/css/recommenderstudio.css")) frag.add_javascript_url( "//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js") frag.add_javascript( pkg_resources.resource_string( __name__, "static/js/src/recommenderstudio.js")) frag.initialize_js('RecommenderXBlock') return frag def add_xml_to_node(self, node): """ Serialize the XBlock to XML for exporting. """ node.tag = 'recommender' node.set('intro_enabled', 'true' if (self.intro_enabled) else 'false') node.set( 'disable_dev_ux', 'true' if (self.client_configuration['disable_dev_ux']) else 'false') node.set('entries_per_page', str(self.client_configuration['entries_per_page'])) node.set('page_span', str(self.client_configuration['page_span'])) el = etree.SubElement(node, 'resources') ## Note: The line below does not work in edX platform. ## We should figure out if the appropriate scope is available during import/export ## TODO: Talk to Cale el.text = json.dumps(self.recommendations).encode("utf-8") @staticmethod def workbench_scenarios(): """ A test sample scenario for display in the workbench. """ return [ ("RecommenderXBlock", """ <vertical_demo> <html_demo><img class="question" src="http://people.csail.mit.edu/swli/edx/recommendation/img/pset.png"></img></html_demo> <recommender intro_enabled="true" disable_dev_ux="true" entries_per_page="2" page_span="1"> <resources> [ {"id": 1, "title": "Covalent bonding and periodic trends", "upvotes" : 15, "downvotes" : 5, "url" : "https://courses.edx.org/courses/MITx/3.091X/2013_Fall/courseware/SP13_Week_4/SP13_Periodic_Trends_and_Bonding/", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/videopage1.png", "descriptionText" : "short description for Covalent bonding and periodic trends"}, {"id": 2, "title": "Polar covalent bonds and electronegativity", "upvotes" : 10, "downvotes" : 7, "url" : "https://courses.edx.org/courses/MITx/3.091X/2013_Fall/courseware/SP13_Week_4/SP13_Covalent_Bonding/", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/videopage2.png", "descriptionText" : "short description for Polar covalent bonds and electronegativity"}, {"id": 3, "title": "Longest wavelength able to to break a C-C bond ...", "upvotes" : 1230, "downvotes" : 7, "url" : "https://answers.yahoo.com/question/index?qid=20081112142253AA1kQN1", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/dispage1.png", "descriptionText" : "short description for Longest wavelength able to to break a C-C bond ..."}, {"id": 4, "title": "Calculate the maximum wavelength of light for ...", "upvotes" : 10, "downvotes" : 3457, "url" : "https://answers.yahoo.com/question/index?qid=20100110115715AA6toHw", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/dispage2.png", "descriptionText" : "short description for Calculate the maximum wavelength of light for ..."}, {"id": 5, "title": "Covalent bond - wave mechanical concept", "upvotes" : 10, "downvotes" : 7, "url" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage1.png", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage1.png", "descriptionText" : "short description for Covalent bond - wave mechanical concept"}, {"id": 6, "title": "Covalent bond - Energetics of covalent bond", "upvotes" : 10, "downvotes" : 7, "url" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage2.png", "description" : "http://people.csail.mit.edu/swli/edx/recommendation/img/textbookpage2.png", "descriptionText" : "short description for Covalent bond - Energetics of covalent bond"} ] </resources> </recommender> <recommender /> </vertical_demo> """), ] @classmethod def parse_xml(cls, node, runtime, keys, _id_generator): # pylint: disable=unused-argument """ Parse the XML for the XBlock. It is a list of dictionaries of default recommendations. """ block = runtime.construct_xblock_from_class(cls, keys) if node.tag != 'recommender': raise UpdateFromXmlError( "XML content must contain an 'recommender' root element.") if node.get('intro_enabled'): block.intro_enabled = (node.get('intro_enabled').lower().strip() not in ['false', '0', '']) if node.get('disable_dev_ux'): block.client_configuration['disable_dev_ux'] = ( node.get('disable_dev_ux').lower().strip() not in ['false', '0', '']) for tag in ['entries_per_page', 'page_span']: if node.get(tag): block.client_configuration[tag] = int(node.get(tag)) for child in node: if child.tag == 'resources' and child.text: lines = json.loads(child.text) block.default_recommendations = data_structure_upgrade(lines) return block
class DocxCheckerXBlock(XBlock): lab_scenario = Integer(display_name=u"Номер сценария", help=(u"Номер сценария", u"Номер сценария"), default=9999, scope=Scope.settings) scenarios_settings = JSONField( display_name=u"Настройки сценария", help=u"Настройки сценария", default={ "1": { "instruction_name": "Лабораторная 1. Указания к работе.docx", "template_name": "lab1_template.docx", "correct_name": "lab1_correct.docx", "title": "Стилевое форматирование процессоре Microsoft Office Word" }, "2": { "instruction_name": "Лабораторная 2. Указания к работе.docx", "template_name": "lab2_template.docx", "correct_name": "lab2_correct.docx", "title": "Создание и форматирование таблиц Microsoft Office Word" }, }, scope=Scope.settings) instruction_link = String( default='', scope=Scope.settings, help='Link for instruction download', ) template_link = String( default='', scope=Scope.settings, help='Link for template download', ) correct_link = String( default='', scope=Scope.settings, help='Link for correct file', ) source_docx_uid = String( default='', scope=Scope.settings, help='Unformatted file for student', ) source_docx_name = String( default='', scope=Scope.settings, help='Name of unformatted file for student', ) student_docx_uid = String( default='', scope=Scope.user_state, help='Studen file from student', ) student_docx_name = String( default='', scope=Scope.user_state, help='Name of student file from student', ) docx_analyze = JSONField( default={}, scope=Scope.user_state, help='Analyze document', ) display_name = String(display_name=u"Название", help=u"Название задания, которое увидят студенты.", default=u'Проверка стилевого оформления', scope=Scope.settings) question = String( # TODO: list display_name=u"Вопрос", help=u"Текст задания.", default=u"Текст вопроса", scope=Scope.settings) weight = Integer(display_name=u"Максимальное количество баллов", help=(u"Максимальное количество баллов", u"которое может получить студент."), default=10, scope=Scope.settings) #TODO: 1! max_attempts = Integer(display_name=u"Максимальное количество попыток", help=u"", default=10, scope=Scope.settings) attempts = Integer(display_name=u"Количество использованных попыток", help=u"", default=0, scope=Scope.user_state) points = Integer(display_name=u"Текущее количество баллов студента", default=None, scope=Scope.user_state) def resource_string(self, path): """Handy helper for getting resources from our kit.""" data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") # TO-DO: change this view to display your data your own way. def student_view(self, context=None): context = { "display_name": self.display_name, "weight": self.weight, "question": self.question, "student_docx_name": self.student_docx_name, "points": self.points, "attempts": self.attempts, "instruction_link": self.runtime.local_resource_url( self, 'public/instructions/' + self.instruction_link), "template_link": self.runtime.local_resource_url( self, 'public/templates/' + self.template_link), "lab_scenario": self.lab_scenario, "download_template_icon": self.runtime.local_resource_url( self, 'public/images/download_template_icon.png'), "download_instruction_icon": self.runtime.local_resource_url( self, 'public/images/download_instruction_icon.png'), } if self.max_attempts != 0: context["max_attempts"] = self.max_attempts if self.past_due(): context["past_due"] = True if answer_opportunity(self): context["answer_opportunity"] = True fragment = Fragment() fragment.add_content( render_template("static/html/docx_checker.html", context)) js_urls = ("static/js/src/docx_checker.js", ) css_urls = ("static/css/docx_checker.css", ) load_resources(js_urls, css_urls, fragment) fragment.initialize_js( 'DocxCheckerXBlock', { 'lab_scenario': self.lab_scenario, 'student_docx_name': self.student_docx_name, 'docx_analyze': self.docx_analyze }) return fragment def studio_view(self, context=None): scenarios = [] for index, key in enumerate(self.scenarios_settings.keys()): element = {} element["title"] = self.scenarios_settings[str(index + 1)]["title"] element["number"] = str(index + 1) scenarios.append(element) context = { "display_name": self.display_name, "weight": self.weight, "question": self.question, "max_attempts": self.max_attempts, "lab_scenario": self.lab_scenario, "scenarios": scenarios, } fragment = Fragment() fragment.add_content( render_template("static/html/docx_checker_studio.html", context)) js_urls = ("static/js/src/docx_checker_studio.js", ) css_urls = ("static/css/docx_checker_studio.css", ) load_resources(js_urls, css_urls, fragment) fragment.initialize_js('DocxCheckerXBlockEdit') return fragment # 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 [ ("DocxCheckerXBlock", """<docx_checker/> """), ("Multiple DocxCheckerXBlock", """<vertical_demo> <docx_checker/> <docx_checker/> <docx_checker/> </vertical_demo> """), ] @XBlock.json_handler def student_submit(self, data, suffix=''): def check_answer(): return 55 student_path = self._students_storage_path(self.student_docx_uid, self.student_docx_name) self.docx_analyze["errors"] = [] if str(self.lab_scenario) == "1": result = lab_1_check_answer( default_storage.open(student_path), '/home/edx/edxwork/docx_checker/docx_checker/corrects/' + self.scenarios_settings[str( self.lab_scenario)]["correct_name"]) self.docx_analyze = result if str(self.lab_scenario) == "2": result = lab_2_check_answer( default_storage.open(student_path), '/home/edx/edxwork/docx_checker/docx_checker/corrects/' + self.scenarios_settings[str( self.lab_scenario)]["correct_name"]) self.docx_analyze = result grade_global = check_answer() self.points = grade_global self.points = grade_global * self.weight / 100 self.points = int(round(self.points)) self.attempts += 1 self.runtime.publish(self, 'grade', { 'value': self.points, 'max_value': self.weight, }) res = { "success_status": 'ok', "points": self.points, "weight": self.weight, "attempts": self.attempts, "max_attempts": self.max_attempts, "docx_analyze": self.docx_analyze } return res @XBlock.json_handler def studio_submit(self, data, suffix=''): self.display_name = data.get('display_name') self.question = data.get('question') self.weight = data.get('weight') self.max_attempts = data.get('max_attempts') self.lab_scenario = data.get('lab_scenario') self.instruction_link = self.scenarios_settings[str( self.lab_scenario)]["instruction_name"] self.template_link = self.scenarios_settings[str( self.lab_scenario)]["template_name"] self.correct_link = self.runtime.local_resource_url( self, 'corrects/' + self.scenarios_settings[str(self.lab_scenario)]["correct_name"]) self.display_name = 'Проверка MS Word. ' + self.scenarios_settings[str( self.lab_scenario)]["title"] if str(self.lab_scenario) == "1": pass if str(self.lab_scenario) == "2": pass return {'result': 'success'} @XBlock.handler def student_filename(self, request, suffix=''): return Response(json_body={'student_filename': self.student_docx_name}) @XBlock.handler def download_student_file(self, request, suffix=''): path = self._students_storage_path(self.student_docx_uid, self.student_docx_name) return self.download(path, mimetypes.guess_type(self.student_docx_name)[0], self.student_docx_name) @XBlock.handler def download_instruction(self, request, suffix=''): path = self.runtime.local_resource_url( self, 'public/instructions/' + self.instruction_link) return self.download(path, 'docx', self.instruction_link) def is_course_staff(self): # pylint: disable=no-member """ Check if user is course staff. """ return getattr(self.xmodule_runtime, 'user_is_staff', False) @XBlock.handler def upload_student_file(self, request, suffix=''): upload = request.params['studentFile'] if upload.file.size < STUDENT_FILE_MAX_SIZE: self.student_docx_name = upload.file.name self.student_docx_uid = uuid.uuid4().hex print "!!!!!!!!!!!!!!!!!!!!!!SIZE: ", os.path.splitext( upload.file.name) path = self._students_storage_path(self.student_docx_uid, self.student_docx_name) if not default_storage.exists(path): default_storage.save(path, File(upload.file)) # obj = path return Response( json_body={ "status": True, "path": path, "student_filename": self.student_docx_name }) else: return Response( json_body={ "status": False, "max_size_limit": STUDENT_FILE_MAX_SIZE, "path": path, "student_filename": self.student_docx_name }) def _file_storage_path(self, uid, filename): # pylint: disable=no-member """ Get file path of storage. """ path = ('{loc.org}/{loc.course}/{loc.block_type}' '/{uid}{ext}'.format(loc=self.location, uid=uid, ext=os.path.splitext(filename)[1])) return path def _students_storage_path(self, uid, filename): # pylint: disable=no-member """ Get file path of storage. """ path = ('{loc.org}/{loc.course}/{loc.block_type}/students' '/{uid}{ext}'.format(loc=self.location, uid=uid, ext=os.path.splitext(filename)[1])) return path def download(self, path, mime_type, filename, require_staff=False): """ Return a file from storage and return in a Response. """ try: file_descriptor = default_storage.open(path) app_iter = iter(partial(file_descriptor.read, BLOCK_SIZE), '') return Response(app_iter=app_iter, content_type=mime_type, content_disposition="attachment; filename=" + filename.encode('utf-8')) except IOError: if require_staff: return Response("Sorry, assignment {} cannot be found at" " {}. Please contact {}".format( filename.encode('utf-8'), path, settings.TECH_SUPPORT_EMAIL), status_code=404) return Response("Sorry, the file you uploaded, {}, cannot be" " found. Please try uploading it again or contact" " course staff".format(filename.encode('utf-8')), status_code=404) def past_due(self): """ Проверка, истекла ли дата для выполнения задания. """ due = get_extended_due_date(self) if due is not None: if _now() > due: return False return True def is_course_staff(self): """ Проверка, является ли пользователь автором курса. """ return getattr(self.xmodule_runtime, 'user_is_staff', False) def is_instructor(self): """ Проверка, является ли пользователь инструктором. """ return self.xmodule_runtime.get_user_role() == 'instructor'
class GenesysXBlock(StudioEditableXBlockMixin, ScorableXBlockMixin, XBlockWithSettingsMixin, XBlock, PublishEventMixin): """ This XBlock connects to the Genesys API """ # Fields are defined on the class. You can access them in your code as # self.<fieldname>. display_name = String( display_name="Display Name", help= "This name appears in the horizontal navigation at the top of the page.", scope=Scope.settings, default=u"Genesys") instruction = String( display_name="Instruction Message", help= "The instruction message that appears above the hyperlink to the Genesys test", scope=Scope.settings, default=u"Click on the link below when you are ready to start the test." ) start_now = String(display_name="Start Message", help="The text value of the hyperlink", scope=Scope.settings, default=u"Start test now!") completed_message = String(display_name="Completed Message", help="The message for completing the test", scope=Scope.settings, default=u"You have completed all the tests.") invitation_url = String( help= "The invitation url used to access tests by respondents on Genesys", scope=Scope.user_state, default=None) respondent_id = Integer( help="The id of the respondent created/used for this invitation.", scope=Scope.user_state, default=None) invitation_id = Integer( help="The numerical id of the invitation created on Genesys system.", scope=Scope.user_state, default=u"") questionnaire_id = String( display_name="Questionnaire ID", help=("Genesys Questionnaire ID needed to access test"), scope=Scope.settings, default=None) external_id = String(display_name="External ID", help=("Genesys external ID needed to access test"), scope=Scope.settings, default=None) expiry_date = String(display_name="Expiry Date", help=("Test Expriry Date"), scope=Scope.settings, default='') test_started = Boolean(scope=Scope.user_state, default=False) invitation_successful = Boolean(scope=Scope.user_state, default=False) test_completed = Boolean(scope=Scope.user_state, default=False) insufficient_credit = Boolean(scope=Scope.user_state, default=False) test_id_list = List( display_name="Genesys Test ID's and Scores", help="Test ID's of the Genesys test you wish to include in Xblock.", allow_reset=False, scope=Scope.settings) score = JSONField(help="Dictionary with the current student score", scope=Scope.user_state) editable_fields = ( 'display_name', 'questionnaire_id', 'external_id', 'expiry_date', 'test_id_list', 'instruction', 'start_now', 'completed_message', ) has_score = True def validate_field_data(self, validation, data): """ Validate this block's field data. We are validating that the chosen freetextresponse xblocks ID's exist in the course """ if len(data.test_id_list) == 0: validation.add( ValidationMessage( ValidationMessage.ERROR, u"Please specify Genesys Test ID's and Scores.")) if data.external_id is None: validation.add( ValidationMessage(ValidationMessage.ERROR, u"Please specify an external ID.")) if data.questionnaire_id is None: validation.add( ValidationMessage(ValidationMessage.ERROR, u"Please specify Questionnaire ID.")) @property def api_configuration_id(self): """ Returns the Geneysis API token from Settings Service. The API key should be set in both lms/cms env.json files inside XBLOCK_SETTINGS. Example: "XBLOCK_SETTINGS": { "GenesysXBlock": { "GENESYS_CONFIG_ID": "YOUR API KEY GOES HERE" } }, """ return self.get_xblock_settings().get('GENESYS_CONFIG_ID', '') # return 'harambee-staging' @property def api_base_url(self): """ Returns the URL of the Geneysis domain from the Settings Service. The URL hould be set in both lms/cms env.json files inside XBLOCK_SETTINGS. Example: "XBLOCK_SETTINGS": { "GenesysXBlock": { "GENESYS_BASE_URL": "YOUR URL GOES HERE" } }, """ return self.get_xblock_settings().get('GENESYS_BASE_URL', '') # return 'https://api-rest.genesysonline.net/' @property def api_invitation_url(self): """ The Genesys Invitations endpoint """ return "{}invitations/{}".format(self.api_base_url, self.api_configuration_id) @property def api_results_url(self): """ The Genesys results endpoint """ return "{}/results/{}?respondentId={}".format( self.api_base_url, self.api_configuration_id, self.respondent_id) @property def get_headers(self): """ Get headers required for the Genesys Platform """ return self.get_xblock_settings().get('GENESYS_HEADERS', '') def api_invitation_params(self, user): """ Define the Genesys invitation params sent to the Genesys invitations endpoint """ # Check the user has a first and last name, and gender defined in profile if user.profile.gender is None: user.profile.gender = 'o' user.profile.save() if not user.first_name or not user.last_name: user.first_name = user.profile.name.split(' ')[0] user.last_name = user.profile.name.split(' ')[1] user.save() params = { "respondentFirstName": user.first_name, "respondentFamilyName": user.last_name, "respondentGender": user.profile.gender, "respondentEmailAddress": user.email, "questionnaireId": self.questionnaire_id, "externalId": self.external_id, "expiryDate": self.expiry_date } data = json.dumps(params) return data def get_genesys_invitation(self, user): """ This function sends a request to the Genesys invitations endpoint. It raises an Exception is the request is not successful. """ invitation = requests.post( url=self.api_invitation_url, headers=self.get_headers, data=self.api_invitation_params(user), ) if invitation.ok: self.invitation_id = invitation.json()['invitationId'] self.respondent_id = invitation.json()['respondentId'] self.invitation_url = invitation.json()['invitationUrl'] self.invitation_successful = True self.insufficient_credit = False return { 'invitation_id': self.invitation_id, 'respondent_id': self.respondent_id, 'invitation_url': self.invitation_url } elif "Insufficient Credits for Request" in invitation.text: self.insufficient_credit = True raise Exception( 'There was an error with the Genesys invitations endpoint. {}'. format(str(invitation.text))) else: raise Exception( 'There was an error with the Genesys invitations endpoint. {}'. format(str(invitation.text))) def get_genesys_test_result(self): """ Using the Genesys respondent ID, it checks for an fetches the respondents test results using the Genesys results endpoint """ result = requests.get(url=self.api_results_url, headers=self.get_headers) if result.ok: self.test_completed = True self.invitation_successful = True # force set insufficient_credit to false, as we have everything we need now. self.insufficient_credit = False # set the score in the user state self.score = self.get_individual_test_scores(result) # publish the raw_earned and raw_possible score calculated_total_score = self.calculate_score(result) self.publish_grade(score=calculated_total_score) elif "Insufficient Credits for Request" in result.text: self.insufficient_credit = True raise Exception( 'The was an error with the Genesys results endpoint. {}'. format(str(result.text))) else: raise Exception( 'The was an error with the Genesys results endpoint. {}'. format(str(result.text))) def max_score(self): """ Using the total scores for the tests specified in Studio settings, tally up the sum of the test scores. """ total_test_score = 0.0 for test_score in self.test_id_list: total_test_score += float(test_score[1]) return total_test_score def get_individual_test_scores(self, result): """ Using the result obtained from get genesys_test_result(), clean up the JSON and return a tidy dictionary for all the individual test results. """ individual_test_scores = {} cleaned_results = {} result_dict = json.loads(result.text) result_list = result_dict[0]['results'] for i in range(len(result_list)): cleaned_results[result_list[i] ['testId']] = result_list[i]['scales'][0]['raw'] for i in range(len(self.test_id_list)): individual_test_scores[self.test_id_list[i][0]] = float( self.test_id_list[i][1]) final_scores = {} for key, value in cleaned_results.items(): try: final_scores[str(key)] = (value, individual_test_scores[key]) except KeyError as e: logger.error(str(e)) final_scores[str( key)] = 'Test ID {} does not exist in results.'.format( str(key)) return final_scores def extract_earned_test_scores(self, result): """ Using the result obtained from get genesys_test_result(), clean up the JSON and return the SUM of score earned for all tests specified in test_id_list """ cleaned_results = {} result_dict = json.loads(result.text) result_list = result_dict[0]['results'] earned_test_score = 0.0 # Total the test score for i in range(len(result_list)): earned_test_score += result_list[i]['scales'][0]['raw'] return earned_test_score # TO-DO: change this view to display your data your own way. def student_view(self, context=None): """ The primary view of the GenesysXBlock, shown to students when viewing courses. """ no_name = False studio_runtime = False bugs_email = getattr(settings, 'BUGS_EMAIL', '') # Check if the runtime is cms or lms if settings.ROOT_URLCONF == 'cms.urls': studio_runtime = True student_account_url = '' else: student_account_url = reverse('account_settings') # If no invitation has been received, call Genesys invitations endpoint try: user = self.runtime.get_real_user( self.runtime.anonymous_student_id) if not user.first_name or not user.last_name: no_name = True except Exception as e: logger.error(str(e)) if self.respondent_id is None and no_name is False: try: invitation = self.get_genesys_invitation(user) except Exception as e: logger.error(str(e)) elif not self.test_completed: # If an invitation has been received, # try fetch the results, ideally this should happen when the webhook is POSTed to try: result = self.get_genesys_test_result() except Exception as e: logger.error(str(e)) else: pass context = { "no_name": no_name, "invitation_successful": self.invitation_successful, "src_url": self.invitation_url, "display_name": self.display_name, "instruction": self.instruction, "start_now": self.start_now, "completed": self.test_completed, "test_started": self.test_started, "studio_runtime": studio_runtime, "completed_message": self.completed_message, "student_account_url": student_account_url, "bugs_email": bugs_email, "insufficient_credit": self.insufficient_credit } frag = Fragment( loader.render_django_template("static/html/genesys.html", context).format(self=self)) frag.add_css(self.resource_string("static/css/genesys.css")) frag.add_javascript(self.resource_string("static/js/src/genesys.js")) frag.initialize_js('GenesysXBlock') return frag def studio_view(self, context): """ Render a form for editing this XBlock """ frag = Fragment() context = { 'fields': [], 'test_id_list': self.test_id_list, } # Build a list of all the fields that can be edited: for field_name in self.editable_fields: field = self.fields[field_name] if field.scope not in (Scope.content, Scope.settings): logger.error( "Only Scope.content or Scope.settings fields can be used with " "StudioEditableXBlockMixin. Other scopes are for user-specific data and are " "not generally created/configured by content authors in Studio." ) field_info = self._make_field_info(field_name, field) if field_info is not None: context["fields"].append(field_info) frag.content = loader.render_django_template( "static/html/genesys_edit.html", context) frag.add_javascript( loader.load_unicode("static/js/src/genesys_edit.js")) frag.initialize_js('StudioEditableXBlockMixin') return frag 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 set_score(self, score): """ Sets the internal score for the problem. This is not derived directly from the internal LCP in keeping with the ScorableXBlock spec. """ pass def calculate_score(self, result): """ Calculate a new raw score based on the state of the problem. This method should not modify the state of the XBlock. Returns: Score(raw_earned=float, raw_possible=float) """ earned = self.extract_earned_test_scores(result) possible = self.max_score() return Score(raw_earned=earned, raw_possible=possible) def publish_grade(self, score=None): """ Publishes the student's current grade to the system as an event """ if score is None: score = Score(earned=self._get_earned_from_saved_score(), possible=self.max_score()) self.runtime.publish(self, 'grade', { 'value': score.raw_earned, 'max_value': self.max_score(), }) return {'grade': score.raw_earned, 'max_grade': score.possible} def _get_earned_from_saved_score(): total = 0 if self.score: for key, value in self.score.items(): total += value[0] if total == 0: logger.warn("Score could not be calculated from user state score") return total @XBlock.json_handler def test_started_handler(self, data, suffix=''): ''' This is a XBlock json handler to store if the hyperlink to the Genesys invitation url has been clicked ''' self.test_started = True return {"started": True} @XBlock.json_handler def test_completed_handler(self, data, suffix=''): ''' This is a XBlock json handler check if the results for Genesys tests are available ''' result = requests.get(url=self.api_results_url, headers=self.get_headers) if result.ok: return {"completed": True} else: return {"completed": False} # 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 [ ("GenesysXBlock", """<genesys/> """), ("Multiple GenesysXBlock", """<vertical_demo> <genesys/> <genesys/> <genesys/> </vertical_demo> """), ]
class AdaptiveTestXBlock(XBlock): """ An adaptive-learning testing xblock. This Xblock allows instructors to selected one of many avlaiable tests (currently Kolb and Dominancia Cerebral) and provide an output of the student's learning style via a survey. Improvements to this Xblock include Course Modification (see TODOs). """ # Scopes. Persistent variables # See scopes definition for user_state (per user) and user_state_summary (global), among others. testNumber = Integer( default=0, scope=Scope.user_state_summary, help="Test number (0: Not avaliable, 1: Kolb, 2: Dominancia", ) # TestResult contains object: { result: string } testResult = JSONField( default="", scope=Scope.user_state, help="String identifying student learning style, according to test", ) # TestResults[] contains per item: # { test: number, result: object, user_id: string, user_full_name: string } testResults = JSONField( default=[], scope=Scope.user_state_summary, help="Array containing student information and results", ) testSolved = Boolean( default=False, scope=Scope.user_state, help="Flag if the user already solved the test", ) 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 student_view(self, context=None): """ The primary view of the StudentAdaptiveTestXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/student_adaptive_test.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.add_javascript( self.resource_string("static/js/src/jquery-1.12.4.js")) frag.add_javascript(self.resource_string("static/js/src/jquery-ui.js")) frag.add_javascript( self.resource_string("static/js/src/student_adaptive_test.js")) frag.initialize_js('StudentAdaptiveTestXBlock') return frag #Create studio_analytics view to show test results as a table def studio_analytics(self, context=None): html = self.resource_string("static/html/studio_analytics.html") frag = Fragment(html.format(self=self)) frag.add_javascript( self.resource_string("static/js/src/studio_analytics.js")) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.initialize_js('StudioAnalyticsXBlock') return frag #Studio view only used to select the test def studio_view(self, context=None): """ The primary view of the StudioAdaptiveTestXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/studio_adaptive_test.html") frag = Fragment(html.format(self=self)) frag.add_javascript( self.resource_string("static/js/src/studio_adaptive_test.js")) frag.add_css(self.resource_string("static/css/adaptive_test.css")) frag.initialize_js('StudioAdaptiveTestXBlock') # Notice return frag @XBlock.json_handler def select_test(self, data, suffix=''): """ Instructor's selected test handler. JS returned data is saved into global testNumber """ self.testNumber = data return True @XBlock.json_handler def load_test(self, data, suffix=''): """ Handler that returns the test currently used """ #Create variables according to test numbers, to be compared with the tests names in databes test_name = "Not selected" if (self.testNumber == 1): test_name = "Kolb" if (self.testNumber == 2): test_name = "Hermann" if (self.testNumber == 3): test_name = "Inteligencias Multiples" if (self.testNumber == 4): test_name = "Honey-Alonso" #Database query to bring student ids and resolved test by each student conn = psycopg2.connect(database='db_user', user='******', password='******', host='localhost') cur2 = conn.cursor() cur2.execute("SELECT * FROM resultadostest") rows = cur2.fetchall() conn.close() #check if logged student has resolved the test selected y the teacher flag = False for i in range(len(rows)): if ((str(rows[i][1]) == self.scope_ids.user_id) and (rows[i][3] == test_name)): flag = True result = rows[i][4] # Returns results in case student already has resolved teh selected test, returns only the test number otherwise. if flag: return {'test': self.testNumber, 'test_result': result} else: return {'test': self.testNumber} @XBlock.json_handler def submit_test(self, data, suffix=''): """ An example handler, which increments the data. """ collectedTest = data user_test_result = {} # Something should be modified in the course # EDXCUT: https://github.com/mitodl/edxcut showed to be an option. # Testing was unabled to use it correctly. # TODO: Take collectedTest and make modifications into the course content user_test_result["result"] = collectedTest user_test_result["test"] = self.testNumber user_test_result['user_id'] = self.scope_ids.user_id user_service = self.runtime.service(self, 'user') xb_user = user_service.get_current_user() user_test_result['user_full_name'] = xb_user.full_name self.testResults.append(user_test_result) self.testResult = collectedTest self.testSolved = True return True @XBlock.json_handler def load_analytics(self, data, suffix=''): """ An example handler, which increments the data. """ #Database query to bring all data, ando show it in studio_analytics view conn = psycopg2.connect(database='db_user', user='******', password='******', host='localhost') cur3 = conn.cursor() cur3.execute("SELECT * FROM resultadostest ORDER BY id_estudiante") rows = cur3.fetchall() conn.close() results = [] #devide results for each student in an array of python dictionaries for i in range(len(rows)): individual_result = {} individual_result["id_estudiante"] = rows[i][1] individual_result["fecha"] = str(rows[i][2]) individual_result["test"] = rows[i][3] individual_result["resultado"] = rows[i][4] results.append(individual_result) return results #*********Database Handler*********** @XBlock.json_handler def update(self, data, suffix=''): #Database, user and password must be changed according to the local database conn = psycopg2.connect(database='db_user', user='******', password='******', host='localhost') cur = conn.cursor() cur.execute( "INSERT INTO resultadostest (id_estudiante, fecha, nombre_test, resultado) VALUES (%s,CURRENT_DATE,%s, %s)", (self.scope_ids.user_id, data['test_name'], data['result'])) conn.commit() cur.close() conn.close() return True # Workbench scenarios. Ignore, unless you know how to use them. @staticmethod def workbench_scenarios(): """A canned scenario for display in the workbench.""" return [ ("AdaptiveTestXBlock", """<adaptive_test/> """), ]
class ReglasXBlock(XBlock): """ TO-DO: document what your XBlock does. """ # Fields are defined on the class. You can access them in your code as # self.<fieldname>. # TO-DO: delete count, and define your own fields. count = Integer( default=0, scope=Scope.user_state, help="A simple counter, to show something happening", ) resources_taged = JSONField( default=[], scope=Scope.user_state_summary, help="Array containing resource tagged information", ) def resource_string(self, path): """Handy helper for getting resources from our kit.""" data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") # TO-DO: change this view to display your data your own way. def studio_view(self, context=None): """ The primary view of the ReglasXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/reglasxblock.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/reglasxblock.css")) frag.add_javascript( self.resource_string("static/js/src/reglasxblock.js")) frag.initialize_js('ReglasXBlock') return frag # TO-DO: change this view to display your data your own way. def student_view(self, context=None): """ The primary view of the ReglasXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/student_reglasxblock.html") frag = Fragment(html.format(self=self)) frag.add_css( self.resource_string("static/css/student_reglasxblock.css")) frag.add_javascript( self.resource_string("static/js/src/student_reglasxblock.js")) frag.initialize_js('StudentReglasXBlock') return frag # TO-DO: change this handler to perform your own actions. You may need more # than one handler, or you may not need any handlers at all. @XBlock.json_handler def tag_resource(self, data, suffix=''): """ An example handler, which increments the data. """ # Just to show data coming in... # assert data['hello'] == 'world' print(data['tag']) print(data['resource']) self.resources_taged.append(data) # self.style_learn(data[tag]) print(self.resources_taged) # self.count += 1 return {"tag": data} @XBlock.json_handler def show_resources(self, data, suffix=''): return self.resources_taged # 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 [ ("ReglasXBlock", """<reglasxblock/> """), ("Multiple ReglasXBlock", """<vertical_demo> <reglasxblock/> <reglasxblock/> <reglasxblock/> </vertical_demo> """), ]
class QuestionsBankXBlock(XBlock): """ A Questions Bank XBlock that allows instructors to create a set of questions (only single and multiple option supported) and their answers to be shown at students. Displayed questions are randomized and disposed in number as desired by the instructor. XBlock behaves a grading problem. """ hasScore = True # TODO: some of the functionalities above are not yet supported. Check through time # or contact [email protected] # Fields (see Fields API or fields.py at edx-platform) questions = JSONField( default={}, scope=Scope.user_state_summary, help= "Questions created by the bank. Representation can be found at JS documentation.", ) studentHasCompleted = Boolean( default=False, scope=Scope.user_state, help="A flag for user input validation. A user can only submit once.", ) studentQuestionary = JSONField( default={}, scope=Scope.user_state, help="Student questionary containing both questions and answers.", ) studentAnsweredQuestions = JSONField( default=[], scope=Scope.user_state_summary, help="Solved questionary with details about the student.", ) def resource_string(self, path): """Handy helper for getting resources from our kit.""" data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") #@XBlock.service_declaration("user") # not working def student_view(self, context=None): """ The primary view of the QuestionsBankXBlock, shown to students when viewing courses. """ html = self.resource_string("static/html/student_questions_bank.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/questions_bank.css")) frag.add_javascript( self.resource_string("static/js/src/jquery-2.1.4.min.js")) frag.add_javascript( self.resource_string("static/js/src/jquery-ui.min.js")) frag.add_javascript( self.resource_string("static/js/src/student_questions_bank.js")) frag.initialize_js('StudentQuestionsBankXBlock') return frag # Important. Documentation isn't clear. Found at thumbs.py example. Problems use to declare problem_view in their source code. problem_view = student_view #TODO: provide instructors information and bank keys(?) def studio_view(self, context=None): """ The primary view of the QuestionsBankXBlock, shown to students when viewing courses. """ # Notice the Studio prefix at HTML and JS file, also JS initializer if (len(self.studentAnsweredQuestions)): html = self.resource_string("static/html/studio_analytics.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/questions_bank.css")) frag.add_javascript( self.resource_string("static/js/src/jquery-2.1.4.min.js")) frag.add_javascript( self.resource_string("static/js/src/jquery-ui.min.js")) frag.add_javascript( self.resource_string("static/js/src/studio_analytics.js")) frag.initialize_js('StudioAnalytics') else: html = self.resource_string( "static/html/studio_questions_bank.html") frag = Fragment(html.format(self=self)) frag.add_css(self.resource_string("static/css/questions_bank.css")) frag.add_javascript( self.resource_string("static/js/src/jquery-2.1.4.min.js")) frag.add_javascript( self.resource_string("static/js/src/jquery-ui.min.js")) frag.add_javascript( self.resource_string("static/js/src/studio_questions_bank.js")) frag.initialize_js('StudioQuestionsBankXBlock') return frag author_view = studio_view @XBlock.json_handler def create_bank(self, data, suffix=''): """ Handler to gather created bank data. Receives a JSON which stores globally """ # Saves to global user_state_summary self.questions = data return {'msg': "success"} @XBlock.json_handler def load_bank(self, data, suffix=''): """ Handler to load an already created bank data. Returns global 'questions'. """ form_id = data # TODO: not used yet, see JS # Returns only questions (bank) content return self.questions[1]['value'] if self.questions else { } # Handle {} in JS @XBlock.json_handler def load_questionary(self, data, suffix=''): """ Provides a handler to load a randomly generated questionary. Questions and answers are saved to user_questions (user_state) but ONLY questions are sent to the Javascript file. If hasCompleted is true, a message is shown. """ formID = data # TODO: show a view with completed questions (requires use case where a long time # or instructor approval is needed to release answers) # To return ONLY questions not sel (answer) attribute. And ONLY questions (bank) content only_questions = {} if not self.studentHasCompleted: if self.questions: # Load the number of questions per student (see estructure at the begining of th script) num_questions = int(self.questions[2]['value']) if self.studentQuestionary: # If an already created questionary exists, use it only_questions = self.studentQuestionary else: # Obtain only a N sample from the questions content (value at [1]) only_questions = random.sample( copy.deepcopy(self.questions[1]['value']), num_questions) # Saves a questionary per student, to keep track of it self.studentQuestionary = copy.deepcopy(only_questions) # Now we delete the answers, to avoid (partially) a frontend hack for question in only_questions: # Obtain choices (questions also contain type, req and label) for option in question['choices']: # And then delete 'sel' attribute del option['sel'] return only_questions # Handle {} in JS @XBlock.json_handler def complete_questions(self, data, suffix=''): """ Handler to load an already created bank data. Returns global 'questions'. """ scores = [] for idx_qst in range(len(self.studentQuestionary)): question = self.studentQuestionary[idx_qst]['choices'] answer = data[idx_qst]['choices'] total_valid = 0 user_right = 0 for idx_cho in range(len(question)): qst_ans = question[idx_cho]['sel'] stu_ans = answer[idx_cho]['sel'] if qst_ans == 1: total_valid += 1 if stu_ans == qst_ans: user_right += 1 qst_score = float(user_right) / total_valid scores.append(qst_score) if len(scores) > 0: score = (sum(scores) / len(scores)) * 100 else: score = 0 # Notice data structure at the answered questionary answeredQuestions = {} answeredQuestions['user_id'] = self.scope_ids.user_id user_service = self.runtime.service(self, 'user') xb_user = user_service.get_current_user() answeredQuestions['user_full_name'] = xb_user.full_name #answeredQuestions['user_email'] = xb_user.email #TODO: Implement in Studio, in workbench doesnt work answeredQuestions['student_answers'] = copy.deepcopy(data) answeredQuestions['student_questionary'] = copy.deepcopy( self.studentQuestionary) # TODO: TEST:grade this value into platform # Score value is based on 100 answeredQuestions['score'] = score # On publish a JSON, mind using string-defined properties like 'value':, rather than just value: self.runtime.publish(self, "grade", { 'value': score, 'max_value': 100.0 }) self.studentAnsweredQuestions.append(json.dumps(answeredQuestions)) self.studentHasCompleted = True # COMMENT for testing reasons related to grading # Returns only questions (bank) content return {'score': score} @XBlock.json_handler def load_analytics(self, data, suffix=''): """ Handler to load an already created bank data. Returns global 'questions'. """ formID = data #TODO: not implemented yet return self.studentAnsweredQuestions # Scenarios for the workbench. Ignore. @staticmethod def workbench_scenarios(): """A canned scenario for display in the workbench.""" return [("QuestionsBankXBlock", """<questions_bank/> """)]
class InfoSecureXBlock(StudioEditableXBlockMixin, XBlock): display_name = String( display_name='Display Name', default="infosecurexblock", scope=Scope.settings ) task_text = String( display_name='Task text', default="Task", multiline_editor=True, resettable_editor=False, scope=Scope.settings ) weight = Integer( display_name=u"Maximum number of points", help=u"", default=60, scope=Scope.settings ) lab_id = Integer( display_name='Lab ID', default=1, scope=Scope.settings ) answer = JSONField( display_name=u"Ответ студента", default={}, scope=Scope.user_state ) max_attempts = Integer( display_name=u"Maximum number of attempts", help=u"", default=1, scope=Scope.settings ) attempts = Integer( display_name=u"Количество сделанных попыток", default=0, scope=Scope.user_state ) points = Integer( display_name=u"Количество баллов студента", default=0, scope=Scope.user_state ) grade = Integer( display_name=u"Количество баллов студента", default=0, scope=Scope.user_state ) lab_settings = JSONField( display_name='Lab settings', default=1, scope=Scope.settings ) editable_fields = ('display_name', 'task_text', "lab_id", "max_attempts", "weight", "lab_settings") 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 student_view(self, context=None): """ The primary view of the InfoSecureXBlock, shown to students when viewing courses. """ context = { "display_name": self.display_name, "task_text": self.task_text, "weight": self.weight, "max_attempts": self.max_attempts, "attempts": self.attempts, "points": self.points, } if answer_opportunity(self): context["answer_opportunity"] = True fragment = Fragment() fragment.add_content( render_template( "static/html/infosecurexblock.html", context ) ) js_urls = ( "static/js/src/infosecurexblock.js", # "static/js/src/main.js", ) css_context = dict( comp_icon=self.runtime.local_resource_url(self, "public/images/comp.svg"), transfer_icon=self.runtime.local_resource_url(self, "public/images/transfer.svg"), monitor_icon=self.runtime.local_resource_url(self, "public/images/monitor.svg"), server_3_icon=self.runtime.local_resource_url(self, "public/images/server-3.svg"), file_icon=self.runtime.local_resource_url(self, "public/images/file.svg"), wifi_icon=self.runtime.local_resource_url(self, "public/images/wifi.svg"), ) css_urls = ("static/css/infosecurexblock.css",) # css_context load_resources(js_urls, css_urls, fragment) fragment.initialize_js('InfoSecureXBlock') return fragment def studio_view(self, context): """ Render a form for editing this XBlock """ fragment = Fragment() context = {'fields': []} # Build a list of all the fields that can be edited: for field_name in self.editable_fields: field = self.fields[field_name] assert field.scope in (Scope.content, Scope.settings), ( "Only Scope.content or Scope.settings fields can be used with " "StudioEditableXBlockMixin. Other scopes are for user-specific data and are " "not generally created/configured by content authors in Studio." ) field_info = self._make_field_info(field_name, field) if field_info is not None: context["fields"].append(field_info) fragment.content = loader.render_template('static/html/infosecurexblock_studio.html', context) fragment.add_javascript(loader.load_unicode('static/js/src/infosecurexblock_studio.js')) css_urls = ( "static/css/infosecurexblock_studio.css", ) load_resources([], css_urls, fragment) fragment.initialize_js('StudioEditableXBlockMixin') return fragment @XBlock.handler def rect1(self, data, suffix=''): dir = os.path.dirname(os.path.realpath(__file__)) file = open(os.path.join(dir, ('static/js/src/lab_{0}_rect{1}.json'.format( self.lab_id, data.params["lab_id"] )))).read() return Response(body=file, charset='UTF-8', content_type='text/plain') # elif(lab_id==2): # print('test') @XBlock.json_handler def checkLab(self, data, unused_suffix=''): return {'result': 'success' if answer_opportunity(self) else "fail", 'lab_id': self.lab_id } @XBlock.json_handler def check(self, data, unused_suffix=''): self.answer = data def checkLabs(data): if self.lab_id == 1: ip, d, N, answer0, key = data["ip"], int(data["d"]), int(data["N"]), int(data["e"]), data["key"] answer2 = [int(r) for r in list(str(answer0))] if (key == "mes_id1" or key == "mes_id2"): right = [14, 10, 18, 16, 14] elif (key == "mes_id3" or key == "mes_id4"): right = [2, 6, 9, 16, 17, 1, 19, 15, 16, 19, 20, 30] elif (key == "mes_id5"): right = [31, 12, 18, 1, 15] elif (key == "mes_id6"): right = [19, 10, 13, 1] elif (key == "mes_id7"): right = [19, 17, 1, 14] elif (key == "mes_id8"): right = [3, 10, 18, 21, 19, 29] elif (key == "mes_id9"): right = [22, 10, 26, 10, 15, 4] elif (key == "mes_id10"): right = [5, 16, 19, 20, 21, 17, 1] if IsTheNumberSimple(d): for j, k in enumerate(copy.deepcopy(right)): right[j] = right[j] ** d % N if (str(right) == str(answer2)) & (ip == "192.168.0.4"): return 1 else: return 0 elif self.lab_id == 2: correctness_list = [data["answerBlockRedac"], data["answerBlockAdmin"], data["answerBlockUsers"], ] return sum(correctness_list) / float(len(correctness_list)) elif self.lab_id == 3: correctness_list = [data["link1"], data["link2"], data["link3"], data["link4"], data["link5"], data["link6"], data["link7"], data["link8"], data["link9"], data["link10"], data["link11"], data["link12"], data["link13"], data["link14"], data["link15"], data["link16"], ] return sum(correctness_list) / float(len(correctness_list) - 6) elif self.lab_id == 4: event = int(data["event"]) event_id = str(data["eventId"]) # event_id = event_id[:-1] if (event == 0 and (event_id=="textRectEventId1" or event_id=="textRectEventId2" or event_id=="textRectEventId3")): return 1 elif(event == 1 and (event_id=="textRectEventId4" or event_id=="textRectEventId5")): return 1 else: return 0 elif self.lab_id == 5: answer0 = data["e"] key = str(data["key"]) if (key=="mas1" and answer0 == "0LHQtdC30L7Qv9Cw0YHQvdC+0YHRgtGM"): return 1 if (key=="mas2" and answer0 == "0LjQvdGE0L7RgNC80LDRgtC40LrQsA=="): return 1 if (key=="mas3" and answer0 == "0YjQuNGE0YDQvtCy0LDQvdC40LU="): return 1 if (key=="mas4" and answer0 =="0LrQuNCx0LXRgNCx0LXQt9C+0L/QsNGB0L3QvtGB0YLRjA=="): return 1 if (key=="mas5" and answer0 =="0LjQvdGE0L7RgNC80LDRhtC40Y8="): return 1 if (key=="mas6" and answer0 =="0LzRg9C70YzRgtC40LzQtdC00LjQsA=="): return 1 if (key=="mas7" and answer0 =="0L/RgNC+0LPRgNCw0LzQvNCw"): return 1 if (key=="mas8" and answer0 =="0LjQvdGC0LXRgNC90LXRgg=="): return 1 if (key=="mas9" and answer0 =="0LzQtdC00LjQsNC60L7QvNC80YPQvdC40LrQsNGG0LjRjw=="): return 1 if (key=="mas10" and answer0 =="0LDQu9Cz0L7RgNC40YLQvA=="): return 1 else: return 0 def IsTheNumberSimple(n): if n < 2: return False if n == 2: return True for l in range(2, n): if n % 2 == 0: return False else: return True if answer_opportunity(self): grade = checkLabs(data) self.grade = grade self.points = grade * self.weight self.runtime.publish(self, 'grade', { 'value': self.grade, 'max_value': self.weight, }) self.attempts += 1 response = {'result': 'success', 'correct': grade, 'weight': self.weight, "max_attempts": self.max_attempts, "attempts": self.attempts, "points": self.points, } else: response = {'result': 'fail', "max_attempts": self.max_attempts, "attempts": self.attempts } return response
class MultiEngineXBlock(XBlock): icon_class = 'problem' has_score = True # settings display_name = String( display_name=u"Название", help=u"Название задания, которое увидят студенты.", default=u'MultiEngine', scope=Scope.settings ) question = String( display_name=u"Вопрос", help=u"Текст задания.", default=u"Вы готовы?", scope=Scope.settings ) correct_answer = JSONField( display_name=u"Правильный ответ", help=u"Скрытое поле для правильного ответа в формате json.", default={}, scope=Scope.settings ) weight = Integer( display_name=u"Максимальное количество баллов", help=(u"Максимальное количество баллов", u"которое может получить студент."), default=100, scope=Scope.settings ) grade_steps = Integer( display_name=u"Шаг оценивания", help=u"Количество диапазонов оценивания", default=0, scope=Scope.settings ) scenario = String( display_name=u"Сценарий", help=u"Выберите один из сценариев отображения задания.", scope=Scope.settings, default=None, ) max_attempts = Integer( display_name=u"Максимальное количество попыток", help=u"", default=0, scope=Scope.settings ) # user_state points = Integer( display_name=u"Количество баллов студента", default=None, scope=Scope.user_state ) answer = JSONField( display_name=u"Ответ пользователя", default={"answer": {}}, scope=Scope.user_state ) attempts = Integer( display_name=u"Количество сделанных попыток", default=0, scope=Scope.user_state ) student_state_json = JSONField( display_name=u"Сохраненное состояние", scope=Scope.user_state ) student_view_template = String( display_name=u"Шаблон сценария", default='', scope=Scope.settings ) sequence = Boolean( display_name=u"Учитывать последовательность выбранных вариантов?", help=u"Работает не для всех сценариев.", default=False, scope=Scope.settings ) MULTIENGINE_ROOT = path(__file__).abspath().dirname().dirname() + '/multiengine' SCENARIOS_ROOT = MULTIENGINE_ROOT + '/public/scenarios/' def is_repo(self): repo_exists = False if os.path.exists(self.SCENARIOS_ROOT) and os.path.isdir(self.SCENARIOS_ROOT): for file_item in os.listdir(self.SCENARIOS_ROOT): if file_item and file_item == '.git': repo_exists = True elif not file_item: pass else: pass return repo_exists @staticmethod def clean_repo_path(scenarios_root=SCENARIOS_ROOT): """ Удаление локального репозитория сценариев """ shutil.rmtree(scenarios_root, ignore_errors=True) def update_local_repo(self): """ Обновление локального репозитория сценариев """ latest = False scenarios_repo = git.Repo(self.SCENARIOS_ROOT) scenarios_repo_remote = git.Remote( scenarios_repo, 'master') info = scenarios_repo_remote.fetch()[0] remote_commit = info.commit if scenarios_repo.commit().hexsha == remote_commit.hexsha: latest = True while remote_commit.hexsha != scenarios_repo.commit().hexsha: remote_commit = remote_commit.parents[0] return latest def clone_repo(self): """ Клонирование репозитория со сценариями. Адрес репозитория хранится в переменной GIT_REPO_URL в settings.py. """ scenarios_repo = git.Repo.clone_from( GIT_REPO_URL, self.SCENARIOS_ROOT, branch=GIT_BRANCH ) scenarios_repo = git.Repo(self.SCENARIOS_ROOT) latest = True return scenarios_repo, latest def load_scenarios(self, keys=None): """ Загрузка сценариев из локального репозитория в список. """ scenarios = {} _sc_keys = [ 'name::', 'description::', 'html::', 'javascriptStudent::', 'javascriptStudio::', 'css::', 'cssStudent::', ] if keys == "get": return _sc_keys if os.path.exists(self.SCENARIOS_ROOT) and os.path.isdir(self.SCENARIOS_ROOT): def _scenario_parser(scenario_file): _scenario_content = {} with open(self.SCENARIOS_ROOT + scenario_file) as scf: for line in scf: if any(ext in line for ext in _sc_keys): _current_key = line.strip().strip(':') else: if _current_key in _scenario_content: _scenario_content[_current_key] += line.decode('utf-8') else: _scenario_content[_current_key] = line.strip().decode('utf-8') return _scenario_content for scenario_file in os.listdir(self.SCENARIOS_ROOT): if scenario_file.endswith(".sc"): scenarios[os.path.splitext(scenario_file)[0]] = _scenario_parser(scenario_file) return scenarios def get_scenario_content(self, scenario): """ Получение текста сценария. """ try: scenario_file = open(self.SCENARIOS_ROOT + scenario + '.cs', 'r') with scenario_file as jsfile: scenario_content = jsfile.read() except: scenario_content = 'alert("Scenario file not found!");' logger.debug("[MultiEngineXBlock]: " + "Scenario file not found!") return scenario_content send_button = '' @staticmethod def resource_string(path): """ Handy helper for getting resources from our kit. """ data = pkg_resources.resource_string(__name__, path) return data.decode("utf8") def load_resources(self, js_urls, css_urls, fragment): """ Загрузка локальных статических ресурсов. """ for js_url in js_urls: if js_url.startswith('public/'): fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url)) elif js_url.startswith('static/'): fragment.add_javascript(_resource(js_url)) else: pass for css_url in css_urls: if css_url.startswith('public/'): fragment.add_css_url(self.runtime.local_resource_url(self, css_url)) elif css_url.startswith('static/'): fragment.add_css(_resource(css_url)) else: pass @property def course_id(self): return self._serialize_opaque_key(self.xmodule_runtime.course_id) # pylint:disable=E1101 def get_anonymous_user_id(self, username, course_id): """ Get the anonymous user id from Xblock user service. Args: username(str): user's name entered by staff to get info. course_id(str): course id. Returns: A unique id for (user, course) pair """ return self.runtime.service(self, 'user').get_anonymous_user_id(username, course_id) def get_student_item_dict(self, anonymous_user_id=None): """Create a student_item_dict from our surrounding context. See also: submissions.api for details. Args: anonymous_user_id(str): A unique anonymous_user_id for (user, course) pair. Returns: (dict): The student item associated with this XBlock instance. This includes the student id, item id, and course id. """ item_id = self._serialize_opaque_key(self.scope_ids.usage_id) # This is not the real way course_ids should work, but this is a # temporary expediency for LMS integration if hasattr(self, "xmodule_runtime"): course_id = self.course_id # pylint:disable=E1101 if anonymous_user_id: student_id = anonymous_user_id else: student_id = self.xmodule_runtime.anonymous_student_id # pylint:disable=E1101 student_item_dict = dict( student_id=student_id, item_id=item_id, course_id=course_id, item_type='multiengine' ) return student_item_dict def student_view(self, *args, **kwargs): """ Отображение MultiEngineXBlock студенту (LMS). """ scenarios = self.load_scenarios context = { "display_name": self.display_name, "weight": self.weight, "question": self.question, "correct_answer": self.correct_answer, "answer": self.answer, "attempts": self.attempts, "student_state_json": self.student_state_json, "student_view_template": self.student_view_template, "scenario": self.scenario, "scenarios": scenarios, } # Rescore student score = submissions_api.get_score(self.get_student_item_dict()) # It's temporary! It's crutch, not magick. self.runtime.publish(self, 'grade', { 'value': self.points, 'max_value': self.weight, }) if self.max_attempts != 0: context["max_attempts"] = self.max_attempts if self.past_due(): context["past_due"] = True if self.answer != '{}': context["points"] = self.points if answer_opportunity(self): context["answer_opportunity"] = True if self.is_course_staff() is True or self.is_instructor() is True: context['is_course_staff'] = True fragment = Fragment() fragment.add_content( render_template( 'static/html/multiengine.html', context ) ) js_urls = ( 'static/js/multiengine.js', ) css_urls = ( 'static/css/multiengine.css', ) self.load_resources(js_urls, css_urls, fragment) fragment.initialize_js('MultiEngineXBlock') return fragment def studio_view(self, *args, **kwargs): """ Отображение MultiEngineXBlock разработчику (CMS). """ scenarios = self.load_scenarios() context = { "display_name": self.display_name, "weight": self.weight, "question": self.question, "correct_answer": self.correct_answer, "answer": self.answer, "sequence": self.sequence, "scenario": self.scenario, "max_attempts": self.max_attempts, "student_view_template": self.student_view_template, "scenarios": scenarios, } if self.scenario: scenario_content = self.get_scenario_content(self.scenario) context["scenario_content"] = scenario_content fragment = Fragment() fragment.add_content( render_template( 'static/html/multiengine_edit.html', context ) ) js_urls = ( "static/js/multiengine_edit.js", ) css_urls = ( 'static/css/multiengine.css', ) self.load_resources(js_urls, css_urls, fragment) fragment.initialize_js('MultiEngineXBlockEdit') try: correct_answer = json.loads(self.correct_answer) except: correct_answer = json.loads('{}') logger.debug("[MultiEngineXBlock]: " + "Empty correct answer!") correct_answer = json.dumps(correct_answer) context["correct_answer"] = correct_answer return fragment # 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 [ ("MultiEngineXBlock", """<vertical_demo> <multiengine/> <multiengine/> <multiengine/> </vertical_demo> """), ] # Deprecated @staticmethod def download(path, filename): """ Возвращает клиенту файл. Deprecated. """ res = Response(content_type='text/javascript', app_iter=None) try: res.body = open(path + filename, 'r').read() except: res.body = 'alert("Scenario file not found!");' logger.debug("[MultiEngineXBlock]: " + "Scenario file not found!") return res @XBlock.json_handler def save_student_state(self, data, suffix=''): """ Handler for saving student state (save student answer without checking). :param request: :param suffix: :return: """ self.student_state_json = data return {'result': 'success'} @XBlock.handler def get_student_state(self, data, suffix=''): """ Return student state as json. :param request: :param suffix: :return: """ body = self.student_state_json # body = {"student_state_json": self.student_state_json, "result": "success"} это не работает!!! отдавалось:'{"' response = Response(body=body, content_type='application/json' ) return response @XBlock.handler def send_scenario(self, request, suffix=''): """ Отправляет сценарий пользователю. """ scenarios = self.load_scenarios() if smart_text(self.scenario) in scenarios: context = {} _sc_keys = self.load_scenarios("get") for key in _sc_keys: key = key.strip(':') if key in scenarios[smart_text(self.scenario)]: context[key] = scenarios[smart_text(self.scenario)][key].strip() else: context = { "name": '', "html": 'Scenario not found', "css": '', "javascriptStudent": '', "javascriptStudio": '', "description": '', "cssStudent": '', } response = Response(body=json.dumps(context), content_type='text/plain') return response @XBlock.handler def update_scenarios_repo(self, request, suffix=''): """ Обновление репозитория сценариев из внешнего git-репозитория. """ #require(self.is_course_staff()) # TODO Узнать почему 403 в Студии if self.is_repo(): try: self.update_local_repo() except: self.clean_repo_path() logger.debug("[MultiEngineXBlock]: " + "Clean repo path") self.clone_repo() logger.debug("[MultiEngineXBlock]: " + "Cloning repo...") elif not self.is_repo(): self.clone_repo() logger.debug("[MultiEngineXBlock]: " + "Cloning repo...") response = Response(body='{"result": "success"}', content_type='application/json' ) return response # Deprecated @XBlock.handler def download_scenario(self, request, suffix=''): """ ! Deprecated ! Хендлер выгрузки файла сценария. """ if self.scenario: return self.download(self.SCENARIOS_ROOT, self.scenario + '.sc') @XBlock.json_handler def studio_submit(self, data, suffix=''): self.display_name = data.get('display_name') self.question = data.get('question') self.weight = data.get('weight') self.correct_answer = data.get('correct_answer') self.sequence = data.get('sequence') self.scenario = data.get('scenario') self.max_attempts = data.get('max_attempts') self.student_view_template = data.get('student_view_template') return {'result': 'success'} @XBlock.json_handler def student_submit(self, data, suffix=''): student_json = json.loads(data) student_answer = student_json["answer"] self.answer = data correct_json = json.loads(self.correct_answer) correct_answer = correct_json["answer"] try: settings = correct_json["settings"] except: settings = {} settings['sequence'] = self.sequence def multicheck(student_answer, correct_answer, settings): """ Сравнивает 2 словаря вида: {"name1": ["param1", "param2"], "name2": ["param3", "param4"]} с произвольным количеством ключей, возвращает долю совпавших значений. """ keywords = ('or', 'and', 'not', 'or-and') def max_length(lst): length = 0 for element in lst: if len(element) > length: length = len(element) return length def _compare_answers_not_sequenced(student_answer, correct_answer, checked=0, correct=0): fail = False right_answers = [] wrong_answers = [] correct_answers_list = [] student_answers_list = [] for key in student_answer: student_answers_list += student_answer[key] for key in correct_answer: for value in correct_answer[key]: with_keyword = False if value in keywords: if value == "or": keyword = value correct_values = correct_answer[key][keyword] for correct_value in correct_values: correct_answers_list += correct_value if len(set(correct_value) - set(student_answer[key])) == 0: with_keyword = True break if with_keyword: checked += len(student_answer[key]) correct += len(student_answer[key]) else: checked += len(student_answer[key]) elif value == "or-and": keyword = value max_points_current = 0 correct_variant_len = 0 checked_objects = [] student_answer_key = set(student_answer[key]) for obj in correct_answer[key][keyword]: if len(set(obj)) > max_points_current: max_points_current = len(set(obj)) max_entry_variant = 0 for obj in correct_answer[key][keyword]: correct_answers_list += obj if max_entry_variant < len(set(obj)): max_entry_variant = len(set(obj)) correct_variant_len = max_points_current = len(correct_answer[key][keyword]) for answer in copy.deepcopy(student_answer_key): if answer in obj and obj not in checked_objects: correct += 1 checked_objects.append(obj) elif answer not in obj: pass else: fail = True checked += correct_variant_len elif value in student_answer[key]: correct_answers_list.append(value) right_answers.append(value) checked += 1 correct += 1 else: correct_answers_list.append(value) wrong_answers.append(value) checked += 1 if len(set(student_answers_list) - set(correct_answers_list)) or fail: print(set(student_answers_list)) print(set(correct_answers_list)) correct = 0 checks = {"result": correct / float(checked), "right_answers": right_answers, "wrong_answers": wrong_answers, "checked": checked } return checks def _compare_answers_sequenced(student_answer, correct_answer, checked=0, correct=0): """ Вычисляет долю выполненных заданий с учетом последовательности элементов в области. """ right_answers = [] wrong_answers = [] answer_condition = False for key in correct_answer: student_answer_true = [] if not isinstance(correct_answer[key], dict): for answer_item in student_answer[key]: if answer_item in correct_answer[key]: student_answer_true.append(answer_item) try: answer_condition = ''.join(student_answer_true) == ''.join(correct_answer[key]) except: answer_condition = str(student_answer_true) == str(correct_answer[key]) if answer_condition: right_answers += student_answer_true correct += len(correct_answer[key]) else: wrong_answers += student_answer_true checked += len(correct_answer[key]) else: for keyword in keywords: if keyword in correct_answer[key].keys(): correct_values = correct_answer[key][keyword] for correct_value in correct_values: try: answer_condition = ''.join(student_answer[key]) == ''.join(correct_value) except: answer_condition = str(student_answer[key]) == str(correct_value) if answer_condition: break checked += max_length(correct_values) if answer_condition: right_answers += student_answer[key] correct += len(student_answer[key]) else: wrong_answers += student_answer[key] checks = {"result": correct / float(checked), "right_answers": right_answers, "wrong_answers": wrong_answers, } return checks def _result_postproduction(result): # , settings['postproduction_rule']=None): result = int(round(result * self.weight)) return result if settings['sequence'] is True: checks = _compare_answers_sequenced(student_answer, correct_answer) elif settings['sequence'] is False: checks = _compare_answers_not_sequenced(student_answer, correct_answer) else: pass return _result_postproduction(checks["result"]), checks["right_answers"], checks["wrong_answers"] if answer_opportunity(self): checks = multicheck(student_answer, correct_answer, settings) correct = checks[0] right_answers = checks[1] wrong_answers = checks[2] self.points = correct self.attempts += 1 self.runtime.publish(self, 'grade', { 'value': correct, 'max_value': self.weight, }) return {'result': 'success', 'correct': correct, 'weight': self.weight, 'attempts': self.attempts, 'max_attempts': self.max_attempts, 'right_answers': right_answers, "wrong_answers": wrong_answers, } else: return('Max attempts exception!') def past_due(self): """ Проверка, истекла ли дата для выполнения задания. """ due = get_extended_due_date(self) if due is not None: if _now() > due: return False return True def is_course_staff(self): """ Проверка, является ли пользователь автором курса. """ return getattr(self.xmodule_runtime, 'user_is_staff', False) def is_instructor(self): """ Проверка, является ли пользователь инструктором. """ return self.xmodule_runtime.get_user_role() == 'instructor' 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)