diff --git a/cms/djangoapps/contentstore/rest_api/v2/serializers/utils.py b/cms/djangoapps/contentstore/rest_api/v2/serializers/utils.py new file mode 100644 index 000000000000..b11329c72e23 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v2/serializers/utils.py @@ -0,0 +1,17 @@ +""" +Serializers for the contentstore v2 utils views module. + +This module contains DRF serializers for different utils like validations. +""" + +from rest_framework import serializers + + +class NumericalInputValidationRequestSerializer(serializers.Serializer): + formula = serializers.CharField() + + +class NumericalInputValidationReponseSerializer(serializers.Serializer): + preview = serializers.CharField() + is_valid = serializers.BooleanField() + error = serializers.CharField(allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v2/urls.py b/cms/djangoapps/contentstore/rest_api/v2/urls.py index 5fa954a4ef82..893069f89aec 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v2/urls.py @@ -3,7 +3,7 @@ from django.conf import settings from django.urls import path, re_path -from cms.djangoapps.contentstore.rest_api.v2.views import downstreams, home +from cms.djangoapps.contentstore.rest_api.v2.views import downstreams, home, utils app_name = "v2" @@ -33,4 +33,8 @@ downstreams.SyncFromUpstreamView.as_view(), name="sync_from_upstream" ), + re_path( + '^validate/numerical-input/$', + utils.NumericalInputValidationView.as_view(), + name='numerical_input_validation'), ] diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/utils.py b/cms/djangoapps/contentstore/rest_api/v2/views/utils.py new file mode 100644 index 000000000000..8a6fe461fdb7 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v2/views/utils.py @@ -0,0 +1,22 @@ +""" +Common utilities for V2 APIs. +""" +from rest_framework.response import Response +from rest_framework.generics import GenericAPIView +from rest_framework import permissions +from cms.djangoapps.contentstore.rest_api.v2.serializers.utils import NumericalInputValidationRequestSerializer +from xmodule.capa.inputtypes import preview_numeric_input + + +class NumericalInputValidationView(GenericAPIView): + """Class in charge of NumericalInputValidations""" + permission_classes = (permissions.IsAuthenticated,) + serializer_class = NumericalInputValidationRequestSerializer + + def post(self, request): + """function to validate a math expression (formula) and return of the numeric input is valid or not""" + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + formula = serializer.validated_data['formula'] + result = preview_numeric_input(formula) + return Response(result, status=200) diff --git a/xmodule/capa/inputtypes.py b/xmodule/capa/inputtypes.py index e85cb1a25b5a..97f0d5e6f713 100644 --- a/xmodule/capa/inputtypes.py +++ b/xmodule/capa/inputtypes.py @@ -49,9 +49,9 @@ import html5lib import nh3 -import pyparsing import six from calc.preview import latex_preview +import pyparsing from chem import chemcalc from lxml import etree @@ -1300,22 +1300,47 @@ def preview_formcalc(self, get): result["request_start"] = int(get.get("request_start", 0)) + # TODO add references to valid variables and functions + # At some point, we might want to mark invalid variables as red + # or something, and this is where we would need to pass those in. try: - # TODO add references to valid variables and functions - # At some point, we might want to mark invalid variables as red - # or something, and this is where we would need to pass those in. - result["preview"] = latex_preview(formula) + numeric_result = preview_numeric_input(formula) + # Map results into the correct format + result["preview"] = numeric_result["preview"] + if numeric_result["error"]: + result["error"] = numeric_result["error"] + # if formula is invalid return formula + if not numeric_result["is_valid"]: + result["formula"] = formula except pyparsing.ParseException: - result["error"] = _("Sorry, couldn't parse formula") - result["formula"] = formula + result['error'] = _("Sorry, couldn't parse formula") + result['formula'] = formula except Exception: # lint-amnesty, pylint: disable=broad-except - # this is unexpected, so log log.warning("Error while previewing formula", exc_info=True) result["error"] = _("Error while rendering preview") + return result return result +def preview_numeric_input(formula): + """ + Handles numeric validations, validates that the formula provided is a valid formula. + """ + result = {'preview': '', 'is_valid': True, 'error': ''} + try: + result['preview'] = latex_preview(formula) + except pyparsing.ParseException: + result["error"] = "Sorry, couldn't parse formula" + result['is_valid'] = False + return result + except Exception: # pylint: disable=broad-exception-caught + log.warning("Error while previewing formula", exc_info=True) + result['error'] = "Error while rendering preview" + result['is_valid'] = False + return result + + # ----------------------------------------------------------------------------- diff --git a/xmodule/capa/tests/test_inputtypes.py b/xmodule/capa/tests/test_inputtypes.py index c6b2b705adee..80d8427b0c74 100644 --- a/xmodule/capa/tests/test_inputtypes.py +++ b/xmodule/capa/tests/test_inputtypes.py @@ -1361,7 +1361,7 @@ def test_ajax_parse_err(self): With parse errors, FormulaEquationInput should give an error message """ # Simulate answering a problem that raises the exception - with patch("xmodule.capa.inputtypes.latex_preview") as mock_preview: + with patch('xmodule.capa.inputtypes.preview_numeric_input') as mock_preview: mock_preview.side_effect = ParseException("Oopsie") response = self.the_input.handle_ajax( "preview_formcalc", @@ -1379,7 +1379,7 @@ def test_ajax_other_err(self, mock_log): """ With other errors, test that FormulaEquationInput also logs it """ - with patch("xmodule.capa.inputtypes.latex_preview") as mock_preview: + with patch('xmodule.capa.inputtypes.preview_numeric_input') as mock_preview: mock_preview.side_effect = Exception() response = self.the_input.handle_ajax( "preview_formcalc",