test.py
7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import os, sys, fnmatch
import random
import sqlite3
from datetime import datetime
try:
import yaml
except ImportError:
print('The package "yaml" is missing. See README.md for instructions.')
sys.exit(1)
try:
import json
except ImportError:
print('The package "json" is missing. See README.md for instructions.')
sys.exit(1)
# my code
import questions
import database
# ===========================================================================
def read_configuration(filename, debug=False, show_points=False, show_hints=False, practice=False, save_answers=False, show_ref=False):
# FIXME validar se ficheiros e directorios existem???
try:
f = open(filename, 'r', encoding='utf-8')
except IOError:
print('[ ERROR ] Cannot open YAML file "%s"' % filename)
sys.exit(1)
else:
with f:
try:
test = yaml.load(f)
except yaml.YAMLError as exc:
mark = exc.problem_mark
print('[ ERROR ] In YAML file "{0}" near line {1}, column {2}.'.format(filename,mark.line,mark.column+1))
sys.exit(1)
# -- test yaml was loaded ok
errors = 0
# defaults:
test['ref'] = str(test.get('ref', filename))
test['title'] = str(test.get('title', ''))
test['show_hints'] = bool(test.get('show_hints', show_hints))
test['show_points'] = bool(test.get('show_points', show_points))
test['practice'] = bool(test.get('practice', practice))
test['debug'] = bool(test.get('debug', debug))
test['show_ref'] = bool(test.get('show_ref', show_ref))
# this is the base directory where questions are stored
test['questions_dir'] = os.path.normpath(os.path.expanduser(str(test.get('questions_dir', os.path.curdir))))
if not os.path.exists(test['questions_dir']):
print('[ ERROR ] Questions directory "{0}" does not exist.\n Fix the "questions_dir" key in the configuration file "{1}".'.format(test['questions_dir'], filename))
errors += 1
# where to put the students answers (optional)
if 'answers_dir' not in test:
print('[ WARNG ] Missing "answers_dir" in the test configuration file "{0}".\n Tests are NOT being saved. Grades are still going into the database.'.format(filename))
test['save_answers'] = False
else:
test['answers_dir'] = os.path.normpath(os.path.expanduser(str(test['answers_dir'])))
if not os.path.isdir(test['answers_dir']):
print('[ ERROR ] Directory "{0}" does not exist.'.format(test['answers_dir']))
errors += 1
test['save_answers'] = True
# database with login credentials and grades
if 'database' not in test:
print('[ ERROR ] Missing "database" key in the test configuration "{0}".'.format(filename))
errors += 1
else:
test['database'] = os.path.normpath(os.path.expanduser(str(test['database'])))
if not os.path.exists(test['database']):
print('[ ERROR ] Database "{0}" not found.'.format(test['database']))
errors += 1
if errors > 0:
print('{0} error(s) found. Aborting!'.format(errors))
sys.exit(1)
# deal with questions files
if 'files' not in test:
# no files were defined = load all from questions_dir
test['files'] = fnmatch.filter(os.listdir(test['questions_dir']), '*.yaml')
print('[ WARNG ] All YAML files from directory were loaded. Might not be such a good idea...')
else:
# only one file
if isinstance(test['files'], str):
test['files'] = [test['files']]
# replace ref,points by actual questions from pool
pool = questions.QuestionsPool()
pool.add_from_files(files=test['files'], path=test['questions_dir'])
for i, q in enumerate(test['questions']):
# each question is a list of alternative versions, even if the list
# contains only one element
if isinstance(q, str):
# normalize question to a dict
# - some_ref
# becomes
# - ref: some_ref
# points: 1.0
test['questions'][i] = [pool[q]] # list with just one question
test['questions'][i][0]['points'] = 1.0
# Note: at this moment we do not know the questions types.
# Some questions, like information, should have default points
# set to 0. That must be done later when the question is
# instantiated.
elif isinstance(q, dict):
if 'ref' not in q:
print(' * Found a question without a "ref" key in the test "{}"'.format(filename))
print(' Dictionary contents:', q)
sys.exit(1)
if isinstance(q['ref'], str):
q['ref'] = [q['ref']] # ref is always a list
p = float(q.get('points', 1.0)) # default points is 1.0
# create list of alternatives, normalized
l = []
for r in q['ref']:
try:
qq = pool[r]
except KeyError:
print('[ WARNG ] Question reference "{0}" of test "{1}" not found. Skipping...'.format(r, test['ref']))
continue
qq['points'] = p
l.append(qq)
# add question (i.e. list of alternatives) to the test
test['questions'][i] = l
return test
# ===========================================================================
class Test(dict):
# -----------------------------------------------------------------------
def __init__(self, d):
super().__init__(d)
qlist = []
for i, qq in enumerate(self['questions']):
try:
q = random.choice(qq) # select from alternative versions
except IndexError:
print(qq) # FIXME
qlist.append(questions.create_question(q)) # create instance
self['questions'] = qlist
self['start_time'] = datetime.now()
# -----------------------------------------------------------------------
def update_answers(self, ans):
'''given a dictionary ans={'ref':'some answer'} updates the answers
of the test. FIXME: check if answer is to be corrected or not
'''
for q in self['questions']:
q['answer'] = ans[q['ref']] if q['ref'] in ans else None
# -----------------------------------------------------------------------
def correct(self):
'''Corrects all the answers and computes the final grade.'''
self['finish_time'] = datetime.now()
total_points = 0.0
final_grade = 0.0
for q in self['questions']:
final_grade += q.correct() * q['points']
total_points += q['points']
final_grade = 20.0 * max(final_grade / total_points, 0.0)
self['grade'] = final_grade
return final_grade
# -----------------------------------------------------------------------
def save_json(self, path):
filename = ' -- '.join((str(self['number']), self['ref'],
str(self['finish_time']))) + '.json'
filepath = os.path.abspath(os.path.join(path, filename))
with open(filepath, 'w') as f:
json.dump(self, f, indent=2, default=str)
# HACK default=str is required for datetime objects