rewrite frontend and logic in core app
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
from django.contrib import admin
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Subject
|
||||
from core.prediction import predict_transactions
|
||||
|
||||
past_lookup_delta = relativedelta(weeks=2)
|
||||
future_lookup_delta = relativedelta(months=2)
|
||||
from core.prediction import predict_all
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -21,29 +17,11 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
start_balance = Decimal(options["start"])
|
||||
|
||||
today = datetime.now().date()
|
||||
past_lookup_bound = today - past_lookup_delta
|
||||
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)
|
||||
prediction_list = predict_all(Subject.objects.order_by("name"))
|
||||
today = timezone.now().date()
|
||||
|
||||
future_transactions = []
|
||||
for subject, prediction in transaction_dict.items():
|
||||
transactions, info = prediction[0], prediction[1]
|
||||
for subject, transactions, info in prediction_list:
|
||||
print(f">>> {subject}")
|
||||
if info:
|
||||
rec_days, rec_months, day_of_month = \
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Iterable
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Subject, Transaction
|
||||
|
||||
@@ -85,3 +88,28 @@ def predict_transactions(subject: Subject):
|
||||
"day_of_month": day_of_month,
|
||||
}
|
||||
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