Commit bfedb0e29e181a6dfece6a7cc0bd52fa9535da30
1 parent
4f960e53
Exists in
master
and in
1 other branch
initial support for submitting code to a JOBE server
Showing
7 changed files
with
188 additions
and
7 deletions
Show diff stats
demo/demo.yaml
demo/questions/questions-tutorial.yaml
| ... | ... | @@ -609,3 +609,49 @@ |
| 609 | 609 | generate-question | yamllint - |
| 610 | 610 | correct-answer | yamllint - |
| 611 | 611 | ``` |
| 612 | + | |
| 613 | +# ---------------------------------------------------------------------------- | |
| 614 | +- type: code | |
| 615 | + ref: tut-code | |
| 616 | + title: Submissão de código (JOBE) | |
| 617 | + text: | | |
| 618 | + É possível enviar código para ser compilado e executado por um servidor | |
| 619 | + JOBE instalado separadamente, ver [JOBE](https://github.com/trampgeek/jobe). | |
| 620 | + | |
| 621 | + ```yaml | |
| 622 | + - type: code | |
| 623 | + ref: tut-code | |
| 624 | + title: Submissão de código (JOBE) | |
| 625 | + text: | | |
| 626 | + Escreva um programa em C que recebe uma string no standard input e | |
| 627 | + mostra a mensagem `hello ` seguida da string. | |
| 628 | + Por exemplo, se o input for `Maria`, o output deverá ser `hello Maria`. | |
| 629 | + server: 127.0.0.1 # replace by appropriate address | |
| 630 | + language: c | |
| 631 | + correct: | |
| 632 | + - stdin: 'Maria' | |
| 633 | + stdout: 'hello Maria' | |
| 634 | + - stdin: 'xyz' | |
| 635 | + stdout: 'hello xyz' | |
| 636 | + ``` | |
| 637 | + | |
| 638 | + Existem várias linguagens suportadas pelo servidor JOBE (C, C++, Java, | |
| 639 | + Python2, Python3, Octave, Pascal, PHP). | |
| 640 | + O campo `correct` deverá ser uma lista de casos a testar. | |
| 641 | + Se um caso incluir `stdin`, este será enviado para o programa e o `stdout` | |
| 642 | + obtido será comparado com o declarado. A pergunta é considerada correcta se | |
| 643 | + todos os outputs coincidirem. | |
| 644 | + answer: | | |
| 645 | + #include <stdio.h> | |
| 646 | + int main() { | |
| 647 | + char name[20]; | |
| 648 | + scanf("%s", name); | |
| 649 | + printf("hello %s", name); | |
| 650 | + } | |
| 651 | + server: 192.168.1.141 | |
| 652 | + language: c | |
| 653 | + correct: | |
| 654 | + - stdin: 'Maria' | |
| 655 | + stdout: 'hello Maria' | |
| 656 | + - stdin: 'xyz' | |
| 657 | + stdout: 'hello xyz' | ... | ... |
perguntations/__init__.py
perguntations/app.py
| ... | ... | @@ -190,7 +190,7 @@ class App(): |
| 190 | 190 | raise AppException('Failed to create test factory!') from exc |
| 191 | 191 | |
| 192 | 192 | # ------------------------------------------------------------------------ |
| 193 | - def _pregenerate_tests(self, num): | |
| 193 | + def _pregenerate_tests(self, num): # TODO needs improvement | |
| 194 | 194 | event_loop = asyncio.get_event_loop() |
| 195 | 195 | self.pregenerated_tests += [ |
| 196 | 196 | event_loop.run_until_complete(self.testfactory.generate()) | ... | ... |
perguntations/questions.py
| ... | ... | @@ -13,6 +13,12 @@ import re |
| 13 | 13 | from typing import Any, Dict, NewType |
| 14 | 14 | import uuid |
| 15 | 15 | |
| 16 | + | |
| 17 | +from urllib.error import HTTPError | |
| 18 | +import json | |
| 19 | +import http.client | |
| 20 | + | |
| 21 | + | |
| 16 | 22 | # this project |
| 17 | 23 | from perguntations.tools import run_script, run_script_async |
| 18 | 24 | |
| ... | ... | @@ -584,6 +590,130 @@ class QuestionTextArea(Question): |
| 584 | 590 | |
| 585 | 591 | |
| 586 | 592 | # ============================================================================ |
| 593 | +class QuestionCode(Question): | |
| 594 | + '''An instance of QuestionCode will always have the keys: | |
| 595 | + type (str) | |
| 596 | + text (str) | |
| 597 | + correct (str with script to run) | |
| 598 | + answer (None or an actual answer) | |
| 599 | + ''' | |
| 600 | + | |
| 601 | + _outcomes = { | |
| 602 | + 0: 'JOBE outcome: Successful run', | |
| 603 | + 11: 'JOBE outcome: Compile error', | |
| 604 | + 12: 'JOBE outcome: Runtime error', | |
| 605 | + 13: 'JOBE outcome: Time limit exceeded', | |
| 606 | + 15: 'JOBE outcome: Successful run', | |
| 607 | + 17: 'JOBE outcome: Memory limit exceeded', | |
| 608 | + 19: 'JOBE outcome: Illegal system call', | |
| 609 | + 20: 'JOBE outcome: Internal error, please report', | |
| 610 | + 21: 'JOBE outcome: Server overload', | |
| 611 | + } | |
| 612 | + | |
| 613 | + # ------------------------------------------------------------------------ | |
| 614 | + def __init__(self, q: QDict) -> None: | |
| 615 | + super().__init__(q) | |
| 616 | + | |
| 617 | + self.set_defaults(QDict({ | |
| 618 | + 'text': '', | |
| 619 | + 'timeout': 5, # seconds | |
| 620 | + 'server': '127.0.0.1', # JOBE server | |
| 621 | + 'language': 'c', | |
| 622 | + 'correct': [{'stdin': '', 'stdout': '', 'stderr': '', 'args': ''}], | |
| 623 | + })) | |
| 624 | + | |
| 625 | + # ------------------------------------------------------------------------ | |
| 626 | + def correct(self) -> None: | |
| 627 | + super().correct() | |
| 628 | + | |
| 629 | + if self['answer'] is None: | |
| 630 | + return | |
| 631 | + | |
| 632 | + # submit answer to JOBE server | |
| 633 | + resource = '/jobe/index.php/restapi/runs/' | |
| 634 | + headers = {"Content-type": "application/json; charset=utf-8", | |
| 635 | + "Accept": "application/json"} | |
| 636 | + | |
| 637 | + for expected in self['correct']: | |
| 638 | + data_json = json.dumps({ | |
| 639 | + 'run_spec' : { | |
| 640 | + 'language_id': self['language'], | |
| 641 | + 'sourcecode': self['answer'], | |
| 642 | + 'input': expected.get('stdin', ''), | |
| 643 | + }, | |
| 644 | + }) | |
| 645 | + | |
| 646 | + try: | |
| 647 | + connect = http.client.HTTPConnection(self['server']) | |
| 648 | + connect.request( | |
| 649 | + method='POST', | |
| 650 | + url=resource, | |
| 651 | + body=data_json, | |
| 652 | + headers=headers | |
| 653 | + ) | |
| 654 | + response = connect.getresponse() | |
| 655 | + logger.debug('JOBE response status %d', response.status) | |
| 656 | + if response.status != 204: | |
| 657 | + content = response.read().decode('utf8') | |
| 658 | + if content: | |
| 659 | + result = json.loads(content) | |
| 660 | + connect.close() | |
| 661 | + | |
| 662 | + except (HTTPError, ValueError): | |
| 663 | + logger.error('HTTPError while connecting to JOBE server') | |
| 664 | + | |
| 665 | + try: | |
| 666 | + outcome = result['outcome'] | |
| 667 | + except (NameError, TypeError, KeyError): | |
| 668 | + logger.error('Bad result returned from JOBE server: %s', result) | |
| 669 | + return | |
| 670 | + logger.debug(self._outcomes[outcome]) | |
| 671 | + | |
| 672 | + if result['cmpinfo']: # compiler errors and warnings | |
| 673 | + self['comments'] = f'Erros de compilação:\n{result["cmpinfo"]}' | |
| 674 | + self['grade'] = 0.0 | |
| 675 | + return | |
| 676 | + | |
| 677 | + if result['stdout'] != expected['stdout']: | |
| 678 | + self['comments'] = 'O output gerado é diferente do esperado.' | |
| 679 | + self['grade'] = 0.0 | |
| 680 | + return | |
| 681 | + | |
| 682 | + self['comments'] = 'Ok!' | |
| 683 | + self['grade'] = 1.0 | |
| 684 | + | |
| 685 | + | |
| 686 | + # ------------------------------------------------------------------------ | |
| 687 | + async def correct_async(self) -> None: | |
| 688 | + self.correct() | |
| 689 | + | |
| 690 | + | |
| 691 | + # out = run_script( | |
| 692 | + # script=self['correct'], | |
| 693 | + # args=self['args'], | |
| 694 | + # stdin=self['answer'], | |
| 695 | + # timeout=self['timeout'] | |
| 696 | + # ) | |
| 697 | + | |
| 698 | + # if out is None: | |
| 699 | + # logger.warning('No grade after running "%s".', self["correct"]) | |
| 700 | + # self['comments'] = 'O programa de correcção abortou...' | |
| 701 | + # self['grade'] = 0.0 | |
| 702 | + # elif isinstance(out, dict): | |
| 703 | + # self['comments'] = out.get('comments', '') | |
| 704 | + # try: | |
| 705 | + # self['grade'] = float(out['grade']) | |
| 706 | + # except ValueError: | |
| 707 | + # logger.error('Output error in "%s".', self["correct"]) | |
| 708 | + # except KeyError: | |
| 709 | + # logger.error('No grade in "%s".', self["correct"]) | |
| 710 | + # else: | |
| 711 | + # try: | |
| 712 | + # self['grade'] = float(out) | |
| 713 | + # except (TypeError, ValueError): | |
| 714 | + # logger.error('Invalid grade in "%s".', self["correct"]) | |
| 715 | + | |
| 716 | +# ============================================================================ | |
| 587 | 717 | class QuestionInformation(Question): |
| 588 | 718 | ''' |
| 589 | 719 | Not really a question, just an information panel. |
| ... | ... | @@ -645,6 +775,7 @@ class QFactory(): |
| 645 | 775 | 'text-regex': QuestionTextRegex, |
| 646 | 776 | 'numeric-interval': QuestionNumericInterval, |
| 647 | 777 | 'textarea': QuestionTextArea, |
| 778 | + 'code': QuestionCode, | |
| 648 | 779 | # -- informative panels -- |
| 649 | 780 | 'information': QuestionInformation, |
| 650 | 781 | 'success': QuestionInformation, | ... | ... |
perguntations/serve.py
| ... | ... | @@ -227,6 +227,7 @@ class RootHandler(BaseHandler): |
| 227 | 227 | 'text-regex': 'question-text.html', |
| 228 | 228 | 'numeric-interval': 'question-text.html', |
| 229 | 229 | 'textarea': 'question-textarea.html', |
| 230 | + 'code': 'question-textarea.html', | |
| 230 | 231 | # -- information panels -- |
| 231 | 232 | 'information': 'question-information.html', |
| 232 | 233 | 'success': 'question-information.html', |
| ... | ... | @@ -289,7 +290,7 @@ class RootHandler(BaseHandler): |
| 289 | 290 | else: |
| 290 | 291 | ans[i] = ans[i][0] |
| 291 | 292 | elif question['type'] in ('text', 'text-regex', 'textarea', |
| 292 | - 'numeric-interval'): | |
| 293 | + 'numeric-interval', 'code'): | |
| 293 | 294 | ans[i] = ans[i][0] |
| 294 | 295 | |
| 295 | 296 | # correct answered questions and logout |
| ... | ... | @@ -477,6 +478,7 @@ class ReviewHandler(BaseHandler): |
| 477 | 478 | 'text-regex': 'review-question-text.html', |
| 478 | 479 | 'numeric-interval': 'review-question-text.html', |
| 479 | 480 | 'textarea': 'review-question-text.html', |
| 481 | + 'code': 'review-question-text.html', | |
| 480 | 482 | # -- information panels -- |
| 481 | 483 | 'information': 'review-question-information.html', |
| 482 | 484 | 'success': 'review-question-information.html', | ... | ... |
perguntations/templates/review-question.html
| ... | ... | @@ -47,7 +47,7 @@ |
| 47 | 47 | pontos |
| 48 | 48 | </p> |
| 49 | 49 | <p class="text-warning">{{ q['comments'] }}</p> |
| 50 | - {% if q.get('solution', '') %} | |
| 50 | + {% if q['solution'] %} | |
| 51 | 51 | <hr> |
| 52 | 52 | {{ md('**Solução:** \n\n' + q['solution']) }} |
| 53 | 53 | {% end %} |
| ... | ... | @@ -57,8 +57,8 @@ |
| 57 | 57 | {{ round(q['grade'] * q['points'], 2) }} |
| 58 | 58 | pontos |
| 59 | 59 | </p> |
| 60 | - <p class="text-danger">{{ q['comments'] }}</p> | |
| 61 | - {% if q.get('solution', '') %} | |
| 60 | + <p class="text-danger"><pre>{{ q['comments'] }}</pre></p> | |
| 61 | + {% if q['solution'] %} | |
| 62 | 62 | <hr> |
| 63 | 63 | {{ md('**Solução:** \n\n' + q['solution']) }} |
| 64 | 64 | {% end %} |
| ... | ... | @@ -102,7 +102,7 @@ |
| 102 | 102 | <i class="fas fa-ban fa-3x" aria-hidden="true"></i> |
| 103 | 103 | {{ round(q['grade'] * q['points'], 2) }} pontos<br> |
| 104 | 104 | {{ q['comments'] }} |
| 105 | - {% if q.get('solution', '') %} | |
| 105 | + {% if q['solution'] %} | |
| 106 | 106 | <hr> |
| 107 | 107 | {{ md('**Solução:** \n\n' + q['solution']) }} |
| 108 | 108 | {% end %} | ... | ... |