From: <umg...@us...> - 2007-02-28 18:21:38
|
Revision: 355 http://svn.sourceforge.net/pybridge/?rev=355&view=rev Author: umgangee Date: 2007-02-28 10:21:26 -0800 (Wed, 28 Feb 2007) Log Message: ----------- New SQLObject object-relational database model, replacing flat-file user account store. Modified Paths: -------------- trunk/pybridge/pybridge/server/checker.py trunk/pybridge/pybridge/server/database.py trunk/pybridge/pybridge/server/server.py trunk/pybridge/pybridge/server/user.py Modified: trunk/pybridge/pybridge/server/checker.py =================================================================== --- trunk/pybridge/pybridge/server/checker.py 2007-02-28 18:11:30 UTC (rev 354) +++ trunk/pybridge/pybridge/server/checker.py 2007-02-28 18:21:26 UTC (rev 355) @@ -1,5 +1,5 @@ # PyBridge -- online contract bridge made easy. -# Copyright (C) 2004-2006 PyBridge Project. +# Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -10,7 +10,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. @@ -21,7 +21,7 @@ from twisted.python import failure, log from zope.interface import implements -from database import database +import database as db class Checker: @@ -34,35 +34,33 @@ def __init__(self): - self.database = database self.users = {} # Users online, from Server object. def requestAvatarId(self, credentials): - - def gotUser(user): - password = user.get('password', '') - d = defer.maybeDeferred(credentials.checkPassword, password) - d.addCallback(passwordMatch) - return d - + + def unauthorized(reason): + log.msg("Login failed for %s: %s" % (credentials.username, reason)) + return failure.Failure(error.UnauthorizedLogin(reason)) + def passwordMatch(matched): if matched: - if credentials.username in self.users.keys(): - raise unauthorized('Already logged in') + if credentials.username in self.users: + # TODO: delete old session and use this one instead? + return unauthorized("User is already logged in") else: return credentials.username else: - return unauthorized('Incorrect password') - - def unauthorized(reason): - log.msg('Login failed for %s: %s' % (credentials.username, reason)) - return failure.Failure(error.UnauthorizedLogin(reason)) - + return unauthorized("Incorrect password for user") + if credentials.username == '': - return checkers.ANONYMOUS - else: - d = self.database.getUser(credentials.username) - d.addCallbacks(gotUser, lambda e: unauthorized('No user account')) - return d + return checkers.ANONYMOUS # TODO: if allowAnonymousRegistration. + users = db.UserAccount.selectBy(username=credentials.username) + if users.count() is 0: + return unauthorized("User not known on server") + + d = defer.maybeDeferred(credentials.checkPassword, users[0].password) + d.addCallback(passwordMatch) + return d + Modified: trunk/pybridge/pybridge/server/database.py =================================================================== --- trunk/pybridge/pybridge/server/database.py 2007-02-28 18:11:30 UTC (rev 354) +++ trunk/pybridge/pybridge/server/database.py 2007-02-28 18:21:26 UTC (rev 355) @@ -1,5 +1,5 @@ # PyBridge -- online contract bridge made easy. -# Copyright (C) 2004-2006 PyBridge Project. +# Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -10,81 +10,141 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -import shelve, anydbm, dbhash -from twisted.internet import defer -from twisted.python import failure +import os, re +from datetime import datetime +from sqlobject import * +from sqlobject.inheritance import InheritableSQLObject -from pybridge.environment import environment +import pybridge.environment as env +from pybridge.bridge.deck import Seat -class DuplicateError(Exception): - pass +backend = "sqlite" -class UnknownError(Exception): - pass +# Initiate connection to the appropriate database backend. +if backend == "sqlite": + db_filename = env.find_config_server("pybridge-server.db") + connection_string = "sqlite://" + db_filename # TODO: fix for Win32. +connection = connectionForURI(connection_string) +sqlhub.processConnection = connection # Set all classes to use connection. -class UserDatabase: - """A simple database of user accounts.""" +class UserAccount(SQLObject): + """A store of user information. + + A user account is created when a user is registered. + """ - def __init__(self): - # Open the database file. - dbfile = environment.find_configfile('users.db') - self.accounts = shelve.open(dbfile, 'c', writeback=True) + username = StringCol(length=20, notNone=True, unique=True, alternateID=True) + password = StringCol(length=40, notNone=True) # Store SHA-1 hex hashes. + allowLogin = BoolCol(default=True) # If False, account login is disabled. + email = StringCol(default=None, length=320) # See RFC 2821 section 4.5.3.1. + realname = UnicodeCol(default=None, length=40) + profile = UnicodeCol(default=None) + created = DateTimeCol(default=datetime.now) + lastLogin = DateTimeCol(default=None) + # friends = MultipleJoin('UserFriend', joinColumn='from_user') + def _set_username(self, value): + if not isinstance(value, str) or not(1 <= len(value) <= 20): + raise ValueError, "Invalid specification of username" + if re.search("[^A-z0-9_]", value): + raise ValueError, "Username can only contain alphanumeric characters" + self._SO_set_username(value) - def addUser(self, username, **attrs): - """Adds a new user.""" - if self.accounts.has_key(username): - f = failure.Failure(DuplicateError()) - return defer.fail(f) - - profile = attrs.copy() - profile['username'] = username - self.accounts[username] = profile - - return defer.succeed(username) + def _set_password(self, value): + if not isinstance(value, str) or not(1 <= len(value) <= 40): + raise ValueError, "Invalid specification of password" + self._SO_set_password(value) + def _set_email(self, value): + # This regexp matches virtually all well-formatted email addresses. + if value and not re.match("^[A-z0-9_.+-]+@([A-z0-9-]+\.)+[A-z]{2,6}$", value): + raise ValueError, "Invalid or ill-formatted email address" + self._SO_set_email(value) - def removeUser(self, username): - """Removes an existing user.""" - if not self.accounts.has_key(username): - f = failure.Failure(UnknownError()) - return defer.fail(f) - - del self.accounts[username] - - return defer.succeed(username) - - def updateUser(self, username, **attrs): - """Updates attributes for an existing user.""" - if not self.accounts.has_key(username): - f = failure.Failure(UnknownError()) - return defer.fail(f) - - self.accounts[username].update(attrs) - - return defer.succeed(username) +for table in [UserAccount]: + table.createTable(ifNotExists=True) - def getUser(self, username): - """Returns a dict of information for an existing user.""" - if not self.accounts.has_key(username): - f = failure.Failure(UnknownError()) - return defer.fail(f) - - info = self.accounts[username] - - return defer.succeed(info) +# The following tables are not used by PyBridge 0.3. +# They will be enhanced and used in future releases. -database = UserDatabase() +class UserFriend(SQLObject): + """Models the social interconnections that exist between users. + + Client software may use this information to provide visual clues to users + that members of their "social circle" are online. + + Users may specify the nature of their relationships: this takes inspiration + from the XFN (XHTML Friend Network) model: see http://gmpg.org/xfn/. The + symmetry arising from some types of relationship is eschewed for simplicity. + + This relation is irreflexive: no user can form a friendship with themselves! + """ + fromUser = ForeignKey('UserAccount') # The creator of the relationship. + toUser = ForeignKey('UserAccount') # The subject of the relationship. + fromToIndex = DatabaseIndex('fromUser', 'toUser', unique=True) + + # XFN attributes. + friendship = EnumCol(default=None, enumValues=['friend', 'acquaintance', 'contact']) + physical = BoolCol(default=False) # Having met in person. + professional = EnumCol(default=None, enumValues=['co-worker', 'colleague']) + geographical = EnumCol(default=None, enumValues=['co-resident', 'neighbour']) + family = EnumCol(default=None, enumValues=['child', 'parent', 'sibling', 'spouse', 'kin']) + romantic = EnumCol(default=None, enumValues=['muse', 'crush', 'date', 'sweetheart']) + + +class Game(InheritableSQLObject): + """Captures game attributes common to all games. + + Implementations of specific games should inherit from this class. + """ + + start = DateTimeCol() + complete = DateTimeCol() + + +class BridgeGame(Game): + """Captures game attributes specific to bridge games. + + """ + + board = ForeignKey('BridgeBoard') + + declarer = EnumCol(enumValues=list(Seat)) +# contract = + trickCount = IntCol() # Number of tricks won by + score = IntCol() + + # Although key attributes of games are stored in fields (for searching), + # the complete game is represented in PBN format. + pbn = StringCol() + + # Players: no player may occupy more than one position. + north = ForeignKey('UserAccount') + east = ForeignKey('UserAccount') + south = ForeignKey('UserAccount') + west = ForeignKey('UserAccount') + + +class BridgeBoard(SQLObject): + """Encapsulates the attributes which may be common to multiple bridge games. + + Separating board attributes from . + """ + + deal = IntCol() + dealer = EnumCol(enumValues=list(Seat)) + vuln = EnumCol(enumValues=['none', 'ns', 'ew', 'all']) + Modified: trunk/pybridge/pybridge/server/server.py =================================================================== --- trunk/pybridge/pybridge/server/server.py 2007-02-28 18:11:30 UTC (rev 354) +++ trunk/pybridge/pybridge/server/server.py 2007-02-28 18:21:26 UTC (rev 355) @@ -1,5 +1,5 @@ # PyBridge -- online contract bridge made easy. -# Copyright (C) 2004-2006 PyBridge Project. +# Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -10,16 +10,19 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from datetime import datetime from twisted.python import log -from database import database -import pybridge +import database as db +from pybridge import __version__ + +from pybridge.network.error import DeniedRequest, IllegalRequest from pybridge.network.tablemanager import LocalTableManager from pybridge.network.usermanager import LocalUserManager @@ -32,13 +35,14 @@ def __init__(self): self.tables = LocalTableManager() self.users = LocalUserManager() - self.version = pybridge.__version__ + self.version = __version__ self.supported = ['bridge'] def userConnects(self, user): """""" self.users.userLoggedIn(user) + db.UserAccount.byUsername(user.name).set(lastLogin=datetime.now()) log.msg("User %s connected" % user.name) @@ -51,11 +55,26 @@ # Methods invoked by user perspectives. - def userRegister(self, username, password): + def registerUser(self, username, password): + """Registers a new user account in the database. + + @param username: the unique username requested by user. + @param password: the password to be associated with the account. + """ + # Check that username has not already been registered. + if db.UserAccount.selectBy(username=username).count() > 0: + raise DeniedRequest, "Username already registered" + try: + # Create user account. + db.UserAccount(username=username, password=password, allowLogin=True) + log.msg("New user %s registered" % username) + except ValueError, err: + raise IllegalRequest, err + + + def userChangePassword(self, user, password): """""" - d = database.addUser(username, password=password) - log.msg("New user %s registered" % username) - return d + pass def createTable(self, tableid, tabletype): Modified: trunk/pybridge/pybridge/server/user.py =================================================================== --- trunk/pybridge/pybridge/server/user.py 2007-02-28 18:11:30 UTC (rev 354) +++ trunk/pybridge/pybridge/server/user.py 2007-02-28 18:21:26 UTC (rev 355) @@ -1,5 +1,5 @@ # PyBridge -- online contract bridge made easy. -# Copyright (C) 2004-2006 PyBridge Project. +# Copyright (C) 2004-2007 PyBridge Project. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -10,13 +10,15 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. - +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import re +#from twisted.internet import defer +#from twisted.python import failure from twisted.spread import pb from pybridge.network.error import DeniedRequest, IllegalRequest @@ -73,13 +75,13 @@ def perspective_hostTable(self, tableid, tabletype): """Creates a new table.""" if not isinstance(tableid, str): - raise IllegalRequest, 'Invalid parameter for table identifier' + raise IllegalRequest, "Invalid parameter for table identifier" elif not(0 < len(tableid) < 21) or re.search("[^A-Za-z0-9_ ]", tableid): - raise IllegalRequest, 'Invalid table identifier format' + raise IllegalRequest, "Invalid table identifier format" elif tableid in self.server.tables: - raise DeniedRequest, 'Table name exists' + raise DeniedRequest, "Table name exists" elif tabletype not in self.server.supported: - raise DeniedRequest, 'Table type not suppported by this server' + raise DeniedRequest, "Table type not suppported by this server" self.server.createTable(tableid, tabletype) return self.perspective_joinTable(tableid) # Force join to table. @@ -88,11 +90,11 @@ def perspective_joinTable(self, tableid): """Joins an existing table.""" if not isinstance(tableid, str): - raise IllegalRequest, 'Invalid parameter for table name' + raise IllegalRequest, "Invalid parameter for table name" elif tableid not in self.server.tables: - raise DeniedRequest, 'No such table' + raise DeniedRequest, "No such table" elif tableid in self.tables: - raise DeniedRequest, 'Already joined table' + raise DeniedRequest, "Already joined table" table = self.server.tables[tableid] self.tables[tableid] = table @@ -103,11 +105,11 @@ def perspective_leaveTable(self, tableid): """Leaves a table.""" if not isinstance(tableid, str): - raise IllegalRequest, 'Invalid parameter for table name' + raise IllegalRequest, "Invalid parameter for table name" elif tableid not in self.tables: - raise DeniedRequest, 'Not joined to table' + raise DeniedRequest, "Not joined to table" - del self.tables[tableid] + del self.tables[tableid] # Implicitly removes user from table. @@ -116,11 +118,7 @@ def perspective_register(self, username, password): - """Register a user account with given username and password.""" - if not isinstance(username, str): - raise IllegalRequest, 'Invalid parameter for user name' - elif not isinstance(password, str): - raise IllegalRequest, 'Invalid parameter for password' - - self.server.userRegister(username, password) + """Create a user account with specified username and password.""" + # TODO: consider defer.succeed, defer.fail, failure.Failure + self.server.registerUser(username, password) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |