diff --git a/client/protocols/common/models.py b/client/protocols/common/models.py index 271dec3b..3fd0ec93 100644 --- a/client/protocols/common/models.py +++ b/client/protocols/common/models.py @@ -33,6 +33,7 @@ class ResearchProtocol(Protocol): CS61A_ID = '61a' C88C_ID = '88c' UNKNOWN_COURSE = '' + UNKNOWN_EMAIL = '' GET_CONSENT = True CONSENT_CACHE = '.ok_consent' diff --git a/client/protocols/followup.py b/client/protocols/followup.py index 84140f70..6e96d01d 100644 --- a/client/protocols/followup.py +++ b/client/protocols/followup.py @@ -10,6 +10,12 @@ import os import logging import json +import re +import requests +import hmac +import pickle + +from urllib.parse import urlencode from client.utils.printer import print_error @@ -20,9 +26,10 @@ class FollowupProtocol(models.ResearchProtocol, UnlockProtocol): PROTOCOL_NAME = 'followup' - FOLLOWUP_ENDPOINT = models.ResearchProtocol.SERVER + '/questions' + FOLLOWUP_ENDPOINT = models.ResearchProtocol.SERVER + '/successQuestions?' + RESPONSE_ENDPOINT = models.ResearchProtocol.SERVER + '/successQuestionsResponse' GET_CONSENT = True - FOLLOWUPS_FILE = 'followups.json' + FOLLOWUP_CACHE = '.ok_followups' def run(self, messages): config = config_utils._get_config(self.args.config) @@ -31,21 +38,20 @@ def run(self, messages): return check_solved = self._check_solved(messages) - failed, active_function = check_solved['failed'], check_solved['active_function'] + failed, _ = check_solved['failed'], check_solved['active_function'] if failed: return - if self.FOLLOWUPS_FILE not in os.listdir(): - followup_data = [] - else: - followup_data = json.loads(open(self.FOLLOWUPS_FILE).read()) + email = messages.get('email') or self.UNKNOWN_EMAIL + responded_followups = self._get_followups(email) followup_queue = [] - for entry in followup_data: - if entry['name'] == active_function: - for followup in entry['followups']: - if not followup['response']: - followup_queue.append(followup) + for question_id, analytic in messages.get('grading', {}).items(): + if analytic['failed'] == 0 and question_id not in responded_followups: + followup_queue.append(question_id) + + consent = None if len(followup_queue) > 0: + consent = self._get_consent(email) format.print_line('~') print('Follow-up questions') print() @@ -54,21 +60,44 @@ def run(self, messages): self.PROMPT)) print('Type {} to quit'.format(self.EXIT_INPUTS[0])) print() - - for followup in followup_queue: - response = self._ask_followup(followup) - followup['response'] = response - - with open(self.FOLLOWUPS_FILE, 'w') as f: - f.write(json.dumps(followup_data, indent=2)) + filename = config['src'][0] + hw_id = str(int(re.findall(r'hw(\d+)\.(py|scm|sql)', filename)[0][0])) + for q_id in followup_queue: + params = { + 'hwId': hw_id, + 'questionId': q_id, + } + server_response = requests.get(self.FOLLOWUP_ENDPOINT + urlencode(params)) + if server_response.status_code == 200: + followup = server_response.json() + response_data = self._ask_followup(followup) + if response_data: + payload = { + 'email': email, + 'consent': consent, + 'hwId': hw_id, + 'activeFunction': q_id, + 'responseIndex': response_data.get('response_index', None), + 'responseText': response_data.get('response_text', None) + } + try: + server_response = requests.post(self.RESPONSE_ENDPOINT, json=payload).json() + if server_response.get('status', '') != 'ok': + print_error("Error reaching 61a-bot server. Please inform the course staff on Ed and try again later.") + else: + self._append_followups(email, q_id) + except Exception as e: + print_error("Error reaching 61a-bot server. Please inform the course staff on Ed and try again later.") + else: + break def _ask_followup(self, followup): question, choices = followup['question'], followup['choices'] print(question) print() - for c in choices: - print(c) + for i, c in enumerate(choices): + print(f"{chr(ord('A') + i)}. {c}") print() valid_responses = [chr(ord('A') + i) for i in range(len(choices))] + [chr(ord('a') + i) for i in range(len(choices))] + list(self.EXIT_INPUTS) response = None @@ -79,6 +108,34 @@ def _ask_followup(self, followup): if response not in self.EXIT_INPUTS: print(f'LOG: received {response.upper()} from student') - return response.upper() + response_index = ord(response.upper()) - ord('A') + response_text = choices[response_index] + return { + 'response_index': response_index, + 'response_text': response_text + } + return {} + + def _get_followups(self, email): + if self.FOLLOWUP_CACHE in os.listdir(): + try: + with open(self.FOLLOWUP_CACHE, 'rb') as f: + data = pickle.load(f) + if not hmac.compare_digest(data.get('mac'), self._mac(email, data.get('followups', []))): + os.remove(self.FOLLOWUP_CACHE) + return self._get_context(email) + return data.get('followups', []) + + except: + os.remove(self.FOLLOWUP_CACHE) + return self._get_context(email) + else: + return [] + + def _append_followups(self, email, responded): + followups = self._get_followups(email) + followups.append(responded) + with open(self.FOLLOWUP_CACHE, 'wb') as f: + pickle.dump({'followups': followups, 'mac': self._mac(email, followups)}, f, protocol=pickle.HIGHEST_PROTOCOL) protocol = FollowupProtocol diff --git a/client/protocols/help.py b/client/protocols/help.py index 45e7d24f..fb3f4562 100644 --- a/client/protocols/help.py +++ b/client/protocols/help.py @@ -44,7 +44,6 @@ class HelpProtocol(models.ResearchProtocol): CONTEXT_CACHE = '.ok_context' CONTEXT_LENGTH = 3 DISABLED_CACHE = '.ok_disabled' - UNKNOWN_EMAIL = '' BOT_PREFIX = '[61A-bot]: ' HELP_TYPE_PROMPT = BOT_PREFIX + "Would you like to receive debugging help (d) or help understanding the problem (p)? You can also type a specific question.\nPress return/enter to receive no help. Type \"never\" to turn off 61a-bot for this assignment." NO_HELP_TYPE_PROMPT = BOT_PREFIX + "Would you like to receive 61A-bot feedback on your code (y/N/never)? " @@ -120,7 +119,6 @@ def animate(): try: help_response = requests.post(self.HELP_ENDPOINT, json=help_payload).json() except Exception as e: - # print(requests.post(self.HELP_ENDPOINT, json=help_payload)) print_error("Error generating hint. Please try again later.") return if 'output' not in help_response: