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 @@ |
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 | 4 | # The program runs a web server where students login to answer a sequence of questions. |
5 | 5 | # Their grades are automatically calculated and stored in a sqlite3 database. |
6 | 6 | |
7 | - | |
8 | 7 | import cherrypy |
9 | 8 | from mako.lookup import TemplateLookup |
10 | 9 | import sqlite3 |
... | ... | @@ -12,20 +11,11 @@ import yaml |
12 | 11 | import argparse |
13 | 12 | from datetime import datetime |
14 | 13 | import os.path |
15 | -from myauth import AuthController, require | |
16 | 14 | |
15 | +# my code | |
16 | +from myauth import AuthController, require | |
17 | 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 | 21 | # Classes that respond to HTTP |
... | ... | @@ -33,10 +23,11 @@ def allgrades(db, uid): |
33 | 23 | class Root(object): |
34 | 24 | |
35 | 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 | 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 | 32 | # --- DEFAULT ------------------------------------------------------------ |
42 | 33 | # any path, e.g. /xpto/aargh is redirected to the test |
... | ... | @@ -45,40 +36,27 @@ class Root(object): |
45 | 36 | def default(self, *args): |
46 | 37 | raise cherrypy.HTTPRedirect('/test') |
47 | 38 | |
39 | + # --- STUDENTS ----------------------------------------------------------- | |
48 | 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 | 61 | # --- TEST --------------------------------------------------------------- |
84 | 62 | @cherrypy.expose |
... | ... | @@ -91,12 +69,14 @@ class Root(object): |
91 | 69 | name = cherrypy.session.get('name') |
92 | 70 | t = cherrypy.session.get('test', None) |
93 | 71 | if t is None: |
72 | + # create instance and add the name and number of the student | |
94 | 73 | cherrypy.session['test'] = t = test.Test(self.testconf) |
95 | 74 | t['number'] = uid |
96 | 75 | t['name'] = name |
76 | + self.loggedin.add(uid) # track logged in students # FIXME should be in the auth module... | |
97 | 77 | |
98 | 78 | # Generate question |
99 | - template = mylookup.get_template('test.html') | |
79 | + template = self.templates.get_template('test.html') | |
100 | 80 | return template.render(t=t, questions=t['questions']) |
101 | 81 | |
102 | 82 | # --- CORRECT ------------------------------------------------------------ |
... | ... | @@ -107,34 +87,36 @@ class Root(object): |
107 | 87 | the student, corrects and saves the answers, computes the final grade |
108 | 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 | 91 | t = cherrypy.session.get('test') |
112 | 92 | if t is None: |
113 | 93 | raise cherrypy.HTTPRedirect('/test') |
114 | 94 | |
115 | 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 | 97 | ans = {} |
118 | 98 | for q in t['questions']: |
119 | 99 | if 'answered-' + q['ref'] in kwargs: |
120 | 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 | 104 | t.update_answers(ans) |
124 | 105 | t.correct() |
125 | 106 | if t['save_answers']: |
126 | 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 | 110 | # ---- Expire session ---- |
111 | + self.loggedin.discard(t['number']) | |
131 | 112 | cherrypy.lib.sessions.expire() # session coockie expires client side |
132 | 113 | cherrypy.session['userid'] = cherrypy.request.login = None |
133 | 114 | cherrypy.log.error('Student %s terminated with grade = %.2f points.' % |
134 | 115 | (t['number'], t['grade']), 'APPLICATION') |
135 | 116 | |
136 | 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 | 120 | return template.render(t=t, allgrades=grades) |
139 | 121 | |
140 | 122 | # ============================================================================ |
... | ... | @@ -153,8 +135,8 @@ if __name__ == '__main__': |
153 | 135 | print('=' * 79) |
154 | 136 | print('- Title: %s' % testconf['title']) |
155 | 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 | 140 | print('-' * 79) |
159 | 141 | print('- Starting server...') |
160 | 142 | ... | ... |
templates/results.html
... | ... | @@ -4,18 +4,13 @@ |
4 | 4 | <meta charset="UTF-8"> |
5 | 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
6 | 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
7 | - <title> Resultados </title> | |
7 | + <title> ${t['title']} </title> | |
8 | 8 | <link rel="icon" href="favicon.ico"> |
9 | 9 | |
10 | 10 | <!-- Bootstrap --> |
11 | 11 | <link rel="stylesheet" href="/css/bootstrap.min.css"> |
12 | 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 | 14 | <script src="/js/jquery.min.js"></script> |
20 | 15 | <script src="/js/bootstrap.min.js"></script> |
21 | 16 | |
... | ... | @@ -35,40 +30,10 @@ |
35 | 30 | <!-- ===================================================================== --> |
36 | 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 | 33 | <div class="container"> |
69 | 34 | <div class="panel panel-default drop-shadow"> |
70 | 35 | <div class="panel-heading"> |
71 | - Testes realizados | |
36 | + ${t['title']} | |
72 | 37 | </div> |
73 | 38 | <!-- <div class="panel-body"> --> |
74 | 39 | <table class="table"> | ... | ... |
... | ... | @@ -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> | ... | ... |