rewrite frontend and logic in core app
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from core.models import Subject, Transaction
|
from core.models import Subject, Transaction
|
||||||
from financeplanner.admin import admin_site
|
|
||||||
|
|
||||||
|
class CustomAdminSite(admin.AdminSite):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
admin_site = CustomAdminSite()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Subject, site=admin_site)
|
@admin.register(Subject, site=admin_site)
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Subject
|
from core.models import Subject
|
||||||
from core.prediction import predict_transactions
|
from core.prediction import predict_all
|
||||||
|
|
||||||
past_lookup_delta = relativedelta(weeks=2)
|
|
||||||
future_lookup_delta = relativedelta(months=2)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -21,29 +17,11 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
start_balance = Decimal(options["start"])
|
start_balance = Decimal(options["start"])
|
||||||
|
|
||||||
today = datetime.now().date()
|
prediction_list = predict_all(Subject.objects.order_by("name"))
|
||||||
past_lookup_bound = today - past_lookup_delta
|
today = timezone.now().date()
|
||||||
future_lookup_bound = today + future_lookup_delta
|
|
||||||
|
|
||||||
transaction_dict = {}
|
|
||||||
for subject in Subject.objects.all():
|
|
||||||
transactions = []
|
|
||||||
for tr in subject.transactions.order_by("booking_date"):
|
|
||||||
if past_lookup_bound <= tr.booking_date <= future_lookup_bound:
|
|
||||||
transactions.append(tr)
|
|
||||||
predicted_transaction, prediction_info = predict_transactions(subject)
|
|
||||||
if predicted_transaction:
|
|
||||||
first_predicted_date = predicted_transaction[0].booking_date
|
|
||||||
if first_predicted_date >= past_lookup_bound:
|
|
||||||
# if two weeks after the first predicted transaction have passed, the subject is considered done
|
|
||||||
for tr in predicted_transaction:
|
|
||||||
if past_lookup_bound <= tr.booking_date <= future_lookup_bound:
|
|
||||||
transactions.append(tr)
|
|
||||||
transaction_dict[subject] = (transactions, prediction_info)
|
|
||||||
|
|
||||||
future_transactions = []
|
future_transactions = []
|
||||||
for subject, prediction in transaction_dict.items():
|
for subject, transactions, info in prediction_list:
|
||||||
transactions, info = prediction[0], prediction[1]
|
|
||||||
print(f">>> {subject}")
|
print(f">>> {subject}")
|
||||||
if info:
|
if info:
|
||||||
rec_days, rec_months, day_of_month = \
|
rec_days, rec_months, day_of_month = \
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.models import Subject, Transaction
|
from core.models import Subject, Transaction
|
||||||
|
|
||||||
@@ -85,3 +88,28 @@ def predict_transactions(subject: Subject):
|
|||||||
"day_of_month": day_of_month,
|
"day_of_month": day_of_month,
|
||||||
}
|
}
|
||||||
return transactions, prediction_info
|
return transactions, prediction_info
|
||||||
|
|
||||||
|
|
||||||
|
def predict_all(subjects: Iterable[Subject], past_days=30, future_days=60):
|
||||||
|
today = timezone.now().date()
|
||||||
|
past_lookup_bound = today - timedelta(days=past_days)
|
||||||
|
future_lookup_bound = today + timedelta(days=future_days)
|
||||||
|
lookup_bounds = (past_lookup_bound, future_lookup_bound)
|
||||||
|
|
||||||
|
prediction_list = []
|
||||||
|
for subject in subjects:
|
||||||
|
transactions = list(
|
||||||
|
subject.transactions.filter(booking_date__range=lookup_bounds).order_by("booking_date"))
|
||||||
|
|
||||||
|
predicted_transactions, prediction_info = predict_transactions(subject)
|
||||||
|
if predicted_transactions:
|
||||||
|
first_predicted_date = predicted_transactions[0].booking_date
|
||||||
|
if first_predicted_date >= past_lookup_bound:
|
||||||
|
# if two weeks after the first predicted transaction have passed, the subject is considered done
|
||||||
|
for transaction in predicted_transactions:
|
||||||
|
if past_lookup_bound <= transaction.booking_date <= future_lookup_bound:
|
||||||
|
transactions.append(transaction)
|
||||||
|
|
||||||
|
prediction_list.append((subject, transactions, prediction_info))
|
||||||
|
|
||||||
|
return prediction_list
|
||||||
|
|||||||
152
core/static/core/style.css
Normal file
152
core/static/core/style.css
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 600px) and (max-width: 899px) {
|
||||||
|
html, body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 900px) {
|
||||||
|
html, body {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #111111;
|
||||||
|
color: #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, p, a {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title, #navi, #content {
|
||||||
|
flex: 0 1 0;
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #6699EE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navi {
|
||||||
|
border-top: 1px solid #223366;
|
||||||
|
border-bottom: 1px solid #223366;
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #6699EE;
|
||||||
|
transition: color 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, a:focus {
|
||||||
|
color: #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_username, #id_password, #login-warning, #login-button {
|
||||||
|
flex: 0 1 0;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, thead, tbody, tfoot, tr, td {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 3px 4px;
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-cell {
|
||||||
|
max-width: 100px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 600px) and (max-width: 899px) {
|
||||||
|
table {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-cell {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 900px) {
|
||||||
|
table {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 3px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-cell {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
core/templates/core/base.html
Normal file
26
core/templates/core/base.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>finance planner</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'core/style.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="title">
|
||||||
|
📊 finance planner 💰
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navi">
|
||||||
|
{% block navi %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
core/templates/core/subjects.html
Normal file
45
core/templates/core/subjects.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% block navi %}
|
||||||
|
<a href="{% url 'admin:index' %}">admin</a>
|
||||||
|
|
|
||||||
|
<a href="{% url 'logout' %}">logout</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>current subjects</h1>
|
||||||
|
|
||||||
|
{% if prediction_list %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td class="subject-cell">subject</td>
|
||||||
|
<td class="date-cell">date</td>
|
||||||
|
<td class="amount-cell">€</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for subject, transactions, info in prediction_list %}
|
||||||
|
{% for transaction in transactions %}
|
||||||
|
<tr>
|
||||||
|
{% if forloop.counter == 1 %}
|
||||||
|
<td rowspan="{{ transactions|length }}" class="subject-cell">{{ subject.name }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="date-cell">
|
||||||
|
{% if transaction.pk %}🗒{% else %}🔮{% endif %}
|
||||||
|
{{ transaction.booking_date|date:"d.m.y" }}
|
||||||
|
</td>
|
||||||
|
<td class="amount-cell">{{ transaction.amount }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
no data to show :/
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
11
core/templates/registration/logged_out.html
Normal file
11
core/templates/registration/logged_out.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% block navi %}
|
||||||
|
<a href="{% url 'admin:index' %}">admin</a>
|
||||||
|
|
|
||||||
|
<a href="{% url 'login' %}">login</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
You have been logged out.
|
||||||
|
{% endblock %}
|
||||||
22
core/templates/registration/login.html
Normal file
22
core/templates/registration/login.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% block navi %}
|
||||||
|
<a href="{% url 'admin:index' %}">admin</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form id="login-form" method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label for="id_username">username</label>
|
||||||
|
<input id="id_username" name="username" type="text" placeholder="username">
|
||||||
|
<label for="id_password">password</label>
|
||||||
|
<input id="id_password" name="password" type="password" placeholder="password">
|
||||||
|
{% if form.errors %}
|
||||||
|
<div id="login-warning">
|
||||||
|
nope.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input id="login-button" type="submit" value="login"/>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
10
core/urls.py
Normal file
10
core/urls.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
|
from core.views import SubjectsView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", RedirectView.as_view(pattern_name="core:subjects", permanent=False), name="index"),
|
||||||
|
path("subjects/", login_required(SubjectsView.as_view()), name="subjects"),
|
||||||
|
]
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
from django.shortcuts import render
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
# Create your views here.
|
from core.models import Subject
|
||||||
|
from core.prediction import predict_all
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectsView(TemplateView):
|
||||||
|
template_name = "core/subjects.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["prediction_list"] = predict_all(Subject.objects.order_by("name"))
|
||||||
|
return context
|
||||||
|
|||||||
Reference in New Issue
Block a user