Merged extended database API into a branch derived from master. WIP; needs unit tests.

This commit is contained in:
s3lph 2018-07-17 22:02:53 +02:00
commit 105a10e91b
3 changed files with 109 additions and 15 deletions

View file

@ -75,14 +75,30 @@ class MatematDatabase(object):
users: List[User] = [] users: List[User] = []
with self.db.transaction(exclusive=False) as c: with self.db.transaction(exclusive=False) as c:
for row in c.execute(''' for row in c.execute('''
SELECT user_id, username, email, is_admin, is_member SELECT user_id, username, email, is_admin, is_member, balance
FROM users FROM users
'''): '''):
# Decompose each row and put the values into a User object # Decompose each row and put the values into a User object
user_id, username, email, is_admin, is_member = row user_id, username, email, is_admin, is_member, balance = row
users.append(User(user_id, username, email, is_admin, is_member)) users.append(User(user_id, username, balance, email, is_admin, is_member))
return users return users
def get_user(self, uid: int) -> User:
"""
Return a user identified by its user ID.
:param uid: The user's ID.
"""
with self.db.transaction(exclusive=False) as c:
# Fetch all values to construct the user
c.execute('SELECT user_id, username, email, is_admin, is_member, balance FROM users WHERE user_id = ?',
[uid])
row = c.fetchone()
if row is None:
raise ValueError(f'No user with user ID {uid} exists.')
# Unpack the row and construct the user
user_id, username, email, is_admin, is_member, balance = row
return User(user_id, username, balance, email, is_admin, is_member)
def create_user(self, def create_user(self,
username: str, username: str,
password: str, password: str,
@ -121,7 +137,7 @@ class MatematDatabase(object):
# Fetch the new user's rowid. # Fetch the new user's rowid.
c.execute('SELECT last_insert_rowid()') c.execute('SELECT last_insert_rowid()')
user_id = int(c.fetchone()[0]) user_id = int(c.fetchone()[0])
return User(user_id, username, email, admin, member) return User(user_id, username, 0, email, admin, member)
def login(self, username: str, password: Optional[str] = None, touchkey: Optional[str] = None) -> User: def login(self, username: str, password: Optional[str] = None, touchkey: Optional[str] = None) -> User:
""" """
@ -138,14 +154,14 @@ class MatematDatabase(object):
raise ValueError('Exactly one of password and touchkey must be provided') raise ValueError('Exactly one of password and touchkey must be provided')
with self.db.transaction(exclusive=False) as c: with self.db.transaction(exclusive=False) as c:
c.execute(''' c.execute('''
SELECT user_id, username, email, password, touchkey, is_admin, is_member SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance
FROM users FROM users
WHERE username = ? WHERE username = ?
''', [username]) ''', [username])
row = c.fetchone() row = c.fetchone()
if row is None: if row is None:
raise AuthenticationError('User does not exist') raise AuthenticationError('User does not exist')
user_id, username, email, pwhash, tkhash, admin, member = row user_id, username, email, pwhash, tkhash, admin, member, balance = row
if password is not None and not compare_digest(crypt.crypt(password, pwhash), pwhash): if password is not None and not compare_digest(crypt.crypt(password, pwhash), pwhash):
raise AuthenticationError('Password mismatch') raise AuthenticationError('Password mismatch')
elif touchkey is not None \ elif touchkey is not None \
@ -154,7 +170,7 @@ class MatematDatabase(object):
raise AuthenticationError('Touchkey mismatch') raise AuthenticationError('Touchkey mismatch')
elif touchkey is not None and tkhash is None: elif touchkey is not None and tkhash is None:
raise AuthenticationError('Touchkey not set') raise AuthenticationError('Touchkey not set')
return User(user_id, username, email, admin, member) return User(user_id, username, balance, email, admin, member)
def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None: def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None:
""" """
@ -225,14 +241,18 @@ class MatematDatabase(object):
with self.db.transaction() as c: with self.db.transaction() as c:
c.execute(''' c.execute('''
UPDATE users SET UPDATE users SET
username = :username,
email = :email, email = :email,
balance = :balance,
is_admin = :is_admin, is_admin = :is_admin,
is_member = :is_member, is_member = :is_member,
lastchange = STRFTIME('%s', 'now') lastchange = STRFTIME('%s', 'now')
WHERE user_id = :user_id WHERE user_id = :user_id
''', { ''', {
'user_id': user.id, 'user_id': user.id,
'username': user.name,
'email': user.email, 'email': user.email,
'balance': user.balance,
'is_admin': user.is_admin, 'is_admin': user.is_admin,
'is_member': user.is_member 'is_member': user.is_member
}) })
@ -265,13 +285,32 @@ class MatematDatabase(object):
products: List[Product] = [] products: List[Product] = []
with self.db.transaction(exclusive=False) as c: with self.db.transaction(exclusive=False) as c:
for row in c.execute(''' for row in c.execute('''
SELECT product_id, name, price_member, price_non_member SELECT product_id, name, price_member, price_non_member, stock
FROM products FROM products
'''): '''):
product_id, name, price_member, price_external = row product_id, name, price_member, price_external, stock = row
products.append(Product(product_id, name, price_member, price_external)) products.append(Product(product_id, name, price_member, price_external, stock))
return products return products
def get_product(self, pid: int) -> Product:
"""
Return a product identified by its product ID.
:param pid: The products's ID.
"""
with self.db.transaction(exclusive=False) as c:
# Fetch all values to construct the product
c.execute('''
SELECT product_id, name, price_member, price_non_member, stock
FROM products
WHERE product_id = ?''',
[pid])
row = c.fetchone()
if row is None:
raise ValueError(f'No product with product ID {pid} exists.')
# Unpack the row and construct the product
product_id, name, price_member, price_non_member, stock = row
return Product(product_id, name, price_member, price_non_member, stock)
def create_product(self, name: str, price_member: int, price_non_member: int) -> Product: def create_product(self, name: str, price_member: int, price_non_member: int) -> Product:
""" """
Creates a new product. Creates a new product.
@ -296,7 +335,7 @@ class MatematDatabase(object):
}) })
c.execute('SELECT last_insert_rowid()') c.execute('SELECT last_insert_rowid()')
product_id = int(c.fetchone()[0]) product_id = int(c.fetchone()[0])
return Product(product_id, name, price_member, price_non_member) return Product(product_id, name, price_member, price_non_member, 0)
def change_product(self, product: Product) -> None: def change_product(self, product: Product) -> None:
with self.db.transaction() as c: with self.db.transaction() as c:
@ -305,13 +344,15 @@ class MatematDatabase(object):
SET SET
name = :name, name = :name,
price_member = :price_member, price_member = :price_member,
price_non_member = :price_non_member price_non_member = :price_non_member,
stock = :stock
WHERE product_id = :product_id WHERE product_id = :product_id
''', { ''', {
'product_id': product.id, 'product_id': product.id,
'name': product.name, 'name': product.name,
'price_member': product.price_member, 'price_member': product.price_member,
'price_non_member': product.price_non_member 'price_non_member': product.price_non_member,
'stock': product.stock
}) })
affected = c.execute('SELECT changes()').fetchone()[0] affected = c.execute('SELECT changes()').fetchone()[0]
if affected != 1: if affected != 1:

View file

@ -3,16 +3,31 @@ from typing import Any
class Product(object): class Product(object):
"""
Representation of a product offered by the Matemat, with a name, prices for users, and the number of items
currently in stock.
"""
def __init__(self, def __init__(self,
product_id: int, product_id: int,
name: str, name: str,
price_member: int, price_member: int,
price_non_member: int) -> None: price_non_member: int,
stock: int) -> None:
"""
Create a new product instance with the given arguments.
:param product_id: The product ID in the database.
:param name: The product's name.
:param price_member: The price of a unit of this product for users marked as "members".
:param price_non_member: The price of a unit of this product for users NOT marked as "members".
:param stock: The number of items of this product currently in stock.
"""
self._product_id: int = product_id self._product_id: int = product_id
self._name: str = name self._name: str = name
self._price_member: int = price_member self._price_member: int = price_member
self._price_non_member: int = price_non_member self._price_non_member: int = price_non_member
self._stock: int = stock
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if other is None or not isinstance(other, Product): if other is None or not isinstance(other, Product):
@ -20,7 +35,8 @@ class Product(object):
return self._product_id == other._product_id \ return self._product_id == other._product_id \
and self._name == other._name \ and self._name == other._name \
and self._price_member == other._price_member \ and self._price_member == other._price_member \
and self._price_non_member == other._price_non_member and self._price_non_member == other._price_non_member \
and self._stock == other._stock
@property @property
def id(self) -> int: def id(self) -> int:
@ -49,3 +65,11 @@ class Product(object):
@price_non_member.setter @price_non_member.setter
def price_non_member(self, price: int) -> None: def price_non_member(self, price: int) -> None:
self._price_non_member = price self._price_non_member = price
@property
def stock(self) -> int:
return self._stock
@stock.setter
def stock(self, stock: int) -> None:
self._stock = stock

View file

@ -3,18 +3,35 @@ from typing import Optional, Any
class User(object): class User(object):
"""
Representation of a user registered with the Matemat, with a name, e-mail address (optional), whether the user is a
member of the organization the Matemat instance is used in, whether the user is an administrator, and the user's
account balance.
"""
def __init__(self, def __init__(self,
user_id: int, user_id: int,
username: str, username: str,
balance: int,
email: Optional[str] = None, email: Optional[str] = None,
admin: bool = False, admin: bool = False,
member: bool = True) -> None: member: bool = True) -> None:
"""
Create a new user instance with the given arguments.
:param user_id: The user ID in the database.
:param username: The user's name.
:param balance: The balance of the user's account.
:param email: The user's e-mail address (optional).
:param admin: Whether the user is an administrator.
:param member: Whether the user is a member.
"""
self._user_id: int = user_id self._user_id: int = user_id
self._username: str = username self._username: str = username
self._email: Optional[str] = email self._email: Optional[str] = email
self._admin: bool = admin self._admin: bool = admin
self._member: bool = member self._member: bool = member
self._balance: int = balance
def __eq__(self, other: Any) -> bool: def __eq__(self, other: Any) -> bool:
if other is None or not isinstance(other, User): if other is None or not isinstance(other, User):
@ -33,6 +50,10 @@ class User(object):
def name(self) -> str: def name(self) -> str:
return self._username return self._username
@name.setter
def name(self, value):
self._username = value
@property @property
def email(self) -> Optional[str]: def email(self) -> Optional[str]:
return self._email return self._email
@ -56,3 +77,11 @@ class User(object):
@is_member.setter @is_member.setter
def is_member(self, member: bool) -> None: def is_member(self, member: bool) -> None:
self._member = member self._member = member
@property
def balance(self) -> int:
return self._balance
@balance.setter
def balance(self, balance: int) -> None:
self._balance = balance