Database Programming is Program with Data

The Tri 2 Final Project is an example of a Program with Data.

Prepare to use SQLite in common Imperative Technique

Schema of Users table in Sqlite.db

Uses PRAGMA statement to read schema.

Describe Schema, here is resource Resource

  • What is a database schema?

A database schema is essentially a column defined by certian information in a database.

  • What is the purpose of identity Column in SQL database?

It is to locate which row of a data table a certain entry is found.

  • What is the purpose of a primary key in SQL database?

The primary key is the main way to access a certain piece of information in a database. Generally, unlike the identity column, a primary key should never change for a piece of data.

  • What are the Data Types in SQL table?

Integer, float, string, Boolean, images; really any data type or data structure (list, dictionary, class).

import sqlite3

database = 'instance/sqlite.db' # this is location of database

def schema():
    
    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    
    # Fetch results of Schema
    results = cursor.execute("PRAGMA table_info('users')").fetchall()

    # Print the results
    for row in results:
        print(row)

    # Close the database connection
    conn.close()
    
schema()
(0, 'id', 'INTEGER', 1, None, 1)
(1, '_name', 'VARCHAR(255)', 1, None, 0)
(2, '_uid', 'VARCHAR(255)', 1, None, 0)
(3, '_password', 'VARCHAR(255)', 1, None, 0)
(4, '_dob', 'DATE', 0, None, 0)

Reading Users table in Sqlite.db

Uses SQL SELECT statement to read data

  • What is a connection object? After you Google it, what do you think it does?

From what I understand, a connection object is basically an object that is used to connect to a database so that you can interact with it with various other functions such as the cursor object below. To use it, the argument is a string with the path to the database.

connectino object
The attributes of the connection object.
  • Same for cursor object?

The cursor object can select data and execute code within the database after connecting. It essentially allows you to do what you would otherwise do with sqlite3 in the terminal.

cursor object
The attributes of the cursor object.
  • Look at conn object and cursor object in VSCode debugger. What attributes are in the object?

There are many special variables, function variables, and class variables in both. The unique attributes that I'm seeing are in_transaction:False, isolation_level: '', row_factory: None, and total_changes: 0 in the conn object and arraysize, lastrowid, row_factory, and rowcount.

  • Is "results" an object? How do you know?

It is an object because we can see that it has attributes, which are exclusive to objects.

results object
The attributes of the object saved under the variable 'results'.
import sqlite3

def read():
    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    
    # Execute a SELECT statement to retrieve data from a table
    results = cursor.execute('SELECT * FROM users').fetchall()

    # Print the results
    if len(results) == 0:
        print("Table is empty")
    else:
        for row in results:
            print(row)

    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
read()
(1, 'Thomas Edison', 'toby', 'sha256$178F1eHuRpw20ZGo$feb058fe482af33aa9e864986549c42245f3a70d87cd53b72bbd334aed5c0915', '1847-02-11')
(2, 'Nikola Tesla', 'niko', 'sha256$JPorVW1dBYaWtYdj$f6ff3ab3835f62e9e116ef793f25c33b1dd5df95ee46e02e4c6ed18b02b660b6', '2023-03-15')
(4, 'Eli Whitney', 'whit', 'sha256$NrSysJ9GQueU8Qaj$764f0bd228ab1e86203ad4f6c6ee7dc24701a0103af76130e252fb99194d36ba', '2023-03-15')
(5, 'Indiana Jones', 'indi', 'sha256$EA4t5WiMqOeqQtXC$243329b57d674ca11a23b0a7840f828a064414091eec0264a1272a5a7780821b', '1920-10-21')
(6, 'Marion Ravenwood', 'raven', 'sha256$QwKSrtvysyLsWTTm$5da65ce9a81bd379ec5d653bee364838f3a3c9ca3e8aa11faa11d24db208ba29', '1921-10-21')
(7, 'AJ Ruiz', 'kkcbal', 'sha256$RN6Q5qWnLZldKoAN$c684956f996a55f5ecd847c984d5f037d2289a0afb5adc09ba7f0fbd3124a7dd', '2006-05-18')
(8, 'Drew Reed', 'mrdew', 'budthefox', '2005-11-07')

Create a new User in table in Sqlite.db

Uses SQL INSERT to add row

  • Compare create() in both SQL lessons. What is better or worse in the two implementations?

The create() function in the first lesson is, in my opinion, simpler and easier to use because it works within the SQLite User data directly. It helps me, at least, to pull the User attributes and data and interact with them in Python. However, this create() function can be because it's a bit easier to use on a smaller scale without reliance on objects.

  • Explain purpose of SQL INSERT. Is this the same as User __init__?

SQL INSERT is very different from __init__ because, while __init__ initializes the User object and creates a set of attributes that the user can work with, SQL INSERT acts like a create function by adding in a new User object with the given attributes.

import sqlite3

def create():
    name = input("Enter your name:")
    uid = input("Enter your user id:")
    password = input("Enter your password")
    dob = input("Enter your date of birth 'YYYY-MM-DD'")
    
    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()

    try:
        # Execute an SQL command to insert data into a table
        cursor.execute("INSERT INTO users (_name, _uid, _password, _dob) VALUES (?, ?, ?, ?)", (name, uid, password, dob))
        
        # Commit the changes to the database
        conn.commit()
        print(f"A new user record {uid} has been created")
                
    except sqlite3.Error as error:
        print("Error while executing the INSERT:", error)


    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
#create()
A new user record mrdew has been created

Updating a User in table in Sqlite.db

Uses SQL UPDATE to modify password

  • What does the hacked part do?

The "hacked" part activates if the new password the person inputted is less than 2 characters. This is kind of a mean way to say that the password the person put in is not safe, and therefore is invalid.

  • Explain try/except. When would except occur?

Using try allows you to make code run up until an error occurs, at which point the except condition is activated. This allows programs to provide failsafes for errors or invalid user interactions.

  • What code seems to be repeated in each of these examples and why is it repeated?

Creating the connection and cursor objects is done in every block because, at the end of each, the connection and cursor are both closed. In order to use them again, they need to be re-activated.

import sqlite3

def update():
    uid = input("Enter user id to update")
    password = input("Enter updated password")
    if len(password) < 2:
        message = "hacked"
        password = 'gothackednewpassword123'
    else:
        message = "successfully updated"

    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()

    try:
        # Execute an SQL command to update data in a table
        cursor.execute("UPDATE users SET _password = ? WHERE _uid = ?", (password, uid))
        if cursor.rowcount == 0:
            # The uid was not found in the table
            print(f"No uid {uid} was not found in the table")
        else:
            print(f"The row with user id {uid} the password has been {message}")
            conn.commit()
    except sqlite3.Error as error:
        print("Error while executing the UPDATE:", error)
        
    
    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
#update()

Delete a User in table in Sqlite.db

Uses a delete function to remove a user based on a user input of the id.

  • Is DELETE a dangerous operation? Why?

DELETE can be a dangerous operation when left to user inputs. User error could lead to the deletion of important information that is difficult to retrieve.

  • What is the "f" and {uid} do?

The "f" prepares a print statement to read {uid} as the value of the variable uid rather than the literal text "{uid}".

import sqlite3

def delete():
    uid = input("Enter user id to delete")

    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()
    
    try:
        cursor.execute("DELETE FROM users WHERE _uid = ?", (uid,))
        if cursor.rowcount == 0:
            # The uid was not found in the table
            print(f"No uid {uid} was not found in the table")
        else:
            # The uid was found in the table and the row was deleted
            print(f"The row with uid {uid} was successfully deleted")
        conn.commit()
    except sqlite3.Error as error:
        print("Error while executing the DELETE:", error)
        
    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
#delete()

Menu Interface to CRUD operations

CRUD and Schema interactions from one location by running menu. Observe input at the top of VSCode, observe output underneath code cell.

  • Why does the menu repeat?
  • Could you refactor this menu? Make it work with a List?
def menu():
    operation = input("Enter: (C)reate, (R)ead, (U)pdate, (D)elete or (S)chema")
    if operation.lower() == 'c':
        create()
    elif operation.lower() == 'r':
        read()
    elif operation.lower() == 'u':
        update()
    elif operation.lower() == 'd':
        delete()
    elif operation.lower() == 's':
        schema()
    elif len(operation) == 0: # Escape Key
        return
    else:
        print("Please enter c, r, u, or d") 
    menu() # recursion, repeat menu
        
try:
    menu() # start menu
except:
    print("Perform Jupyter 'Run All' prior to starting menu")
(1, 'Thomas Edison', 'toby', 'sha256$178F1eHuRpw20ZGo$feb058fe482af33aa9e864986549c42245f3a70d87cd53b72bbd334aed5c0915', '1847-02-11')
(2, 'Nikola Tesla', 'niko', 'sha256$JPorVW1dBYaWtYdj$f6ff3ab3835f62e9e116ef793f25c33b1dd5df95ee46e02e4c6ed18b02b660b6', '2023-03-15')
(4, 'Eli Whitney', 'whit', 'sha256$NrSysJ9GQueU8Qaj$764f0bd228ab1e86203ad4f6c6ee7dc24701a0103af76130e252fb99194d36ba', '2023-03-15')
(5, 'Indiana Jones', 'indi', 'sha256$EA4t5WiMqOeqQtXC$243329b57d674ca11a23b0a7840f828a064414091eec0264a1272a5a7780821b', '1920-10-21')
(6, 'Marion Ravenwood', 'raven', 'sha256$QwKSrtvysyLsWTTm$5da65ce9a81bd379ec5d653bee364838f3a3c9ca3e8aa11faa11d24db208ba29', '1921-10-21')
(7, 'AJ Ruiz', 'kkcbal', 'sha256$RN6Q5qWnLZldKoAN$c684956f996a55f5ecd847c984d5f037d2289a0afb5adc09ba7f0fbd3124a7dd', '2006-05-18')
(8, 'Drew Reed', 'mrdew', 'budthefox', '2005-11-07')

Hacks

My Own Database

I decided to use the object oriented form of database because I prefer the orderliness that it affords me.

Features

Because I've been working with Gamemaker recently to try to make an RPG game (mainly for fun, but also to try applying what I'm learning in different ways), I decided to make a database with various (fake) video game bosses, with attributes for their stats, weaknesses, descriptions, etc.

To add a twist on the easier databases we've been working with recently, I decided to make it like a user forum. Each character in the database has room for users to add their strategies on how to beat it.

On top of that, to leave a forum comment, I decided to make it based on the user database from 2.4a. You'll have to connect to one of the users in the original database using your username and password. This connection uses the other type we worked on today.

Initializing and Creating the Database

The big code block below initializes and creates the database.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Setup of key Flask object (app)
app = Flask(__name__)
# Setup SQLAlchemy object and properties for the database (db)
database = 'sqlite:///vgenemy.db'  # path and filename of database
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = database
app.config['SECRET_KEY'] = 'SECRET_KEY'
db = SQLAlchemy()

# This belongs in place where it runs once per project
db.init_app(app)

""" database dependencies to support sqlite examples """
import datetime
from datetime import datetime
import json
from sqlalchemy.exc import IntegrityError

class Comment(db.Model):
    __tablename__ = 'comments'

    # Define the Notes schema
    id = db.Column(db.Integer, primary_key=True)
    note = db.Column(db.String(255), unique=False, nullable=False)
    userID = db.Column(db.String(255), unique=False, nullable=False)
    monsterID = db.Column(db.Integer, db.ForeignKey('enemies.id'))

    # Constructor of a Notes object, initializes of instance variables within object
    def __init__(self, uid, note):
        self.userID = uid
        self.note = note

    # Returns a string representation of the Notes object, similar to java toString()
    # returns string
    def __repr__(self):
        return "Notes(" + str(self.id) + "," + self.note + "," + str(self.userID) + ")"

    # CRUD create, adds a new record to the Notes table
    # returns the object added or None in case of an error
    def create(self):
        try:
            # creates a Notes object from Notes(db.Model) class, passes initializers
            db.session.add(self)  # add prepares to persist person object to Notes table
            db.session.commit()  # SqlAlchemy "unit of work pattern" requires a manual commit
            return self
        except IntegrityError:
            db.session.remove()
            return None

    # CRUD read, returns dictionary representation of Notes object
    # returns dictionary
    def read(self):
        return {
            "id": self.id,
            "userID": self.userID,
            "note": self.note,
            "monsterID": self.monsterID
        }

class Enemy(db.Model):
    __tablename__ = 'enemies'  # table name is plural, class name is singular
    # Define the User schema with "vars" from object
    id = db.Column(db.Integer, primary_key=True)
    _name = db.Column(db.String(255), unique=False, nullable=False)
    _desc = db.Column(db.String(255), unique=True, nullable=False)
    _att = db.Column(db.Integer, unique=False, nullable=False)
    _mag = db.Column(db.Integer, unique=False, nullable=False)
    _dfs = db.Column(db.Integer, unique=False, nullable=False)
    _spd = db.Column(db.Integer, unique=False, nullable=False)
    _comments = db.relationship('Comment', backref='enemies', lazy=True)
    # constructor of a User object, initializes the instance variables within object (self)
    def __init__(self, name, desc, att, mag, dfs, spd):
        self._name = name    # variables with self prefix become part of the object, 
        self._desc = desc
        self._att = att
        self._mag = mag
        self._dfs = dfs
        self._spd = spd
    @property
    def name(self):
        return self._name
    @name.setter
    def name(self, name):
        self._name = name
    @property
    def desc(self):
        return self._desc
    @desc.setter
    def desc(self, desc):
        self._desc = desc
    @property
    def att(self):
        return self._att
    @att.setter
    def att(self, att):
        self._att = att
    @property
    def mag(self):
        return self._mag
    @mag.setter
    def mag(self, mag):
        self._mag = mag
    @property
    def dfs(self):
        return self._dfs
    @dfs.setter
    def dfs(self, dfs):
        self._dfs = dfs
    @property
    def spd(self):
        return self._spd
    @spd.setter
    def spd(self, spd):
        self._spd = spd
    
    # output content using str(object) in human readable form, uses getter
    # output content using json dumps, this is ready for API response
    def __str__(self):
        return json.dumps(self.read())

    # CRUD create/add a new record to the table
    # returns self or None on error
    def create(self):
        try:
            # creates a person object from Enemy(db.Model) class, passes initializers
            db.session.add(self)  # add prepares to persist person object to enemies table
            db.session.commit()  # SqlAlchemy "unit of work pattern" requires a manual commit
            return self
        except IntegrityError:
            db.session.remove()
            return None
    # CRUD read converts self to dictionary
    # returns dictionary
    def read(self):
        return {
            "id": self.id,
            "name": self.name,
            "desc": self.desc,
            "att": self.att,
            "mag": self.mag,
            "dfs": self.dfs,
            "spd": self.spd,
        }
    # CRUD update: updates all values
    # returns self
    def update(self, name="", desc="", att=1, mag=1, dfs=1, spd=1):
        """only updates values with length"""
        if len(name) > 0:
            self.name = name
        if len(desc) > 0:
            self.desc = desc
        self.att = att
        self.mag = mag
        self.dfs = dfs
        self.spd = spd
        db.session.add(self)
        db.session.commit()
        return self

    # CRUD delete: remove self
    # None
    def delete(self):
        db.session.delete(self)
        db.session.commit()
        return None

"""Database Creation and Testing """


# Builds working data for testing
def initEnemies():
    with app.app_context():
        """Create database and tables"""
        db.create_all()
        """Tester data for table"""
        e1 = Enemy(name='Forest Guardian', desc='A lumbering green beast with sharp horns.', att=85, mag=70, dfs=80, spd=50)
        e2 = Enemy(name='Sea Guardian', desc='A strange, unpredictable fish with long whiskers.', att=60, mag=100, dfs=85, spd=80)
        e3 = Enemy(name='Terra Guardian', desc='A keen, sharp-clawed mole with a drill tail.', att=110, mag=80, dfs=60, spd=100)
        e4 = Enemy(name='Molten Guardian', desc='A giant lava monster with metal accessories.', att=85, mag=115, dfs=85, spd=90)
        e5 = Enemy(name='Sky Guardian', desc='A valiant, flying knight bird with armor.', att=100, mag=100, dfs=100, spd=100)

        enemies = [e1, e2, e3, e4, e5]

        """Builds sample user/note(s) data"""
        for enemy in enemies:
            try:
                comment = "Share your strategies here!"
                enemy._comments.append(Comment("Admin", comment))
                '''add user to table'''
                object = enemy.create()
                print(f'Created new enemy "{object.name}".')
            except:  # error raised if object nit created
                '''fails with bad or duplicate data'''
                print(f"Error creating {enemy.name} data.")
                
initEnemies()
Created new enemy "Forest Guardian".
Created new enemy "Sea Guardian".
Created new enemy "Terra Guardian".
Created new enemy "Molten Guardian".
Created new enemy "Sky Guardian".

Enemy Read Function

A read function is nice to have for future use in the commenting code. This is the code corresponding to the enemy database, however; it will not have full CRUD code because the list of monsters is simply there for reference. I made it a corresponding parent database for simplicity.

def e_read():
    with app.app_context():
        table = Enemy.query.all()
    json_ready = [enemy.read() for enemy in table] # each user adds user.read() to list
    return json_ready

The code above successfully creates all five of the template enemies.

Code for Adding Comments (Create)

Now that the enemies have been made in the database, I created a function below that lets the user comment on a specific monster. The parameters correspond to code in the verify function found later.

def make_comment(userID):
    with app.app_context():
        id_inp = input("Please input the number corresponding to the enemy you would like to comment on.")
        try:
            id_sel = int(id_inp)
        except:
            print("Please input an integer corresponding to an enemy.")
            make_comment(userID)
        enemy = Enemy.query.filter_by(id=id_sel).first()
        if enemy:
            print("Enemy found. Please enter a comment.")
            c_enter = input("Please discuss your thoughts on/strategies for this enemy.")
            print("Your comment will look like this:")
            print(f'{userID}: {c_enter}')
            print("Is this alright?")
            consent = input('Enter "y" for YES and "n" for NO.').lower()
            if consent == "y":
                # Connect to the database file
                conn = sqlite3.connect('instance/vgenemy.db')
                # Create a cursor object to execute SQL commands
                cursor = conn.cursor()
                try:
                    # Execute an SQL command to insert data into a table
                    cursor.execute("INSERT INTO comments (note, userID, monsterID) VALUES (?, ?, ?)", (c_enter, userID, id_sel))
                    # Commit the changes to the database
                    conn.commit()
                except sqlite3.Error as error:
                    print("Error while executing the INSERT:", error)
                # Close the cursor and connection objects
                cursor.close()
                conn.close()
                print("Comment posted successfully!")
            else:
                make_comment(userID)
        else:
            print("Enemy not found.")
            make_comment(userID)

Connecting to the Users Database

The code below uses the sqlite3 library to connect to the User database and verify the user with a user ID and password input.

import sqlite3

udb = 'instance/sqlite.db' # this is location of database

def verify():
    # Connect to the database file
    conn = sqlite3.connect(udb)

    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    
    # get user input
    uid = input("Enter your user ID: ")
    password = input("Enter your password: ")

    # Execute a SELECT statement to retrieve data from the table
    cursor.execute("SELECT * FROM users WHERE _uid = ? AND _password = ?", (uid, password))

    # fetch the first matching row
    user = cursor.fetchone()

    #closing cursor
    cursor.close()
    conn.close()
    
    #checking if user with matching data exists
    if user:
        print(f"Welcome, {user[1]}!")
        uid = user[2]
        for enemy in e_read():
            print(f'{enemy["id"]}. {enemy["name"]}')
        make_comment(uid)
    else:
        print(f'User with ID "{uid}" and password "{password}" not found.')
        return

Testing Functionality

Here's a test to show the process of adding a comment.

verify()
Welcome, Drew Reed!
1. Forest Guardian
2. Sea Guardian
3. Terra Guardian
4. Molten Guardian
5. Sky Guardian
Enemy found. Please enter a comment.
Your comment will look like this:
mrdew: He always charges up strong attacks, so guard after he does! He is weak to fire magic.
Is this alright?
Comment posted successfully!

Displaying Comments for a Certain Enemy

Now that I have successfully created code to comment on the enemies in the database, it's time to create some code to display the information and comments on a certain enemy.

I did so in the function below.

from IPython.display import clear_output

def forum():
    for enemy in e_read():
        print(f'{enemy["id"]}. {enemy["name"]}')
    id_inp = input("Please input the number corresponding to the enemy you would like to comment on.")
    try:
        id_sel = int(id_inp)
    except:
        print("Please input an integer corresponding to an enemy.")
        forum()
    with app.app_context():
        enemy = Enemy.query.filter_by(id=id_sel).first()
        if enemy:
            table = Comment.query.all()
            comments = [comment.read() for comment in table]
            print("\n" + enemy._name + "\n")
            print('Base Stats:')
            print(f'Attack: {enemy._att} | Magic: {enemy._mag} | Defense: {enemy._dfs} | Speed: {enemy._spd}\n')
            print('Comments:')
            for comment in comments:
                if comment["monsterID"] == id_sel:
                    print(f'{comment["userID"]}: {comment["note"]}')
        else:
            print("Enemy not found. Make sure to input a corresponding integer.")

forum()
1. Forest Guardian
2. Sea Guardian
3. Terra Guardian
4. Molten Guardian
5. Sky Guardian

Forest Guardian

Base Stats:
Attack: 85 | Magic: 70 | Defense: 80 | Speed: 50

Comments:
Admin: Share your strategies here!
mrdew: He always charges up strong attacks, so guard after he does! He is weak to fire magic.

Finishing the Comments CRUD

I already made the make_comment function that essentially acts like a create function. The grading standards got changed to include the necessity to make a menu with create, read, update and delete, so I decided to go through and make the three remaining functions for the comments.

Read

I don't have a formal read function for comments, so I made it below.

import sqlite3

def c_read():
    with app.app_context():
        table = Comment.query.all()
    json_ready = [comment.read() for comment in table] # each user adds user.read() to list
    return json_ready
    
c_read()

Update

This one is less self-explanatory and a bit more difficult, so I describe how it works below the code.

cdb = 'instance/vgenemy.db'

def c_update(userID):
    # Printing all of the user's comments
    i = 0
    u_comments = []
    for comment in c_read():
        if comment['userID'] == userID:
            i += 1
            print(str(i) + f'. {comment["userID"]}: {comment["note"]}')
            u_comments.append(comment)
    e_comment = input("Which comment would you like to edit?")
    try:
        c_sel = int(e_comment) - 1 # using user input as index
        chosen_c = u_comments[c_sel]
        pass
    except:
        print("Invalid input. Enter an integer corresponding to a listed comment.")
        c_update(userID)
    new_note = input("What would you like your comment to say instead?")
    print("Your comment will look like this:")
    print(f'{userID}: {new_note}')
    print("Is this alright?")
    consent = input('Enter "y" for YES and "n" for NO.').lower()
    if consent == "y":
        # Connect to the database file
        conn = sqlite3.connect(cdb)
        # Create a cursor object to execute SQL commands
        cursor = conn.cursor()
        try:
            # Execute an SQL command to update data in a table
            cursor.execute("UPDATE comments SET note = ? WHERE id = ?", (new_note, chosen_c["id"]))
            if cursor.rowcount == 0:
                # The uid was not found in the table
                print(f"No ID {chosen_c['id']} was not found in the table.")
            else:
                print("Your comment has been updated successfully!")
                conn.commit()
        except sqlite3.Error as error:
            print("Error while executing the UPDATE:", error)
        # Close the cursor and connection objects
        cursor.close()
        conn.close()
    else:
        return
1. mrdew: He always charges up strong attacks, so guard after he does! He is weak to fire magic.
Your comment will look like this:
mrdew: He always charges up strong attacks, so guard after he does. He takes more damage from magic attacks, so try setting up weather if you can!
Is this alright?
Your comment has been updated successfully!

I left a sample of it running with my username above. It works by first printing out all of the comments created by the logged-in user. The user then selects a comment to be edited based on the number associated with it. That number corresponds to the index of one of the listed comments, which the user can then change by inputting a new comment. That new comment replaces the old one with the execute function.

Delete

The delete code borrows a lot from the update code, so most of how that one worked applies to this one. The main difference is the execute code.

def c_delete(userID):
    # Printing all of the user's comments
    i = 0
    u_comments = []
    for comment in c_read():
        if comment['userID'] == userID:
            i += 1
            print(str(i) + f'. {comment["userID"]}: {comment["note"]}')
            u_comments.append(comment)
    d_comment = input("Which comment would you like to delete?")
    try:
        c_sel = int(d_comment) - 1 # using user input as index
        chosen_c = u_comments[c_sel]
        pass
    except:
        print("Invalid input. Enter an integer corresponding to a listed comment.")
        update(userID)
    print("Your comment will be permanently deleted. Is that alright?")
    consent = input('Enter "y" for YES and "n" for NO.').lower()
    if consent == "y":
        # Connect to the database file
        conn = sqlite3.connect(cdb)
        # Create a cursor object to execute SQL commands
        cursor = conn.cursor()
        try:
            # Execute an SQL command to update data in a table
            cursor.execute("DELETE FROM comments WHERE id = ?", (chosen_c['id'],))
            if cursor.rowcount == 0:
                # The uid was not found in the table
                print(f"No ID {chosen_c['id']} was not found in the table.")
            else:
                print("Your comment has been deleted successfully!")
                conn.commit()
        except sqlite3.Error as error:
            print("Error while executing the UPDATE:", error)
        # Close the cursor and connection objects
        cursor.close()
        conn.close()
    else:
        return

Now that all of the CRUD features are done, I decided to make a menu that takes advantage of the user log-in capabilities I created before. The user starts by logging in (based on the information provided to the users database). After that, the user can make comments, read other comments, or update and delete their own comments.

Because of the forum-like nature of this menu system, the c_read function created above is not used in the menu directly. Instead, the forum function, which also reads comment data, is placed in the menu instead.

udb = 'instance/sqlite.db'

def login():
    # Connect to the database file
    conn = sqlite3.connect(udb)
    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    # get user input
    uid = input("Enter your user ID: ")
    password = input("Enter your password: ")
    # Execute a SELECT statement to retrieve data from the table
    cursor.execute("SELECT * FROM users WHERE _uid = ? AND _password = ?", (uid, password))
    # fetch the first matching row
    user = cursor.fetchone()
    #closing cursor
    cursor.close()
    conn.close()
    #checking if user with matching data exists
    if user:
        print(f"Welcome, {user[1]}!")
        uid = user[2]
        c_menu(uid)
    else:
        print("User not found. Make sure that your user ID and password are inputted correctly.")
        login()

# Even COOLER menu to run other cells from one control point
def c_menu(userID):
    operation = input("Enter: (C)reate, (R)ead, (U)pdate or (D)elete")
    if operation.lower() == 'c':
        for enemy in e_read():
            print(f'{enemy["id"]}. {enemy["name"]}')
        make_comment(userID)
    elif operation.lower() == 'r':
        forum()
    elif operation.lower() == 'u':
        c_update(userID)
    elif operation.lower() == 'd':
        c_delete(userID)
    elif len(operation) == 0: # Escape Key
        return
    else:
        print("Please enter c, r, u, or d") 
    c_menu(userID) # repeat the menu
        
try:
    login() # log in to your account
except:
    print("Bye-bye!")
Welcome, Drew Reed!
1. Forest Guardian
2. Sea Guardian
3. Terra Guardian
4. Molten Guardian
5. Sky Guardian
Enemy found. Please enter a comment.
Your comment will look like this:
mrdew: His eyes glow red when he's charging a magic attack, and since they do less damage, you can spend that turn attacking or healing. If his eyes glow blue, it means he's charging a strong physical attack, so guard!
Is this alright?
Comment posted successfully!
1. mrdew: He always charges up strong attacks, so guard after he does. He takes more damage from magic attacks, so try setting up weather if you can!
2. mrdew: His eyes glow red when he's charging a magic attack, and since they do less damage, you can spend that turn attacking or healing. If his eyes glow blue, it means he's charging a strong physical attack, so guard!
Your comment will be permanently deleted. Is that alright?
Your comment has been deleted successfully!

Reflection

I had never worked with parent-child objects before this, nor had I understood how to use ForeignKey attributes before this, but I worked on it for a very long time and eventually still got it to work.

Doing this made me realize that sometimes, the procedural, non-object-oriented method we learned in class today is just easier. The classes were a lot to juggle, and I found myself really struggling to jump between the two different methods.

At the same time, I was doing that to demonstrate knowledge. If I didn't have to do any of that, I could have more easily used two separate sqlite tables together.

Procedural Abstraction Question

We were tasked to answer where procedural abstraction can be seen in our new databases. In mine, it can very easily be seen all throughout the process, starting with the Comment and Enemy class functions and ending with the forum function. In each case, I created new functions to prevent other functions from becoming too bulky or difficult to understand.

For example, I made sure that the primary focus of verify was to log the user in before making a comment. Then, I had it transition into the make_comment function when it was time for that step in the process.

I also used it for e_read so that I could grab the up-to-date enemy table data in an easy-to-manipulate JSON format whenever I needed to in other functions, including make_comment and forum. I think this was some pretty valuable foresight.

The CRUD all works with named procedures to make the menu more accessible.