Commit 2826a6019e2b26af5a9263b97aaba28ff5f54667

Authored by Miguel Barao
1 parent ab185529
Exists in master and in 1 other branch dev

Moved question factory to questions.yaml

Moved menu item "topicos" to menu bar.
aprendizations/factory.py
@@ -1,99 +0,0 @@ @@ -1,99 +0,0 @@
1 -# QFactory is a class that can generate question instances, e.g. by shuffling  
2 -# options, running a script to generate the question, etc.  
3 -#  
4 -# To generate an instance of a question we use the method generate() where  
5 -# the argument is the reference of the question we wish to produce.  
6 -#  
7 -# Example:  
8 -#  
9 -# # read question from file  
10 -# qdict = tools.load_yaml(filename)  
11 -# qfactory = QFactory(qdict)  
12 -# question = qfactory.generate()  
13 -#  
14 -# # experiment answering one question and correct it  
15 -# question.updateAnswer('42') # insert answer  
16 -# grade = question.correct() # correct answer  
17 -  
18 -# An instance of an actual question is an object that inherits from Question()  
19 -#  
20 -# Question - base class inherited by other classes  
21 -# QuestionInformation - not a question, just a box with content  
22 -# QuestionRadio - single choice from a list of options  
23 -# QuestionCheckbox - multiple choice, equivalent to multiple true/false  
24 -# QuestionText - line of text compared to a list of acceptable answers  
25 -# QuestionTextRegex - line of text matched against a regular expression  
26 -# QuestionTextArea - corrected by an external program  
27 -# QuestionNumericInterval - line of text parsed as a float  
28 -  
29 -# base  
30 -from os import path  
31 -import logging  
32 -  
33 -# project  
34 -from aprendizations.tools import run_script  
35 -from aprendizations.questions import (QuestionInformation, QuestionRadio,  
36 - QuestionCheckbox, QuestionText,  
37 - QuestionTextRegex, QuestionTextArea,  
38 - QuestionNumericInterval,  
39 - QuestionException)  
40 -  
41 -# setup logger for this module  
42 -logger = logging.getLogger(__name__)  
43 -  
44 -  
45 -# ===========================================================================  
46 -# Question Factory  
47 -# ===========================================================================  
48 -class QFactory(object):  
49 - # Depending on the type of question, a different question class will be  
50 - # instantiated. All these classes derive from the base class `Question`.  
51 - _types = {  
52 - 'radio': QuestionRadio,  
53 - 'checkbox': QuestionCheckbox,  
54 - 'text': QuestionText,  
55 - 'text-regex': QuestionTextRegex,  
56 - 'numeric-interval': QuestionNumericInterval,  
57 - 'textarea': QuestionTextArea,  
58 - # -- informative panels --  
59 - 'information': QuestionInformation,  
60 - 'warning': QuestionInformation,  
61 - 'alert': QuestionInformation,  
62 - 'success': QuestionInformation,  
63 - }  
64 -  
65 - def __init__(self, question_dict={}):  
66 - self.question = question_dict  
67 -  
68 - # -----------------------------------------------------------------------  
69 - # Given a ref returns an instance of a descendent of Question(),  
70 - # i.e. a question object (radio, checkbox, ...).  
71 - # -----------------------------------------------------------------------  
72 - def generate(self):  
73 - logger.debug(f'Generating "{self.question["ref"]}"...')  
74 - # Shallow copy so that script generated questions will not replace  
75 - # the original generators  
76 - q = self.question.copy()  
77 -  
78 - # If question is of generator type, an external program will be run  
79 - # which will print a valid question in yaml format to stdout. This  
80 - # output is then yaml parsed into a dictionary `q`.  
81 - if q['type'] == 'generator':  
82 - logger.debug(f' \\_ Running "{q["script"]}".')  
83 - q.setdefault('args', [])  
84 - q.setdefault('stdin', '')  
85 - script = path.join(q['path'], q['script'])  
86 - out = run_script(script=script, args=q['args'], stdin=q['stdin'])  
87 - q.update(out)  
88 -  
89 - # Finally we create an instance of Question()  
90 - try:  
91 - qinstance = self._types[q['type']](q) # instance matching class  
92 - except QuestionException as e:  
93 - logger.error(e)  
94 - raise e  
95 - except KeyError:  
96 - logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')  
97 - raise  
98 - else:  
99 - return qinstance  
aprendizations/learnapp.py
@@ -15,7 +15,7 @@ import networkx as nx @@ -15,7 +15,7 @@ import networkx as nx
15 # this project 15 # this project
16 from aprendizations.models import Student, Answer, Topic, StudentTopic 16 from aprendizations.models import Student, Answer, Topic, StudentTopic
17 from aprendizations.knowledge import StudentKnowledge 17 from aprendizations.knowledge import StudentKnowledge
18 -from aprendizations.factory import QFactory 18 +from aprendizations.questions import QFactory
19 from aprendizations.tools import load_yaml 19 from aprendizations.tools import load_yaml
20 20
21 # setup logger for this module 21 # setup logger for this module
aprendizations/questions.py
@@ -30,7 +30,7 @@ class Question(dict): @@ -30,7 +30,7 @@ class Question(dict):
30 def __init__(self, q): 30 def __init__(self, q):
31 super().__init__(q) 31 super().__init__(q)
32 32
33 - # add these if missing 33 + # add required keys if missing
34 self.set_defaults({ 34 self.set_defaults({
35 'title': '', 35 'title': '',
36 'answer': None, 36 'answer': None,
@@ -368,3 +368,81 @@ class QuestionInformation(Question): @@ -368,3 +368,81 @@ class QuestionInformation(Question):
368 def correct(self): 368 def correct(self):
369 super().correct() 369 super().correct()
370 self['grade'] = 1.0 # always "correct" but points should be zero! 370 self['grade'] = 1.0 # always "correct" but points should be zero!
  371 +
  372 +
  373 +# ===========================================================================
  374 +# QFactory is a class that can generate question instances, e.g. by shuffling
  375 +# options, running a script to generate the question, etc.
  376 +#
  377 +# To generate an instance of a question we use the method generate() where
  378 +# the argument is the reference of the question we wish to produce.
  379 +# The generate() method returns a question instance of the correct class.
  380 +#
  381 +# Example:
  382 +#
  383 +# # generate a question instance from a dictionary
  384 +# qdict = {
  385 +# 'type': 'radio',
  386 +# 'text': 'Choose one',
  387 +# 'options': ['a', 'b']
  388 +# }
  389 +# qfactory = QFactory(qdict)
  390 +# question = qfactory.generate()
  391 +#
  392 +# # answer one question and correct it
  393 +# question['answer'] = 42 # set answer
  394 +# question.correct() # correct answer
  395 +# print(question['grade']) # print grade
  396 +# ===========================================================================
  397 +class QFactory(object):
  398 + # Depending on the type of question, a different question class will be
  399 + # instantiated. All these classes derive from the base class `Question`.
  400 + _types = {
  401 + 'radio': QuestionRadio,
  402 + 'checkbox': QuestionCheckbox,
  403 + 'text': QuestionText,
  404 + 'text-regex': QuestionTextRegex,
  405 + 'numeric-interval': QuestionNumericInterval,
  406 + 'textarea': QuestionTextArea,
  407 + # -- informative panels --
  408 + 'information': QuestionInformation,
  409 + 'warning': QuestionInformation,
  410 + 'alert': QuestionInformation,
  411 + 'success': QuestionInformation,
  412 + }
  413 +
  414 + def __init__(self, question_dict={}):
  415 + self.question = question_dict
  416 +
  417 + # -----------------------------------------------------------------------
  418 + # Given a ref returns an instance of a descendent of Question(),
  419 + # i.e. a question object (radio, checkbox, ...).
  420 + # -----------------------------------------------------------------------
  421 + def generate(self):
  422 + logger.debug(f'Generating "{self.question["ref"]}"...')
  423 + # Shallow copy so that script generated questions will not replace
  424 + # the original generators
  425 + q = self.question.copy()
  426 +
  427 + # If question is of generator type, an external program will be run
  428 + # which will print a valid question in yaml format to stdout. This
  429 + # output is then yaml parsed into a dictionary `q`.
  430 + if q['type'] == 'generator':
  431 + logger.debug(f' \\_ Running "{q["script"]}".')
  432 + q.setdefault('args', [])
  433 + q.setdefault('stdin', '')
  434 + script = path.join(q['path'], q['script'])
  435 + out = run_script(script=script, args=q['args'], stdin=q['stdin'])
  436 + q.update(out)
  437 +
  438 + # Finally we create an instance of Question()
  439 + try:
  440 + qinstance = self._types[q['type']](q) # instance matching class
  441 + except QuestionException as e:
  442 + logger.error(e)
  443 + raise e
  444 + except KeyError:
  445 + logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
  446 + raise
  447 + else:
  448 + return qinstance
aprendizations/serve.py
@@ -454,20 +454,21 @@ def main(): @@ -454,20 +454,21 @@ def main():
454 logging.info('====================== Start Logging ======================') 454 logging.info('====================== Start Logging ======================')
455 455
456 # --- start application 456 # --- start application
457 - logging.info('Starting App') 457 + logging.info('Starting App...')
458 try: 458 try:
459 - learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db, check=arg.check)  
460 - except Exception as e:  
461 - logging.critical('Failed to start application')  
462 - raise e 459 + learnapp = LearnApp(arg.conffile, prefix=arg.prefix, db=arg.db,
  460 + check=arg.check)
  461 + except Exception:
  462 + logging.critical('Failed to start application.')
  463 + sys.exit(1)
463 464
464 # --- create web application 465 # --- create web application
465 - logging.info('Starting Web App (tornado)') 466 + logging.info('Starting Web App (tornado)...')
466 try: 467 try:
467 webapp = WebApplication(learnapp, debug=arg.debug) 468 webapp = WebApplication(learnapp, debug=arg.debug)
468 - except Exception as e: 469 + except Exception:
469 logging.critical('Failed to start web application.') 470 logging.critical('Failed to start web application.')
470 - raise e 471 + sys.exit(1)
471 472
472 # --- get SSL certificates 473 # --- get SSL certificates
473 if 'XDG_DATA_HOME' in os.environ: 474 if 'XDG_DATA_HOME' in os.environ:
@@ -481,21 +482,21 @@ def main(): @@ -481,21 +482,21 @@ def main():
481 path.join(certs_dir, 'privkey.pem')) 482 path.join(certs_dir, 'privkey.pem'))
482 except FileNotFoundError: 483 except FileNotFoundError:
483 logging.critical(f'SSL certificates missing in {certs_dir}') 484 logging.critical(f'SSL certificates missing in {certs_dir}')
484 - sys.exit(-1) 485 + sys.exit(1)
485 486
486 # --- create webserver 487 # --- create webserver
487 try: 488 try:
488 httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx) 489 httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)
489 except ValueError: 490 except ValueError:
490 logging.critical('Certificates cert.pem and privkey.pem not found') 491 logging.critical('Certificates cert.pem and privkey.pem not found')
491 - sys.exit(-1) 492 + sys.exit(1)
492 493
493 httpserver.listen(arg.port) 494 httpserver.listen(arg.port)
494 logging.info(f'Listening on port {arg.port}.') 495 logging.info(f'Listening on port {arg.port}.')
495 496
496 # --- run webserver 497 # --- run webserver
497 - logging.info('Webserver running... (Ctrl-C to stop)')  
498 signal.signal(signal.SIGINT, signal_handler) 498 signal.signal(signal.SIGINT, signal_handler)
  499 + logging.info('Webserver running. (Ctrl-C to stop)')
499 500
500 try: 501 try:
501 tornado.ioloop.IOLoop.current().start() # running... 502 tornado.ioloop.IOLoop.current().start() # running...
aprendizations/templates/maintopics-table.html
@@ -33,7 +33,11 @@ @@ -33,7 +33,11 @@
33 </button> 33 </button>
34 34
35 <div class="collapse navbar-collapse" id="navbarText"> 35 <div class="collapse navbar-collapse" id="navbarText">
36 - <ul class="navbar-nav mr-auto"></ul> 36 + <ul class="navbar-nav mr-auto">
  37 + <li class="nav-item active">
  38 + <a class="nav-link" href="#">Tópicos<span class="sr-only">(current)</span></a>
  39 + </li>
  40 + </ul>
37 41
38 <ul class="navbar-nav"> 42 <ul class="navbar-nav">
39 <li class="nav-item dropdown"> 43 <li class="nav-item dropdown">
aprendizations/templates/topic.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html> 2 <html>
3 <head> 3 <head>
4 - <title>iLearn</title>  
5 - <link rel="icon" href="/static/favicon.ico"> 4 + <title>iLearn</title>
  5 + <link rel="icon" href="/static/favicon.ico">
6 6
7 - <meta charset="utf-8">  
8 - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">  
9 - <meta name="author" content="Miguel Barão"> 7 + <meta charset="utf-8">
  8 + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  9 + <meta name="author" content="Miguel Barão">
10 10
11 <!-- MathJax --> 11 <!-- MathJax -->
12 - <script type="text/x-mathjax-config">  
13 - MathJax.Hub.Config({  
14 - tex2jax: {  
15 - inlineMath: [["$$$","$$$"], ["$","$"], ["\\(","\\)"]]  
16 - }  
17 - });  
18 - </script>  
19 - <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script> 12 + <script type="text/x-mathjax-config">
  13 + MathJax.Hub.Config({
  14 + tex2jax: {
  15 + inlineMath: [["$$$","$$$"], ["$","$"], ["\\(","\\)"]]
  16 + }
  17 + });
  18 + </script>
  19 + <script type="text/javascript" src="/static/mathjax/MathJax.js?delayStartupUntil=onload&config=TeX-AMS_CHTML-full"></script>
20 20
21 <!-- Styles --> 21 <!-- Styles -->
22 - <link rel="stylesheet" href="/static/mdbootstrap/css/bootstrap.min.css">  
23 - <link rel="stylesheet" href="/static/mdbootstrap/css/mdb.min.css">  
24 - <link rel="stylesheet" href="/static/codemirror/lib/codemirror.css">  
25 - <link rel="stylesheet" href="/static/css/animate.min.css">  
26 - <link rel="stylesheet" href="/static/css/github.css">  
27 - <link rel="stylesheet" href="/static/css/topic.css"> 22 + <link rel="stylesheet" href="/static/mdbootstrap/css/bootstrap.min.css">
  23 + <link rel="stylesheet" href="/static/mdbootstrap/css/mdb.min.css">
  24 + <link rel="stylesheet" href="/static/codemirror/lib/codemirror.css">
  25 + <link rel="stylesheet" href="/static/css/animate.min.css">
  26 + <link rel="stylesheet" href="/static/css/github.css">
  27 + <link rel="stylesheet" href="/static/css/topic.css">
28 28
29 <!-- Scripts --> 29 <!-- Scripts -->
30 - <script defer src="/static/mdbootstrap/js/jquery-3.3.1.min.js"></script>  
31 - <script defer src="/static/mdbootstrap/js/popper.min.js"></script>  
32 - <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script>  
33 - <script defer src="/static/mdbootstrap/js/mdb.min.js"></script>  
34 - <script defer src="/static/fontawesome-free/js/all.min.js"></script>  
35 - <script defer src="/static/codemirror/lib/codemirror.js"></script>  
36 - <script defer src="/static/js/topic.js"></script> 30 + <script defer src="/static/mdbootstrap/js/jquery-3.3.1.min.js"></script>
  31 + <script defer src="/static/mdbootstrap/js/popper.min.js"></script>
  32 + <script defer src="/static/mdbootstrap/js/bootstrap.min.js"></script>
  33 + <script defer src="/static/mdbootstrap/js/mdb.min.js"></script>
  34 + <script defer src="/static/fontawesome-free/js/all.min.js"></script>
  35 + <script defer src="/static/codemirror/lib/codemirror.js"></script>
  36 + <script defer src="/static/js/topic.js"></script>
37 37
38 </head> 38 </head>
39 <!-- ===================================================================== --> 39 <!-- ===================================================================== -->
40 <body> 40 <body>
41 <!-- Navbar --> 41 <!-- Navbar -->
42 <nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-primary"> 42 <nav class="navbar navbar-expand-sm fixed-top navbar-dark bg-primary">
43 - <img src="/static/logo_horizontal.png" height="48" width="120" class="navbar-brand" alt="UEvora">  
44 - <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">  
45 - <span class="navbar-toggler-icon"></span>  
46 - </button>  
47 -  
48 - <div class="collapse navbar-collapse" id="navbarText">  
49 - <ul class="navbar-nav mr-auto"></ul>  
50 -  
51 - <ul class="navbar-nav">  
52 - <li class="nav-item dropdown">  
53 - <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">  
54 - <i class="fas fa-user" aria-hidden="true"></i>  
55 - <span id="name">{{ escape(name) }}</span>  
56 - <span class="caret"></span>  
57 - </a>  
58 - <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">  
59 - <a class="dropdown-item" href="/">Voltar aos tópicos</a>  
60 - </div>  
61 - </li>  
62 - </ul>  
63 - </div> 43 + <img src="/static/logo_horizontal.png" height="48" width="120" class="navbar-brand" alt="UEvora">
  44 + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
  45 + <span class="navbar-toggler-icon"></span>
  46 + </button>
  47 +
  48 + <div class="collapse navbar-collapse" id="navbarText">
  49 + <ul class="navbar-nav mr-auto">
  50 + <li class="nav-item">
  51 + <a class="nav-link" href="/">Tópicos</a>
  52 + </li>
  53 + </ul>
  54 +
  55 + <ul class="navbar-nav">
  56 + <li class="nav-item dropdown">
  57 + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
  58 + <i class="fas fa-user" aria-hidden="true"></i>
  59 + <span id="name">{{ escape(name) }}</span>
  60 + <span class="caret"></span>
  61 + </a>
  62 + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
  63 + <a class="dropdown-item" href="/logout">Sair</a>
  64 + </div>
  65 + </li>
  66 + </ul>
  67 + </div>
64 </nav> 68 </nav>
65 69
66 <!-- ===================================================================== --> 70 <!-- ===================================================================== -->
67 <div class="progress"> 71 <div class="progress">
68 - <div class="progress-bar bg-primary" id="topic_progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 1em;width: 0%"></div> 72 + <div class="progress-bar bg-primary" id="topic_progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 1em;width: 0%"></div>
69 </div> 73 </div>
70 74
71 <!-- ===================================================================== --> 75 <!-- ===================================================================== -->
72 <!-- main panel with questions --> 76 <!-- main panel with questions -->
73 <div class="container" id="container"> 77 <div class="container" id="container">
74 78
75 - <div id="notifications"></div>  
76 -  
77 - <div class="my-5" id="content">  
78 - <form action="/question" method="post" id="question_form" autocomplete="off">  
79 - {% module xsrf_form_html() %}  
80 - <div id="question_div"></div>  
81 - </form>  
82 -  
83 - <div id="comments"></div>  
84 - </div>  
85 -  
86 - <div id="wrong" style="display: none">  
87 - <div class="alert alert-danger">  
88 - <i class="fas fa-thumbs-down fa-3x"></i>  
89 - <br><br>  
90 - Desta vez não acertou, mas também se aprende com os erros...  
91 - <br>  
92 - <div id="show_solution_on_right">  
93 - <a href="#solution" id="link_solution_on_wrong">Ver solução</a>  
94 - </div>  
95 - </div>  
96 - </div>  
97 -  
98 - <div id="right" style="display: none">  
99 - <div class="alert alert-success">  
100 - <i class="fas fa-thumbs-up fa-3x"></i>  
101 - <br><br>  
102 - Excelente resposta!  
103 - <br>  
104 - <div id="show_solution_on_right">  
105 - <a href="#solution" id="link_solution_on_right">Ver solução</a>  
106 - </div>  
107 - </div>  
108 - </div>  
109 -  
110 - <div id="solution"></div>  
111 -  
112 - <!-- reponder / continuar -->  
113 - <a class="btn btn-primary btn-lg btn-block my-5" id="submit" data-toggle="tooltip" data-placement="right" href="#solution"></a>  
114 - <!-- title="Shift-Enter" --> 79 + <div id="notifications"></div>
  80 +
  81 + <div class="my-5" id="content">
  82 + <form action="/question" method="post" id="question_form" autocomplete="off">
  83 + {% module xsrf_form_html() %}
  84 + <div id="question_div"></div>
  85 + </form>
  86 +
  87 + <div id="comments"></div>
  88 + </div>
  89 +
  90 + <div id="wrong" style="display: none">
  91 + <div class="alert alert-danger">
  92 + <i class="fas fa-thumbs-down fa-3x"></i>
  93 + <br><br>
  94 + Desta vez não acertou, mas também se aprende com os erros...
  95 + <br>
  96 + <div id="show_solution_on_right">
  97 + <a href="#solution" id="link_solution_on_wrong">Ver solução</a>
  98 + </div>
  99 + </div>
  100 + </div>
  101 +
  102 + <div id="right" style="display: none">
  103 + <div class="alert alert-success">
  104 + <i class="fas fa-thumbs-up fa-3x"></i>
  105 + <br><br>
  106 + Excelente resposta!
  107 + <br>
  108 + <div id="show_solution_on_right">
  109 + <a href="#solution" id="link_solution_on_right">Ver solução</a>
  110 + </div>
  111 + </div>
  112 + </div>
  113 +
  114 + <div id="solution"></div>
  115 +
  116 + <!-- reponder / continuar -->
  117 + <a class="btn btn-primary btn-lg btn-block my-5" id="submit" data-toggle="tooltip" data-placement="right" href="#solution"></a>
  118 + <!-- title="Shift-Enter" -->
115 </div> 119 </div>
116 120
117 </body> 121 </body>