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> | ... | ... |