Пример #1
0
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/>
             """),
        ]
Пример #2
0
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>
             """),
        ]
Пример #3
0
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
Пример #4
0
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'
Пример #5
0
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>
             """),
        ]
Пример #6
0
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/>
             """),
        ]
Пример #7
0
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>
             """),
        ]
Пример #8
0
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
Пример #10
0
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)