diff --git a/course/utils.py b/course/utils.py index 38532f81ffbc92e0486f95daa2153287fe2dbe9e..d93022d1f1b920a50f75092055f36647d25072c9 100644 --- a/course/utils.py +++ b/course/utils.py @@ -155,6 +155,59 @@ class FlowSessionGradingRule(FlowSessionRuleBase): self.bonus_points = bonus_points +def _eval_http_rule(rule, participation, flow_id): + get_parameters = {} + + from relate.utils import struct_to_dict + for key, value in six.iteritems(struct_to_dict(rule.get_parameters)): + if value[0] == "literal": + _, get_value = value + elif value[0] == "field": + _, field_name = value + if field_name == "email": + get_value = participation.user.email + elif field_name == "institutional_id": + get_value = participation.user.institutional_id + else: + raise ValueError("invalid field name '%s'" % field_name) + else: + raise ValueError("invalid GET parameter type '%s'" % value[0]) + + get_parameters[key] = str(get_value) + + # TODO: caching + # TODO: documentation + + import requests + try: + r = requests.get(rule.url, params=get_parameters) + r.raise_for_status() + except requests.exceptions.RequestException as e: + return False + + try: + response_json = r.json() + except ValueError: + return False + + for key, value in six.iteritems(struct_to_dict(rule.response)): + json_value = response_json.get(key) + if value[0] == "required_value": + _, req_value = value + + if json_value != req_value: + return False + + elif value[0] == "cache_until": + pass + # TODO + + else: + raise ValueError("invalid response processing directive '%s'" % value[0]) + + return True + + def _eval_generic_conditions( rule, # type: Any course, # type: Course @@ -188,6 +241,11 @@ def _eval_generic_conditions( if login_exam_ticket.exam.flow_id != flow_id: return False + if hasattr(rule, "if_http_api_call"): + if not _eval_http_rule( + rule.if_external_http, participation, flow_id): + return False + return True diff --git a/course/validation.py b/course/validation.py index df9d585f1abddf9ed7b1268edf01095ed7074a22..989243c8d1e1e587270105f0328f98983acd5978 100644 --- a/course/validation.py +++ b/course/validation.py @@ -584,6 +584,17 @@ def validate_flow_group(vctx, location, grp): # {{{ flow rules +def validate_http_api_call_rule(vctx, location, http_rule): + validate_struct( + vctx, location, http_rule, + required_attrs=[ + ("url", str), + ("get_parameters", Struct), + ("cache_for_max_seconds", int), + ("response", Struct), + ]) + + def validate_session_start_rule(vctx, location, nrule, tags): # type: (ValidationContext, Text, Any, List[Text]) -> None validate_struct( @@ -600,6 +611,8 @@ def validate_session_start_rule(vctx, location, nrule, tags): ("if_has_session_tagged", (six.string_types, type(None))), ("if_has_fewer_sessions_than", int), ("if_has_fewer_tagged_sessions_than", int), + ("if_external_http", Struct), + ("if_signed_in_with_matching_exam_ticket", bool), ("tag_session", (six.string_types, type(None))), ("may_start_new_session", bool), @@ -636,6 +649,9 @@ def validate_session_start_rule(vctx, location, nrule, tags): validate_identifier(vctx, "%s: if_has_session_tagged" % location, nrule.if_has_session_tagged) + if hasattr(nrule, "if_http_api_call"): + validate_http_api_call_rule(vctx, location, nrule.if_external_http) + if not hasattr(nrule, "may_start_new_session"): vctx.add_warning( location+", rules", diff --git a/requirements.txt b/requirements.txt index fc35b1dd5ec357eadfe2025d9369564437c5eb26..d6b3d8f4913f70e9dd18c5b763b8330bab4f632b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -100,6 +100,8 @@ typing>=3.6.1 # For unittest by mock (only required for Py2) # mock +requests + # For rendering ipython notebook nbconvert>=5.2.1 IPython