Merge branch 'fix-receipt-empty-db' into 'staging'

Added database support for empty receipts

See merge request s3lph/matemat!53
This commit is contained in:
s3lph 2018-12-29 19:58:11 +00:00
commit 7f23fff033
6 changed files with 159 additions and 11 deletions

View file

@ -647,14 +647,14 @@ class MatematDatabase(object):
else: else:
t = Transaction(ta_id, user, value, old_balance, datetime.fromtimestamp(date)) t = Transaction(ta_id, user, value, old_balance, datetime.fromtimestamp(date))
transactions.append(t) transactions.append(t)
if write and len(transactions) > 0: if write:
cursor.execute(''' cursor.execute('''
INSERT INTO receipts (user_id, first_ta_id, last_ta_id) INSERT INTO receipts (user_id, first_ta_id, last_ta_id)
VALUES (:user_id, :first_ta, :last_ta) VALUES (:user_id, :first_ta, :last_ta)
''', { ''', {
'user_id': user.id, 'user_id': user.id,
'first_ta': transactions[0].id, 'first_ta': transactions[0].id if len(transactions) != 0 else None,
'last_ta': transactions[-1].id 'last_ta': transactions[-1].id if len(transactions) != 0 else None
}) })
cursor.execute('''SELECT last_insert_rowid()''') cursor.execute('''SELECT last_insert_rowid()''')
receipt_id: int = int(cursor.fetchone()[0]) receipt_id: int = int(cursor.fetchone()[0])

View file

@ -201,3 +201,41 @@ def migrate_schema_2_to_3(c: sqlite3.Cursor):
ON DELETE SET NULL ON UPDATE CASCADE ON DELETE SET NULL ON UPDATE CASCADE
) )
''') ''')
def migrate_schema_3_to_4(c: sqlite3.Cursor):
# Change receipts schema to allow null for transaction IDs
c.execute('''
CREATE TEMPORARY TABLE receipts_temp (
receipt_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
first_ta_id INTEGER DEFAULT NULL,
last_ta_id INTEGER DEFAULT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''')
c.execute('INSERT INTO receipts_temp SELECT * FROM receipts')
c.execute('DROP TABLE receipts')
c.execute('''
CREATE TABLE receipts (
receipt_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
first_ta_id INTEGER DEFAULT NULL,
last_ta_id INTEGER DEFAULT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''')
c.execute('INSERT INTO receipts SELECT * FROM receipts_temp')
c.execute('DROP TABLE receipts_temp')

View file

@ -178,3 +178,80 @@ SCHEMAS[3] = [
ON DELETE SET NULL ON UPDATE CASCADE ON DELETE SET NULL ON UPDATE CASCADE
); );
'''] ''']
SCHEMAS[4] = [
'''
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT NULL,
password TEXT NOT NULL,
touchkey TEXT DEFAULT NULL,
is_admin INTEGER(1) NOT NULL DEFAULT 0,
is_member INTEGER(1) NOT NULL DEFAULT 1,
balance INTEGER(8) NOT NULL DEFAULT 0,
lastchange INTEGER(8) NOT NULL DEFAULT 0,
receipt_pref INTEGER(1) NOT NULL DEFAULT 0,
created INTEGER(8) NOT NULL DEFAULT 0
);
''',
'''
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
stock INTEGER(8) NOT NULL DEFAULT 0,
price_member INTEGER(8) NOT NULL,
price_non_member INTEGER(8) NOT NULL
);
''',
'''
CREATE TABLE transactions ( -- "superclass" of the following 3 tables
ta_id INTEGER PRIMARY KEY,
user_id INTEGER DEFAULT NULL,
value INTEGER(8) NOT NULL,
old_balance INTEGER(8) NOT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''',
'''
CREATE TABLE consumptions ( -- transactions involving buying a product
ta_id INTEGER PRIMARY KEY,
product TEXT NOT NULL,
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE deposits ( -- transactions involving depositing cash
ta_id INTEGER PRIMARY KEY,
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE modifications ( -- transactions involving balance modification by an admin
ta_id INTEGER NOT NULL,
agent TEXT NOT NULL,
reason TEXT DEFAULT NULL,
PRIMARY KEY (ta_id),
FOREIGN KEY (ta_id) REFERENCES transactions(ta_id)
ON DELETE CASCADE ON UPDATE CASCADE
);
''',
'''
CREATE TABLE receipts ( -- receipts sent to the users
receipt_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
first_ta_id INTEGER DEFAULT NULL,
last_ta_id INTEGER DEFAULT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE,
FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id)
ON DELETE SET NULL ON UPDATE CASCADE
);
''']

View file

@ -506,14 +506,14 @@ class DatabaseTest(unittest.TestCase):
db.increment_consumption(user, product) db.increment_consumption(user, product)
db.deposit(user, 1337) db.deposit(user, 1337)
receipt1: Receipt = db.create_receipt(user, write=True) receipt1: Receipt = db.create_receipt(user, write=True)
# Attempt to create a receipt with zero transactions. Won't be written to DB. # Attempt to create a receipt with zero transactions. Will carry NULL transaction IDs
receipt2: Receipt = db.create_receipt(user, write=True) receipt2: Receipt = db.create_receipt(user, write=True)
self.assertEqual(-1, receipt2.id) self.assertNotEqual(-1, receipt2.id)
self.assertEqual(0, len(receipt2.transactions)) self.assertEqual(0, len(receipt2.transactions))
with db.transaction() as c: with db.transaction() as c:
c.execute('SELECT COUNT(receipt_id) FROM receipts') c.execute('SELECT COUNT(receipt_id) FROM receipts')
self.assertEqual(1, c.fetchone()[0]) self.assertEqual(2, c.fetchone()[0])
db.increment_consumption(user, product) db.increment_consumption(user, product)
db.change_user(user, agent=admin, balance=4200) db.change_user(user, agent=admin, balance=4200)
@ -529,7 +529,7 @@ class DatabaseTest(unittest.TestCase):
with db.transaction() as c: with db.transaction() as c:
c.execute('SELECT COUNT(receipt_id) FROM receipts') c.execute('SELECT COUNT(receipt_id) FROM receipts')
self.assertEqual(1, c.fetchone()[0]) self.assertEqual(2, c.fetchone()[0])
self.assertEqual(user, receipt1.user) self.assertEqual(user, receipt1.user)
self.assertEqual(3, len(receipt1.transactions)) self.assertEqual(3, len(receipt1.transactions))

View file

@ -183,3 +183,36 @@ class TestMigrations(unittest.TestCase):
self.assertEqual('Flora Power Mate', cursor.fetchone()[0]) self.assertEqual('Flora Power Mate', cursor.fetchone()[0])
cursor.execute('''SELECT product FROM consumptions WHERE ta_id = 5''') cursor.execute('''SELECT product FROM consumptions WHERE ta_id = 5''')
self.assertEqual('<unknown>', cursor.fetchone()[0]) self.assertEqual('<unknown>', cursor.fetchone()[0])
def test_upgrade_3_to_4(self):
# Setup test db with example entries to test schema change
self._initialize_db(3)
cursor: sqlite3.Cursor = self.db._sqlite_db.cursor()
cursor.execute('''
INSERT INTO users VALUES
(1, 'testadmin', 'a@b.c', '$2a$10$herebehashes', NULL, 1, 1, 1337, 0, 0, 0)
''')
cursor.execute('''
INSERT INTO products VALUES
(1, 'Club Mate', 42, 200, 250)
''')
cursor.execute('''
INSERT INTO transactions VALUES (1, 1, 4200, 0, 1000)
''')
cursor.execute('''
INSERT INTO receipts VALUES (1, 1, 1, 1, 1337)
''')
cursor.execute('PRAGMA user_version = 3')
# Kick off the migration
schema_version = self.db.SCHEMA_VERSION
self.db.SCHEMA_VERSION = 4
self.db._setup()
self.db.SCHEMA_VERSION = schema_version
# Make sure entries from the receipts table are preserved
cursor.execute('''SELECT COUNT(receipt_id) FROM receipts''')
self.assertEqual(1, cursor.fetchone()[0])
# Make sure transaction IDs can be set to NULL
cursor.execute('UPDATE receipts SET first_ta_id = NULL, last_ta_id = NULL')

View file

@ -2,11 +2,9 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional from typing import Any, Optional
import sqlite3
from matemat.exceptions import DatabaseConsistencyError from matemat.exceptions import DatabaseConsistencyError
from matemat.db.schemas import SCHEMAS from matemat.db.schemas import SCHEMAS
from matemat.db.migrations import migrate_schema_1_to_2, migrate_schema_2_to_3 from matemat.db.migrations import *
class DatabaseTransaction(object): class DatabaseTransaction(object):
@ -43,7 +41,7 @@ class DatabaseTransaction(object):
class DatabaseWrapper(object): class DatabaseWrapper(object):
SCHEMA_VERSION = 3 SCHEMA_VERSION = 4
def __init__(self, filename: str) -> None: def __init__(self, filename: str) -> None:
self._filename: str = filename self._filename: str = filename
@ -86,6 +84,8 @@ class DatabaseWrapper(object):
migrate_schema_1_to_2(c) migrate_schema_1_to_2(c)
if from_version <= 2 and to_version >= 3: if from_version <= 2 and to_version >= 3:
migrate_schema_2_to_3(c) migrate_schema_2_to_3(c)
if from_version <= 3 and to_version >= 4:
migrate_schema_3_to_4(c)
def connect(self) -> None: def connect(self) -> None:
if self.is_connected(): if self.is_connected():