Commit 9b8add7ea0422d9f069e60fbe8c8a2f14cf8f699
1 parent
21ab708a
Exists in
master
and in
1 other branch
- new database.py makes the interface with sqlite, no more sqlite in
serve.py.
Showing
4 changed files
with
140 additions
and
94 deletions
Show diff stats
@@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
1 | + | ||
2 | +import sqlite3 | ||
3 | + | ||
4 | +class Database(object): | ||
5 | + def __init__(self, db): | ||
6 | + self.db = db # sqlite3 filename | ||
7 | + | ||
8 | + # get results from previous tests of a student | ||
9 | + def student_grades(self, uid): | ||
10 | + with sqlite3.connect(self.db) as c: | ||
11 | + grades = c.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid]) | ||
12 | + return grades.fetchall() | ||
13 | + | ||
14 | + # return students results from a given test | ||
15 | + def test_grades(self, test_id): | ||
16 | + with sqlite3.connect(self.db) as c: | ||
17 | + cmd = 'SELECT student_id,name,grade FROM students INNER JOIN tests ON students.number=tests.student_id WHERE test_id==? ORDER BY grade DESC;' | ||
18 | + results = c.execute(cmd, [test_id]) | ||
19 | + return results.fetchall() | ||
20 | + | ||
21 | + # get list of students in the database | ||
22 | + def students(self): | ||
23 | + with sqlite3.connect(self.db) as c: | ||
24 | + students = c.execute('SELECT number,name,password FROM students ORDER BY number ASC;') | ||
25 | + return students.fetchall() | ||
26 | + |
serve.py
@@ -4,7 +4,6 @@ | @@ -4,7 +4,6 @@ | ||
4 | # The program runs a web server where students login to answer a sequence of questions. | 4 | # The program runs a web server where students login to answer a sequence of questions. |
5 | # Their grades are automatically calculated and stored in a sqlite3 database. | 5 | # Their grades are automatically calculated and stored in a sqlite3 database. |
6 | 6 | ||
7 | - | ||
8 | import cherrypy | 7 | import cherrypy |
9 | from mako.lookup import TemplateLookup | 8 | from mako.lookup import TemplateLookup |
10 | import sqlite3 | 9 | import sqlite3 |
@@ -12,20 +11,11 @@ import yaml | @@ -12,20 +11,11 @@ import yaml | ||
12 | import argparse | 11 | import argparse |
13 | from datetime import datetime | 12 | from datetime import datetime |
14 | import os.path | 13 | import os.path |
15 | -from myauth import AuthController, require | ||
16 | 14 | ||
15 | +# my code | ||
16 | +from myauth import AuthController, require | ||
17 | import test | 17 | import test |
18 | - | ||
19 | -mylookup = TemplateLookup(directories=['templates'], input_encoding='utf-8') | ||
20 | - | ||
21 | -# get results from previous tests | ||
22 | -def allgrades(db, uid): | ||
23 | - conn = sqlite3.connect(db) | ||
24 | - grades = conn.execute('SELECT test_id,grade,finish_time FROM tests WHERE student_id==?', [uid]).fetchall() | ||
25 | - conn.commit() | ||
26 | - conn.close() | ||
27 | - return grades | ||
28 | - | 18 | +import database |
29 | 19 | ||
30 | # ============================================================================ | 20 | # ============================================================================ |
31 | # Classes that respond to HTTP | 21 | # Classes that respond to HTTP |
@@ -33,10 +23,11 @@ def allgrades(db, uid): | @@ -33,10 +23,11 @@ def allgrades(db, uid): | ||
33 | class Root(object): | 23 | class Root(object): |
34 | 24 | ||
35 | def __init__(self, testconf): | 25 | def __init__(self, testconf): |
36 | - '''The values defined here are common to all students. | ||
37 | - Data is stored in the student's respective sessions.''' | ||
38 | - self.testconf = testconf | 26 | + self.testconf = testconf # base test dict (not instance) |
27 | + self.database = database.Database(testconf['database']) | ||
39 | self.auth = AuthController(database=testconf['database']) | 28 | self.auth = AuthController(database=testconf['database']) |
29 | + self.templates = TemplateLookup(directories=['templates'], input_encoding='utf-8') | ||
30 | + self.loggedin = set() # students currently logged in #FIXME should be in database instead?? | ||
40 | 31 | ||
41 | # --- DEFAULT ------------------------------------------------------------ | 32 | # --- DEFAULT ------------------------------------------------------------ |
42 | # any path, e.g. /xpto/aargh is redirected to the test | 33 | # any path, e.g. /xpto/aargh is redirected to the test |
@@ -45,40 +36,27 @@ class Root(object): | @@ -45,40 +36,27 @@ class Root(object): | ||
45 | def default(self, *args): | 36 | def default(self, *args): |
46 | raise cherrypy.HTTPRedirect('/test') | 37 | raise cherrypy.HTTPRedirect('/test') |
47 | 38 | ||
39 | + # --- STUDENTS ----------------------------------------------------------- | ||
48 | @cherrypy.expose | 40 | @cherrypy.expose |
49 | - def results(self): | ||
50 | - '''Show list of students''' | ||
51 | - conn = sqlite3.connect(self.testconf['database']) | ||
52 | - | ||
53 | - cmd = 'SELECT student_id,name,grade FROM students INNER JOIN tests ON students.number=tests.student_id WHERE test_id==? ORDER BY grade DESC;' | ||
54 | - results = conn.execute(cmd, [self.testconf['ref']]).fetchall() | ||
55 | - conn.commit() | ||
56 | - conn.close() | ||
57 | - | ||
58 | - print(str(results)) | ||
59 | - template = mylookup.get_template('results.html') | ||
60 | - return template.render(results=results) | ||
61 | - | ||
62 | - | ||
63 | - # --- SEQUENCE------------------------------------------------------------ | ||
64 | - # @cherrypy.expose | ||
65 | - # @require() | ||
66 | - # def question(self): | ||
67 | - # uid = cherrypy.session.get('userid') | ||
68 | - # name = cherrypy.session.get('name') | ||
69 | - # t = cherrypy.session.get('test', None) | ||
70 | - # if t is None: | ||
71 | - # cherrypy.session['test'] = t = test.Test(self.testconf) | ||
72 | - # t['number'] = uid | ||
73 | - # t['name'] = name | ||
74 | - # t['current_question'] = 0 | ||
75 | - # else: | ||
76 | - # t['current_question'] += 1 | ||
77 | - | ||
78 | - # # Generate question | ||
79 | - # template = mylookup.get_template('question.html') | ||
80 | - # return template.render(t=t) | 41 | + def students(self, reset_pw=None): |
42 | + if cherrypy.session.get('userid') != '0': | ||
43 | + raise cherrypy.HTTPRedirect('/') | ||
44 | + | ||
45 | + print(reset_pw) | ||
46 | + if reset_pw is not None: | ||
47 | + self.database.reset_pw(reset_pw) | ||
48 | + | ||
49 | + students = self.database.students() | ||
50 | + # print(students) | ||
51 | + template = self.templates.get_template('students.html') | ||
52 | + return template.render(students=students, loggedin=self.loggedin) | ||
81 | 53 | ||
54 | + # --- RESULTS ------------------------------------------------------------ | ||
55 | + @cherrypy.expose | ||
56 | + def results(self): | ||
57 | + results = self.database.test_grades(self.testconf['ref']) | ||
58 | + template = self.templates.get_template('results.html') | ||
59 | + return template.render(t=self.testconf, results=results) | ||
82 | 60 | ||
83 | # --- TEST --------------------------------------------------------------- | 61 | # --- TEST --------------------------------------------------------------- |
84 | @cherrypy.expose | 62 | @cherrypy.expose |
@@ -91,12 +69,14 @@ class Root(object): | @@ -91,12 +69,14 @@ class Root(object): | ||
91 | name = cherrypy.session.get('name') | 69 | name = cherrypy.session.get('name') |
92 | t = cherrypy.session.get('test', None) | 70 | t = cherrypy.session.get('test', None) |
93 | if t is None: | 71 | if t is None: |
72 | + # create instance and add the name and number of the student | ||
94 | cherrypy.session['test'] = t = test.Test(self.testconf) | 73 | cherrypy.session['test'] = t = test.Test(self.testconf) |
95 | t['number'] = uid | 74 | t['number'] = uid |
96 | t['name'] = name | 75 | t['name'] = name |
76 | + self.loggedin.add(uid) # track logged in students # FIXME should be in the auth module... | ||
97 | 77 | ||
98 | # Generate question | 78 | # Generate question |
99 | - template = mylookup.get_template('test.html') | 79 | + template = self.templates.get_template('test.html') |
100 | return template.render(t=t, questions=t['questions']) | 80 | return template.render(t=t, questions=t['questions']) |
101 | 81 | ||
102 | # --- CORRECT ------------------------------------------------------------ | 82 | # --- CORRECT ------------------------------------------------------------ |
@@ -107,34 +87,36 @@ class Root(object): | @@ -107,34 +87,36 @@ class Root(object): | ||
107 | the student, corrects and saves the answers, computes the final grade | 87 | the student, corrects and saves the answers, computes the final grade |
108 | of the test and stores in the database.''' | 88 | of the test and stores in the database.''' |
109 | 89 | ||
110 | - # if there is no test, redirect to the test | 90 | + # shouldn't be here if there is no test |
111 | t = cherrypy.session.get('test') | 91 | t = cherrypy.session.get('test') |
112 | if t is None: | 92 | if t is None: |
113 | raise cherrypy.HTTPRedirect('/test') | 93 | raise cherrypy.HTTPRedirect('/test') |
114 | 94 | ||
115 | # each question that is marked to be classified must have an answer. | 95 | # each question that is marked to be classified must have an answer. |
116 | - # ans contains the answers to be corrected | 96 | + # variable `ans` contains the answers to be corrected |
117 | ans = {} | 97 | ans = {} |
118 | for q in t['questions']: | 98 | for q in t['questions']: |
119 | if 'answered-' + q['ref'] in kwargs: | 99 | if 'answered-' + q['ref'] in kwargs: |
120 | ans[q['ref']] = kwargs.get(q['ref'], None) | 100 | ans[q['ref']] = kwargs.get(q['ref'], None) |
121 | 101 | ||
122 | - # store the answers provided by the student and correct the test | 102 | + # store the answers in the Test, correct it, save JSON and |
103 | + # store results in the database | ||
123 | t.update_answers(ans) | 104 | t.update_answers(ans) |
124 | t.correct() | 105 | t.correct() |
125 | if t['save_answers']: | 106 | if t['save_answers']: |
126 | t.save_json(self.testconf['answers_dir']) | 107 | t.save_json(self.testconf['answers_dir']) |
127 | - t.save_database(self.testconf['database']) | ||
128 | - grades = allgrades(self.testconf['database'], t['number']) | 108 | + t.save_database(self.testconf['database']) # FIXME save_database should be in database.py |
129 | 109 | ||
130 | # ---- Expire session ---- | 110 | # ---- Expire session ---- |
111 | + self.loggedin.discard(t['number']) | ||
131 | cherrypy.lib.sessions.expire() # session coockie expires client side | 112 | cherrypy.lib.sessions.expire() # session coockie expires client side |
132 | cherrypy.session['userid'] = cherrypy.request.login = None | 113 | cherrypy.session['userid'] = cherrypy.request.login = None |
133 | cherrypy.log.error('Student %s terminated with grade = %.2f points.' % | 114 | cherrypy.log.error('Student %s terminated with grade = %.2f points.' % |
134 | (t['number'], t['grade']), 'APPLICATION') | 115 | (t['number'], t['grade']), 'APPLICATION') |
135 | 116 | ||
136 | # ---- Show result to student ---- | 117 | # ---- Show result to student ---- |
137 | - template = mylookup.get_template('grade.html') | 118 | + grades = self.database.student_grades(t['number']) |
119 | + template = self.templates.get_template('grade.html') | ||
138 | return template.render(t=t, allgrades=grades) | 120 | return template.render(t=t, allgrades=grades) |
139 | 121 | ||
140 | # ============================================================================ | 122 | # ============================================================================ |
@@ -153,8 +135,8 @@ if __name__ == '__main__': | @@ -153,8 +135,8 @@ if __name__ == '__main__': | ||
153 | print('=' * 79) | 135 | print('=' * 79) |
154 | print('- Title: %s' % testconf['title']) | 136 | print('- Title: %s' % testconf['title']) |
155 | print('- StudentsDB: %s' % testconf['database']) | 137 | print('- StudentsDB: %s' % testconf['database']) |
156 | - print('- Questions: ', ', '.join(testconf['files'])) | ||
157 | - print(' (%i questions read)' % len(testconf['questions'])) | 138 | + print('- Questions:\n ', '\n '.join(testconf['files'])) |
139 | + print(' (%i questions read)' % len(testconf['questions'])) | ||
158 | print('-' * 79) | 140 | print('-' * 79) |
159 | print('- Starting server...') | 141 | print('- Starting server...') |
160 | 142 |
templates/results.html
@@ -4,18 +4,13 @@ | @@ -4,18 +4,13 @@ | ||
4 | <meta charset="UTF-8"> | 4 | <meta charset="UTF-8"> |
5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
7 | - <title> Resultados </title> | 7 | + <title> ${t['title']} </title> |
8 | <link rel="icon" href="favicon.ico"> | 8 | <link rel="icon" href="favicon.ico"> |
9 | 9 | ||
10 | <!-- Bootstrap --> | 10 | <!-- Bootstrap --> |
11 | <link rel="stylesheet" href="/css/bootstrap.min.css"> | 11 | <link rel="stylesheet" href="/css/bootstrap.min.css"> |
12 | <link rel="stylesheet" href="/css/bootstrap-theme.min.css"> <!-- optional --> | 12 | <link rel="stylesheet" href="/css/bootstrap-theme.min.css"> <!-- optional --> |
13 | 13 | ||
14 | -<!-- <link rel="stylesheet" href="/js/jquery/jquery.mobile-1.4.5.min.css" /> | ||
15 | - <script src="/js/jquery/jquery-2.1.1.min.js"></script> | ||
16 | - <script src="/js/jquery/jquery.mobile-1.4.5.min.js"></script> | ||
17 | - --> | ||
18 | - | ||
19 | <script src="/js/jquery.min.js"></script> | 14 | <script src="/js/jquery.min.js"></script> |
20 | <script src="/js/bootstrap.min.js"></script> | 15 | <script src="/js/bootstrap.min.js"></script> |
21 | 16 | ||
@@ -35,40 +30,10 @@ | @@ -35,40 +30,10 @@ | ||
35 | <!-- ===================================================================== --> | 30 | <!-- ===================================================================== --> |
36 | <body> | 31 | <body> |
37 | 32 | ||
38 | -<nav class="navbar navbar-default navbar-fixed-top" role="navigation"> | ||
39 | - <div class="container-fluid drop-shadow"> | ||
40 | - <div class="navbar-header"> | ||
41 | - <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar"> | ||
42 | - <span class="icon-bar"></span> | ||
43 | - <span class="icon-bar"></span> | ||
44 | - <span class="icon-bar"></span> | ||
45 | - </button> | ||
46 | - <a class="navbar-brand" href="#">UÉvora</a> | ||
47 | - </div> | ||
48 | - <div class="collapse navbar-collapse" id="myNavbar"> | ||
49 | -<!-- <ul class="nav navbar-nav"> | ||
50 | - <li><a href="#">Teoria</a></li> | ||
51 | - <li><a href="#">Exemplos</a></li> | ||
52 | - <li><a href="#">Exercícios</a></li> | ||
53 | - </ul> | ||
54 | - --> | ||
55 | - <ul class="nav navbar-nav navbar-right"> | ||
56 | - <li class="dropdown"> | ||
57 | - <a class="dropdown-toggle" data-toggle="dropdown" href="#">${t['number']} - ${t['name']} <span class="caret"></span></a> | ||
58 | -<!-- <ul class="dropdown-menu"> | ||
59 | - <li><a href="#">Toggle colors (day/night)</a></li> | ||
60 | - <li><a href="#">Change password</a></li> | ||
61 | - </ul> | ||
62 | - --> </li> | ||
63 | - </ul> | ||
64 | - </div> | ||
65 | - </div> | ||
66 | -</nav> | ||
67 | - | ||
68 | <div class="container"> | 33 | <div class="container"> |
69 | <div class="panel panel-default drop-shadow"> | 34 | <div class="panel panel-default drop-shadow"> |
70 | <div class="panel-heading"> | 35 | <div class="panel-heading"> |
71 | - Testes realizados | 36 | + ${t['title']} |
72 | </div> | 37 | </div> |
73 | <!-- <div class="panel-body"> --> | 38 | <!-- <div class="panel-body"> --> |
74 | <table class="table"> | 39 | <table class="table"> |
@@ -0,0 +1,73 @@ | @@ -0,0 +1,73 @@ | ||
1 | +<!DOCTYPE html> | ||
2 | +<html> | ||
3 | +<head> | ||
4 | + <meta charset="UTF-8"> | ||
5 | + <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
6 | + <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
7 | + <title> List of students </title> | ||
8 | + <link rel="icon" href="favicon.ico"> | ||
9 | + | ||
10 | + <!-- Bootstrap --> | ||
11 | + <link rel="stylesheet" href="/css/bootstrap.min.css"> | ||
12 | + <link rel="stylesheet" href="/css/bootstrap-theme.min.css"> <!-- optional --> | ||
13 | + | ||
14 | + <script src="/js/jquery.min.js"></script> | ||
15 | + <script src="/js/bootstrap.min.js"></script> | ||
16 | + | ||
17 | + <style> | ||
18 | + /* Fixes navigation panel overlaying content */ | ||
19 | + body { | ||
20 | + padding-top: 80px; | ||
21 | + background: #aaa; | ||
22 | + } | ||
23 | + .drop-shadow { | ||
24 | + -webkit-box-shadow: 0 0 5px 2px rgba(0, 0, 0, .5); | ||
25 | + box-shadow: 0px 2px 10px 3px rgba(0, 0, 0, .2); | ||
26 | + border-radius:5px; | ||
27 | + } | ||
28 | + </style> | ||
29 | + </head> | ||
30 | + <!-- ===================================================================== --> | ||
31 | +<body> | ||
32 | + | ||
33 | +<div class="container"> | ||
34 | + <div class="panel panel-default drop-shadow"> | ||
35 | + <div class="panel-heading"> | ||
36 | + List of students | ||
37 | + </div> | ||
38 | + <!-- <div class="panel-body"> --> | ||
39 | + <table class="table"> | ||
40 | + <thead> | ||
41 | + <tr> | ||
42 | + <th>Número</th> | ||
43 | + <th>Nome</th> | ||
44 | + <th>Password</th> | ||
45 | + </tr> | ||
46 | + </thead> | ||
47 | + <tbody> | ||
48 | + % for r in students: | ||
49 | + % if r[0] in loggedin: | ||
50 | + <tr class="success"> | ||
51 | + % else: | ||
52 | + <tr> | ||
53 | + % endif | ||
54 | + <td>${r[0]}</td> <!-- numero --> | ||
55 | + <td>${r[1]}</td> <!-- nome --> | ||
56 | + <td class="col-sm-6"> | ||
57 | + <form action="/students" method="post"> | ||
58 | + <div class="input-group"> | ||
59 | + <span class="input-group-btn"> | ||
60 | + <button class="btn btn-danger" type="submit">reset</button> | ||
61 | + </span> | ||
62 | + <input type="text" class="form-control" placeholder="${r[2]}" name="number"> | ||
63 | + </div><!-- /input-group --> | ||
64 | + </form> | ||
65 | + </td> <!-- password --> | ||
66 | + </tr> | ||
67 | + % endfor | ||
68 | + </tbody> | ||
69 | + </table> | ||
70 | + </div> <!-- panel --> | ||
71 | +</div> <!-- container --> | ||
72 | +</body> | ||
73 | +</html> |