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   -# 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 15 # this project
16 16 from aprendizations.models import Student, Answer, Topic, StudentTopic
17 17 from aprendizations.knowledge import StudentKnowledge
18   -from aprendizations.factory import QFactory
  18 +from aprendizations.questions import QFactory
19 19 from aprendizations.tools import load_yaml
20 20  
21 21 # setup logger for this module
... ...
aprendizations/questions.py
... ... @@ -30,7 +30,7 @@ class Question(dict):
30 30 def __init__(self, q):
31 31 super().__init__(q)
32 32  
33   - # add these if missing
  33 + # add required keys if missing
34 34 self.set_defaults({
35 35 'title': '',
36 36 'answer': None,
... ... @@ -368,3 +368,81 @@ class QuestionInformation(Question):
368 368 def correct(self):
369 369 super().correct()
370 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 454 logging.info('====================== Start Logging ======================')
455 455  
456 456 # --- start application
457   - logging.info('Starting App')
  457 + logging.info('Starting App...')
458 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 465 # --- create web application
465   - logging.info('Starting Web App (tornado)')
  466 + logging.info('Starting Web App (tornado)...')
466 467 try:
467 468 webapp = WebApplication(learnapp, debug=arg.debug)
468   - except Exception as e:
  469 + except Exception:
469 470 logging.critical('Failed to start web application.')
470   - raise e
  471 + sys.exit(1)
471 472  
472 473 # --- get SSL certificates
473 474 if 'XDG_DATA_HOME' in os.environ:
... ... @@ -481,21 +482,21 @@ def main():
481 482 path.join(certs_dir, 'privkey.pem'))
482 483 except FileNotFoundError:
483 484 logging.critical(f'SSL certificates missing in {certs_dir}')
484   - sys.exit(-1)
  485 + sys.exit(1)
485 486  
486 487 # --- create webserver
487 488 try:
488 489 httpserver = tornado.httpserver.HTTPServer(webapp, ssl_options=ssl_ctx)
489 490 except ValueError:
490 491 logging.critical('Certificates cert.pem and privkey.pem not found')
491   - sys.exit(-1)
  492 + sys.exit(1)
492 493  
493 494 httpserver.listen(arg.port)
494 495 logging.info(f'Listening on port {arg.port}.')
495 496  
496 497 # --- run webserver
497   - logging.info('Webserver running... (Ctrl-C to stop)')
498 498 signal.signal(signal.SIGINT, signal_handler)
  499 + logging.info('Webserver running. (Ctrl-C to stop)')
499 500  
500 501 try:
501 502 tornado.ioloop.IOLoop.current().start() # running...
... ...
aprendizations/templates/maintopics-table.html
... ... @@ -33,7 +33,11 @@
33 33 </button>
34 34  
35 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 42 <ul class="navbar-nav">
39 43 <li class="nav-item dropdown">
... ...
aprendizations/templates/topic.html
1 1 <!DOCTYPE html>
2 2 <html>
3 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 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 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 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 38 </head>
39 39 <!-- ===================================================================== -->
40 40 <body>
41 41 <!-- Navbar -->
42 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 68 </nav>
65 69  
66 70 <!-- ===================================================================== -->
67 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 73 </div>
70 74  
71 75 <!-- ===================================================================== -->
72 76 <!-- main panel with questions -->
73 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 119 </div>
116 120  
117 121 </body>
... ...