├── .gitignore
├── app
├── static
│ ├── fonts
│ │ └── JosefinSans-Medium.ttf
│ └── css
│ │ ├── settings.css
│ │ ├── admin.css
│ │ ├── home.css
│ │ ├── statements.css
│ │ └── main.css
├── templates
│ ├── admin
│ │ ├── base.html
│ │ ├── login.html
│ │ ├── index.html
│ │ ├── user.html
│ │ └── users.html
│ ├── auth
│ │ └── index.html
│ ├── home
│ │ ├── new_statement.html
│ │ ├── index.html
│ │ ├── statement.html
│ │ └── statements.html
│ ├── base.html
│ └── settings
│ │ └── index.html
├── commands.py
├── __init__.py
├── db.py
├── views
│ ├── auth.py
│ ├── settings.py
│ ├── home.py
│ └── admin.py
├── app_functions.py
├── functions.py
└── forms.py
├── main.py
├── requirements.txt
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | env/
2 | migrations/
3 | instance/
4 | __pycache__/
--------------------------------------------------------------------------------
/app/static/fonts/JosefinSans-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hakiKhuva/expense-tracker/HEAD/app/static/fonts/JosefinSans-Medium.ttf
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from app import create_app
2 |
3 | app = create_app()
4 |
5 | if __name__ == "__main__":
6 | app.run(debug=True, host="0.0.0.0")
--------------------------------------------------------------------------------
/app/static/css/settings.css:
--------------------------------------------------------------------------------
1 | form .br {
2 | border-bottom: 2px solid var(--fg1) !important;
3 | margin: 15px 0px;
4 | }
5 |
6 | form input[type="submit"]{
7 | width: max-content;
8 | padding: 10px 30px !important;
9 | border-radius: 30px !important;
10 | }
--------------------------------------------------------------------------------
/app/templates/admin/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block head %}
4 |
5 | {% endblock %}
6 |
7 | {% block body_before %}
8 | {% if not is_loggedin is false %}
9 |
17 | {% endif %}
18 | {% endblock %}
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic==1.8.1
2 | click==8.1.3
3 | colorama==0.4.6
4 | contourpy==1.0.6
5 | cycler==0.11.0
6 | dnspython==2.2.1
7 | email-validator==1.3.0
8 | Flask==2.2.5
9 | Flask-Migrate==4.0.0
10 | Flask-SQLAlchemy==3.0.2
11 | Flask-WTF==1.0.1
12 | fonttools==4.38.0
13 | greenlet==2.0.1
14 | idna==3.4
15 | itsdangerous==2.1.2
16 | Jinja2==3.1.2
17 | kiwisolver==1.4.4
18 | Mako==1.2.4
19 | MarkupSafe==2.1.1
20 | matplotlib==3.6.2
21 | mysql-connector-python==8.0.31
22 | numpy==1.23.5
23 | packaging==22.0
24 | Pillow==9.3.0
25 | protobuf==3.19.6
26 | pyparsing==3.0.9
27 | python-dateutil==2.8.2
28 | six==1.16.0
29 | SQLAlchemy==1.4.44
30 | ua-parser==0.16.1
31 | user-agents==2.2.0
32 | waitress==2.1.2
33 | Werkzeug==2.2.3
34 | WTForms==3.0.1
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expense Tracker
2 |
3 | Expense tracker in Python Flask supports following actions
4 |
5 | - Login/Signup
6 | - Add statement
7 | - Delete statement
8 | - Calculate final amount
9 | - Show all statements
10 | - Admin functionality
11 | - Download all statements
12 |
13 | ## Run the project
14 |
15 | > You must have python3 installed on your system before running
16 |
17 | > used python 3.10.2 for this project
18 |
19 | ### Create virtual environment and activate it
20 |
21 | ```bash
22 | # bash
23 | $ python3 -m venv env
24 |
25 | # windows
26 | > python -m venv env
27 | > env\Scripts\activate
28 | ```
29 |
30 | ### Install requirements
31 |
32 | ```bash
33 | $ pip install -r requirements.txt
34 | ```
35 |
36 | ### Set secret key and database URI
37 |
38 | ```bash
39 | # bash
40 | (env) $ export DB_URI="database uri"
41 | (env) $ export SECRET_KEY="secret key"
42 |
43 | # windows
44 | (env) > SET DB_URI=database_uri
45 | (env) > SET SECRET_KEY=secret_key
46 | ```
47 |
48 | ### final run
49 | ```bash
50 | $ flask run
51 | ```
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Harkishan Khuva
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/templates/admin/login.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% block body %}
4 |
36 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/admin/index.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% block body %}
4 |
5 |
Dashboard
6 |
7 |
8 |
9 | | Application Statistics |
10 |
11 | {% for key, value in table_data.items() %}
12 |
13 | |
14 | {{key}}
15 | |
16 |
17 | {{value}}
18 | |
19 |
20 | {% endfor %}
21 |
22 |
23 |
24 |
25 |

26 |
27 |
28 |
29 |

30 |
31 |
32 |
33 |

34 |
35 |
36 |
37 |
38 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/auth/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
36 | {% endblock %}
--------------------------------------------------------------------------------
/app/commands.py:
--------------------------------------------------------------------------------
1 | import click
2 | from flask.cli import with_appcontext
3 | from .db import Admin, db
4 | from werkzeug.security import generate_password_hash
5 | from .functions import generate_id
6 | import re
7 |
8 | regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
9 | regex = re.compile(regex)
10 |
11 | @click.command(name="createadminuser")
12 | @with_appcontext
13 | def create_admin_user():
14 | username = ""
15 | while not username.strip():
16 | username = input("Enter username : ")
17 |
18 | email = ""
19 | while not email.strip():
20 | email = input("Enter your email : ")
21 | if not regex.match(email):
22 | print("Invalid email address")
23 | email = ""
24 |
25 | password = ""
26 | while not password.strip() or len(password) < 8:
27 | password = input("Enter your password : ")
28 |
29 | account = Admin.query.filter(Admin.email == email).first()
30 |
31 | if account:
32 | print("Email already used with another account!")
33 | return
34 |
35 | hashed_password = generate_password_hash(password, "SHA256")
36 | session_id = generate_id(username, email, hashed_password)
37 |
38 | admin = Admin(
39 | username = username,
40 | email = email,
41 | password = hashed_password,
42 | session_id = session_id
43 | )
44 |
45 | db.session.add(admin)
46 | db.session.commit()
47 |
48 | print("Admin created successfully")
--------------------------------------------------------------------------------
/app/templates/admin/user.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/base.html' %}
2 |
3 | {% block body %}
4 |
46 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/home/new_statement.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
47 | {% endblock %}
--------------------------------------------------------------------------------
/app/static/css/admin.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg : #fff;
3 | --shade : #444;
4 | --fg : rgb(0, 65, 25);
5 | --fg1 : #000;
6 | --max-w : 1250px;
7 | }
8 |
9 | .head {
10 | font-size: 22.5px;
11 | margin: 20px 0px;
12 | }
13 |
14 | .container {
15 | max-width: var(--max-w);
16 | margin: auto;
17 | }
18 |
19 | .flexbox {
20 | display: flex;
21 | flex-direction: row;
22 | align-items: stretch;
23 | justify-content: space-between;
24 | flex-wrap: wrap;
25 | }
26 |
27 | .flex-column {
28 | flex-direction: column;
29 | align-items: center;
30 | justify-content: center;
31 | }
32 |
33 | .container .content {
34 | box-shadow: 0px 0px 1px 0px var(--fg);
35 | border-radius: 5px;
36 | padding: 20px;
37 | margin: 20px;
38 | }
39 |
40 | .container .image {
41 | border-radius: 5px;
42 | }
43 |
44 | table {
45 | margin: 50px;
46 | width: 100vw;
47 | max-width: calc(100% - 110px);
48 | }
49 |
50 | table, table tr, table td {
51 | /* border: 1px solid var(--fg); */
52 | box-shadow: 0px 0px 1px 0px var(--fg1);
53 | border-collapse: collapse;
54 | }
55 |
56 | table td, table th {
57 | padding: 15px;
58 | font-size: 18px;
59 | }
60 |
61 | nav {
62 | margin-bottom: 50px;
63 | display: flex;
64 | flex-direction: row;
65 | flex-wrap: wrap;
66 | max-width: calc(100%);
67 | align-items: center;
68 | justify-content: center;
69 | margin-top: 0px;
70 | width: 100%;
71 | top: 0;
72 | left: 0;
73 | right: 0;
74 | position: sticky;
75 | }
76 |
77 | nav,nav * {
78 | background-color: #ddd;
79 | }
80 |
81 | nav > a, nav input[type="submit"] {
82 | display: block;
83 | padding: 25px 20px;
84 | transition: 200ms;
85 | border: 0px;
86 | border-bottom: 1px solid #000;
87 | }
88 |
89 | nav > a:hover, nav input[type="submit"]:hover {
90 | filter: invert(100%);
91 | transition: 400ms;
92 | }
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | from flask import Flask, request
3 | from flask_migrate import Migrate
4 | from user_agents import parse
5 | import datetime
6 |
7 | from .commands import create_admin_user
8 | from .db import db, VisitorStats
9 |
10 | migrate = Migrate(render_as_batch=True)
11 |
12 | def create_app():
13 | app = Flask(__name__)
14 |
15 | app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "NOTHING_IS_SECRET")
16 | app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DB_URI", "sqlite:///master.sqlite3")
17 | app.config["PERMANENT_SESSION_LIFETIME"] = datetime.timedelta(days=7)
18 |
19 | db.init_app(app)
20 | migrate.init_app(app, db)
21 |
22 | @app.after_request
23 | def after_request_(response):
24 | if request.endpoint != "static":
25 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
26 | return response
27 |
28 | from .views.auth import auth
29 | from .views.home import home
30 | from .views.settings import settings
31 | from .views.admin import admin
32 |
33 | blueprints = [auth, home, settings, admin ]
34 |
35 | # request.blueprint != admin.name and
36 | @app.before_request
37 | def app_before_data():
38 | if request.endpoint != "static":
39 | user_agent = parse(request.user_agent.string)
40 | browser = user_agent.browser.family
41 | device = user_agent.get_device()
42 | operating_system = user_agent.get_os()
43 | bot = user_agent.is_bot
44 |
45 | stat = VisitorStats(
46 | browser = browser,
47 | device = device,
48 | operating_system = operating_system,
49 | is_bot = bot
50 | )
51 | db.session.add(stat)
52 | db.session.commit()
53 |
54 | for bp in blueprints: app.register_blueprint(bp)
55 |
56 | app.cli.add_command(create_admin_user)
57 |
58 | return app
--------------------------------------------------------------------------------
/app/templates/home/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block head %}
4 |
5 |
6 | {% endblock %}
7 |
8 | {% block body %}
9 |
10 |
Hello, {{ user.name }}
11 |
12 |
13 |
14 | ₹
15 | {{ user.account_balance }}
16 |
17 |
18 |
25 |
26 |
27 |
Last statements
28 |
43 |
44 |
45 | {% endblock %}
--------------------------------------------------------------------------------
/app/static/css/home.css:
--------------------------------------------------------------------------------
1 | #expense-card {
2 | border-radius: 10px;
3 | }
4 |
5 |
6 | #expense-card > #name {
7 | font-size: 35px;
8 | margin: 20px auto;
9 | text-align: center;
10 | }
11 |
12 | #expense-card #balance {
13 | display: flex;
14 | flex-direction: row;
15 | font-size: 45px;
16 | justify-content: center;
17 | }
18 |
19 | #expense-card #balance > #currency {
20 | margin: 0px 5px;
21 | }
22 |
23 | #expense-card #balance > #num, .amount {
24 | margin: 0px 5px;
25 | font-family: var(--font-s);
26 | }
27 |
28 | #expense-card > #top {
29 | display: flex;
30 | flex-direction: row;
31 | flex-wrap: wrap;
32 | align-content: stretch;
33 | justify-content: space-between;
34 | }
35 |
36 | #expense-card > #top .part {
37 | width: calc( 50% - 70px );
38 | }
39 |
40 | .part,
41 | .part * {
42 | background-color: #444;
43 | }
44 |
45 | .part {
46 | padding: 25px;
47 | border-radius: 10px;
48 | margin: 10px;
49 | }
50 |
51 | #expense-card #operations {
52 | display: flex;
53 | flex-direction: column;
54 | }
55 |
56 | #expense-card #operations > .btn-a {
57 | display: block;
58 | text-decoration: none;
59 | }
60 |
61 | #expense-card #operations > button[type="button"],
62 | #expense-card #operations > .btn-a {
63 | text-align: center;
64 | background-color: #333;
65 | color: var(--fg1);
66 | padding: 10px 0px;
67 | font-size: 16.5px;
68 | margin: 10px auto;
69 | border: 0px;
70 | box-shadow: 0px 0px 2px 0px var(--fg1);
71 | border-radius: 3px;
72 | cursor: pointer;
73 | width: 200px;
74 | max-width: 100%;
75 | }
76 |
77 | #statements > .statement,
78 | #statements > .statement * {
79 | background-color: #373737 !important;
80 | }
81 |
82 | @media (max-width:700px) {
83 | #expense-card > #top {
84 | flex-direction: column;
85 | }
86 |
87 | #expense-card > #top .part {
88 | width: calc( 100% - 70px );
89 | }
90 |
91 | #expense-card {
92 | padding: 10px;
93 | margin: 10px auto;
94 | width: calc(100% - 30px);
95 | max-width: 850px;
96 | }
97 | }
--------------------------------------------------------------------------------
/app/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | import datetime
3 |
4 | db = SQLAlchemy()
5 |
6 | class User(db.Model):
7 | __tablename__ = "user"
8 | id = db.Column(db.Integer, primary_key=True)
9 | name = db.Column(db.String(50), nullable=False)
10 | email = db.Column(db.String(100), nullable=False, unique=True)
11 | password = db.Column(db.Text, nullable=False)
12 | session_id = db.Column(db.Text, nullable=False)
13 |
14 | statements = db.relationship("Statements", backref=db.backref("user"), passive_deletes=True)
15 |
16 | def __repr__(self) -> str:
17 | return ""%(str(self.id))
18 |
19 |
20 | class VisitorStats(db.Model):
21 | __tablename__ = "visitor_stats"
22 | id = db.Column(db.Integer, primary_key=True)
23 | browser = db.Column(db.String(100))
24 | device = db.Column(db.String(100))
25 | operating_system = db.Column(db.String(100))
26 | is_bot = db.Column(db.Boolean())
27 | date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
28 |
29 | def __repr__(self) -> str:
30 | return ""%(str(self.id))
31 |
32 |
33 | class Statements(db.Model):
34 | __tablename__ = "statements"
35 | id = db.Column(db.Integer, primary_key=True)
36 | description = db.Column(db.String(200), nullable=False)
37 | amount = db.Column(db.Numeric(10,2), nullable=False)
38 | operation_time = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
39 | statement_id = db.Column(db.String(50), nullable=False, unique=True)
40 |
41 | user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"))
42 |
43 | def __repr__(self) -> str:
44 | return ""%(str(self.id))
45 |
46 |
47 | class Admin(db.Model):
48 | __tablename__ = "admin"
49 | id = db.Column(db.Integer, primary_key=True)
50 | username = db.Column(db.String(50), nullable=False)
51 | email = db.Column(db.String(100), nullable=False, unique=True)
52 | password = db.Column(db.Text, nullable=False)
53 | session_id = db.Column(db.String(50), nullable=False, unique=True)
54 |
55 | def __repr__(self) -> str:
56 | return ""%(str(self.id))
--------------------------------------------------------------------------------
/app/templates/home/statement.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block head %}
4 |
5 | {% endblock %}
6 |
7 | {% block body %}
8 |
9 |
57 |
58 | {% endblock %}
--------------------------------------------------------------------------------
/app/views/auth.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, request, flash, redirect, url_for, session
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 |
4 | from ..forms import AuthForm
5 | from ..functions import generate_id, generate_string, get_base64_encode
6 | from ..app_functions import get_login, user_login_required
7 | from ..db import User, db
8 |
9 |
10 | auth = Blueprint("Auth",__name__, url_prefix="/auth")
11 |
12 | @auth.route("/", methods=("GET","POST"))
13 | def auth_index():
14 | if get_login() is True:
15 | return redirect(url_for("Home.home_index"))
16 |
17 | form = AuthForm(request.form)
18 |
19 | if form.validate_on_submit():
20 | email = form.email.data
21 | password = form.password.data
22 |
23 | data = User.query.filter(User.email == email).first()
24 |
25 | if data:
26 | if check_password_hash(data.password, password) is True:
27 | session.permanent = True
28 | session["session-sign-id"] = get_base64_encode(data.session_id)
29 | return redirect(url_for("Home.home_index"))
30 | else:
31 | flash("Incorrect password for entered email account!", "red")
32 | else:
33 | hashed_password = generate_password_hash(password, "SHA256")
34 | name = email.split("@")[0][:18]
35 | user = User(
36 | name = name,
37 | email = email,
38 | password = hashed_password,
39 | session_id = generate_id(name, email, hashed_password)
40 | )
41 |
42 | db.session.add(user)
43 | db.session.commit()
44 |
45 | flash("New account has been created, enter credentials to signin.", "green")
46 |
47 | return redirect(url_for(".auth_index"))
48 |
49 | return render_template(
50 | "auth/index.html",
51 | title="Login",
52 | form=form
53 | )
54 |
55 | @auth.route("/logout", methods=["GET","POST"])
56 | @user_login_required
57 | def logout():
58 | if "session-sign-id" in session:
59 | session.pop("session-sign-id")
60 | session.permanent = False
61 | return redirect(url_for(".auth_index"))
--------------------------------------------------------------------------------
/app/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% if title %}
8 | {{ title }}
9 | {% else %}
10 | Expense-Tracker
11 | {% endif %}
12 |
13 |
14 |
15 | {% block head %}
16 | {% endblock %}
17 |
18 |
23 |
24 |
25 |
26 |
29 | {% with msgs = get_flashed_messages(with_categories=true) %}
30 | {% if msgs %}
31 |
32 | {% for cat, msg in msgs %}
33 |
34 | {{msg}}
35 |
36 |
37 | {% endfor %}
38 |
39 | {% endif %}
40 | {% endwith %}
41 |
42 | {% block body_before %}
43 | {% endblock %}
44 | {% block body %}
45 | {% endblock %}
46 |
47 | {% if footer is true %}
48 |
65 | {% endif %}
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/app_functions.py:
--------------------------------------------------------------------------------
1 | from flask import session, redirect, url_for
2 | from .db import User, Statements, Admin
3 | from .functions import get_base64_decode
4 | from sqlalchemy import func
5 | from functools import wraps
6 |
7 |
8 | def get_login() -> bool:
9 | """
10 | returns whether user is logged in or not
11 | """
12 | sign_id = session.get("session-sign-id")
13 | if sign_id:
14 | sign_id = get_base64_decode(sign_id)
15 | if sign_id and sign_id != "":
16 | user = User.query.filter(User.session_id == sign_id).first()
17 | if user:
18 | return True
19 | session.pop("session-sign-id")
20 | return False
21 |
22 |
23 | def get_admin_login() -> bool:
24 | """
25 | returns whether user is logged in or not
26 | """
27 | sign_id = session.get("admin-sign-id")
28 | if sign_id:
29 | sign_id = get_base64_decode(sign_id)
30 | if sign_id and sign_id != "":
31 | user = Admin.query.filter(User.session_id == sign_id).first()
32 | if user:
33 | return True
34 | session.pop("admin-sign-id")
35 | return False
36 |
37 |
38 | def get_current_user():
39 | """
40 | returns current user if logged in, run `get_login`
41 | before running this function to check login status
42 |
43 | returns User if loggedin
44 |
45 | return None if not loggedin
46 | """
47 | sign_id = session.get("session-sign-id")
48 | if sign_id:
49 | sign_id = get_base64_decode(sign_id)
50 | if sign_id and sign_id != "":
51 | user = User.query.filter(User.session_id == sign_id).first()
52 | return user
53 | return None
54 |
55 | # def get_current_user_account(user_id):
56 | def get_current_user_balance():
57 | """
58 | returns the user_account table user information
59 | """
60 | user_id = get_current_user().id
61 | account_balance = Statements.query.with_entities(func.sum(Statements.amount)).filter(Statements.user_id == user_id).first()[0]
62 | if account_balance is None:
63 | return 0.00
64 | return account_balance
65 |
66 |
67 |
68 | def user_login_required(f):
69 | @wraps(f)
70 | def fun(*args, **kwargs):
71 | if get_login() is True:
72 | return f(*args, **kwargs)
73 | return redirect(url_for("Auth.auth_index"))
74 | return fun
75 |
76 |
77 | def admin_login_required(f):
78 | @wraps(f)
79 | def fun(*args, **kwargs):
80 | if 'admin-sign-id' in session:
81 | return f(*args, **kwargs)
82 | return redirect(url_for("Admin.admin_login"))
83 | return fun
84 |
--------------------------------------------------------------------------------
/app/static/css/statements.css:
--------------------------------------------------------------------------------
1 | #statements {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | #statements > .statement {
7 | display: flex;
8 | flex-direction: row;
9 | flex-wrap: wrap;
10 | align-items: center;
11 | justify-content: space-between;
12 | margin: 10px 2.5px;
13 | padding: 20px 15px;
14 | box-shadow: 0px 0px 1px 0px var(--fg1);
15 | border-radius: 5px;
16 | width: 100%;
17 | max-width: calc(100% - 5px - 30px) !important;
18 | overflow: auto;
19 | }
20 |
21 | #statements > .statement,
22 | #statements > .statement * {
23 | background-color: #424242;
24 | text-decoration: none;
25 | }
26 |
27 | #statements > .statement:hover {
28 | box-shadow: 0px 0px 2px 0px var(--fg1);
29 | }
30 |
31 | #statements > .statement .desc {
32 | font-size: 22.5px;
33 | margin-bottom: 2.5px;
34 | }
35 |
36 | #statements > .statement .at {
37 | font-size: 12.5px;
38 | margin-top: 5px;
39 | }
40 |
41 | #statements > .statement .amount {
42 | font-size: 22.5px;
43 | margin-top: 15px;
44 | width: auto;
45 | text-align: center;
46 | }
47 |
48 | @media (max-width:650px) {
49 | #statements > .statement {
50 | flex-direction: column;
51 | align-items: flex-start;
52 | }
53 |
54 | #statements > .statement .amount {
55 | align-self: flex-end;
56 |
57 | }
58 | }
59 |
60 | .details-e {
61 | display: flex;
62 | flex-direction: row;
63 | flex-wrap: wrap;
64 | width: 100%;
65 | justify-content: space-between;
66 | margin: 50px 0px;
67 | }
68 |
69 | .details-e > .part-d {
70 | padding: 25px;
71 | border: 0px;
72 | box-shadow: 0px 0px 1px 0px var(--fg1);
73 | border-radius: 5px;
74 | margin: 10px;
75 | max-width: calc( 100% - 70px);
76 | overflow: hidden;
77 | min-width: calc(100% / 2.5);
78 | }
79 |
80 | .details-e > .part-d,
81 | .details-e > .part-d * {
82 | background-color: #272727;
83 | }
84 |
85 | .details-e > .part-d > .head {
86 | font-size: 20px;
87 | margin: 10px 3px;
88 | }
89 |
90 | .details-e > .part-d > .money {
91 | font-size: 30px;
92 | margin: 10px 3px;
93 | }
94 |
95 | .details-e > .part-d > .money > span {
96 | margin: 2px 3px;
97 | }
98 |
99 | #type {
100 | font-size: 17px;
101 | padding: 20px 30px;
102 | background-color: #262626;
103 | width: 100%;
104 | margin-bottom: 25px;
105 | border-radius: 50px;
106 | text-align: center;
107 | }
108 |
109 | @media (max-width:770px) {
110 | .details-e {
111 | flex-direction: column;
112 | }
113 | }
--------------------------------------------------------------------------------
/app/views/settings.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, request, flash, redirect, url_for
2 | from werkzeug.security import generate_password_hash, check_password_hash
3 |
4 | from ..forms import SettingsForm1, SettingsForm2, SettingsForm3
5 | from ..db import db
6 | from ..app_functions import get_current_user, user_login_required
7 |
8 |
9 | settings = Blueprint("Settings", __name__, url_prefix="/settings")
10 |
11 |
12 | @settings.route("/", methods=("GET","POST"))
13 | @user_login_required
14 | def settings_index():
15 | form1 = SettingsForm1(request.form)
16 | form2 = SettingsForm2(request.form)
17 | form3 = SettingsForm3(request.form)
18 |
19 | if form1.validate_on_submit():
20 | name = form1.name.data.strip()
21 |
22 | user = get_current_user()
23 | user.name = name
24 | db.session.commit()
25 |
26 | flash("Name was changed successfully", "green")
27 | return redirect(url_for(".settings_index"))
28 |
29 | elif form3.validate_on_submit():
30 | password = form3.password.data
31 | new_password = form3.new_password.data
32 |
33 | update_password = form3.update_password.data
34 | delete_account = form3.delete_account.data
35 |
36 | user = get_current_user()
37 |
38 | if check_password_hash(user.password, password):
39 | if update_password:
40 | if password == new_password:
41 | flash("Could not update the password as same as old password!","red")
42 | else:
43 | user.password = generate_password_hash(new_password)
44 | db.session.commit()
45 | flash("Password updated successfully","green")
46 |
47 | elif delete_account:
48 | if password != new_password:
49 | flash("current and confirm password does not matched!")
50 | else:
51 | db.session.delete(user)
52 | db.session.commit()
53 | flash("Account deleted successfully.")
54 |
55 | else:
56 | flash("cannot process your request, tryagain", "red")
57 | return redirect(url_for("Auth.auth_index"))
58 | else:
59 | flash("Incorrect password to perform operation!","red")
60 | return redirect(url_for(".settings_index"))
61 |
62 | user = get_current_user()
63 |
64 | form1.name.data = user.name
65 | form2.email.data = user.email
66 |
67 | return render_template(
68 | "settings/index.html",
69 | title="Settings",
70 | form1=form1,
71 | form2=form2,
72 | form3=form3,
73 | footer=True
74 | )
--------------------------------------------------------------------------------
/app/functions.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import hashlib
3 | import random
4 | import uuid
5 | import string
6 | import base64
7 | import typing
8 | import io
9 | from matplotlib.figure import Figure
10 | from matplotlib.axes import Axes
11 |
12 | def generate_id(name, email, hashed_password, limit=16):
13 | req = "--".join([name, email, hashed_password, str(datetime.datetime.now())])
14 | req += uuid.uuid4().hex
15 | return hashlib.sha1("".join(random.choice(req) for _ in range(limit)).encode()).hexdigest()
16 |
17 |
18 | def generate_string(limit:int = 12):
19 | data = str(datetime.datetime.now())
20 | uuid4 = uuid.uuid4().hex
21 | confirm_data = string.ascii_letters+data+uuid4
22 | return "".join(random.choice(confirm_data) for _ in range(limit))
23 |
24 |
25 | def get_base64_encode(buffer:str):
26 | if isinstance(buffer, str):
27 | buffer = buffer.encode()
28 | return base64.urlsafe_b64encode(buffer).decode()
29 |
30 | def get_base64_decode(buffer):
31 | try:
32 | return base64.urlsafe_b64decode(buffer).decode()
33 | except ValueError:
34 | return ""
35 |
36 |
37 | def get_pie_chart(data:list[tuple[str,typing.Union[int, float]]], title:str):
38 | pie_data = [x[1] for x in data]
39 |
40 | percentages = [x*100/sum(pie_data) for x in pie_data]
41 | labels = [f"{data[i][0]}({data[i][1]}) - {percentages[i]:.2f}%" for i in range(len(data))]
42 | explode = [0.001*x for x in percentages]
43 |
44 | data_buf = io.BytesIO()
45 | fig = Figure(figsize=(10,4))
46 |
47 | ax = fig.subplots()
48 | ax.pie(
49 | pie_data,
50 | explode=explode,
51 | startangle=90,
52 | )
53 | ax.set_title(title)
54 |
55 | fig.tight_layout()
56 | fig.legend(labels=labels, loc="lower left",)
57 |
58 | fig.savefig(data_buf, format="png", transparent=True)
59 | data_buf.seek(0)
60 | return base64.b64encode(data_buf.read()).decode("utf-8")
61 |
62 |
63 | def get_line_chart(data:list[tuple[str,typing.Union[float, int]]], xlabel:str, ylabel:str, title:str):
64 | fig = Figure(figsize=(10,4))
65 |
66 | data_buf = io.BytesIO()
67 |
68 | ax:Axes = fig.subplots()
69 | ax.plot(
70 | [x[0] for x in data],
71 | [x[1] for x in data],
72 | )
73 | ax.set_title(title)
74 | ax.set_xlabel(xlabel)
75 | ax.set_ylabel(ylabel)
76 | fig.tight_layout()
77 |
78 | fig.savefig(data_buf, transparent=True)
79 |
80 | data_buf.seek(0)
81 | return base64.b64encode(data_buf.read()).decode("utf-8")
82 |
83 |
84 | def get_bar_chart(data:list[tuple[str,typing.Union[float, int]]], xlabel:str, ylabel:str, title:str):
85 | fig = Figure(figsize=(10,4))
86 |
87 | data_buf = io.BytesIO()
88 |
89 | ax:Axes = fig.subplots()
90 | ax.bar(
91 | [x[0] for x in data],
92 | [x[1] for x in data],
93 | )
94 | ax.set_title(title)
95 | ax.set_xlabel(xlabel)
96 | ax.set_ylabel(ylabel)
97 | fig.tight_layout()
98 |
99 | fig.savefig(data_buf, transparent=True)
100 |
101 | data_buf.seek(0)
102 | return base64.b64encode(data_buf.read()).decode("utf-8")
--------------------------------------------------------------------------------
/app/templates/admin/users.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base.html" %}
2 |
3 | {% block body %}
4 |
5 |
{{page_title}} data
6 |
7 |
8 |
9 |
Total users : {{ users_count }}
10 |
Users per page : {{ users_per_page }}
11 |
Page count : {{ page_count }}
12 |
13 | Change users per page
14 |
23 |
24 |
25 |
26 | Search user
27 |
37 |
38 |
39 |
40 |
41 | {% if users %}
42 |
43 |
44 | {% if page_title == "Admin" %}
45 | | Username |
46 | {% else %}
47 | Name |
48 | {% endif %}
49 |
50 | Email |
51 | Session id |
52 | Actions |
53 |
54 | {% for user in users %}
55 |
56 | {% if page_title == "Admin" %}
57 | | {{user.username}} |
58 | {% else %}
59 | {{user.name}} |
60 | {% endif %}
61 | {{user.email}} |
62 | {{user.session_id}} |
63 |
64 | Edit
65 | |
66 |
67 | {% endfor %}
68 |
69 |
70 | {% if prev_btn %}
71 |
Previous page
72 | {% endif %}
73 | {% if next_btn %}
74 |
Next page
75 | {% endif %}
76 |
77 | {% else %}
78 |
79 | User(s) doesn't exists on this page
80 |
81 | {% endif %}
82 |
83 | {% endblock %}
--------------------------------------------------------------------------------
/app/templates/settings/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block head %}
4 |
5 | {% endblock %}
6 |
7 | {% block body %}
8 |
91 | {% endblock %}
--------------------------------------------------------------------------------
/app/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms.validators import Email, Length, DataRequired,NumberRange,EqualTo
3 | from wtforms.fields import EmailField, PasswordField, SubmitField, TextAreaField, FloatField, DateTimeLocalField, StringField, SelectField
4 |
5 |
6 | class AuthForm(FlaskForm):
7 | email = EmailField(
8 | "Your email",
9 | [
10 | DataRequired("Email is required!"),
11 | Email("Enter a valid email!"),
12 | Length(
13 | max=95,
14 | message="Email should not contain more than 95 characters!"
15 | )
16 | ]
17 | )
18 |
19 | password = PasswordField(
20 | "Password",
21 | [
22 | DataRequired("Password is required!"),
23 | Length(
24 | 8, 30, "Password should contain 8 to 30 characters."
25 | )
26 | ]
27 | )
28 |
29 | submit = SubmitField(
30 | "Log in"
31 | )
32 |
33 | class NewStatementForm(FlaskForm):
34 | amount = FloatField(
35 | "Enter amount",
36 | [
37 | DataRequired("Amount is required!"),
38 | NumberRange(0.0001,9999999999.99, "Entered amount could not be added to your statement!")
39 | ]
40 | )
41 | description = TextAreaField(
42 | "Enter description",
43 | [
44 | DataRequired("Description is required!"),
45 | Length(5,180,"Description can contain 5 to 180 characters!")
46 | ],
47 | render_kw={
48 | "class" : "textarea-h",
49 | "rows" : "2"
50 | }
51 | )
52 |
53 | datetime_data = DateTimeLocalField(
54 | format="%Y-%m-%dT%H:%M",
55 | validators=[
56 | DataRequired("This field is required!")
57 | ]
58 | )
59 |
60 | expense = SubmitField("Add Expense")
61 | income = SubmitField("Add Income")
62 |
63 |
64 | class SettingsForm1(FlaskForm):
65 | name = StringField(
66 | "Your name",
67 | [
68 | DataRequired("Cannot set empty string as the name!"),
69 | Length(
70 | min=5, max=45,
71 | message="Name can contain 5 to 45 characters."
72 | )
73 | ]
74 | )
75 |
76 | update_name = SubmitField("Update name")
77 |
78 | class SettingsForm2(FlaskForm):
79 | email = EmailField(
80 | "Your email",
81 | render_kw={
82 | "disabled" : "true"
83 | }
84 | )
85 |
86 | class SettingsForm3(FlaskForm):
87 | password = PasswordField(
88 | "Current password",
89 | [
90 | DataRequired("Current password is required!"),
91 | Length(
92 | 8, 30, "Password should contain 8 to 30 characters."
93 | )
94 | ]
95 | )
96 |
97 | new_password = PasswordField(
98 | "New/Confirm password",
99 | [
100 | DataRequired("New/Confirm Password password is required!"),
101 | Length(
102 | 8, 30, "Password should contain 8 to 30 characters."
103 | ),
104 | # not EqualTo("password", "Password should not match with older password!")
105 | ]
106 | )
107 | update_password = SubmitField("Update password")
108 |
109 | delete_account = SubmitField("Delete account", render_kw={
110 | "class" : "delete"
111 | })
112 |
113 | class StatementEditForm(NewStatementForm):
114 | expense = SubmitField("Update as Expense")
115 | income = SubmitField("Update as Income")
116 | delete_statement = SubmitField("Delete Statement", render_kw={
117 | "class" : "delete"
118 | })
119 |
120 |
121 |
122 | class EditUserForm(SettingsForm1, AuthForm):
123 | update_name = None
124 | submit = None
125 |
126 | password = PasswordField(
127 | "Password to update",
128 | [
129 | Length(
130 | max=30, message="Password should contain 8 to 30 characters."
131 | )
132 | ]
133 | )
134 |
135 | update_account = SubmitField(
136 | "Update data"
137 | )
138 |
139 | delete_account = SubmitField(
140 | "Delete account", render_kw={
141 | "class" : "delete"
142 | })
143 |
144 | def __init__(self, formdata=..., **kwargs):
145 | super().__init__(formdata, **kwargs)
146 | self.name.label = "Name"
147 | self.email.label = "Email"
--------------------------------------------------------------------------------
/app/static/css/main.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "JosefinSans-Medium";
3 | src: url("/static/fonts/JosefinSans-Medium.ttf");
4 | }
5 |
6 | :root {
7 | --bg : #222;
8 | --shade : #999;
9 | --fg : rgb(0, 235, 90);
10 | --fg1 : #fff;
11 | --font : "JosefinSans-Medium";
12 | --font-s : "JosefinSans-Medium";
13 | }
14 |
15 | * {
16 | background-color: var(--bg);
17 | color: var(--fg1);
18 | font-family: var(--font);
19 | }
20 |
21 | body {
22 | padding: 0px;
23 | margin: 0px;
24 | }
25 |
26 | .header {
27 | font-size: 19.5px;
28 | color: var(--fg);
29 | box-shadow: 0px 0px 1px 0px var(--fg1);
30 | padding: 15px;
31 | text-align: center;
32 | }
33 |
34 | .messages-s {
35 | margin: 10px 0px;
36 | }
37 |
38 | .messages-s > .msg {
39 | font-size: 17px;
40 | margin: 5px 0px;
41 | padding: 10px;
42 | color: #fff;
43 | border: 5px solid #fff;
44 | border-left: 0px;
45 | border-right: 0px;
46 | width: 100vw;
47 | max-width: calc( 100% - 20px );
48 | display: flex;
49 | justify-content: space-between;
50 | align-items: center;
51 | }
52 |
53 | .messages-s > .msg-green {
54 | border-color: rgb(0, 192, 74);
55 | }
56 |
57 | .messages-s > .msg-red {
58 | border-color: rgb(250, 43, 32);
59 | }
60 |
61 | .messages-s .end-btn {
62 | border: 0px;
63 | padding: 0px;
64 | margin: 2px 10px;
65 | font-size: 26.5px;
66 | }
67 |
68 | .form {
69 | box-shadow: 0px 0px 1px 0px var(--fg1);
70 | width: 350px;
71 | max-width: calc(100% - 50px);
72 | padding: 25px;
73 | margin: 25px auto;
74 | }
75 |
76 | .form > form,
77 | .form > form > .label {
78 | display: flex;
79 | flex-direction: column;
80 | }
81 |
82 | .form > form > .label {
83 | margin: 15px 5px;
84 | }
85 |
86 | .form > form > .label > label {
87 | font-size: 16px;
88 | margin: 2px 0px;
89 | }
90 |
91 | .form > form > .label > input,
92 | .form > form > .label > select,
93 | .form > form > .label > textarea {
94 | font-size: 18.5px;
95 | padding: 10px;
96 | border: 1px solid var(--fg1);
97 | outline: none;
98 | border-radius: 2px;
99 | }
100 |
101 | .form > form > .label > input:focus ,
102 | .form > form > .label > textarea:focus {
103 | box-shadow: 0px 0px 0px 2px var(--fg);
104 | border: 1px solid transparent;
105 | }
106 |
107 | .form > form input[type="submit"], .btn{
108 | padding: 7px 10px;
109 | font-size: 17px;
110 | margin: 10px 2px;
111 | background-color: var(--bg);
112 | color: var(--fg);
113 | border: 1.5px solid var(--fg);
114 | border-radius: 3px;
115 | transition: 200ms;
116 | }
117 |
118 | .form > form input:disabled {
119 | border: 2px solid var(--shade);
120 | color: var(--shade);
121 | }
122 |
123 | .form > form input[type="submit"]:hover,
124 | .form > form input[type="submit"]:focus,
125 | .btn:hover, .btn:focus {
126 | color: var(--bg);
127 | background-color: var(--fg);
128 | transition: 150ms;
129 | cursor: pointer;
130 | }
131 |
132 | .form > form > .label > .errors {
133 | margin: 3px 0px;
134 | }
135 |
136 | .form > form > .label > .errors > p {
137 | margin: 2px 0px;
138 | }
139 |
140 | .form > form > .label > .errors > p::before {
141 | content: "*";
142 | }
143 |
144 | .form .note {
145 | font-size: 15px;
146 | }
147 |
148 | .shadow-card {
149 | padding: 20px;
150 | margin: 20px auto;
151 | width: calc(100% - 50px);
152 | max-width: 850px;
153 | border-radius: 10px;
154 | }
155 |
156 | .shadow-card,
157 | .shadow-card * {
158 | background-color: #333;
159 | }
160 |
161 | .flex {
162 | display: flex;
163 | flex-direction: column;
164 | }
165 |
166 | .flex-s {
167 | display: flex;
168 | flex-direction: row;
169 | justify-content: space-between;
170 | align-items: center;
171 | }
172 |
173 | .w-btn {
174 | color: #fff !important;
175 | border-color: #fff !important;
176 | font-size: 17px;
177 | padding: 15px 30px;
178 | }
179 |
180 | .w-btn:hover, .w-btn:focus {
181 | background-color: #fff !important;
182 | color: #000 !important;
183 | }
184 |
185 | .footer {
186 | width: calc( 100% - 100px );
187 | border-top: 0.5px solid var(--fg1);
188 | padding: 50px;
189 | margin-top: 50px;
190 | }
191 |
192 | .footer > footer {
193 | max-width: 1250px;
194 | margin: auto;
195 | display: flex;
196 | flex-wrap: wrap;
197 | flex-direction: row;
198 | justify-content: space-between;
199 | }
200 |
201 | .footer > footer > .part {
202 | margin: 10px 20px;
203 | }
204 |
205 | .footer > footer > .part > .head {
206 | font-size: 25px;
207 | }
208 |
209 |
210 | form .delete {
211 | border-color: rgb(255, 19, 19) !important;
212 | color: rgb(255, 43, 43) !important;
213 | }
214 |
215 | form .delete:hover,
216 | form .delete:focus {
217 | background-color: rgb(255, 19, 19) !important;
218 | color: #fff !important;
219 | }
--------------------------------------------------------------------------------
/app/templates/home/statements.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block head %}
4 |
5 |
6 |
7 |
74 | {% endblock %}
75 |
76 | {% block body %}
77 |
78 |
81 |
Statements
82 |
83 |
84 |
Total Income
85 |
₹{{round(total_income,3)}}
86 |
87 |
88 |
Total Expense
89 |
₹{{round(total_expense,3)}}
90 |
91 |
92 |
Current balance
93 |
₹{{round(current_balance,3)}}
94 |
95 |
Download statements
96 |
97 |
98 |
99 |
104 |
105 |
106 |
123 |
124 |
125 |
132 | {% endblock %}
--------------------------------------------------------------------------------
/app/views/home.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, render_template, redirect, url_for, flash, Response, stream_with_context
2 |
3 | from ..app_functions import get_current_user, get_current_user_balance, user_login_required
4 | from ..functions import generate_string
5 | from ..forms import NewStatementForm, StatementEditForm
6 | from ..db import db, Statements, User
7 | from sqlalchemy import func
8 |
9 |
10 | home = Blueprint("Home", __name__)
11 |
12 |
13 | @home.route("/")
14 | @user_login_required
15 | def home_index():
16 | user = get_current_user()
17 |
18 | user_details = {}
19 | user_details["name"] = user.name
20 | user_details["account_balance"] = round(get_current_user_balance(),2)
21 |
22 | statements = Statements.query.filter(Statements.user_id == user.id).order_by(Statements.operation_time.desc()).limit(5).all()
23 |
24 | user_details["statements"] = list(
25 | {"desc":x.description, "amount":x.amount, "at":x.operation_time, "id": x.statement_id} for x in statements
26 | )
27 |
28 | return render_template(
29 | "home/index.html",
30 | user= user_details
31 | )
32 |
33 | @home.route("/new", methods=("GET","POST"))
34 | @user_login_required
35 | def new_statement():
36 | form = NewStatementForm(request.form)
37 | if form.validate_on_submit():
38 | amount = form.amount.data
39 | description = form.description.data
40 | at = form.datetime_data.data
41 | income = form.income.data
42 | expense = form.expense.data
43 |
44 | if not income and not expense:
45 | flash("Cannot add statement that is neither income nor expense!","red")
46 | return redirect(url_for(".new_statement"))
47 |
48 | amount = abs(amount)
49 | if expense is True:
50 | amount = -amount
51 |
52 | current_user = get_current_user()
53 | user_id = current_user.id
54 |
55 | statement = Statements(
56 | description = description,
57 | amount = amount,
58 | operation_time = at,
59 | user_id = user_id,
60 | statement_id = generate_string()
61 | )
62 |
63 | db.session.add(statement)
64 | db.session.commit()
65 |
66 | flash("Statement was added to your account successfully.", "green")
67 | return redirect(url_for("Home.home_index"))
68 |
69 | return render_template(
70 | "home/new_statement.html",
71 | title="New statement",
72 | form=form
73 | )
74 |
75 | @home.route("/statements")
76 | @user_login_required
77 | def statements():
78 | try:
79 | page = int(request.args.get("page","0"))
80 | except:
81 | page = 0
82 | finally:
83 | if page < 0:
84 | page = 0
85 |
86 | page_size = 6
87 |
88 | user = get_current_user()
89 |
90 | if request.args.get("t") == "expense":
91 | statements = Statements.query.filter(Statements.user_id == user.id, Statements.amount < 0).order_by(Statements.operation_time.desc()).limit(page_size).offset(page_size*page).all()
92 | elif request.args.get("t") == "income":
93 | statements = Statements.query.filter(Statements.user_id == user.id, Statements.amount >= 0).order_by(Statements.operation_time.desc()).limit(page_size).offset(page_size*page).all()
94 | else:
95 | statements = Statements.query.filter(Statements.user_id == user.id).order_by(Statements.operation_time.desc()).limit(page_size).offset(page_size*page).all()
96 |
97 | statements = list(
98 | {"desc":x.description, "amount":x.amount, "at": str(x.operation_time), "id": x.statement_id} for x in statements
99 | )
100 |
101 | current_balance = get_current_user_balance()
102 | total_expense = Statements.query.with_entities(func.sum(Statements.amount)).filter(Statements.user_id == user.id, Statements.amount < 0).first()[0]
103 | if total_expense is None:
104 | total_expense = 0.0
105 | total_expense = abs(total_expense)
106 | total_income = Statements.query.with_entities(func.sum(Statements.amount)).filter(Statements.user_id == user.id, Statements.amount >= 0).first()[0]
107 | if total_income is None:
108 | total_income = 0.0
109 |
110 | if request.args.get("__a") == "1":
111 | return statements
112 |
113 | date = []
114 | expense_amount = []
115 | income_amount = []
116 | amount = []
117 |
118 | for x in Statements.query.filter(Statements.user_id == user.id).order_by(Statements.operation_time).all():
119 | date.append(x.operation_time)
120 | money = x.amount
121 |
122 | amount.append(abs(money))
123 |
124 | if money < 0:
125 | expense_amount.append(abs(money))
126 | income_amount.append(None)
127 | else:
128 | income_amount.append(money)
129 | expense_amount.append(None)
130 |
131 |
132 | return render_template(
133 | "home/statements.html",
134 | title="All statements",
135 | statements=statements,
136 | total_income=total_income,
137 | total_expense=total_expense,
138 | current_balance=current_balance,
139 | page_size=page_size,
140 | round=round
141 | )
142 |
143 | @home.route("/statement/", methods=("GET","POST"))
144 | @user_login_required
145 | def specific_statement(statement_id):
146 | statement = Statements.query.filter(Statements.statement_id == statement_id).first_or_404()
147 |
148 | form = StatementEditForm(request.form)
149 |
150 | if form.validate_on_submit():
151 | amount = form.amount.data
152 | description = form.description.data
153 | date_time = form.datetime_data.data
154 |
155 | income = form.income.data
156 | expense = form.expense.data
157 | delete = form.delete_statement.data
158 |
159 | amount = abs(amount)
160 |
161 | if income is True:
162 | statement.amount = amount
163 | statement.description = description
164 | statement.operation_time = date_time
165 |
166 | db.session.commit()
167 |
168 | flash("Updated statement as income successfully", "green")
169 |
170 | elif expense is True:
171 | statement.amount = -amount
172 | statement.description = description
173 | statement.operation_time = date_time
174 |
175 | db.session.commit()
176 |
177 | flash("Updated statement as expense successfully", "green")
178 |
179 | elif delete is True:
180 | db.session.delete(statement)
181 | db.session.commit()
182 |
183 | flash("The statement was deleted successfully", "green")
184 |
185 | return redirect(url_for(".home_index"))
186 |
187 | else:
188 | flash("Unsupported action to perform!", "red")
189 |
190 | return redirect(url_for(".specific_statement", statement_id=statement_id))
191 |
192 | form.amount.data = abs(statement.amount)
193 | form.description.data = statement.description
194 | form.datetime_data.data = statement.operation_time
195 |
196 |
197 | return render_template(
198 | "home/statement.html",
199 | title="Statement",
200 | form=form,
201 | statement_id=statement_id
202 | )
203 |
204 | @home.route("/download-statements")
205 | @user_login_required
206 | def download_statements():
207 | user = get_current_user()
208 | statements = Statements.query.filter(Statements.user_id == user.id)
209 |
210 | content_disposition = "attachment; filename={}'s-statements.csv".format(user.name)
211 |
212 | def data():
213 | first_iter = True
214 | for statement in statements.yield_per(5):
215 | if first_iter is True:
216 | first_iter = False
217 | yield "amount, description, datetime, type\n"
218 |
219 | if statement.amount < 0:
220 | statement_type = "expense"
221 | else:
222 | statement_type = "income"
223 | yield f"{abs(statement.amount)},{statement.description},{statement.operation_time.isoformat()},{statement_type}\n"
224 |
225 | return Response(stream_with_context(data()), 200, mimetype="text/csv",headers={
226 | "Content-Disposition" : content_disposition
227 | })
--------------------------------------------------------------------------------
/app/views/admin.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, render_template, request, flash, redirect, url_for, session, abort
2 | from werkzeug.security import check_password_hash, generate_password_hash
3 | from ..db import Admin, User, Statements, VisitorStats, db
4 | from sqlalchemy import func, desc, cast, Date
5 | from ..forms import AuthForm, EditUserForm
6 | from ..functions import get_pie_chart, get_line_chart
7 | from ..app_functions import admin_login_required
8 | import math
9 |
10 | admin = Blueprint("Admin",__name__, url_prefix="/admin")
11 |
12 | @admin.route("/")
13 | @admin_login_required
14 | def dashboard():
15 | if 'admin-sign-id' not in session:
16 | return redirect(url_for(".admin_login"))
17 |
18 | admin = Admin.query.filter(Admin.session_id == session.get("admin-sign-id")).first_or_404()
19 |
20 | name = admin.username
21 |
22 | users_count = User.query.with_entities(func.count(User.id)).first()[0]
23 | statements_count = Statements.query.with_entities(func.count(Statements.id)).first()[0]
24 |
25 | visits_count = VisitorStats.query.with_entities(func.count(VisitorStats.browser)).first()[0]
26 | bots_visits = VisitorStats.query.filter(VisitorStats.is_bot == True).with_entities(func.count(VisitorStats.is_bot)).first()[0]
27 | other_user_agent_visits = VisitorStats.query.filter(VisitorStats.browser == "other").with_entities(func.count(VisitorStats.browser)).first()[0]
28 |
29 | most_used_useragent = VisitorStats.query.with_entities(VisitorStats.browser, func.count(VisitorStats.browser).label("browser_count")).group_by(VisitorStats.browser).order_by(desc("browser_count")).limit(1).first()[0]
30 | most_used_operating_system = VisitorStats.query.with_entities(VisitorStats.operating_system, func.count(VisitorStats.operating_system).label("os")).group_by(VisitorStats.operating_system).order_by(desc("os")).limit(1).first()[0]
31 | highest_visits_with_date = VisitorStats.query.with_entities(cast(VisitorStats.date, Date), func.count(cast(VisitorStats.date, Date)).label("date")).group_by(cast(VisitorStats.date, Date)).order_by(desc("date")).limit(1).first()
32 |
33 | less_used_useragent = VisitorStats.query.with_entities(VisitorStats.browser, func.count(VisitorStats.browser).label("browser_count")).group_by(VisitorStats.browser).order_by("browser_count").limit(1).first()[0]
34 | less_used_operating_system = VisitorStats.query.with_entities(VisitorStats.operating_system, func.count(VisitorStats.operating_system).label("os")).group_by(VisitorStats.operating_system).order_by("os").limit(1).first()[0]
35 | lowest_visits_with_date = VisitorStats.query.with_entities(cast(VisitorStats.date, Date), func.count(cast(VisitorStats.date, Date)).label("date")).group_by(VisitorStats.date).order_by("date").limit(1).first()
36 |
37 | browser_data = VisitorStats.query.with_entities(VisitorStats.browser, func.count(VisitorStats.browser)).group_by(VisitorStats.browser).all()
38 | browser_pie_chart = get_pie_chart(browser_data, "Browser used by visitors")
39 |
40 | operating_system_data = VisitorStats.query.with_entities(VisitorStats.operating_system, func.count(VisitorStats.operating_system)).group_by(VisitorStats.operating_system).all()
41 | operating_system_chart = get_pie_chart(operating_system_data, "Operating System of visitors")
42 |
43 | visits_data = VisitorStats.query.with_entities(cast(VisitorStats.date, Date), func.count(VisitorStats.date)).group_by(cast(VisitorStats.date, Date)).order_by(desc(cast(VisitorStats.date, Date))).limit(90).all()
44 | visits_line_chart = get_line_chart(visits_data, "Visit date", "Traffic amount", "Application visits statistics (last active 90 days)")
45 |
46 | return render_template(
47 | "admin/index.html",
48 | title="Admin : Dashboard : {}".format(name),
49 | name=name,
50 | table_data = {
51 | "Total users" : users_count,
52 | "Total Statements" : statements_count,
53 | "Average statements" : statements_count/users_count,
54 | "Total visits" : visits_count,
55 | "Bot(s) visits" : bots_visits,
56 | "Other user-agent visits" : other_user_agent_visits,
57 | "Total visits - Bot(s)" : visits_count - bots_visits,
58 | "Total visits - Bot(s) - Other(s)" : visits_count - bots_visits - other_user_agent_visits,
59 | "Most used user agent" : most_used_useragent,
60 | "Less used user agent" : less_used_useragent,
61 | "Most used OS" : most_used_operating_system,
62 | "Less used OS" : less_used_operating_system,
63 | "Highest hits date(count)" : f"{highest_visits_with_date[0]}({highest_visits_with_date[1]})",
64 | "Lowest hits date(count)" : f"{lowest_visits_with_date[0]}({lowest_visits_with_date[1]})",
65 | },
66 | browser_pie_chart=browser_pie_chart,
67 | visits_line_chart=visits_line_chart,
68 | operating_system_chart=operating_system_chart
69 | )
70 |
71 |
72 | @admin.route("/login", methods=("GET","POST"))
73 | def admin_login():
74 | form = AuthForm(request.form)
75 |
76 | if form.validate_on_submit():
77 | email = form.email.data
78 | password = form.password.data
79 |
80 | account = Admin.query.filter(Admin.email == email).first()
81 |
82 | if not account:
83 | flash("Admin account does not exists!","red")
84 | elif not check_password_hash(account.password, password):
85 | flash("Invalid password for admin account!","red")
86 | else:
87 | session["admin-sign-id"] = account.session_id
88 | return redirect(url_for(".dashboard"))
89 |
90 | return redirect(url_for(".admin_login"))
91 |
92 | return render_template(
93 | "admin/login.html",
94 | title="Admin Login",
95 | form=form,
96 | is_loggedin=False
97 | )
98 |
99 |
100 | @admin.route("/users")
101 | @admin_login_required
102 | def users():
103 | page = request.args.get("page", "1")
104 | users_per_page = request.args.get("upp","10")
105 |
106 | if not page.isnumeric() or int(page) <= 0:
107 | page = "1"
108 | page = int(page)-1
109 |
110 | if not users_per_page.isnumeric() or int(users_per_page) <= 0:
111 | users_per_page = "10"
112 | users_per_page = int(users_per_page)
113 |
114 | users_type = request.args.get("type","none").lower()
115 | if users_type not in ("user","admin"):
116 | users_type = "user"
117 |
118 | if users_type == "user":
119 | users = User.query.limit(users_per_page).offset(page*users_per_page).all()
120 | users_count = User.query.with_entities(func.count(User.id)).first()[0]
121 | page_count = math.ceil(users_count/users_per_page)
122 | else:
123 | users = Admin.query.limit(users_per_page).offset(page*users_per_page).all()
124 | users_count = Admin.query.with_entities(func.count(Admin.id)).first()[0]
125 | page_count = math.ceil(users_count/users_per_page)
126 |
127 |
128 | return render_template(
129 | "admin/users.html",
130 | title="Admin : users : {}".format(users_type.title()),
131 | users=users,
132 | users_count=users_count,
133 | users_per_page=users_per_page,
134 | page_count=page_count,
135 | current_page=page,
136 | prev_btn=page > 0,
137 | next_btn=page+1 < page_count,
138 | page_title=users_type.title()
139 | )
140 |
141 |
142 | @admin.route("/user///", methods=["GET","POST"])
143 | @admin_login_required
144 | def specific_user(member_type,id,email):
145 | member_type = member_type.lower()
146 | if member_type not in ("user", "admin"):
147 | return abort(404)
148 |
149 | if member_type == "user":
150 | user = User.query.filter(User.id == id, User.email == email).first()
151 | else:
152 | user = Admin.query.filter(Admin.id == id, Admin.email == email).first()
153 |
154 | if not user:
155 | return abort(404)
156 |
157 | form = EditUserForm(request.form)
158 |
159 | if form.validate_on_submit():
160 | name = form.name.data
161 | email = form.email.data
162 | password = form.password.data
163 | update_account = form.update_account.data
164 | delete_account = form.delete_account.data
165 |
166 | if delete_account is True:
167 | db.session.delete(user)
168 | db.session.commit()
169 |
170 | flash("User deleted successfully", "green")
171 |
172 | elif update_account is True:
173 | if member_type == "user":
174 | user.name = name
175 | else:
176 | user.username = name
177 | user.email = email
178 |
179 | if password:
180 | user.password = generate_password_hash(password, "SHA256")
181 |
182 | db.session.commit()
183 | flash("User updated successfully", "green")
184 |
185 | else:
186 | flash("Operation is not supported!", "red")
187 |
188 | return redirect(url_for(".specific_user",member_type=member_type, email=email, id=id))
189 |
190 | if member_type == "user":
191 | form.name.data = user.name
192 | else:
193 | form.name.data = user.username
194 |
195 | form.email.data = user.email
196 |
197 | return render_template(
198 | "admin/user.html",
199 | title="Details of user {}".format(form.name.data),
200 | form=form,
201 | )
202 |
203 | @admin.route("/logout", methods=["POST"])
204 | @admin_login_required
205 | def logout_admin():
206 | session.pop("admin-sign-id")
207 | flash("Logged out successfully.", "green")
208 | return redirect(url_for(".admin_login"))
--------------------------------------------------------------------------------