class SyncPanel: """There may be only one instance of this class, because of the slot declarations. """ def __init__(self, settings): self.settings = settings # connect slots slot("sp_newpw", self.slot_newpw) slot("sp_sync", self.slot_sync) slot("sp_browse", self.slot_fileBrowser) def init(self, gui, filepath, forceDialog=False): self.gui = gui self.filepath = filepath # Get the path to the user database file self.dbs = None dbDir = None dbPath = self.settings.getSetting("dbFile") while True: if not self.filepath: if dbPath: dbd = os.path.dirname(dbPath) if os.path.isdir(dbd): dbDir = dbd if os.path.isfile(dbPath): self.filepath = dbPath dbPath = None if forceDialog or not self.filepath: self.filepath = getFile(_("User Database File"), startDir=dbDir, defaultSuffix=u".zga", filter=(_("Database Files"), (u"*.zga",))) if not self.filepath: return self.dbs = DBs(self.filepath) if self.dbs.isOpen(): break dbDir = os.path.dirname(self.filepath) self.filepath = None self.settings.setSetting("dbFile", self.filepath) # Set window title self.gui.setTitle(_("Synchronize %s") % self.filepath) self.dbm = None # Get the default host name from the 'base' data self.dbhost = self.dbs.baseDict[u"masterHost"] # Get information from the 'config' table self.dbname = self.dbs.getConfig(u"dbname") teacher = self.dbs.getConfig(u"me") if not teacher: error(_("'%s' is not a teacher's database file") % self.filepath) self.dbuser = teacher2user(teacher) # Close user database file self.closeFile() # set gui lineEdits self.gui.setDBinfo(self.dbhost, self.dbname, self.dbuser, self.filepath) def slot_fileBrowser(self, arg): self.init(self.gui, None, True) def slot_newpw(self, arg): if not self.connect(): return pw = getPassword() if pw: try: self.dbm.setPassword(self.dbuser, pw) message(_("Password changed")) except: message(_("Couldn't change password")) self.disconnect() def slot_sync(self, arg): if self.connect(): if (self.dbm.readValue(u"config", u"finalized") == u""): self.dlg = Output() synchronize(self.dbm, self.filepath, self.dlg) self.dlg.done() else: warning(_("This database is finalized, you can't access it")) # Disconnect from master database self.disconnect() def closeFile(self): """Close the user database in self.dbs """ if self.dbs: self.dbs.close() self.dbs = None def connect(self): """Connect to master db. """ host = self.gui.getDBhost() pw = getPw(host, self.dbname, self.dbuser) if (pw == None): return False cData = { u"host" : host, u"db" : self.dbname, u"user" : self.dbuser, u"pw" : pw } db = DBm(cData) if not db.isOpen(): warning(_("Couldn't open master database")) return False self.dbm = db return True def disconnect(self): """Disconnect from master db. """ if self.dbm: self.dbm.close() self.dbm = None
class ControlPanel: """There may be only one instance of this class, because of the slot declarations. """ def __init__(self, settings): self.master = None # The (reports) master database self.settings = settings # Coniguration persistence facility # The following item remembers the printer instance # started from the control panel, so that it can be started # multiple times. self.printHandler = None self.configEd = None slot("cp_newdb", self.slot_newdb) slot("cp_updatedb", self.slot_updatedb) slot("cp_dbdel", self.slot_deletedb) slot("cp_newdbIndex", self.slot_newdbIndex) slot("cp_dump", self.slot_dump) slot("cp_genTdb", self.slot_genTeacherDb) slot("cp_restore", self.slot_restore) slot("cp_finalize", self.slot_finalize) slot("cp_selTeachers", self.slot_selectTeachers) slot("cp_finalize", self.slot_finalize) slot("cp_pwd", self.slot_pwReset) slot("cp_print", self.slot_print) slot("cp_sync", self.slot_sync) slot("cp_restoreDataFiles", self.slot_restoreConfigFile) slot("ced_done", self.slot_reEnable) def init(self, gui, db): self.gui = gui self.db = db # The control database self.gui.setDBhost(self.db.dbhost) self.initDBlist() def initDBlist(self): rows = self.db.read(u"""SELECT id, name FROM databases ORDER BY id DESC""") self.dbList = [item[1] for item in rows] self.gui.setDBlist(self.dbList) def addUser(self, login): """Add a new 'normal' user, with limited rights. """ if self.db.userExists(login): if confirmationDialog( _("User name problem"), argSub(_("User '%1' already exists. Try to recreate?"), (login, )), True): if not self.removeUser(login): raise try: self.db.createRole(login, USERROLE) except: #print_exc() message(_("Database Problem: Couldn't create user '%1'"), (login, )) raise def connect(self, dbname): cData = {} cData[u"host"] = self.db.dbhost cData[u"db"] = dbname cData[u"user"] = self.db.dbuser cData[u"pw"] = self.db.dbpasswd return DBm(cData) def getConfigData(self, newdb): """Prepare a configuration data source. Initially the configuration editor is started to ensure that the selected file is valid. When this is quitted, the resulting file can be used, so long as it was error-free. newdb is True when a new database is to be created, otherwise the current database is to be updated. """ self.newdb = newdb if not self.configEd: self.configEd = GuiConfigEd("cfged") self.configEd.init() self.configEd.run() # Now wait until the editor has finished. # Actually, I would need the editor to be modal. # Maybe an alternative would be to disable the control panel until # a done signal is received: self.gui.setEnabled(False) def slot_reEnable(self, arg): """Handle updating and creation of database from a configuration file. Here the configuration editor has finished. """ self.gui.setEnabled(True) if not self.configEd.getSourcePath(): message(_("No data source")) return errors = self.configEd.getErrorCount() if (errors > 0): message(_("%d files containing errors found") % errors) return source = CfgZip(self.configEd.getSourcePath()) if not source.isOpen(): warning( _("The supplied configuration file (%s) could not" " be opened. Actually this shouldn't be possible!") % self.configEd.getSourcePath()) return if self.newdb: self.createNewDb(source) else: self.updateDbConfig(source) source.close() def slot_newdb(self, arg): """Create a new reports database from a layout/config file. """ self.getConfigData(True) def createNewDb(self, source): """Create a new database using the configuration file supplied as a CfgZip object in source. """ dbname = source.cfgName state = 0 try: self.db.send(u"""CREATE DATABASE %s OWNER %s ENCODING 'UTF8'""" % (dbname, ADMIN)) state = 1 # Add to 'databases' table self.db.send(u"INSERT INTO databases VALUES (?, ?, ?, ?)", (self.db.getTime(), dbname, u'', u'')) state = 2 newmaster = self.connect(dbname) state = 3 guimessage = argSub( _("New database '%1' created, now read in the data"), (dbname, )) mm = MakeMaster(source, newmaster) guiReport(_("Create New Database"), mm, guimessage) #message(_("New database now set up")) self.usersPrivileges(newmaster) # Ensure connection is closed mm = None newmaster.close() newmaster = None except: # print_exc() message(_("Couldn't create new database (%1)"), (dbname, )) if (state >= 3): newmaster.close() if (state >= 2): self.db.send(u"DELETE FROM databases WHERE name = ?", (dbname, )) if (state >= 1): self.db.send(u"DROP DATABASE %s" % dbname) # adjust display, select new db self.initDBlist() def slot_updatedb(self, arg): """Update the current reports database from a layout/config file. """ self.getConfigData(False) def updateDbConfig(self, source): """Update the current database using the configuration file supplied as a CfgZip object in source. The selected config file must match the name of the current database. Before updating from this file, dump the current database state to a folder 'dumps' in the same folder as the config file. That is in case something goes wrong and the old state must be recovered. """ if (self.dbname != source.cfgName): message(_("Database name does not match data folder")) return # Backup existing database state. sPath = self.configEd.getSourcePath() budir = os.path.join(os.path.dirname(sPath), 'dumps') if not os.path.isdir(budir): os.mkdir(budir) backup = Dump(self.master, budir) filepath = backup.filepath if not filepath: return guimessage = argSub( _("New backup file '%1' created, now read in the data"), (filepath, )) guiReport(_("Create Backup File"), backup, guimessage) backup = None if not filepath: return try: guimessage = argSub(_("Updating database '%1' from %2"), (self.dbname, sPath)) mm = MakeMaster(source, self.master) guiReport(_("Updating Master Database"), mm, guimessage) mm = None except: print_exc() message(_("Update failed, trying to restore from '%1'"), (filepath, )) restore = Restore(filepath) dbname = restore.getDbName() if not dbname: message(_("Couldn't open database file '%1'"), (filepath, )) return # Delete all tables for t in self.master.getTables(): self.master.send(u"DROP TABLE %s" % t) # Restore old state guimessage = argSub( _("Database '%1' cleared, now restore the data"), (dbname, )) restore.setMaster(self.master) guiReport(_("Restore Database"), restore, guimessage) self.usersPrivileges(self.master) # adjust display, select new db self.initDBlist() def usersPrivileges(self, ndb): """Create new users if necessary (all the teachers) and grant the necessary privileges on the tables of this database. But if the database is finalized, revoke teachers' update privileges. Also remove users that are no longer active. """ # Get database name from configuration data dbname = ndb.readValue(u"config", u"dbname") # Get list of users from the report table names ulist = ndb.getTeacherTables() # SELECT privileges for all on all for table in ("config", "data"): ndb.send(u"GRANT SELECT ON %s TO %s" % (table, USERROLE)) # # Comments can also be inserted # ndb.send(u"GRANT SELECT, INSERT ON comments TO %s" % USERROLE) # Allow access to 'interface' table ndb.send(u"GRANT SELECT, UPDATE ON interface TO %s" % USERROLE) # Get a set of users before the change. set0 = self.activeUsers() fin = ndb.readValue(u"config", u"finalized") # Update the control database entry users = u"" for u in ulist: users += u + u" " self.db.send( u"""UPDATE databases SET finalized= ?, users = ? WHERE name = ?""", (fin, users, dbname)) # Get a set of users after the change. set1 = self.activeUsers() for u in (set0 - set1): # Before removing a user, its privileges must be revoked try: ndb.send(u'REVOKE UPDATE ON %s FROM "%s"' % (u, u)) except: pass self.removeUser(u) # new users newusers = set1 - set0 # If active: # Grant UPDATE privileges on the report tables to the owning # teacher. Everyone else has SELECT only. # If finalized: # Revoke update privileges for u in ulist: if (fin != u""): try: ndb.send(u'REVOKE UPDATE ON %s FROM "%s"' % (u, u)) except: pass else: if u in newusers: # Teacher not already in users list self.addUser(u) ndb.send(u"GRANT SELECT ON %s TO %s" % (u, USERROLE)) ndb.send(u'GRANT UPDATE ON %s TO "%s"' % (u, u)) def activeUsers(self): """Return a set of active users, according to the teacher lists of non-finalized databases. """ uset = set() # Union it with all user lists from non-finalized databases for ul in self.db.read(u"""SELECT users FROM databases WHERE finalized = ''"""): uset = uset.union(ul[0].split()) return uset def slot_deletedb(self, arg): """This is a dangerous one! It will completely delete a database. """ if not confirmationDialog( _("Delete Database?"), argSub(_("Do you really want to delete database '%1'?"), (self.dbname, )), False): return if not self.master: return self.deletedb(self.dbname) # adjust display, select new db self.initDBlist() def deletedb(self, name): if self.master: self.master.close() self.master = None try: self.db.send(u"DROP DATABASE %s" % name) except: print_exc() message(_("Couldn't delete database '%1'.\n Try again ..."), (name, )) return # Get a set of users before the change. set0 = self.activeUsers() self.db.send(u"DELETE FROM databases WHERE name = ?", (name, )) # Get a set of users after the change. set1 = self.activeUsers() for t in (set0 - set1): self.removeUser(t) def slot_newdbIndex(self, index): """The current master database has changed. Disconnect from the old one and connect to the new one. """ if (index < 0): self.gui.setUserList([]) return self.dbname = self.dbList[index] if self.master: self.master.close() self.master = self.connect(self.dbname) # Set 'finalized' state fin = self.master.readValue(u"config", u"finalized").strip() self.showFinalized(fin != u"") # Set 'self.users' to an ordered list of teacher (tag, name) # pairs and set the teacher comboBox teachers = [ path.split(u"/")[1] for path in self.master.listIds(u"data") if path.startswith(u"teachers/") ] teachers.sort() usrstrings = [] self.users = [] for t in teachers: n = sini2dict(self.master.getFile(u"teachers/" + t))[u"Name"] self.users.append((t, n)) usrstrings.append(u"%s (%s)" % (t, n)) self.gui.setUserList(usrstrings) self.slot_selectTeachers(None) def slot_pwReset(self, arg): """Reset all selected users' passwords to their initial state. """ if self.finalized: message(_("Invalid operation on finalized database.")) return users = u"" for u in self.gui.getSelectedUsers(self.getUsers()): self.db.setPassword(u) users += u + u" " message(_("Passwords reset to '%1' for users:\n %2"), (DEFAULTPASSWORD, users)) def slot_selectTeachers(self, arg): """Check all teachers in the comboBox. """ if self.master: for i in range(len(self.users)): self.gui.userListSetChecked(i, True) def slot_dump(self, arg): """Generate a backup file (sqlite database). """ # Get destination directory: dir0 = self.settings.getSetting("destDir") dbpath = getDirectory(_("Destination Folder"), dir0) if not dbpath: return self.settings.setSetting("destDir", dbpath) # Do the dump backup = Dump(self.master, dbpath) if not backup.filepath: return guimessage = argSub( _("New backup file '%1' opened, now write in the data"), (backup.filepath, )) guiReport(_("Create Backup File"), backup, guimessage) def getUsers(self): """Return a list of users (just the login names). """ return [u[0] for u in self.users] def getDbDir(self, settingskey): """Get the directory for storing user database files. Return None if cancelled. """ dir0 = self.settings.getSetting(settingskey) dbpath = getDirectory(_("Database Folder"), dir0) if not dbpath: return None self.settings.setSetting(settingskey, dbpath) return dbpath def getBDbPath(self): """Select a database backup file. Return None if cancelled. """ dir0 = self.settings.getSetting("destDir") dbpath = getFile(_("Source File"), dir0, filter=(_("Backup Files"), (u"*.zgb", ))) if dbpath: self.settings.setSetting("destDir", os.path.dirname(dbpath)) return dbpath def dump(self, dbpath, user, isList=False): if isList and user: backup = DumpUsers(user, self.master, dbpath) guimessage = _("Creating multiple user database files ...\n") else: backup = Dump(self.master, dbpath, user) if not backup.filepath: return None guimessage = argSub( _("Database file '%1' created, now" " read in the data"), (backup.filepath, )) if user: title = _("Create User Database File") else: title = _("Create Full Database Dump File") guiReport(title, backup, guimessage) return backup.filepath def slot_genTeacherDb(self, arg): """Generate a teacher's database file for all selected teachers. """ # Get list of users for whom a database file is to be generated users = self.gui.getSelectedUsers(self.getUsers()) if not users: return # Get destination directory: dbpath = self.getDbDir("teacherDbDir") if not dbpath: return # Dump the database files self.dump(dbpath, users, True) def slot_restore(self, arg): """Restore a dumped database. It can be either an existing one, or one which has been deleted. """ # Get source file: dbpath = self.getBDbPath() if not dbpath: return None restore = Restore(dbpath) dbname = restore.getDbName() if not dbname: message(_("Couldn't open database file '%1'"), (dbpath, )) return state = 0 try: if dbname in self.dbList: if not confirmationDialog( _("Replace Database?"), argSub( _("Are you sure you want to replace database '%1'?" ), (dbname, )), False): restore.close() return self.deletedb(dbname) self.db.send(u"""CREATE DATABASE %s OWNER %s ENCODING 'UTF8'""" % (dbname, ADMIN)) state = 1 # Add to 'databases' table self.db.send(u"INSERT INTO databases VALUES (?, ?, ?, ?)", (self.db.getTime(), dbname, u'', u'')) state = 2 newmaster = self.connect(dbname) state = 3 guimessage = argSub( _("New database '%1' created, now read in the data"), (dbname, )) restore.setMaster(newmaster) guiReport(_("Restore Database"), restore, guimessage) #message(_("New database now set up")) self.usersPrivileges(newmaster) # Ensure connection is closed restore = None newmaster.close() newmaster = None except: print_exc() message(_("Couldn't create new database (%1)"), (dbname, )) if (state >= 3): newmaster.close() if (state >= 2): self.db.send(u"DELETE FROM databases WHERE name = ?", (dbname, )) if (state >= 1): self.db.send(u"DROP DATABASE %s" % dbname) # adjust display, select new db self.initDBlist() def slot_finalize(self, on): """'Finalize' the database. Set the 'finalized' item in the 'config' table and revoke update privelege from teachers. """ if (not self.master) or (on == self.finalized): return if on: if not confirmationDialog( _("Finalize Database?"), argSub( _("Finalizing stops teachers' access to the database.\n" "It may also clear their passwords.\n Continue?"), (self.dbname, )), False): return val = u"1" else: val = u"" self.master.send( u"""UPDATE config SET value= ? WHERE id = 'finalized'""", (val, )) self.showFinalized(on) self.usersPrivileges(self.master) def showFinalized(self, fin): self.finalized = fin self.gui.setFinalized(self.finalized) def slot_print(self, arg): """Pass a database dump file (*.zgb) to the print applicataion. """ dir0 = self.settings.getSetting("destDir") filepath = None if dir0: # See if there is already an adequately new dump file rex = re.compile(r"%s_(\d{8}_\d{6}).zgb$" % self.master.getName()) dumpfiles = [f for f in os.listdir(dir0) if rex.match(f)] if dumpfiles: dumpfiles.sort() latest = dumpfiles[-1] dumptime = rex.match(latest).group(1) udt = self.master.readValue(u"config", u"updatetime") lst = self.master.readValue(u"interface", u"lastsynctime") if (dumptime > udt) and (dumptime > lst): filepath = os.path.join(dir0, latest) if not filepath: dbpath = self.getDbDir("destDir") if not dbpath: return # Dump a full database filepath = self.dump(dbpath, u"") if not filepath: return # Start the printer dialog with this file. if self.printHandler: self.printHandler.init(filepath) else: self.printHandler = GuiPrint("print", filepath) self.printHandler.run() def slot_sync(self, arg): """Perform a synchronization with a selected database file but as adminstrative user. This allows even a finalized database to be updated. """ sfile = self.settings.getSetting("syncFile") if sfile: sdir = os.path.dirname(sfile) sfile = os.path.basename(sfile) else: sdir = None syncfile = getFile(_("User database file"), startDir=sdir, startFile=sfile, defaultSuffix=".zga", filter=(_("Report Files"), (u"*.zga", ))) if not syncfile: return self.settings.setSetting("syncFile", syncfile) dbs = DBs(syncfile) if not dbs.isOpen(): return sdbname = dbs.getConfig(u"dbname") dbs.close() if (self.dbname != sdbname): warning( _("%s: Database name does not match current master name") % syncfile) return self.dlg = Output() synchronize(self.master, syncfile, self.dlg) self.dlg.done() def removeUser(self, user): """Remove a user. Return True if succeeded. """ try: self.db.dropRole(user) except: message(_("Couldn't remove user '%1'"), (user, )) return False return True def slot_restoreConfigFile(self, arg): if self.master: self.restoreConfigFile(self.master) def restoreConfigFile(self, db): """Get a parent directory for the creation of a data file. The file to be created may not exist already. Returns the file-name if successful, otherwise 'None'. """ # put up directory dialog, starting one up from dir0 dir = getDirectory(_("Parent Folder")) if not dir: return None datapath = os.path.join(dir, self.dbname + '.zip') if os.path.exists(datapath): message(_("'%1' already exists"), (datapath, )) return None dest = CfgZip(datapath, True) # open for writing if not dest.isOpen(): message(_("Couldn't open '%s' for writing") % datapath) return None for path, data in db.getAllData(): dest.addFile(path, data) dest.close() message(_("Configuration saved to '%s'") % datapath) return datapath
class ControlPanel: """There may be only one instance of this class, because of the slot declarations. """ def __init__(self, settings): self.master = None # The (reports) master database self.settings = settings # Coniguration persistence facility # The following item remembers the printer instance # started from the control panel, so that it can be started # multiple times. self.printHandler = None self.configEd = None slot("cp_newdb", self.slot_newdb) slot("cp_updatedb", self.slot_updatedb) slot("cp_dbdel", self.slot_deletedb) slot("cp_newdbIndex", self.slot_newdbIndex) slot("cp_dump", self.slot_dump) slot("cp_genTdb", self.slot_genTeacherDb) slot("cp_restore", self.slot_restore) slot("cp_finalize", self.slot_finalize) slot("cp_selTeachers", self.slot_selectTeachers) slot("cp_finalize", self.slot_finalize) slot("cp_pwd", self.slot_pwReset) slot("cp_print", self.slot_print) slot("cp_sync", self.slot_sync) slot("cp_restoreDataFiles", self.slot_restoreConfigFile) slot("ced_done", self.slot_reEnable) def init(self, gui, db): self.gui = gui self.db = db # The control database self.gui.setDBhost(self.db.dbhost) self.initDBlist() def initDBlist(self): rows = self.db.read(u"""SELECT id, name FROM databases ORDER BY id DESC""") self.dbList = [item[1] for item in rows] self.gui.setDBlist(self.dbList) def addUser(self, login): """Add a new 'normal' user, with limited rights. """ if self.db.userExists(login): if confirmationDialog(_("User name problem"), argSub(_("User '%1' already exists. Try to recreate?"), (login,)), True): if not self.removeUser(login): raise try: self.db.createRole(login, USERROLE) except: #print_exc() message(_("Database Problem: Couldn't create user '%1'"), (login,)) raise def connect(self, dbname): cData = {} cData[u"host"] = self.db.dbhost cData[u"db"] = dbname cData[u"user"] = self.db.dbuser cData[u"pw"] = self.db.dbpasswd return DBm(cData) def getConfigData(self, newdb): """Prepare a configuration data source. Initially the configuration editor is started to ensure that the selected file is valid. When this is quitted, the resulting file can be used, so long as it was error-free. newdb is True when a new database is to be created, otherwise the current database is to be updated. """ self.newdb = newdb if not self.configEd: self.configEd = GuiConfigEd("cfged") self.configEd.init() self.configEd.run() # Now wait until the editor has finished. # Actually, I would need the editor to be modal. # Maybe an alternative would be to disable the control panel until # a done signal is received: self.gui.setEnabled(False) def slot_reEnable(self, arg): """Handle updating and creation of database from a configuration file. Here the configuration editor has finished. """ self.gui.setEnabled(True) if not self.configEd.getSourcePath(): message(_("No data source")) return errors = self.configEd.getErrorCount() if ( errors > 0): message(_("%d files containing errors found") % errors) return source = CfgZip(self.configEd.getSourcePath()) if not source.isOpen(): warning(_("The supplied configuration file (%s) could not" " be opened. Actually this shouldn't be possible!") % self.configEd.getSourcePath()) return if self.newdb: self.createNewDb(source) else: self.updateDbConfig(source) source.close() def slot_newdb(self, arg): """Create a new reports database from a layout/config file. """ self.getConfigData(True) def createNewDb(self, source): """Create a new database using the configuration file supplied as a CfgZip object in source. """ dbname = source.cfgName state = 0 try: self.db.send(u"""CREATE DATABASE %s OWNER %s ENCODING 'UTF8'""" % (dbname, ADMIN)) state = 1 # Add to 'databases' table self.db.send(u"INSERT INTO databases VALUES (?, ?, ?, ?)", (self.db.getTime(), dbname, u'', u'')) state = 2 newmaster = self.connect(dbname) state = 3 guimessage = argSub(_("New database '%1' created, now read in the data"), (dbname,)) mm = MakeMaster(source, newmaster) guiReport(_("Create New Database"), mm, guimessage) #message(_("New database now set up")) self.usersPrivileges(newmaster) # Ensure connection is closed mm = None newmaster.close() newmaster = None except: # print_exc() message(_("Couldn't create new database (%1)"), (dbname,)) if (state >= 3): newmaster.close() if (state >= 2): self.db.send(u"DELETE FROM databases WHERE name = ?", (dbname,)) if (state >= 1): self.db.send(u"DROP DATABASE %s" % dbname) # adjust display, select new db self.initDBlist() def slot_updatedb(self, arg): """Update the current reports database from a layout/config file. """ self.getConfigData(False) def updateDbConfig(self, source): """Update the current database using the configuration file supplied as a CfgZip object in source. The selected config file must match the name of the current database. Before updating from this file, dump the current database state to a folder 'dumps' in the same folder as the config file. That is in case something goes wrong and the old state must be recovered. """ if (self.dbname != source.cfgName): message(_("Database name does not match data folder")) return # Backup existing database state. sPath = self.configEd.getSourcePath() budir = os.path.join(os.path.dirname(sPath), 'dumps') if not os.path.isdir(budir): os.mkdir(budir) backup = Dump(self.master, budir) filepath = backup.filepath if not filepath: return guimessage = argSub(_("New backup file '%1' created, now read in the data"), (filepath,)) guiReport(_("Create Backup File"), backup, guimessage) backup = None if not filepath: return try: guimessage = argSub(_("Updating database '%1' from %2"), (self.dbname, sPath)) mm = MakeMaster(source, self.master) guiReport(_("Updating Master Database"), mm, guimessage) mm = None except: print_exc() message(_("Update failed, trying to restore from '%1'"), (filepath,)) restore = Restore(filepath) dbname = restore.getDbName() if not dbname: message(_("Couldn't open database file '%1'"), (filepath,)) return # Delete all tables for t in self.master.getTables(): self.master.send(u"DROP TABLE %s" % t) # Restore old state guimessage = argSub(_("Database '%1' cleared, now restore the data"), (dbname,)) restore.setMaster(self.master) guiReport(_("Restore Database"), restore, guimessage) self.usersPrivileges(self.master) # adjust display, select new db self.initDBlist() def usersPrivileges(self, ndb): """Create new users if necessary (all the teachers) and grant the necessary privileges on the tables of this database. But if the database is finalized, revoke teachers' update privileges. Also remove users that are no longer active. """ # Get database name from configuration data dbname = ndb.readValue(u"config", u"dbname") # Get list of users from the report table names ulist = ndb.getTeacherTables() # SELECT privileges for all on all for table in ("config", "data"): ndb.send(u"GRANT SELECT ON %s TO %s" % (table, USERROLE)) # # Comments can also be inserted # ndb.send(u"GRANT SELECT, INSERT ON comments TO %s" % USERROLE) # Allow access to 'interface' table ndb.send(u"GRANT SELECT, UPDATE ON interface TO %s" % USERROLE) # Get a set of users before the change. set0 = self.activeUsers() fin = ndb.readValue(u"config", u"finalized") # Update the control database entry users = u"" for u in ulist: users += u + u" " self.db.send(u"""UPDATE databases SET finalized= ?, users = ? WHERE name = ?""", (fin, users, dbname)) # Get a set of users after the change. set1 = self.activeUsers() for u in (set0-set1): # Before removing a user, its privileges must be revoked try: ndb.send(u'REVOKE UPDATE ON %s FROM "%s"' % (u, u)) except: pass self.removeUser(u) # new users newusers = set1-set0 # If active: # Grant UPDATE privileges on the report tables to the owning # teacher. Everyone else has SELECT only. # If finalized: # Revoke update privileges for u in ulist: if (fin != u""): try: ndb.send(u'REVOKE UPDATE ON %s FROM "%s"' % (u, u)) except: pass else: if u in newusers: # Teacher not already in users list self.addUser(u) ndb.send(u"GRANT SELECT ON %s TO %s" % (u, USERROLE)) ndb.send(u'GRANT UPDATE ON %s TO "%s"' % (u, u)) def activeUsers(self): """Return a set of active users, according to the teacher lists of non-finalized databases. """ uset = set() # Union it with all user lists from non-finalized databases for ul in self.db.read(u"""SELECT users FROM databases WHERE finalized = ''"""): uset = uset.union(ul[0].split()) return uset def slot_deletedb(self, arg): """This is a dangerous one! It will completely delete a database. """ if not confirmationDialog(_("Delete Database?"), argSub(_("Do you really want to delete database '%1'?"), (self.dbname,)), False): return if not self.master: return self.deletedb(self.dbname) # adjust display, select new db self.initDBlist() def deletedb(self, name): if self.master: self.master.close() self.master = None try: self.db.send(u"DROP DATABASE %s" % name) except: print_exc() message(_("Couldn't delete database '%1'.\n Try again ..."), (name,)) return # Get a set of users before the change. set0 = self.activeUsers() self.db.send(u"DELETE FROM databases WHERE name = ?", (name,)) # Get a set of users after the change. set1 = self.activeUsers() for t in (set0-set1): self.removeUser(t) def slot_newdbIndex(self, index): """The current master database has changed. Disconnect from the old one and connect to the new one. """ if (index < 0): self.gui.setUserList([]) return self.dbname = self.dbList[index] if self.master: self.master.close() self.master = self.connect(self.dbname) # Set 'finalized' state fin = self.master.readValue(u"config", u"finalized").strip() self.showFinalized(fin != u"") # Set 'self.users' to an ordered list of teacher (tag, name) # pairs and set the teacher comboBox teachers = [path.split(u"/")[1] for path in self.master.listIds(u"data") if path.startswith(u"teachers/")] teachers.sort() usrstrings = [] self.users = [] for t in teachers: n = sini2dict(self.master.getFile(u"teachers/" + t))[u"Name"] self.users.append((t, n)) usrstrings.append(u"%s (%s)" % (t, n)) self.gui.setUserList(usrstrings) self.slot_selectTeachers(None) def slot_pwReset(self, arg): """Reset all selected users' passwords to their initial state. """ if self.finalized: message(_("Invalid operation on finalized database.")) return users = u"" for u in self.gui.getSelectedUsers(self.getUsers()): self.db.setPassword(u) users += u + u" " message(_("Passwords reset to '%1' for users:\n %2"), (DEFAULTPASSWORD, users)) def slot_selectTeachers(self, arg): """Check all teachers in the comboBox. """ if self.master: for i in range(len(self.users)): self.gui.userListSetChecked(i, True) def slot_dump(self, arg): """Generate a backup file (sqlite database). """ # Get destination directory: dir0 = self.settings.getSetting("destDir") dbpath = getDirectory(_("Destination Folder"), dir0) if not dbpath: return self.settings.setSetting("destDir", dbpath) # Do the dump backup = Dump(self.master, dbpath) if not backup.filepath: return guimessage = argSub(_("New backup file '%1' opened, now write in the data"), (backup.filepath,)) guiReport(_("Create Backup File"), backup, guimessage) def getUsers(self): """Return a list of users (just the login names). """ return [u[0] for u in self.users] def getDbDir(self, settingskey): """Get the directory for storing user database files. Return None if cancelled. """ dir0 = self.settings.getSetting(settingskey) dbpath = getDirectory(_("Database Folder"), dir0) if not dbpath: return None self.settings.setSetting(settingskey, dbpath) return dbpath def getBDbPath(self): """Select a database backup file. Return None if cancelled. """ dir0 = self.settings.getSetting("destDir") dbpath = getFile(_("Source File"), dir0, filter=(_("Backup Files"), (u"*.zgb",))) if dbpath: self.settings.setSetting("destDir", os.path.dirname(dbpath)) return dbpath def dump(self, dbpath, user, isList=False): if isList and user: backup = DumpUsers(user, self.master, dbpath) guimessage = _("Creating multiple user database files ...\n") else: backup = Dump(self.master, dbpath, user) if not backup.filepath: return None guimessage = argSub(_("Database file '%1' created, now" " read in the data"), (backup.filepath,)) if user: title = _("Create User Database File") else: title = _("Create Full Database Dump File") guiReport(title, backup, guimessage) return backup.filepath def slot_genTeacherDb(self, arg): """Generate a teacher's database file for all selected teachers. """ # Get list of users for whom a database file is to be generated users = self.gui.getSelectedUsers(self.getUsers()) if not users: return # Get destination directory: dbpath = self.getDbDir("teacherDbDir") if not dbpath: return # Dump the database files self.dump(dbpath, users, True) def slot_restore(self, arg): """Restore a dumped database. It can be either an existing one, or one which has been deleted. """ # Get source file: dbpath = self.getBDbPath() if not dbpath: return None restore = Restore(dbpath) dbname = restore.getDbName() if not dbname: message(_("Couldn't open database file '%1'"), (dbpath,)) return state = 0 try: if dbname in self.dbList: if not confirmationDialog(_("Replace Database?"), argSub(_("Are you sure you want to replace database '%1'?"), (dbname,)), False): restore.close() return self.deletedb(dbname) self.db.send(u"""CREATE DATABASE %s OWNER %s ENCODING 'UTF8'""" % (dbname, ADMIN)) state = 1 # Add to 'databases' table self.db.send(u"INSERT INTO databases VALUES (?, ?, ?, ?)", (self.db.getTime(), dbname, u'', u'')) state = 2 newmaster = self.connect(dbname) state = 3 guimessage = argSub(_("New database '%1' created, now read in the data"), (dbname,)) restore.setMaster(newmaster) guiReport(_("Restore Database"), restore, guimessage) #message(_("New database now set up")) self.usersPrivileges(newmaster) # Ensure connection is closed restore = None newmaster.close() newmaster = None except: print_exc() message(_("Couldn't create new database (%1)"), (dbname,)) if (state >= 3): newmaster.close() if (state >= 2): self.db.send(u"DELETE FROM databases WHERE name = ?", (dbname,)) if (state >= 1): self.db.send(u"DROP DATABASE %s" % dbname) # adjust display, select new db self.initDBlist() def slot_finalize(self, on): """'Finalize' the database. Set the 'finalized' item in the 'config' table and revoke update privelege from teachers. """ if (not self.master) or (on == self.finalized): return if on: if not confirmationDialog(_("Finalize Database?"), argSub(_("Finalizing stops teachers' access to the database.\n" "It may also clear their passwords.\n Continue?"), (self.dbname,)), False): return val = u"1" else: val = u"" self.master.send(u"""UPDATE config SET value= ? WHERE id = 'finalized'""", (val,)) self.showFinalized(on) self.usersPrivileges(self.master) def showFinalized(self, fin): self.finalized = fin self.gui.setFinalized(self.finalized) def slot_print(self, arg): """Pass a database dump file (*.zgb) to the print applicataion. """ dir0 = self.settings.getSetting("destDir") filepath = None if dir0: # See if there is already an adequately new dump file rex = re.compile(r"%s_(\d{8}_\d{6}).zgb$" % self.master.getName()) dumpfiles = [f for f in os.listdir(dir0) if rex.match(f)] if dumpfiles: dumpfiles.sort() latest = dumpfiles[-1] dumptime = rex.match(latest).group(1) udt = self.master.readValue(u"config", u"updatetime") lst = self.master.readValue(u"interface", u"lastsynctime") if (dumptime > udt) and (dumptime > lst): filepath = os.path.join(dir0, latest) if not filepath: dbpath = self.getDbDir("destDir") if not dbpath: return # Dump a full database filepath = self.dump(dbpath, u"") if not filepath: return # Start the printer dialog with this file. if self.printHandler: self.printHandler.init(filepath) else: self.printHandler = GuiPrint("print", filepath) self.printHandler.run() def slot_sync(self, arg): """Perform a synchronization with a selected database file but as adminstrative user. This allows even a finalized database to be updated. """ sfile = self.settings.getSetting("syncFile") if sfile: sdir = os.path.dirname(sfile) sfile = os.path.basename(sfile) else: sdir = None syncfile = getFile(_("User database file"), startDir=sdir, startFile=sfile, defaultSuffix=".zga", filter=(_("Report Files"), (u"*.zga",))) if not syncfile: return self.settings.setSetting("syncFile", syncfile) dbs = DBs(syncfile) if not dbs.isOpen(): return sdbname = dbs.getConfig(u"dbname") dbs.close() if (self.dbname != sdbname): warning(_("%s: Database name does not match current master name") % syncfile) return self.dlg = Output() synchronize(self.master, syncfile, self.dlg) self.dlg.done() def removeUser(self, user): """Remove a user. Return True if succeeded. """ try: self.db.dropRole(user) except: message(_("Couldn't remove user '%1'"), (user,)) return False return True def slot_restoreConfigFile(self, arg): if self.master: self.restoreConfigFile(self.master) def restoreConfigFile(self, db): """Get a parent directory for the creation of a data file. The file to be created may not exist already. Returns the file-name if successful, otherwise 'None'. """ # put up directory dialog, starting one up from dir0 dir = getDirectory(_("Parent Folder")) if not dir: return None datapath = os.path.join(dir, self.dbname + '.zip') if os.path.exists(datapath): message(_("'%1' already exists"), (datapath,)) return None dest = CfgZip(datapath, True) # open for writing if not dest.isOpen(): message(_("Couldn't open '%s' for writing") % datapath) return None for path, data in db.getAllData(): dest.addFile(path, data) dest.close() message(_("Configuration saved to '%s'") % datapath) return datapath