From 737c9d687d6068565fa31a4601608a42e6f44d6d Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 14:58:47 -0500 Subject: [PATCH 01/22] adds init file for flask and flask-sqlalchemy --- app/__init__.py | 7 +++++++ app/models.py | 0 requirements.txt | 8 ++++++++ 3 files changed, 15 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/models.py create mode 100644 requirements.txt diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 000000000..c4c45a396 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,7 @@ +# -*- encoding: utf-8 -*- +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////expenses.db' +db = SQLAlchemy(app) diff --git a/app/models.py b/app/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..ce6cc0887 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +click==6.6 +Flask==0.11.1 +Flask-SQLAlchemy==2.1 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 +SQLAlchemy==1.1.4 +Werkzeug==0.11.11 From d19dda60ad8943909ef3dbcceb356afa4d6fb455 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 15:50:55 -0500 Subject: [PATCH 02/22] adds expense model --- .gitignore | 1 + app/models.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..eb8bc17ca --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +expenses \ No newline at end of file diff --git a/app/models.py b/app/models.py index e69de29bb..e11e72efa 100644 --- a/app/models.py +++ b/app/models.py @@ -0,0 +1,32 @@ +# -*- encoding: utf-8 -*- +from app import db + + +class Expense(db.Model): + # id + id = db.Column(db.Integer, primary_key=True) + + # date + date = db.Column(db.Date) + + # category + category = db.relationship( + 'Category', + backref=db.backref('expenses', lazy='dynamic') + ) + + # employee + employee = db.relationship( + 'Employee', + backref=db.backref('expenses', lazy='dynamic') + ) + + # tax + tax_id = db.Column(db.Integer, db.ForeignKey('tax.id')) + tax = db.relationship('Tax') + + # expense description + description = db.Column(db.String(255)) + + # pre-tax amount + pre_tax_amount = db.Column(db.Float) From 5856a3a5f68e7fa7fbb9d56fca53b62f6a084f8a Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 17:29:43 -0500 Subject: [PATCH 03/22] moves models to their own folder and creates Category model --- app/models/Category.py | 10 ++++++++++ app/{models.py => models/Expense.py} | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 app/models/Category.py rename app/{models.py => models/Expense.py} (91%) diff --git a/app/models/Category.py b/app/models/Category.py new file mode 100644 index 000000000..60f742e3e --- /dev/null +++ b/app/models/Category.py @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +from app import db + + +class Category(db.Model): + # id + id = db.Column(db.Integer, primary_key=True) + + # name of category + name = db.Column(db.String(255)) \ No newline at end of file diff --git a/app/models.py b/app/models/Expense.py similarity index 91% rename from app/models.py rename to app/models/Expense.py index e11e72efa..4fb6e2f28 100644 --- a/app/models.py +++ b/app/models/Expense.py @@ -22,7 +22,6 @@ class Expense(db.Model): ) # tax - tax_id = db.Column(db.Integer, db.ForeignKey('tax.id')) tax = db.relationship('Tax') # expense description From 95b07cff8f7feee73a3eed264b69674988ae81c4 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 17:47:39 -0500 Subject: [PATCH 04/22] adds remaining models --- app/__init__.py | 1 + app/__init__.pyc | Bin 0 -> 414 bytes app/models/Category.py | 8 +++++++- app/models/Exmployee.py | 20 ++++++++++++++++++++ app/models/Expense.py | 11 +++++++++++ app/models/Tax.py | 20 ++++++++++++++++++++ app/models/Tax.pyc | Bin 0 -> 860 bytes app/models/__init__.py | 0 app/models/__init__.pyc | Bin 0 -> 109 bytes 9 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 app/__init__.pyc create mode 100644 app/models/Exmployee.py create mode 100644 app/models/Tax.py create mode 100644 app/models/Tax.pyc create mode 100644 app/models/__init__.py create mode 100644 app/models/__init__.pyc diff --git a/app/__init__.py b/app/__init__.py index c4c45a396..389605fbc 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,4 +4,5 @@ app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////expenses.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) diff --git a/app/__init__.pyc b/app/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9572a7fa9f0cbc2c55981788adc8550bdaf7b00b GIT binary patch literal 414 zcmY+AO;5ux42GR{?Z(7JPjExx*3))J6C!k*sMv>S3xaWox+a4{+I32h*qNWq4*;hf zLO98rJh2nUvA=rVGw10cgT2`}U*b^rC>%Qh2~ZGN5+H^xkQNj+ENviyfM~%dIvwCP z2nSwW;4K)6_AfXHwZ}wtfV&X;ZM-`FM)L`-w1YB{sd{U$ix}TdsmxwPu~Wy0YAbUs z&If4XqY|YOs?S&YWFI*VuE(=SHlhi=qVbsBMWH_Z?@J;Y+_2eV6i&i`CgEZp&;0|8 zsk5(5_6P~Ihf*WMq-pbpsD5S#J&cuUAsEx!Rnq_f literal 0 HcmV?d00001 diff --git a/app/models/Category.py b/app/models/Category.py index 60f742e3e..029cb856d 100644 --- a/app/models/Category.py +++ b/app/models/Category.py @@ -7,4 +7,10 @@ class Category(db.Model): id = db.Column(db.Integer, primary_key=True) # name of category - name = db.Column(db.String(255)) \ No newline at end of file + name = db.Column(db.String(255)) + + def __init__(self, name): + self.name = name + + def __repr__(self): + return '' % self.name \ No newline at end of file diff --git a/app/models/Exmployee.py b/app/models/Exmployee.py new file mode 100644 index 000000000..35d97c48f --- /dev/null +++ b/app/models/Exmployee.py @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +from app import db + + +class Employee(db.Model): + # id + id = db.Column(db.Integer, primary_key=True) + + # full name of employee + name = db.Column(db.String(255)) + + # employee's full address + address = db.Column(db.String(255)) + + def __init__(self, name, address): + self.name = name + self.address = address + + def __repr__(self): + return '' % self.name diff --git a/app/models/Expense.py b/app/models/Expense.py index 4fb6e2f28..4bae9c8ab 100644 --- a/app/models/Expense.py +++ b/app/models/Expense.py @@ -29,3 +29,14 @@ class Expense(db.Model): # pre-tax amount pre_tax_amount = db.Column(db.Float) + + def __init__(self, date, category, employee, tax, description, pre_tax_amount): + self.date = date + self.category = category + self.employee = employee + self.tax = tax + self.description = description + self.pre_tax_amount = pre_tax_amount + + def __repr__(self): + return '' % self.description diff --git a/app/models/Tax.py b/app/models/Tax.py new file mode 100644 index 000000000..46af76f97 --- /dev/null +++ b/app/models/Tax.py @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +from app import db + + +class Tax(db.Model): + # id + id = db.Column(db.Integer, primary_key=True) + + # name of tax + name = db.Column(db.String(255)) + + # tax amount as a float + amount = db.Column(db.Float) + + def __init__(self, name, amount): + self.name = name + self.amount = amount + + def __repr__(self): + return '' % self.name diff --git a/app/models/Tax.pyc b/app/models/Tax.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a79f450e41da962835a4c3e86a22ee8e8c86e7c1 GIT binary patch literal 860 zcma)4O>fgc5S_Ktv`HzSQq?0DBwwI^psEm+kT{To(*qc!m5H~NgTJ)9ks7JD@Zb1B z{DAV_Bq}`tmUlcmo_+J)v(sNk)1RX+pBA#aQas;ch1UobZX=CIYost@szjWTWg=Hb zs#Jc6Y(zd6sZ=AGXL~qN-{Hy(vX~IyTO{CRd2>Htg#A0L@D;%caa|(IRO5&v-JWcW zDaH@#`{c3FyEu4IpRia=1;jw6INY%+9!WmWI4K??48Cqlzqa4qy1v7Hv5&w-iWgYn z7~$5)t%yaEXab1>Rn#)e*cl1fm92|oBxT#Lx|k70Uj)}&%NPR(KL8x@y_n>P?3xwJVPjl;k4~5*Mw7e_-0r~;#ymGb+q;g+c-_k z#4JdZ>s}P4!#0RoiX6{X!z>Ws3}7!``EwPgq_-Krpbq~x1MdcJ?F$?T94IC`Lfp}# zN!G?GD%OJks=+pAn_73#H>=T;H6(V{cGw<+vd^N-L=w-WcI%4%c$<= zM9!)258J;`&S?t~0?&<;lk_Bgo=irw?JvTQ2BWw9*!Gc*>!JUGSJ7u%nddY^9eJAm E26v~Ha{vGU literal 0 HcmV?d00001 diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/__init__.pyc b/app/models/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a91a5ca594c19752e2f515430afcc4f18460b6a GIT binary patch literal 109 zcmZSn%*$oW8Xb|$00oRd+5w1*S%5?e14FO|NW@PANHCxg#bQ9Q#DW6--29Z(oMQd> f_{_Y_lK6PNg31yOpp*?zyfi1(4rD+v5HkP(9jFn- literal 0 HcmV?d00001 From 8169e55310f7493be9c2aa7a0bde87702548e1de Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 18:02:03 -0500 Subject: [PATCH 05/22] adds autoincrement --- app/models/Category.py | 2 +- app/models/Exmployee.py | 2 +- app/models/Expense.py | 4 +++- app/models/Tax.py | 2 +- app/models/Tax.pyc | Bin 860 -> 804 bytes 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/Category.py b/app/models/Category.py index 029cb856d..c88d29cf1 100644 --- a/app/models/Category.py +++ b/app/models/Category.py @@ -4,7 +4,7 @@ class Category(db.Model): # id - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) # name of category name = db.Column(db.String(255)) diff --git a/app/models/Exmployee.py b/app/models/Exmployee.py index 35d97c48f..b89ac0745 100644 --- a/app/models/Exmployee.py +++ b/app/models/Exmployee.py @@ -4,7 +4,7 @@ class Employee(db.Model): # id - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) # full name of employee name = db.Column(db.String(255)) diff --git a/app/models/Expense.py b/app/models/Expense.py index 4bae9c8ab..3a53cf977 100644 --- a/app/models/Expense.py +++ b/app/models/Expense.py @@ -4,7 +4,7 @@ class Expense(db.Model): # id - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) # date date = db.Column(db.Date) @@ -40,3 +40,5 @@ def __init__(self, date, category, employee, tax, description, pre_tax_amount): def __repr__(self): return '' % self.description + + def post_tax_amount diff --git a/app/models/Tax.py b/app/models/Tax.py index 46af76f97..b0f22e6a2 100644 --- a/app/models/Tax.py +++ b/app/models/Tax.py @@ -4,7 +4,7 @@ class Tax(db.Model): # id - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_key=True, autoincrement=True) # name of tax name = db.Column(db.String(255)) diff --git a/app/models/Tax.pyc b/app/models/Tax.pyc index a79f450e41da962835a4c3e86a22ee8e8c86e7c1..aa14b395ce503fbd08870ccd0099f896e4cbd9c1 100644 GIT binary patch delta 64 zcmcb^wuFt1`7RF-ll~L~&|PS`agk5d`9uZ~%$a+=86^ z%GA^nkZ?(2MF}^MPDw3JF3Kz@$;{6y;REsuic;f&a`B0|`K5U!lY5yIHdipRGqQ03 M<$|~;w=&HK09^nd!2kdN From 9e32dfa85cadf7aaefa254111f361dade86398a5 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 18:06:22 -0500 Subject: [PATCH 06/22] correct mispelling and adds hybrid property for post tax amount in expense --- app/models/{Exmployee.py => Employee.py} | 0 app/models/Expense.py | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) rename app/models/{Exmployee.py => Employee.py} (100%) diff --git a/app/models/Exmployee.py b/app/models/Employee.py similarity index 100% rename from app/models/Exmployee.py rename to app/models/Employee.py diff --git a/app/models/Expense.py b/app/models/Expense.py index 3a53cf977..968ecdc26 100644 --- a/app/models/Expense.py +++ b/app/models/Expense.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from app import db +from sqlalchemy.ext.hybrid import hybrid_property class Expense(db.Model): @@ -41,4 +42,6 @@ def __init__(self, date, category, employee, tax, description, pre_tax_amount): def __repr__(self): return '' % self.description - def post_tax_amount + @hybrid_property + def post_tax_amount(self): + return self.pre_tax_amount * (1.0 + self.tax.amount) From 834e8dde465ed861f041cc2e9e6cc807e7e78907 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 18:11:58 -0500 Subject: [PATCH 07/22] fixes init file --- app/__init__.py | 2 +- app/__init__.pyc | Bin 414 -> 413 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 389605fbc..bc458f0f3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,6 +3,6 @@ from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////expenses.db' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///expenses.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) diff --git a/app/__init__.pyc b/app/__init__.pyc index 9572a7fa9f0cbc2c55981788adc8550bdaf7b00b..e310044d29c5356ff97cf0a4b8f02ccbf44ef444 100644 GIT binary patch delta 29 lcmbQoJeQe``73bGStr7#NBRb23X(ttPJf004Q(32FcU delta 30 mcmbQsJdc@;`7 Date: Wed, 21 Dec 2016 18:13:20 -0500 Subject: [PATCH 08/22] ignores pyc and db files --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eb8bc17ca..1a4e553ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -expenses \ No newline at end of file +expenses +*.pyc +*.db \ No newline at end of file From f66f1bd974d82b05fec2c1fc1668db04d19308a5 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 18:28:31 -0500 Subject: [PATCH 09/22] adds script to generate databse on init if it doesn't exist --- .gitignore | 3 ++- app/__init__.py | 6 ++++++ app/models/Tax.pyc | Bin 804 -> 828 bytes app/models/__init__.py | 6 ++++++ app/models/__init__.pyc | Bin 109 -> 367 bytes 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1a4e553ac..09904a317 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ expenses *.pyc -*.db \ No newline at end of file +*.db +.DS_Store \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index bc458f0f3..ba16886fa 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,8 +1,14 @@ # -*- encoding: utf-8 -*- +import os.path +from app.models import * from flask import Flask from flask_sqlalchemy import SQLAlchemy + app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///expenses.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) + +if not os.path.isfile('./expenses.db'): + db.create_all() diff --git a/app/models/Tax.pyc b/app/models/Tax.pyc index aa14b395ce503fbd08870ccd0099f896e4cbd9c1..0df08513028754db9b9ae115b8f0cdda744bdf86 100644 GIT binary patch delta 192 zcmZ3&wug<4`7{T%0tqMp`O`ks+0pp_!2>ij5(ai6M&_OtLei zFil)7YsJja!oU#4&5*(ZX7Dfs1#7SZ^_Fl0iGrfc+{B{F`0Uim5?&xbv9u&VGcUO) zH8(Y{Wa5W-HWr{@5bNYFCgsT$j3L}0t_CAWj14UIi?L7&Bvt}4(Jd!Gu_Oq@3StKl Q9Ftp_EEwe`?_){>0GZP%-T(jq delta 169 zcmdnPwuFt1`7U4BQ4U*$QZ@Okjli6#SA9d8B!Q0Zj{wv zVrXGth~j2QVFojJ7=nT|KsrmffkZ)3W^Q6pWqfvO~EN&^7MHy`o< diff --git a/app/models/__init__.py b/app/models/__init__.py index e69de29bb..aac0fb452 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -0,0 +1,6 @@ +from os.path import dirname, basename, isfile +import glob + + +modules = glob.glob(dirname(__file__)+"/*.py") +__all__ = [basename(f)[:-3] for f in modules if isfile(f)] diff --git a/app/models/__init__.pyc b/app/models/__init__.pyc index 1a91a5ca594c19752e2f515430afcc4f18460b6a..661da3d8772ce81b2905bb25096251ab295e6f90 100644 GIT binary patch literal 367 zcmYLEJx{|h5Pi1Oq#$4fAu%APh@t%h2r)2pVkiqTf>TP|V&pWTHUl&Nl|R6Y;LcRy zWWW1-&u8CL_Q3yb%U==zItI-))10_9;P{0;&1zs2x+z1~E zt-%pHTVe&c64wuBuoawvtE9h~!Rc+>7C3#>c%8wHMew#B@x%Uytu`E`O7`%Zbt z=6KOWmy=TF;?f6q6NNK9pA7H*TZlWHW?~;FLzAB8oMWy8w6x@ttI#j9AdzSgWkT1t zZ^6YRv&&o|YC<5YxqyF&hnzOUFlB-ZaZ2RZJ`wQ>S@hyh=u&i|Y Date: Wed, 21 Dec 2016 18:32:11 -0500 Subject: [PATCH 10/22] creates setup script --- app/__init__.py | 5 ----- app/__init__.pyc | Bin 413 -> 484 bytes setup.py | 5 +++++ 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 setup.py diff --git a/app/__init__.py b/app/__init__.py index ba16886fa..fbaced3b7 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,4 @@ # -*- encoding: utf-8 -*- -import os.path -from app.models import * from flask import Flask from flask_sqlalchemy import SQLAlchemy @@ -9,6 +7,3 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///expenses.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) - -if not os.path.isfile('./expenses.db'): - db.create_all() diff --git a/app/__init__.pyc b/app/__init__.pyc index e310044d29c5356ff97cf0a4b8f02ccbf44ef444..4cee56f291498fd46b7a60509627af22969d3941 100644 GIT binary patch delta 103 zcmbQs{Dhf}`7 Date: Wed, 21 Dec 2016 18:57:07 -0500 Subject: [PATCH 11/22] adds some basic templates --- app/controllers/__init__.py | 0 app/controllers/expense.py | 0 app/routes.py | 13 +++++++++++++ app/templates/index.html | 0 app/templates/layout.html | 14 ++++++++++++++ 5 files changed, 27 insertions(+) create mode 100644 app/controllers/__init__.py create mode 100644 app/controllers/expense.py create mode 100644 app/routes.py create mode 100644 app/templates/index.html create mode 100644 app/templates/layout.html diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/expense.py b/app/controllers/expense.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 000000000..d32d3deb8 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,13 @@ +# -*- encoding: utf-8 -*- +from flask import render_template +from repositories.expense import ExpenseRepository + + +@app.route('/') +def index: + return render_template('index.html') + + +@app.route('/expenses/') +def show(company_id=None): + return render_template('index.html', company_id=None) diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/app/templates/layout.html b/app/templates/layout.html new file mode 100644 index 000000000..62fae3ee5 --- /dev/null +++ b/app/templates/layout.html @@ -0,0 +1,14 @@ + + + + {% block head %} + + {% block title %}{% endblock %} + {% endblock %} + + +
+ {% block body %}{% endblock %} +
+ + \ No newline at end of file From 4127db83a644ef57ac7ecd91e1069b02749965b9 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 19:08:56 -0500 Subject: [PATCH 12/22] renames controllers to repositories because that's just what they are --- app/__init__.pyc | Bin 484 -> 484 bytes app/{controllers => repositories}/__init__.py | 0 app/{controllers => repositories}/expense.py | 0 app/routes.py | 4 ++-- app/templates/index.html | 7 +++++++ run.py | 8 ++++++++ 6 files changed, 17 insertions(+), 2 deletions(-) rename app/{controllers => repositories}/__init__.py (100%) rename app/{controllers => repositories}/expense.py (100%) create mode 100644 run.py diff --git a/app/__init__.pyc b/app/__init__.pyc index 4cee56f291498fd46b7a60509627af22969d3941..20079caea852a1f5c56d0e742bdab26806e864b2 100644 GIT binary patch delta 15 WcmaFD{Dhf}`7Index +{% endblock %} \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 000000000..961ed38af --- /dev/null +++ b/run.py @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- +import os +from app import routes +from app import app + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host='0.0.0.0', port=port) From 9d5a2396750a1e45ca4621e41b2a8ab1083211a4 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 20:56:08 -0500 Subject: [PATCH 13/22] allows user to upload a csv and creates all necessary models --- app/models/Expense.py | 6 ++++- app/repositories/expense.py | 53 +++++++++++++++++++++++++++++++++++++ app/routes.py | 21 ++++++++++++++- app/templates/index.html | 6 ++++- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/app/models/Expense.py b/app/models/Expense.py index 968ecdc26..24d734a5e 100644 --- a/app/models/Expense.py +++ b/app/models/Expense.py @@ -11,18 +11,21 @@ class Expense(db.Model): date = db.Column(db.Date) # category + category_id = db.Column(db.Integer, db.ForeignKey('category.id')) category = db.relationship( 'Category', backref=db.backref('expenses', lazy='dynamic') ) # employee + employee_id = db.Column(db.Integer, db.ForeignKey('employee.id')) employee = db.relationship( 'Employee', backref=db.backref('expenses', lazy='dynamic') ) # tax + tax_id = db.Column(db.Integer, db.ForeignKey('tax.id')) tax = db.relationship('Tax') # expense description @@ -31,7 +34,8 @@ class Expense(db.Model): # pre-tax amount pre_tax_amount = db.Column(db.Float) - def __init__(self, date, category, employee, tax, description, pre_tax_amount): + def __init__(self, date, category, employee, tax, description, + pre_tax_amount): self.date = date self.category = category self.employee = employee diff --git a/app/repositories/expense.py b/app/repositories/expense.py index e69de29bb..b575e26b1 100644 --- a/app/repositories/expense.py +++ b/app/repositories/expense.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +import csv +from datetime import datetime + +from app import db +from app.models.category import Category +from app.models.employee import Employee +from app.models.expense import Expense +from app.models.tax import Tax + + +class ExpenseRepository(): + """Repository to create and retrieve Expenses""" + def from_csv(self, csvfile): + categories = {} + employees = {} + taxes = {} + + data = csv.DictReader(csvfile) + + for row in data: + if not row['category'] in categories: + new_category = Category(row['category']) + categories[row['category']] = new_category + db.session.add(new_category) + + if not row['employee name'] in employees: + new_employee = Employee(row['employee name'], + row['employee address']) + employees[row['employee name']] = new_employee + db.session.add(new_employee) + + pre_tax_amount = float(row['pre-tax amount'].replace(',', '')) + tax_amount = float(row['tax amount'].replace(',', '')) + tax_percentage = tax_amount / pre_tax_amount + + if not row['tax name'] in taxes: + new_tax = Tax(row['tax name'], tax_percentage) + taxes[row['tax name']] = new_tax + db.session.add(new_tax) + + # date, category, employee, tax, description, pre_tax_amount + new_expense = Expense( + datetime.strptime(row['date'], "%m/%d/%Y").date(), + categories[row['category']], + employees[row['employee name']], + taxes[row['tax name']], + row['expense description'], + pre_tax_amount + ) + db.session.add(new_expense) + + db.session.commit() diff --git a/app/routes.py b/app/routes.py index c7885d401..e47b9a1e9 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- -from flask import render_template from app import app +from app.repositories.expense import ExpenseRepository +from flask import render_template, request @app.route('/') @@ -8,6 +9,24 @@ def index(): return render_template('index.html') +@app.route('/expenses', methods=['POST']) +def upload(): + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return redirect(request.url) + + expense = ExpenseRepository() + file = request.files['file'] + # if user does not select file, browser also + # submit a empty part without filename + if file.filename == '': + flash('No selected file') + return redirect(request.url) + expense.from_csv(file) + return render_template('index.html') + + @app.route('/expenses/') def show(company_id=None): return render_template('index.html', company_id=None) diff --git a/app/templates/index.html b/app/templates/index.html index 790fded83..e0d615b9e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,5 +3,9 @@ {% block title %}Expenses{% endblock %} {% block body %} -

Index

+

Index

+
+ + +
{% endblock %} \ No newline at end of file From f845fbe09f68b28bb00f2e6cf0d0a413bc65044d Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 21:47:11 -0500 Subject: [PATCH 14/22] will now print total expenses and taxes paid per month --- app/models/Expense.py | 9 ++++----- app/repositories/expense.py | 21 ++++++++++++++++++++- app/routes.py | 8 +++----- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/models/Expense.py b/app/models/Expense.py index 24d734a5e..59125b93b 100644 --- a/app/models/Expense.py +++ b/app/models/Expense.py @@ -34,18 +34,17 @@ class Expense(db.Model): # pre-tax amount pre_tax_amount = db.Column(db.Float) + post_tax_amount = db.Column(db.Float) + def __init__(self, date, category, employee, tax, description, - pre_tax_amount): + pre_tax_amount, post_tax_amount): self.date = date self.category = category self.employee = employee self.tax = tax self.description = description self.pre_tax_amount = pre_tax_amount + self.post_tax_amount = post_tax_amount def __repr__(self): return '' % self.description - - @hybrid_property - def post_tax_amount(self): - return self.pre_tax_amount * (1.0 + self.tax.amount) diff --git a/app/repositories/expense.py b/app/repositories/expense.py index b575e26b1..e37f00e10 100644 --- a/app/repositories/expense.py +++ b/app/repositories/expense.py @@ -1,6 +1,8 @@ # -*- encoding: utf-8 -*- import csv from datetime import datetime +from sqlalchemy import extract +from sqlalchemy.sql import func from app import db from app.models.category import Category @@ -11,6 +13,22 @@ class ExpenseRepository(): """Repository to create and retrieve Expenses""" + def by_month(self): + year = extract('year', Expense.date) + month = extract('month', Expense.date) + + expenses = db.session.query( + year, + month, + func.sum(Expense.pre_tax_amount), + func.sum(Expense.post_tax_amount) + ).group_by( + year, + month + ).all() + + return expenses + def from_csv(self, csvfile): categories = {} employees = {} @@ -46,7 +64,8 @@ def from_csv(self, csvfile): employees[row['employee name']], taxes[row['tax name']], row['expense description'], - pre_tax_amount + pre_tax_amount, + tax_amount ) db.session.add(new_expense) diff --git a/app/routes.py b/app/routes.py index e47b9a1e9..e67be1a2e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -6,6 +6,8 @@ @app.route('/') def index(): + expense = ExpenseRepository() + expense.by_month() return render_template('index.html') @@ -23,10 +25,6 @@ def upload(): if file.filename == '': flash('No selected file') return redirect(request.url) + expense.from_csv(file) return render_template('index.html') - - -@app.route('/expenses/') -def show(company_id=None): - return render_template('index.html', company_id=None) From d9c99b51e8eea8e9fb9dffb55d5821e63004cd03 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 22:34:57 -0500 Subject: [PATCH 15/22] replace template based interface with terminal, because time --- app/routes.py | 19 ++++++++----------- app/templates/index.html | 5 +---- app/templates/layout.html | 2 ++ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/routes.py b/app/routes.py index e67be1a2e..1d4c44184 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,21 +1,13 @@ # -*- encoding: utf-8 -*- from app import app from app.repositories.expense import ExpenseRepository -from flask import render_template, request - - -@app.route('/') -def index(): - expense = ExpenseRepository() - expense.by_month() - return render_template('index.html') +from flask import render_template, request, make_response @app.route('/expenses', methods=['POST']) def upload(): # check if the post request has the file part if 'file' not in request.files: - flash('No file part') return redirect(request.url) expense = ExpenseRepository() @@ -23,8 +15,13 @@ def upload(): # if user does not select file, browser also # submit a empty part without filename if file.filename == '': - flash('No selected file') return redirect(request.url) expense.from_csv(file) - return render_template('index.html') + by_month = expense.by_month() + by_month = reduce( + lambda res, (year, month, total, taxes): res + ("Period %d/%d Total Expenses: %.2f, Total Taxes: %.2f" % (month, year, total, taxes)) + "\n", + by_month, + "" + ) + return make_response(by_month) diff --git a/app/templates/index.html b/app/templates/index.html index e0d615b9e..89551618c 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,9 +3,6 @@ {% block title %}Expenses{% endblock %} {% block body %} -

Index

-
- - +
{% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html index 62fae3ee5..a4387e062 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -3,6 +3,8 @@ {% block head %} + + {% block title %}{% endblock %} {% endblock %} From 74aa7284eb9e363422a9feeb957f351c5bf9f4be Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 22:38:09 -0500 Subject: [PATCH 16/22] makes routes less readable and more pip compliant --- app/routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/routes.py b/app/routes.py index 1d4c44184..bebef77a0 100644 --- a/app/routes.py +++ b/app/routes.py @@ -18,10 +18,12 @@ def upload(): return redirect(request.url) expense.from_csv(file) + table_row = "Period %d/%d Total Expenses: %.2f, Total Taxes: %.2f\n" by_month = expense.by_month() by_month = reduce( - lambda res, (year, month, total, taxes): res + ("Period %d/%d Total Expenses: %.2f, Total Taxes: %.2f" % (month, year, total, taxes)) + "\n", + lambda res, (y, m, e, t): res + (table_row % (m, y, e, t)), by_month, "" ) + return make_response(by_month) From 63e7bc962f40ffd478fb51bf7f7f6e9ad481da95 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Wed, 21 Dec 2016 23:08:35 -0500 Subject: [PATCH 17/22] adds a writeup --- README.markdown | 35 +++++++++++++++++++++++++++++++++++ app/routes.py | 18 +++++++++++------- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/README.markdown b/README.markdown index f93d526ae..5d22162ba 100644 --- a/README.markdown +++ b/README.markdown @@ -1,3 +1,38 @@ +# Setup + +### Prerequisites + + 1. Python 2.7 + 2. Virtualenv + +### Run the demo + + 0. clone the repository and cd into it + 1. `virtualenv expenses` + 2. `source expenses/bin/activate` + 3. `pip install -r requirements.txt` + 4. `python setup.py` + 5. `python run.py` + 6. from another terminal window run + * `curl -i -X POST -H "Content-Type: multipart/form-data" -F "file=@data_example.csv" localhost:5000/expenses` + +### Comments + +The project **lacks a web interface**, and needs to be interacted with through the terminal. I apologize for the incompleteness and lack of tests, but please do note that this solution was build 1/3 on a plane, 1/3 in a car and 1/3 in a room full of christmas guests. + +What I'm particularily proud of in my implementation: when I started working on this, I had grand ideas, which sadly didn't materialize. Now, although my pride is being overshadowed by a sense of forfeit, I can say that I'm satisfied with the derived database schema, which allows, in combination with a nice ORM, to generate complex queries while maintaining a decent level of extensibility. This might sound silly but I'm also satisfied with the folder structure for the project and the concerns separation into files, as it's intuitive and clean. + +### Implementation notes + + 1. **Models**: under the `models` folder you will find some Alchemy models representing the data in the csv in such a way to be easily extended in the future. In particular note that `categories`, `employees` and `taxes` have their own table. + + 2. **Seeding / Uploading**: the .csv is lacking unique ids, thus when creating records for categories, employees and taxes, I had to assume the uniqueness of some of its data, which isn't necessarily true. + + 3. **Testing**: Initially I wanted to use `pytest` to do some basic testing on the csv parsing and model generation results, and the server response, but I did not have enough time available. + + 4. **Templates**: I started building a web interface, but ultimately resorted to a vintage curl-powered, terminal-based solution due to the little time available, but I hear that's all the rage. + + # Wave Software Development Challenge Applicants for the [Software developer](https://wave.bamboohr.co.uk/jobs/view.php?id=1) role at Wave must complete the following challenge, and submit a solution prior to the onsite interview. diff --git a/app/routes.py b/app/routes.py index bebef77a0..6ce6e3076 100644 --- a/app/routes.py +++ b/app/routes.py @@ -4,6 +4,15 @@ from flask import render_template, request, make_response +def render_report(expenses): + table_row = "Period %d/%d Total Expenses: %.2f, Total Taxes: %.2f\n" + by_month = reduce( + lambda res, (y, m, e, t): res + (table_row % (m, y, e, t)), + expenses, + "" + ) + + @app.route('/expenses', methods=['POST']) def upload(): # check if the post request has the file part @@ -18,12 +27,7 @@ def upload(): return redirect(request.url) expense.from_csv(file) - table_row = "Period %d/%d Total Expenses: %.2f, Total Taxes: %.2f\n" - by_month = expense.by_month() - by_month = reduce( - lambda res, (y, m, e, t): res + (table_row % (m, y, e, t)), - by_month, - "" - ) + + render_report(expense.by_month()) return make_response(by_month) From 7594c2cf400df5382a7d19ef08fd3bdfe4e3f8aa Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Thu, 22 Dec 2016 07:40:07 -0500 Subject: [PATCH 18/22] adds a client folder for pretty terminal querying --- data_example.csv => client/data_example.csv | 0 client/expenses.py | 0 requirements.txt | 1 + 3 files changed, 1 insertion(+) rename data_example.csv => client/data_example.csv (100%) create mode 100644 client/expenses.py diff --git a/data_example.csv b/client/data_example.csv similarity index 100% rename from data_example.csv rename to client/data_example.csv diff --git a/client/expenses.py b/client/expenses.py new file mode 100644 index 000000000..e69de29bb diff --git a/requirements.txt b/requirements.txt index ce6cc0887..2e22510f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 SQLAlchemy==1.1.4 +terminaltables==3.1.0 Werkzeug==0.11.11 From 54a6f09cee148878a7ea6c87c8569530efccd04f Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Thu, 22 Dec 2016 08:04:17 -0500 Subject: [PATCH 19/22] makes a simple cli to upload the data --- app/routes.py | 33 ++++++++++++++++++--------------- client/expenses.py | 27 +++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/app/routes.py b/app/routes.py index 6ce6e3076..10d25af93 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,7 +1,8 @@ # -*- encoding: utf-8 -*- +import json from app import app from app.repositories.expense import ExpenseRepository -from flask import render_template, request, make_response +from flask import render_template, request, jsonify def render_report(expenses): @@ -13,21 +14,23 @@ def render_report(expenses): ) -@app.route('/expenses', methods=['POST']) +@app.route('/expenses', methods=['GET', 'POST']) def upload(): - # check if the post request has the file part - if 'file' not in request.files: - return redirect(request.url) - expense = ExpenseRepository() - file = request.files['file'] - # if user does not select file, browser also - # submit a empty part without filename - if file.filename == '': - return redirect(request.url) - - expense.from_csv(file) - - render_report(expense.by_month()) + if request.method == 'POST': + # check if the post request has the file part + if 'file' not in request.files: + return redirect(request.url) + + file = request.files['file'] + # if user does not select file, browser also + # submit a empty part without filename + if file.filename == '': + return redirect(request.url) + + expense.from_csv(file) + return jsonify({'success': True}) + else: + return jsonify(expense.by_month()) return make_response(by_month) diff --git a/client/expenses.py b/client/expenses.py index e69de29bb..401f6a7fe 100644 --- a/client/expenses.py +++ b/client/expenses.py @@ -0,0 +1,27 @@ +import os.path +import click +import requests + + +ENDPOINT = "http://localhost:5000" + + +@click.command() +@click.option('--upload', default=False, help='Path to a well-formatted .csv file') +def expenses(upload): + """Simple querying CLI to upload and display expenses from a csv file""" + if upload: + if os.path.isfile(upload): + r = requests.post("%s/expenses" % ENDPOINT, files={ + 'file': open(upload, 'rb') + }) + if r.status_code == 200: + print "Successfully uploaded %s" % upload + else: + raise "The file %s does not exist, check the given path" % upload + + result = requests.get("%s/expenses" % ENDPOINT) + print result.text + +if __name__ == '__main__': + expenses() diff --git a/requirements.txt b/requirements.txt index 2e22510f0..f8256f2c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ Flask-SQLAlchemy==2.1 itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 +requests==2.12.4 SQLAlchemy==1.1.4 terminaltables==3.1.0 Werkzeug==0.11.11 From 7de671748770acd478a368e5d4060ab795902988 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Thu, 22 Dec 2016 08:11:42 -0500 Subject: [PATCH 20/22] now prints a pretty table in the temrinal --- client/expenses.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/expenses.py b/client/expenses.py index 401f6a7fe..5ba2881fd 100644 --- a/client/expenses.py +++ b/client/expenses.py @@ -1,6 +1,7 @@ import os.path import click import requests +from terminaltables import AsciiTable ENDPOINT = "http://localhost:5000" @@ -21,7 +22,11 @@ def expenses(upload): raise "The file %s does not exist, check the given path" % upload result = requests.get("%s/expenses" % ENDPOINT) - print result.text + header = ['Year', 'Month', 'Total Expenses', 'Total Tax'] + expenses = result.json() + expenses.insert(0, header) + table = AsciiTable(expenses) + print table.table if __name__ == '__main__': expenses() From 5efcf0f7693a5b597a16dcc26f07c2a78ffc4add Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Thu, 22 Dec 2016 08:15:55 -0500 Subject: [PATCH 21/22] updates instructions --- README.markdown | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index 5d22162ba..36e4186d9 100644 --- a/README.markdown +++ b/README.markdown @@ -14,7 +14,9 @@ 4. `python setup.py` 5. `python run.py` 6. from another terminal window run - * `curl -i -X POST -H "Content-Type: multipart/form-data" -F "file=@data_example.csv" localhost:5000/expenses` + * `source expenses/bin/activate` + * `python client/expenses.py --upload ./data_examples.csv` to upload and view a summary of the expenses + * `python client/expenses.py` to only view the summary ### Comments @@ -30,7 +32,7 @@ What I'm particularily proud of in my implementation: when I started working on 3. **Testing**: Initially I wanted to use `pytest` to do some basic testing on the csv parsing and model generation results, and the server response, but I did not have enough time available. - 4. **Templates**: I started building a web interface, but ultimately resorted to a vintage curl-powered, terminal-based solution due to the little time available, but I hear that's all the rage. + 4. **Templates**: I started building a web interface, but ultimately resorted to a vintage terminal-based solution due to the little time available, but I hear that's all the rage anyway. # Wave Software Development Challenge From c50a57b9e533fbcc23bb983452283cb64cbf3fd2 Mon Sep 17 00:00:00 2001 From: Alessandro Marin Date: Thu, 22 Dec 2016 08:21:30 -0500 Subject: [PATCH 22/22] adjusts writeup --- README.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 36e4186d9..8511b65c5 100644 --- a/README.markdown +++ b/README.markdown @@ -26,13 +26,13 @@ What I'm particularily proud of in my implementation: when I started working on ### Implementation notes - 1. **Models**: under the `models` folder you will find some Alchemy models representing the data in the csv in such a way to be easily extended in the future. In particular note that `categories`, `employees` and `taxes` have their own table. + 1. **Models**: under the `models` folder you will find some Alchemy models representing the data in the csv in such a way to be easily extended in the future. In particular note that `categories`, `employees` and `taxes` have their own table. - 2. **Seeding / Uploading**: the .csv is lacking unique ids, thus when creating records for categories, employees and taxes, I had to assume the uniqueness of some of its data, which isn't necessarily true. + 2. **Seeding / Uploading**: the .csv is lacking unique ids, thus when creating records for categories, employees and taxes, I had to assume the uniqueness of some of its data, which isn't necessarily true. - 3. **Testing**: Initially I wanted to use `pytest` to do some basic testing on the csv parsing and model generation results, and the server response, but I did not have enough time available. + 3. **Testing**: Initially I wanted to use `pytest` to do some basic testing on the csv parsing and model generation results, and the server response, but I did not have enough time available. - 4. **Templates**: I started building a web interface, but ultimately resorted to a vintage terminal-based solution due to the little time available, but I hear that's all the rage anyway. + 4. **Templates**: I started building a web interface, but ultimately resorted to a vintage terminal-based solution due to the little time available, but I hear that's all the rage anyway. # Wave Software Development Challenge