Merged extended database API into a branch derived from master. WIP; needs unit tests.
This commit is contained in:
commit
105a10e91b
3 changed files with 109 additions and 15 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue