From 3e9a491228c364001aba5051b35080ba2d883d40 Mon Sep 17 00:00:00 2001
From: morkolai <nikolai-mork@live.no>
Date: Mon, 9 Mar 2020 13:13:15 +0100
Subject: [PATCH] =?UTF-8?q?#29=20Laget=20funksjon=20for=20=C3=A5=20legge?=
 =?UTF-8?q?=20til=201=20ord=20og=20delvis=20testet=20den?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 soitool/coder.py      |  11 ++--
 soitool/database.py   | 118 +++++++++++++++++++++++++++++-------------
 test/test_database.py |  53 ++++++++++++-------
 3 files changed, 122 insertions(+), 60 deletions(-)

diff --git a/soitool/coder.py b/soitool/coder.py
index 509ba2f..8a9dc77 100644
--- a/soitool/coder.py
+++ b/soitool/coder.py
@@ -1,10 +1,11 @@
 """Generate codes."""
 import string
+
 # https://realpython.com/lessons/cryptographically-secure-random-data-python/
 import secrets
 
 
-def get_code(length, mode='ascii'):
+def get_code(length, mode="ascii"):
     """
     Generate a single random code.
 
@@ -27,9 +28,9 @@ def get_code(length, mode='ascii'):
 
     characters = string.ascii_uppercase
 
-    if mode == 'digits':
+    if mode == "digits":
         characters = string.digits
-    elif mode == 'combo':
+    elif mode == "combo":
         characters = string.ascii_uppercase + string.digits
 
     while i < length:
@@ -39,7 +40,7 @@ def get_code(length, mode='ascii'):
     return code
 
 
-def get_code_set(size, length, mode='ascii'):
+def get_code_set(size, length, mode="ascii"):
     """
     Generate a set of codes.
 
@@ -85,7 +86,7 @@ def get_code_length(quantity):
         Lenght of code
     """
     length = 0
-    while len(string.ascii_uppercase)**length < quantity:
+    while len(string.ascii_uppercase) ** length < quantity:
         length = length + 1
 
     return length
diff --git a/soitool/database.py b/soitool/database.py
index 1f4fb25..89ef947 100644
--- a/soitool/database.py
+++ b/soitool/database.py
@@ -7,20 +7,23 @@ import schedule
 import soitool.coder
 
 # Set name and path to (future) database
-DBNAME = 'database'
+DBNAME = "database"
 CURDIR = os.path.dirname(__file__)
 DBPATH = os.path.join(CURDIR, DBNAME)
 
 # DDL-statements for creating tables
-CODEBOOK = 'CREATE TABLE CodeBook' \
-           '(Word VARCHAR PRIMARY KEY, Category VARCHAR,' \
-           'Code CHAR(3) UNIQUE, Type int DEFAULT 0)'
-CATEGORYWORDS = 'CREATE TABLE CategoryWords' \
-                '(Word VARCHAR PRIMARY KEY, Category VARCHAR)'
-BYHEART = 'CREATE TABLE ByHeart(Word VARCHAR PRIMARY KEY)'
-
-
-class Database():
+CODEBOOK = (
+    "CREATE TABLE CodeBook"
+    "(Word VARCHAR PRIMARY KEY, Category VARCHAR,"
+    "Code VARCHAR UNIQUE, Type int DEFAULT 0)"
+)
+CATEGORYWORDS = (
+    "CREATE TABLE CategoryWords" "(Word VARCHAR PRIMARY KEY, Category VARCHAR)"
+)
+BYHEART = "CREATE TABLE ByHeart(Word VARCHAR PRIMARY KEY)"
+
+
+class Database:
     """
     Holds database-connection and related functions.
 
@@ -32,17 +35,17 @@ class Database():
         db_exists = os.path.exists(DBPATH)
 
         if db_exists:
-            print('Connecting to existing DB.')
+            print("Connecting to existing DB.")
             self.conn = sqlite3.connect(DBPATH)
-            print('DB-connection established.')
+            print("DB-connection established.")
 
         else:
-            print('Creating new DB.')
+            print("Creating new DB.")
             self.conn = sqlite3.connect(DBPATH)
             self.create_tables()
-            print('DB created.')
+            print("DB created.")
             self.fill_tables()
-            print('Tables filled with data.')
+            print("Tables filled with data.")
 
         self.conn.row_factory = sqlite3.Row  # Enables row['columnName']
 
@@ -75,19 +78,24 @@ class Database():
         codes = soitool.coder.get_code_set(len(entries), entries_len)
 
         # Insert data in db
-        stmt = 'INSERT INTO CodeBook(Word, Category, Type, Code)' \
-               'VALUES(?,?,?,?)'
+        stmt = (
+            "INSERT INTO CodeBook(Word, Category, Type, Code)"
+            "VALUES(?,?,?,?)"
+        )
+
         for word in entries:
-            self.conn.execute(stmt, (word['word'], word['category'],
-                                     word['type'], codes.pop()))
+            self.conn.execute(
+                stmt,
+                (word["word"], word["category"], word["type"], codes.pop()),
+            )
 
     def fill_by_heart(self):
         """Read data from ByHeart.txt and fill DB-table ByHeart."""
         file_path = os.path.join(CURDIR, "testdata/ByHeart.txt")
-        f = open(file_path, "r", encoding='utf-8')
+        f = open(file_path, "r", encoding="utf-8")
 
         # Loop through words on file and insert them into ByHeart-table
-        stmt = 'INSERT INTO ByHeart(Word) VALUES(?)'
+        stmt = "INSERT INTO ByHeart(Word) VALUES(?)"
         for expr in f:
             self.conn.execute(stmt, (expr.rstrip(),))
         f.close()
@@ -95,7 +103,7 @@ class Database():
     def fill_category_words(self):
         """Read data from CategoryWords.txt and fill DB-table CategoryWords."""
         file_path = os.path.join(CURDIR, "testdata/CategoryWords.txt")
-        f = open(file_path, "r", encoding='utf-8')
+        f = open(file_path, "r", encoding="utf-8")
 
         # Get number of categories on file
         no_of_categories = int(f.readline().rstrip())
@@ -108,7 +116,7 @@ class Database():
             no_of_words = int(line[1].rstrip())
 
             # Loop through words in category and add rows to DB
-            stmt = 'INSERT INTO CategoryWords(Word, Category) VALUES(?, ?)'
+            stmt = "INSERT INTO CategoryWords(Word, Category) VALUES(?, ?)"
             for _ in range(no_of_words):
                 word = f.readline().rstrip()
                 self.conn.execute(stmt, (word, category,))
@@ -123,11 +131,11 @@ class Database():
         List of strings
             Categories
         """
-        stmt = 'SELECT DISTINCT Category FROM CategoryWords'
+        stmt = "SELECT DISTINCT Category FROM CategoryWords"
         queried = self.conn.execute(stmt)
         categories = []
         for row in queried:
-            categories.append(row['Category'])
+            categories.append(row["Category"])
 
         return categories
 
@@ -146,44 +154,82 @@ class Database():
             [{'word': str, 'type': str, 'category': str, 'code': str}]
         """
         # Get either full or small codebook
-        stmt = 'SELECT * FROM CodeBook'
+        stmt = "SELECT * FROM CodeBook"
         if small:
-            stmt = stmt + ' WHERE Type = ?'
+            stmt = stmt + " WHERE Type = ?"
             queried = self.conn.execute(stmt, (1,)).fetchall()
         else:
             queried = self.conn.execute(stmt).fetchall()
 
         codebook = []
         for entry in queried:
-            codebook.append({'word': entry['Word'],
-                             'category': entry['Category'],
-                             'type': entry['Type'],
-                             'code': entry['Code']})
+            codebook.append(
+                {
+                    "word": entry["Word"],
+                    "category": entry["Category"],
+                    "type": entry["Type"],
+                    "code": entry["Code"],
+                }
+            )
 
         return codebook
 
     def update_codebook(self):
         """Update codes in DB."""
         # Get all the words (PK)
-        stmt = 'SELECT Word FROM CodeBook'
+        stmt = "SELECT Word FROM CodeBook"
         words = self.conn.execute(stmt).fetchall()
         # Get number of entries
-        stmt = 'SELECT COUNT(*) FROM CodeBook'
+        stmt = "SELECT COUNT(*) FROM CodeBook"
         number_of_entries = self.conn.execute(stmt).fetchall()[0][0]
         # Generate new codes
         code_len = soitool.coder.get_code_length(number_of_entries)
         codes = soitool.coder.get_code_set(number_of_entries, code_len)
         # Statement for update
-        stmt = 'UPDATE CodeBook SET Code = ? WHERE Word = ?'
+        stmt = "UPDATE CodeBook SET Code = ?"
         # Wiping Code column because of UNIQUE
-        for i in range(number_of_entries):
-            self.conn.execute(stmt, (None, words[i][0]))
+        self.conn.execute(stmt, (None,))
         # Fill Code column with new codes
+        stmt = stmt + "WHERE Word = ?"
         for i in range(number_of_entries):
             self.conn.execute(stmt, (codes.pop(), words[i][0]))
 
         print("Code in CodeBook updated")
 
+    def add_code(self, word, mode="ascii"):
+        """
+        Add a code to the new word.
+
+        Parameters
+        ----------
+        word : string
+            The word to generate a code for.
+
+        mode : string
+            'ascii' for letters (default), 'digits' for digits and 'combo'
+            for combination of letters and digits.
+        """
+        # Get length of codes
+        stmt = "SELECT COUNT(*) FROM CodeBook"
+        number_of_entries = self.conn.execute(stmt).fetchall()[0][0]
+        # TODO ordet ligger alt inne så her er det feil
+        code_len = soitool.coder.get_code_length(number_of_entries)
+        # If adding a word makes the code longer, update whole CodeBook in db
+        if code_len < soitool.coder.get_code_length(number_of_entries + 1):
+            self.update_codebook()
+        else:
+            # Get all the used codes
+            stmt = "SELECT Code FROM CodeBook"
+            codes_li = self.conn.execute(stmt).fetchall()
+            codes_set = set([i[:][0] for i in codes_li])
+            # Get new unique code
+            code = soitool.coder.get_code(code_len, mode)
+            while code in codes_set:
+                code = soitool.coder.get_code(code_len, mode)
+            # Update db with new code
+            stmt = "UPDATE CodeBook SET Code = ? WHERE Word = ?"
+            self.conn.execute(stmt, (code, word))
+
     def update_codebook_auto(self, clock):
         """
         Update DB every day at clock.
diff --git a/test/test_database.py b/test/test_database.py
index 9446f85..1a5525e 100644
--- a/test/test_database.py
+++ b/test/test_database.py
@@ -32,7 +32,7 @@ class DatabaseTest(unittest.TestCase):
         no_of_expr = len(expressions)
 
         # Retrieve expressions from DB:
-        stmt = 'SELECT * FROM ByHeart'
+        stmt = "SELECT * FROM ByHeart"
         queried = self.database.conn.execute(stmt).fetchall()
 
         # Assert equal amount of expressions in table and file
@@ -73,7 +73,7 @@ class DatabaseTest(unittest.TestCase):
         self.assertEqual(categories_db, categories_file)
 
         # Retrieve data from DB
-        stmt = 'SELECT * FROM CategoryWords'
+        stmt = "SELECT * FROM CategoryWords"
         queried = self.database.conn.execute(stmt).fetchall()
 
         # Assert equal categories in table and file
@@ -89,7 +89,7 @@ class DatabaseTest(unittest.TestCase):
         file.close()
 
         # Get data from db
-        stmt = 'SELECT * FROM CodeBook'
+        stmt = "SELECT * FROM CodeBook"
         actual = self.database.conn.execute(stmt).fetchall()
 
         # Check same lenght
@@ -100,10 +100,10 @@ class DatabaseTest(unittest.TestCase):
 
         # Check equality
         for i, entry in enumerate(expected):
-            self.assertEqual(entry['word'], actual[i][0])
-            self.assertEqual(entry['category'], actual[i][1])
+            self.assertEqual(entry["word"], actual[i][0])
+            self.assertEqual(entry["category"], actual[i][1])
             self.assertRegex(actual[i][2], "[A-Z]{" + str(code_len) + "}")
-            self.assertEqual(entry['type'], actual[i][3])
+            self.assertEqual(entry["type"], actual[i][3])
 
     def test_get_categories(self):
         """Assert function get_categories works as expected."""
@@ -145,10 +145,10 @@ class DatabaseTest(unittest.TestCase):
 
         # Compare contents
         for i, entry in enumerate(expected):
-            self.assertEqual(entry['word'], actual[i]['word'])
-            self.assertEqual(entry['category'], actual[i]['category'])
-            self.assertRegex(actual[i]['code'], "[A-Z]{" + str(code_len) + "}")
-            self.assertEqual(entry['type'], actual[i]['type'])
+            self.assertEqual(entry["word"], actual[i]["word"])
+            self.assertEqual(entry["category"], actual[i]["category"])
+            self.assertRegex(actual[i]["code"], "[A-Z]{" + str(code_len) + "}")
+            self.assertEqual(entry["type"], actual[i]["type"])
 
     def test_get_codebook_small(self):
         """Assert function get_codebook only return the small codebook."""
@@ -161,7 +161,7 @@ class DatabaseTest(unittest.TestCase):
         # Fill expected with only small codebook entries
         expected = []
         for entry in data:
-            if entry['type'] == 1:
+            if entry["type"] == 1:
                 expected.append(entry)
 
         # Get small codebook from db
@@ -175,29 +175,44 @@ class DatabaseTest(unittest.TestCase):
 
         # Compare contents
         for i, entry in enumerate(expected):
-            self.assertEqual(entry['word'], actual[i]['word'])
-            self.assertEqual(entry['category'], actual[i]['category'])
-            self.assertRegex(actual[i]['code'], "[A-Z]{" + str(code_len) + "}")
-            self.assertEqual(entry['type'], actual[i]['type'])
+            self.assertEqual(entry["word"], actual[i]["word"])
+            self.assertEqual(entry["category"], actual[i]["category"])
+            self.assertRegex(actual[i]["code"], "[A-Z]{" + str(code_len) + "}")
+            self.assertEqual(entry["type"], actual[i]["type"])
 
     def test_update_codebook(self):
         """Test that the codes get updated."""
         # Get number of entries
-        stmt = 'SELECT COUNT(*) FROM CodeBook ORDER BY Word'
+        stmt = "SELECT COUNT(*) FROM CodeBook ORDER BY Word"
         number_of_entries = self.database.conn.execute(stmt).fetchall()[0][0]
         # Get old and updated word-code combinations
-        stmt = 'SELECT Word, Code FROM CodeBook'
+        stmt = "SELECT Word, Code FROM CodeBook"
         old = self.database.conn.execute(stmt).fetchall()
         self.database.update_codebook()
         updated = self.database.conn.execute(stmt).fetchall()
         # Collect approximately score of not updated pairs
         pairs = 0
         for i in range(0, number_of_entries, 2):
-            if old[i]['Code'] == updated[i]['Code']:
+            if old[i]["Code"] == updated[i]["Code"]:
                 pairs = pairs + 1
         # Test that at least some of the test are new
         self.assertTrue(pairs < number_of_entries)
 
+    def test_add_code(self):
+        """Test add a single code to CodeBook"""
+        testdata = ("testword", "testcategory")
+        stmt = "INSERT INTO CodeBook (Word, Category) VALUES (?,?)"
+        self.database.conn.execute(stmt, (testdata[0], testdata[1]))
+        self.database.add_code(testdata[0])
+        stmt = "SELECT Code FROM CodeBook WHERE Word = ?"
+        code = self.database.conn.execute(stmt, (testdata[0],)).fetchall()[0][
+            0
+        ]
+        self.assertRegex(code, "[A-Z0-9]")
 
-if __name__ == '__main__':
+    def test_add_code_extending_code_length(self):
+        """Test all codes get updated when code lenght i changed."""
+
+
+if __name__ == "__main__":
     unittest.main()
-- 
GitLab