From aec7789965f7720768e481b53bc8e6333d8d252c Mon Sep 17 00:00:00 2001
From: Miguel Barão .
-- teste nao esta a mostrar imagens de vez em quando.
- mensagems de erro do assembler aparecem na mesma linha na correcao e nao fazerm rendering do `$t`, ver se servidor faz parse do markdown dessas mensagens.
- impedir os eventos copy/paste. alunos usam isso para trazer codigo ja feito nos computadores. Obrigar a fazer reset? fazer um copy automaticamente?
- a revisao do teste não mostra as imagens.
- Test.reset_answers() unused.
-- incluir test_id na tabela questions (futuro semestre, pode quebrar compatibilidade).
-- na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo
+- teste nao esta a mostrar imagens de vez em quando.???
# TODO
+- na pagina admin, mostrar com cor vermelha as horas de entrada dos alunos que ja tenham excedido o tempo
+- retornar None quando nao ha alteracoes relativamente à última vez.
+ou usar push (websockets?)
- mudar ref do test para test_id (ref já é usado nas perguntas)
- servidor ntpd no x220 para configurar a data/hora dos portateis dell
- autorização dada, mas teste não disponível até que seja dada ordem para começar.
diff --git a/demo/demo.yaml b/demo/demo.yaml
index 7ca8ce9..7f74340 100644
--- a/demo/demo.yaml
+++ b/demo/demo.yaml
@@ -1,8 +1,10 @@
---
# ============================================================================
# Unique identifier of the test.
+# Valid names can only include letters, digits, dash and underscore,
+# e.g. asc1-test3
# Database queries can be done in the terminal with
-# sqlite3 students.db "select * from tests where ref='tutorial'"
+# sqlite3 students.db "select * from tests where ref='asc1-test3'"
ref: tutorial
# Database file that includes student credentials, tests and questions grades.
@@ -20,9 +22,9 @@ title: Teste de demonstração (tutorial)
# Duration in minutes.
# (0 or undefined means infinite time)
-duration: 2
+duration: 0
-# Automatic test submission after the timeout 'duration'?
+# Automatic test submission after the given 'duration' timeout
# (default: false)
autosubmit: true
@@ -72,7 +74,7 @@ questions:
- tut-warning
- [tut-alert1, tut-alert2]
- tut-generator
-
+ - tut-yamllint
# test:
# - ref1
diff --git a/demo/questions/generators/generate-question.py b/demo/questions/generators/generate-question.py
index 3161ce4..604889c 100755
--- a/demo/questions/generators/generate-question.py
+++ b/demo/questions/generators/generate-question.py
@@ -18,11 +18,10 @@ print(f"""---
type: text
title: Geradores de perguntas
text: |
- Existe a possibilidade da pergunta ser gerada por um programa externo. Este
- programa deve escrever no `stdout` uma pergunta em formato `yaml` como nos
+ Existe a possibilidade da pergunta ser gerada por um programa externo. O
+ programa deve escrever no `stdout` uma pergunta em formato `yaml` tal como os
exemplos anteriores. Pode também receber argumentos para parametrizar a
- geração da pergunta. Aqui está um exemplo de uma pergunta gerada por um
- script python:
+ pergunta. Aqui está um exemplo de uma pergunta gerada por um script python:
```python
#!/usr/bin/env python3
@@ -46,9 +45,7 @@ text: |
A solução é {{r}}.''')
```
- Este script deve ter permissões para poder ser executado no terminal. Dá
- jeito usar o comando `gen-somar.py 1 100 | yamllint -` para validar o `yaml`
- gerado.
+ Este script deve ter permissões para poder ser executado no terminal.
Para indicar que uma pergunta é gerada externamente, esta é declarada com
@@ -56,12 +53,12 @@ text: |
- type: generator
ref: gen-somar
script: gen-somar.py
- # opcional
+ # argumentos opcionais
args: [1, 100]
```
- Os argumentos `args` são opcionais e são passados para o programa como
- argumentos da linha de comando.
+ Opcionalmente, o programa pode receber uma lista de argumentos declarados em
+ `args`.
---
diff --git a/demo/questions/questions-tutorial.yaml b/demo/questions/questions-tutorial.yaml
index b033145..993d8b8 100644
--- a/demo/questions/questions-tutorial.yaml
+++ b/demo/questions/questions-tutorial.yaml
@@ -581,3 +581,25 @@
ref: tut-generator
script: generators/generate-question.py
args: [1, 100]
+
+# ----------------------------------------------------------------------------
+- type: information
+ ref: tut-yamllint
+ title: Sugestões para validar yaml
+ text: |
+ Como os testes e perguntas são ficheiros `yaml`, é conveniente validar se
+ estão correctamente definitos. Um *linter* recomendado é o `yamllint`. Pode
+ ser instalado com `pip install yamllint` e usado do seguinte modo:
+
+ ```sh
+ yamllint test.yaml
+ yamllint questions.yaml
+ ```
+
+ No caso de programas geradores de perguntas e programas de correcção de
+ respostas pode usar-se um *pipe*:
+
+ ```sh
+ generate-question | yamllint -
+ correct-answer | yamllint -
+ ```
diff --git a/perguntations/__init__.py b/perguntations/__init__.py
index a928eb1..ce1af60 100644
--- a/perguntations/__init__.py
+++ b/perguntations/__init__.py
@@ -32,7 +32,7 @@ proof of submission and for review.
'''
APP_NAME = 'perguntations'
-APP_VERSION = '2020.05.dev2'
+APP_VERSION = '2020.05.dev3'
APP_DESCRIPTION = __doc__
__author__ = 'Miguel Barão'
diff --git a/perguntations/app.py b/perguntations/app.py
index c4c229e..3ece150 100644
--- a/perguntations/app.py
+++ b/perguntations/app.py
@@ -276,11 +276,43 @@ class App():
uid, area, win_x, win_y, scr_x, scr_y)
# ------------------------------------------------------------------------
+ # --- GETTERS
+ # ------------------------------------------------------------------------
- # --- helpers (getters)
# def get_student_name(self, uid):
# return self.online[uid]['student']['name']
+ def get_questions_csv(self):
+ '''generates a CSV with the grades of the test'''
+ test_id = self.testfactory['ref']
+
+ with self.db_session() as sess:
+ grades = sess.query(Question.student_id, Question.starttime,
+ Question.ref, Question.grade)\
+ .filter(Question.test_id == test_id)\
+ .order_by(Question.student_id)\
+ .all()
+
+ cols = ['Aluno', 'Início'] + \
+ [r for question in self.testfactory['questions']
+ for r in question['ref']]
+
+ tests = {}
+ for q in grades:
+ student, qref, qgrade = q[:2], q[2], q[3]
+ tests.setdefault(student, {})[qref] = qgrade
+
+ rows = [{'Aluno': test[0], 'Início': test[1], **q}
+ for test, q in tests.items()]
+
+ csvstr = io.StringIO()
+ writer = csv.DictWriter(csvstr, fieldnames=cols, restval=None,
+ delimiter=';', quoting=csv.QUOTE_ALL)
+ writer.writeheader()
+ writer.writerows(rows)
+ return test_id, csvstr.getvalue()
+
+
def get_test_csv(self):
'''generates a CSV with the grades of the test'''
with self.db_session() as sess:
@@ -292,7 +324,7 @@ class App():
csvstr = io.StringIO()
writer = csv.writer(csvstr, delimiter=';', quoting=csv.QUOTE_ALL)
- writer.writerow(('Número', 'Nota', 'Início', 'Fim'))
+ writer.writerow(('Aluno', 'Nota', 'Início', 'Fim'))
writer.writerows(grades)
return self.testfactory['ref'], csvstr.getvalue()
@@ -357,7 +389,10 @@ class App():
# if q['ref'] == ref and key in q['files']:
# return path.abspath(path.join(q['path'], q['files'][key]))
- # --- helpers (change state)
+ # ------------------------------------------------------------------------
+ # --- SETTERS
+ # ------------------------------------------------------------------------
+
def allow_student(self, uid):
'''allow a single student to login'''
self.allowed.add(uid)
diff --git a/perguntations/models.py b/perguntations/models.py
index c02275a..7fc416a 100644
--- a/perguntations/models.py
+++ b/perguntations/models.py
@@ -1,5 +1,7 @@
'''
-Database tables
+SQLAlchemy ORM
+
+The classes below correspond to database tables
'''
@@ -26,10 +28,10 @@ class Student(Base):
questions = relationship('Question', back_populates='student')
def __repr__(self):
- return f'Student:\n\
- id: "{self.id}"\n\
- name: "{self.name}"\n\
- password: "{self.password}"'
+ return (f'Student:\n'
+ f' id: "{self.id}"\n'
+ f' name: "{self.name}"\n'
+ f' password: "{self.password}"\n')
# ----------------------------------------------------------------------------
@@ -38,7 +40,7 @@ class Test(Base):
__tablename__ = 'tests'
id = Column(Integer, primary_key=True) # auto_increment
ref = Column(String)
- title = Column(String) # FIXME depends on ref and should come from another table...
+ title = Column(String)
grade = Column(Float)
state = Column(String) # ACTIVE, FINISHED, QUIT, NULL
comment = Column(String)
@@ -52,17 +54,17 @@ class Test(Base):
questions = relationship('Question', back_populates='test')
def __repr__(self):
- return f'Test:\n\
- id: "{self.id}"\n\
- ref: "{self.ref}"\n\
- title: "{self.title}"\n\
- grade: "{self.grade}"\n\
- state: "{self.state}"\n\
- comment: "{self.comment}"\n\
- starttime: "{self.starttime}"\n\
- finishtime: "{self.finishtime}"\n\
- filename: "{self.filename}"\n\
- student_id: "{self.student_id}"\n'
+ return (f'Test:\n'
+ f' id: "{self.id}"\n'
+ f' ref: "{self.ref}"\n'
+ f' title: "{self.title}"\n'
+ f' grade: "{self.grade}"\n'
+ f' state: "{self.state}"\n'
+ f' comment: "{self.comment}"\n'
+ f' starttime: "{self.starttime}"\n'
+ f' finishtime: "{self.finishtime}"\n'
+ f' filename: "{self.filename}"\n'
+ f' student_id: "{self.student_id}"\n')
# ---------------------------------------------------------------------------
@@ -82,11 +84,11 @@ class Question(Base):
test = relationship('Test', back_populates='questions')
def __repr__(self):
- return f'Question:\n\
- id: "{self.id}"\n\
- ref: "{self.ref}"\n\
- grade: "{self.grade}"\n\
- starttime: "{self.starttime}"\n\
- finishtime: "{self.finishtime}"\n\
- student_id: "{self.student_id}"\n\
- test_id: "{self.test_id}"\n'
+ return (f'Question:\n'
+ f' id: "{self.id}"\n'
+ f' ref: "{self.ref}"\n'
+ f' grade: "{self.grade}"\n'
+ f' starttime: "{self.starttime}"\n'
+ f' finishtime: "{self.finishtime}"\n'
+ f' student_id: "{self.student_id}"\n' # FIXME normal form
+ f' test_id: "{self.test_id}"\n')
diff --git a/perguntations/serve.py b/perguntations/serve.py
index 2f2e703..c0dc987 100644
--- a/perguntations/serve.py
+++ b/perguntations/serve.py
@@ -63,7 +63,8 @@ class WebApplication(tornado.web.Application):
# ----------------------------------------------------------------------------
def admin_only(func):
'''
- Decorator used to restrict access to the administrator. For example:
+ Decorator used to restrict access to the administrator.
+ Example:
@admin_only()
def get(self): ...
@@ -91,7 +92,7 @@ class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
'''
- HTML is stateless, so a cookie is used to identify the user.
+ Since HTTP is stateless, a cookie is used to identify the user.
This function returns the cookie for the current user.
'''
cookie = self.get_secure_cookie('user')
@@ -191,6 +192,15 @@ class AdminWebservice(BaseHandler):
self.write(data)
await self.flush()
+ if cmd == 'questionscsv':
+ test_ref, data = self.testapp.get_questions_csv()
+ self.set_header('Content-Type', 'text/csv')
+ self.set_header('content-Disposition',
+ f'attachment; filename={test_ref}-detailed.csv')
+ self.write(data)
+ await self.flush()
+
+
# ----------------------------------------------------------------------------
class AdminHandler(BaseHandler):
'''Handle /admin'''
@@ -236,7 +246,7 @@ class AdminHandler(BaseHandler):
self.testapp.deny_student(value)
elif cmd == 'reset_password':
- await self.testapp.update_student_password(uid=value, pw='')
+ await self.testapp.update_student_password(uid=value, password='')
elif cmd == 'insert_student':
student = json.loads(value)
@@ -244,7 +254,7 @@ class AdminHandler(BaseHandler):
name=student['name'])
else:
- logging.error(f'Unknown command: "{cmd}"')
+ logging.error('Unknown command: "%s"', cmd)
# ----------------------------------------------------------------------------
@@ -337,11 +347,11 @@ class FileHandler(BaseHandler):
try:
file = open(filepath, 'rb')
except FileNotFoundError:
- logging.error(f'File not found: {filepath}')
+ logging.error('File not found: %s', filepath)
except PermissionError:
- logging.error(f'No permission: {filepath}')
+ logging.error('No permission: %s', filepath)
except OSError:
- logging.error(f'Error opening file: {filepath}')
+ logging.error('Error opening file: %s', filepath)
else:
data = file.read()
file.close()
@@ -466,21 +476,25 @@ class ReviewHandler(BaseHandler):
Opens JSON file with a given corrected test and renders it
'''
test_id = self.get_query_argument('test_id', None)
- logging.info(f'Review test {test_id}.')
+ logging.info('Review test %s.', test_id)
fname = self.testapp.get_json_filename_of_test(test_id)
if fname is None:
raise tornado.web.HTTPError(404) # Not Found
try:
- jsonfile = open(path.expanduser(fname))
- except OSError:
- logging.error(f'Cannot open "{fname}" for review.')
- else:
- with jsonfile:
+ with open(path.expanduser(fname)) as jsonfile:
test = json.load(jsonfile)
- self.render('review.html', t=test, md=md_to_html,
- templ=self._templates)
+ except OSError:
+ logging.error('Cannot open "%s" for review.', fname)
+ raise tornado.web.HTTPError(404) # Not Found
+ except json.JSONDecodeError as exc:
+ logging.error('JSON error in "%s": %s', fname, exc)
+ raise tornado.web.HTTPError(404) # Not Found
+
+ print(test['show_ref'])
+ self.render('review.html', t=test, md=md_to_html,
+ templ=self._templates)
# ----------------------------------------------------------------------------
@@ -517,10 +531,10 @@ def run_webserver(app, ssl_opt, port, debug):
try:
httpserver.listen(port)
except OSError:
- logging.critical(f'Cannot bind port {port}. Already in use?')
+ logging.critical('Cannot bind port %d. Already in use?', port)
sys.exit(1)
- logging.info(f'Webserver listening on {port}... (Ctrl-C to stop)')
+ logging.info('Webserver listening on %d... (Ctrl-C to stop)', port)
signal.signal(signal.SIGINT, signal_handler)
# --- run webserver
diff --git a/perguntations/templates/admin.html b/perguntations/templates/admin.html
index 1468366..fd09824 100644
--- a/perguntations/templates/admin.html
+++ b/perguntations/templates/admin.html
@@ -76,7 +76,8 @@
Base de dados:
--
- Obter CSV com as notas + Obter CSV das notas + Obter CSV detalhado
diff --git a/perguntations/templates/question-information.html b/perguntations/templates/question-information.html index bc24acb..8d059d6 100644 --- a/perguntations/templates/question-information.html +++ b/perguntations/templates/question-information.html @@ -19,8 +19,7 @@ {% if show_ref %}{{ q['path'] }}
{{ q['filename'] }}
{{ q['path'] }}/{{ q['filename'] }}
{{ q['ref'] }}
{% end %}
\ No newline at end of file
diff --git a/perguntations/templates/question.html b/perguntations/templates/question.html
index 3482416..acb9471 100644
--- a/perguntations/templates/question.html
+++ b/perguntations/templates/question.html
@@ -31,8 +31,7 @@
{% if show_ref %}
{% end %}
diff --git a/perguntations/templates/review-question-information.html b/perguntations/templates/review-question-information.html
index b35dbaf..a17a93c 100644
--- a/perguntations/templates/review-question-information.html
+++ b/perguntations/templates/review-question-information.html
@@ -16,4 +16,9 @@
{{ q['path'] }}/{{ q['filename'] }}
{{ q['ref'] }}
+ {% end %}
\ No newline at end of file
diff --git a/perguntations/templates/review-question.html b/perguntations/templates/review-question.html
index 13bcb05..db2d4ce 100644
--- a/perguntations/templates/review-question.html
+++ b/perguntations/templates/review-question.html
@@ -65,7 +65,9 @@
{% end %}
{% if t['show_ref'] %}
- {{q['ref']}}
+ {{ q['path'] }}/{{ q['filename'] }}
{{ q['ref'] }}
{% end %}
@@ -107,7 +109,9 @@
{% if t['show_ref'] %}
- {{ q['ref'] }}
+ {{ q['path'] }}/{{ q['filename'] }}
{{ q['ref'] }}
{% end %}
--
libgit2 0.21.2