From 9e544ec64a3c2f48592040299a90fdd96d306fbe Mon Sep 17 00:00:00 2001 From: Sara Montecino Date: Sun, 30 Oct 2022 20:15:48 -0700 Subject: [PATCH] Add frontend for updating transactions --- .vscode/launch.json | 8 + app.py | 127 ++++++++++++ budget.db | Bin 61440 -> 0 bytes database.py | 16 +- main.py | 18 +- model.py | 4 +- templates/index.html | 455 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 608 insertions(+), 20 deletions(-) create mode 100755 app.py delete mode 100755 budget.db create mode 100755 templates/index.html diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c8615d..a4ddf99 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,14 @@ "program": "database.py", "console": "integratedTerminal", "justMyCode": true + }, + { + "name": "Flask", + "type": "python", + "request": "launch", + "program": "app.py", + "console": "integratedTerminal", + "justMyCode": true } ] } \ No newline at end of file diff --git a/app.py b/app.py new file mode 100755 index 0000000..e436922 --- /dev/null +++ b/app.py @@ -0,0 +1,127 @@ +from datetime import datetime + +from flask import Flask, render_template, request, abort +import peewee +from playhouse.shortcuts import model_to_dict + +import database + +app = Flask(__name__) +app.config["DEBUG"] = True + +@app.before_request +def before_request(): + database.instance.connect() + +@app.after_request +def after_request(response): + database.instance.close() + return response + +@app.route("/") +def home(): + puppies = ['hollie', 'grace', 'loki'] + return render_template('index.html', puppies=puppies) + +@app.route("/users") +def users(): + args = request.args.to_dict() + if 'username' not in args: + abort(400) + + username = args['username'].strip() + try: + return model_to_dict(database.User.get(database.User.username == username)) + except peewee.DoesNotExist: + abort(404) + except: + abort(500) + +@app.route("/transactions", methods=["GET", "POST"]) +def transactions(): + if request.method == "GET": + args = request.args.to_dict() + if 'user_id' not in args: + abort(400) + + user_id = args['user_id'].strip() + try: + july_2022 = datetime(2022, 7, 1, 0) + transactions = database.Transaction.select().where( + (database.Transaction.user == user_id) & + (database.Transaction.transaction_date > july_2022) + ) + return [model_to_dict(t) for t in transactions] + except peewee.DoesNotExist: + abort(404) + except: + abort(500) + + if request.method == "POST": + body = request.get_json() + try: + user = database.User.get(database.User.uuid == body['user_id']) + category = database.TransactionCategory.get(database.TransactionCategory.name == body['category']) + database.Transaction.create( + user=user, + subcategory=category, + transaction_date=body['transaction_date'], + description=body['description'], + amount=body['amount'], + type=body['type'], + notes=body['notes'] + ) + return ('', 200) + except peewee.DoesNotExist: + abort(404) + except KeyError: + abort(400) + + # Default if no method is hit + abort(400) + +@app.route("/transactions/", methods=["PUT"]) +def transaction(uuid=None): + if request.method == "PUT": + if not uuid: + abort(400) + + body = request.get_json() + if 'category' not in body or 'notes' not in body: + abort(400) + + try: + category = database.TransactionCategory.get(database.TransactionCategory.name == body['category']) + transaction = database.Transaction.get(database.Transaction.primary_key == uuid) + transaction.subcategory = category + transaction.notes = body['notes'] + transaction.save() + return ('', 200) + except peewee.DoesNotExist: + abort(404) + except: + abort(500) + + # Default if no method is hit + abort(400) + +@app.route("/categories") +def categories(): + try: + raw_categories = database.TransactionCategory.select() + categories = {} + for category in raw_categories: + if not category.parent: + categories[category.name] = [] + + for category in raw_categories: + if category.parent: + categories[category.parent.name].append(category.name) + + return categories + except peewee.DoesNotExist: + abort(404) + except: + abort(500) + +app.run() \ No newline at end of file diff --git a/budget.db b/budget.db deleted file mode 100755 index c007350b3611e83134ba45e31439decd96feb3d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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 J3-M70{|E6=SDyd? diff --git a/database.py b/database.py index d2f0f6f..2d45cf4 100755 --- a/database.py +++ b/database.py @@ -1,4 +1,5 @@ import datetime +import uuid from peewee import * DATABASE = 'budget.db' @@ -7,7 +8,7 @@ instance = SqliteDatabase(DATABASE, pragmas=[('foreign_keys', 'on')]) def create_tables(): """Helper function to create database tables. Should be called manually.""" with instance: - instance.create_tables([User, TransactionCategory, Transaction, Source]) + instance.create_tables([User, TransactionCategory, Transaction]) def add_user(): # Make my user. @@ -45,24 +46,20 @@ class BaseModel(Model): database = instance class User(BaseModel): - username = CharField(unique=True, primary_key=True) + uuid = UUIDField(unique=True, primary_key=True, default=uuid.uuid4()) + username = CharField(unique=True) class TransactionCategory(BaseModel): primary_key = AutoField(primary_key=True) name = CharField(unique=True) parent = ForeignKeyField('self', null=True, backref='children') -class Source(BaseModel): - filename=CharField(unique=True, primary_key=True) - type = IntegerField() - created_date = DateTimeField(default=datetime.datetime.now) - user = ForeignKeyField(User, backref='transactions') - class Transaction(BaseModel): # Metadata primary_key = AutoField(primary_key=True) - source = ForeignKeyField(Source, backref='transactions') created_date = DateTimeField(default=datetime.datetime.now) + source_file=CharField(null=True) + type = IntegerField() user = ForeignKeyField(User, backref='transactions') # Real data @@ -70,6 +67,7 @@ class Transaction(BaseModel): description = CharField() amount = FloatField() subcategory = ForeignKeyField(TransactionCategory, backref='+', null=True) + notes = CharField(null=True) if __name__ == "__main__": create_tables() diff --git a/main.py b/main.py index 9b28ebf..9a0d7de 100755 --- a/main.py +++ b/main.py @@ -9,10 +9,14 @@ parser = argparse.ArgumentParser(prog="BudgetBear", description="Calculate a bud parser.add_argument('files', type=str, nargs='+', help='File to parse transactions from.') args = parser.parse_args() -username = 'ciphercules' +# Get my user. +database.instance.connect() +user = database.User.select().where(database.User.username == 'ciphercules').get() +database.instance.close() file_parsers = [capital_one.Parser()] for f in args.files: + filename=os.path.basename(f), for file_parser in file_parsers: # Use the first successful parser. transactions = file_parser.parse(f) @@ -22,22 +26,16 @@ for f in args.files: # Add to database database.instance.connect() with database.instance.atomic(): - # Add source file first. - source = database.Source.create( - filename=os.path.basename(f), - type=file_parser.source, - user=username - ) - # Add each transaction for transaction in transactions: date, description, amount = transaction database.Transaction.create( - user=username, + user=user, transaction_date=date, description=description, amount=amount, - source=source + source_filename=filename, + type=file_parser.source ) database.instance.close() diff --git a/model.py b/model.py index fbf73f5..4efc8ec 100755 --- a/model.py +++ b/model.py @@ -11,4 +11,6 @@ class BaseParser: class TransactionSource: """Enum of possible transaction sources""" - CAPITAL_ONE = 1 \ No newline at end of file + CAPITAL_ONE = 1 + SPLITWISE = 2 + COMPANY = 3 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100755 index 0000000..2b805b4 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,455 @@ + + + + BudgetBear + + + + +

BudgetBear

+

The app for bears on a budget

+
+ + + +
+ + +

Transactions

+ + + + + + + + + \ No newline at end of file