redesign, added user system, added api endpoints

This commit is contained in:
Lyxminxx committed 2026-09-20 14:35:20 +02:00
1 parent 2c76e1df78
commit 72b8e569aa
17 files changed
+782 -99

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
from ninja import NinjaAPI, Schema
from typing import List
from datetime import date
from decimal import Decimal
from core.models import Transaction, Category
api = NinjaAPI()
# --- Schemas (Data validation for Flutter) ---
class CategorySchema(Schema):
id: int
name: str
class TransactionOut(Schema):
id: int
title: str
amount: Decimal
date: date
category: CategorySchema | None = None
class TransactionIn(Schema):
title: str
amount: Decimal
date: date
category_id: int | None = None
# --- Endpoints ---
@api.get("/transactions", response=List[TransactionOut])
def list_transactions(request):
return Transaction.objects.select_related('category').all().order_by('-date')
@api.post("/transactions", response=TransactionOut)
def create_transaction(request, payload: TransactionIn):
transaction = Transaction.objects.create(
title=payload.title,
amount=payload.amount,
date=payload.date,
category_id=payload.category_id
)
return transaction
@api.get("/categories", response=List[CategorySchema])
def list_categories(request):
return Category.objects.all()
+19 -4
View File
@@ -1,11 +1,26 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Transaction
class TransactionForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ['title', 'amount', 'category', 'date']
# This widget forces the browser to show a date picker calendar
widgets = {
'date': forms.DateInput(attrs={'type': 'date'}),
}
class CustomUserCreationForm(UserCreationForm):
first_name = forms.CharField(max_length=30, required=True, label="First Name")
last_name = forms.CharField(max_length=30, required=True, label="Last Name")
class Meta(UserCreationForm.Meta):
model = User
fields = UserCreationForm.Meta.fields + ('first_name', 'last_name')
def save(self, commit=True):
user = super().save(commit=False)
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
if commit:
user.save()
return user
+22
View File
@@ -0,0 +1,22 @@
# Generated by Django 6.1.1 on 2026-09-20 11:51
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='transaction',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]
+3
View File
@@ -4,8 +4,11 @@ from django.db import (
from django.utils import(
timezone
)
from django.contrib.auth.models import User
class Transaction(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
amount = models.DecimalField(max_digits=10, decimal_places=2)
+7
View File
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'ninja',
]
MIDDLEWARE = [
@@ -133,3 +134,9 @@ MAILERS = {
},
}
STATIC_ROOT = BASE_DIR / "staticfiles"
LOGIN_REDIRECT_URL = 'home'
LOGIN_URL = 'login'
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
+391
View File
@@ -0,0 +1,391 @@
:root {
/* Gruvbox Dark Palette */
--bg-main: #282828; /* bg0 */
--bg-card: #3c3836; /* bg1 */
--bg-card-hover: #504945; /* bg2 */
--border: #504945; /* bg2 */
--border-hover: #665c54; /* bg3 */
--border-focus: #fabd2f; /* Gruvbox Yellow */
--text-main: #ebdbb2; /* fg1 */
--text-heading: #fbf1c7; /* fg0 */
--text-muted: #a89984; /* fg4 */
--primary: #fabd2f; /* Gruvbox Yellow */
--primary-hover: #d79921; /* Gruvbox Dark Yellow */
--primary-text: #282828; /* Dark contrast text for yellow buttons */
--income: #b8bb26; /* Gruvbox Green */
--expense: #fb4934; /* Gruvbox Red */
--danger: #fb4934; /* Gruvbox Red */
--accent-blue: #83a598; /* Gruvbox Blue */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 920px;
margin: 0 auto;
padding: 24px 20px 60px 20px;
color: var(--text-main);
background-color: var(--bg-main);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* Headings & Links */
h1, h2, h3, h4 {
margin-top: 0;
color: var(--text-heading);
font-weight: 700;
letter-spacing: -0.02em;
}
a {
color: var(--primary);
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: var(--primary-hover);
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--border);
margin-bottom: 32px;
}
.navbar-brand {
font-size: 22px;
font-weight: 800;
letter-spacing: 0.05em;
text-decoration: none;
color: var(--primary);
}
.navbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
/* Header Section & Balance Card */
.header-section {
margin-bottom: 32px;
padding: 24px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.header-section h1 {
margin: 0 0 8px 0;
font-size: 28px;
}
.balance-text {
margin: 0;
font-size: 15px;
color: var(--text-muted);
}
.balance-text strong {
font-size: 24px;
color: var(--primary);
font-weight: 700;
margin-left: 6px;
}
/* Cards & Auth Containers */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
padding: 24px;
border-radius: var(--radius-md);
margin-bottom: 32px;
box-shadow: var(--shadow);
}
.auth-container {
max-width: 420px;
margin: 60px auto;
}
.auth-container h2 {
text-align: center;
margin-bottom: 24px;
}
.auth-container p {
text-align: center;
color: var(--text-muted);
font-size: 14px;
}
/* Tables */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 10px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
th {
background: var(--bg-card-hover);
color: var(--text-muted);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
text-align: left;
}
td {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
font-size: 14px;
color: var(--text-main);
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background-color: var(--bg-card-hover);
}
.income {
color: var(--income);
font-weight: 600;
}
.expense {
color: var(--expense);
font-weight: 500;
}
/* Filter Pills */
.filters {
margin-bottom: 32px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filters a {
padding: 8px 16px;
border-radius: var(--radius-sm);
text-decoration: none;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
}
.filters a:hover {
border-color: var(--border-hover);
color: var(--text-main);
}
.filters a.active {
background: var(--primary);
color: var(--primary-text);
border-color: var(--primary);
font-weight: 600;
box-shadow: 0 0 12px rgba(250, 189, 47, 0.25);
}
/* Chart Container */
.chart-container {
position: relative;
height: 400px;
width: 100%;
margin-top: 24px;
padding: 20px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
/* Buttons */
button, .btn {
background: var(--primary);
color: var(--primary-text);
padding: 9px 18px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
text-decoration: none;
font-size: 14px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
button:hover, .btn:hover {
background: var(--primary-hover);
transform: translateY(-1px);
}
.btn-stats, .btn-nav {
background: var(--bg-card-hover);
color: var(--text-main);
border: 1px solid var(--border-hover);
font-weight: 600;
}
.btn-stats:hover, .btn-nav:hover {
background: var(--border-hover);
color: var(--text-heading);
}
.btn-logout, .btn-delete {
background: rgba(251, 73, 52, 0.15);
color: var(--danger);
border: 1px solid rgba(251, 73, 52, 0.3);
font-weight: 600;
}
.btn-logout:hover, .btn-delete:hover {
background: var(--danger);
color: var(--bg-main);
}
.btn-edit {
background: rgba(131, 165, 152, 0.15);
color: var(--accent-blue);
border: 1px solid rgba(131, 165, 152, 0.3);
text-decoration: none;
padding: 6px 12px;
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 600;
margin-right: 6px;
transition: all 0.15s ease;
}
.btn-edit:hover {
background: var(--accent-blue);
color: var(--bg-main);
}
.cancel {
background: var(--bg-card-hover);
color: var(--text-muted);
text-decoration: none;
padding: 9px 18px;
border-radius: var(--radius-sm);
display: inline-block;
margin-left: 10px;
font-size: 14px;
font-weight: 600;
border: 1px solid var(--border-hover);
transition: all 0.15s ease;
}
.cancel:hover {
background: var(--border-hover);
color: var(--text-main);
}
/* Django Form Styling */
form p {
margin-bottom: 18px;
}
form label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
form input[type="text"],
form input[type="password"],
form input[type="email"],
form input[type="number"],
form input[type="date"],
form select {
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 14px;
background-color: var(--bg-main);
color: var(--text-main);
transition: all 0.15s ease;
}
form input:focus,
form select:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(250, 189, 47, 0.2);
}
/* Django Form Password Requirements & Help Text */
.helptext, form span.helptext {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-top: 6px;
line-height: 1.5;
}
.helptext ul {
margin: 6px 0 10px 0;
padding-left: 18px;
color: var(--text-muted);
font-size: 12px;
}
.helptext li {
margin-bottom: 4px;
}
/* Validation Errors */
.errorlist {
color: var(--expense);
font-size: 13px;
list-style: none;
padding: 10px 14px;
margin: 6px 0 16px 0;
background: rgba(251, 73, 52, 0.1);
border: 1px solid rgba(251, 73, 52, 0.25);
border-radius: var(--radius-sm);
font-weight: 500;
}
.errorlist li {
margin: 2px 0;
}
+6 -8
View File
@@ -1,13 +1,11 @@
{% load static %}
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Edit Transaction</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }
.card { background: #f4f4f5; padding: 20px; border-radius: 8px; }
button { background: #3b82f6; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.cancel { background: #e4e4e7; color: black; text-decoration: none; padding: 8px 16px; border-radius: 4px; display: inline-block; margin-left: 10px; }
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Transaction - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<h2>Editing: {{ transaction.title }}</h2>
+53 -43
View File
@@ -1,25 +1,30 @@
{% load static %}
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>DIME Tracker</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }
.card { background: #f4f4f5; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; border-bottom: 1px solid #e4e4e7; text-align: left; }
.income { color: #16a34a; font-weight: bold; }
.expense { color: #dc2626; }
button { background: #18181b; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-edit { background: #3b82f6; color: white; text-decoration: none; padding: 6px 12px; border-radius: 4px; font-size: 14px; margin-right: 5px; }
.btn-delete { background: #ef4444; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<h1>Just another spending tracker</h1>
<div style="margin-bottom: 20px;">
<h2>Total Balance: {{ total_balance|floatformat:2 }} kr</h2>
<a href="{% url 'stats' %}" style="background: #e4e4e7; color: black; text-decoration: none; padding: 8px 16px; border-radius: 4px; font-weight: bold;">View Statistics 📊</a>
<nav class="navbar">
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
<div class="navbar-actions">
<a href="{% url 'stats' %}" class="btn btn-stats">View Statistics 📊</a>
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
{% csrf_token %}
<button type="submit" class="btn btn-logout">Logout</button>
</form>
</div>
</nav>
<div class="header-section">
<h1>Hello, {% if user.first_name %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}!</h1>
<p class="balance-text">Total Balance: <strong>{{ total_balance|floatformat:2 }} kr</strong></p>
</div>
<div class="card">
<h3>Add Transaction</h3>
<form method="POST">
@@ -30,32 +35,37 @@
</div>
<table>
<tr>
<th>Date</th>
<th>Title</th>
<th>Category</th>
<th>Amount</th>
<th>Actions</th>
</tr>
{% for t in transactions %}
<tr>
<td>{{ t.date }}</td>
<td>{{ t.title }}</td>
<td>{{ t.category.name|default:"-" }}</td>
<td class="{% if t.is_income %}income{% else %}expense{% endif %}">
{{ t.amount }} kr
</td>
<td>
<a href="{% url 'edit_transaction' t.pk %}" class="btn-edit">Edit</a>
<form action="{% url 'delete_transaction' t.pk %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this transaction?');">
{% csrf_token %}
<button type="submit" class="btn-delete">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5">No transactions yet. Start spending!</td></tr>
{% endfor %}
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Category</th>
<th>Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr>
<td>{{ t.date }}</td>
<td>{{ t.title }}</td>
<td>{{ t.category.name|default:"-" }}</td>
<td class="{% if t.is_income %}income{% else %}expense{% endif %}">
{{ t.amount }} kr
</td>
<td>
<a href="{% url 'edit_transaction' t.pk %}" class="btn-edit">Edit</a>
<form action="{% url 'delete_transaction' t.pk %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this transaction?');">
{% csrf_token %}
<button type="submit" class="btn-delete">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5">No transactions yet. Start spending!</td></tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="auth-container">
<h2>Login to JAST</h2>
<div class="card">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
</div>
<p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
</div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="auth-container">
<h2>Register for JAST</h2>
<div class="card">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Sign Up</button>
</form>
</div>
<p>Already have an account? <a href="{% url 'login' %}">Log in here</a></p>
</div>
</body>
</html>
+43 -20
View File
@@ -1,25 +1,30 @@
{% load static %}
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>DIME Statistics</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAST Statistics</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; }
.nav { margin-bottom: 20px; }
.nav a { background: #e4e4e7; color: black; text-decoration: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; }
.filters { margin-bottom: 30px; display: flex; gap: 10px; }
.filters a { padding: 6px 12px; border-radius: 4px; text-decoration: none; border: 1px solid #d4d4d8; color: black; }
.filters a.active { background: #18181b; color: white; border-color: #18181b; }
.chart-container { position: relative; height: 400px; width: 100%; margin-top: 20px; }
</style>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="nav">
<a href="{% url 'home' %}">← Back to Tracker</a>
</div>
<!-- Top Navbar -->
<nav class="navbar">
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
<div class="navbar-actions">
<a href="{% url 'home' %}" class="btn btn-nav">← Back to Dashboard</a>
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
{% csrf_token %}
<button type="submit" class="btn btn-logout">Logout</button>
</form>
</div>
</nav>
<h1>Spending Statistics</h1>
<!-- Time Period Filters -->
<div class="filters">
<a href="?filter=week" class="{% if current_filter == 'week' %}active{% endif %}">Last 7 Days</a>
<a href="?filter=month" class="{% if current_filter == 'month' %}active{% endif %}">Last 30 Days</a>
@@ -27,19 +32,20 @@
<a href="?filter=all" class="{% if current_filter == 'all' %}active{% endif %}">All Time</a>
</div>
<!-- Chart Canvas -->
<div class="chart-container">
<canvas id="categoryChart"></canvas>
</div>
<script>
// Parse the JSON data passed from Django
const labels = JSON.parse('{{ labels|safe }}');
const data = JSON.parse('{{ data|safe }}');
const ctx = document.getElementById('categoryChart').getContext('2d');
if (data.length === 0) {
ctx.font = "16px system-ui";
ctx.font = "16px system-ui, sans-serif";
ctx.fillStyle = "#a89984"; // Gruvbox muted text
ctx.fillText("No expenses logged for this time period.", 10, 50);
} else {
new Chart(ctx, {
@@ -49,16 +55,33 @@
datasets: [{
label: 'Amount Spent (kr)',
data: data,
// A simple color palette
backgroundColor: ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6'],
borderWidth: 1
backgroundColor: [
'#fabd2f', // Gruvbox Yellow
'#fe8019', // Gruvbox Orange
'#fb4934', // Gruvbox Red
'#b8bb26', // Gruvbox Green
'#83a598', // Gruvbox Blue
'#d3869b', // Gruvbox Purple
'#8ec07c' // Gruvbox Aqua
],
borderColor: '#3c3836', // Card background color for clean slice gaps
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right' }
legend: {
position: 'right',
labels: {
color: '#ebdbb2', // Gruvbox primary text
font: {
family: 'system-ui, -apple-system, sans-serif',
size: 13
}
}
}
}
}
});
+14 -1
View File
@@ -1,11 +1,24 @@
from django.contrib import admin
from django.urls import path
from django.contrib.auth import views as auth_views
from core import views
from core.api import api # Import your Django Ninja API instance
urlpatterns = [
# Admin Panel
path('admin/', admin.site.urls),
# Web App Views
path('', views.home, name='home'),
path('edit/<int:pk>/', views.edit_transaction, name='edit_transaction'),
path('delete/<int:pk>/', views.delete_transaction, name='delete_transaction'),
path('stats/', views.stats, name='stats'), # Add this line
path('stats/', views.stats, name='stats'),
# Auth Views
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
path('register/', views.register, name='register'),
# Django Ninja API
path('api/', api.urls),
]
+27 -23
View File
@@ -1,30 +1,41 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Sum
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
from .models import Transaction
from .forms import TransactionForm
from .forms import TransactionForm, CustomUserCreationForm
import json
from datetime import timedelta
from django.utils import timezone
from django.db.models import Sum
def register(request):
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('home')
else:
form = CustomUserCreationForm()
return render(request, 'registration/register.html', {'form': form})
@login_required
def home(request):
# 1. Handle form submission
if request.method == 'POST':
form = TransactionForm(request.POST)
if form.is_valid():
form.save()
return redirect('home') # Refresh the page
transaction = form.save(commit=False)
transaction.user = request.user
transaction.save()
return redirect('home')
else:
form = TransactionForm()
# 2. Fetch data for the table
transactions = Transaction.objects.all()
# Calculate total balance (returns 0 if database is empty)
transactions = Transaction.objects.filter(user=request.user)
total_balance = transactions.aggregate(Sum('amount'))['amount__sum'] or 0
# 3. Send data to the template
context = {
'form': form,
'transactions': transactions,
@@ -32,9 +43,9 @@ def home(request):
}
return render(request, 'home.html', context)
@login_required
def edit_transaction(request, pk):
transaction = get_object_or_404(Transaction, pk=pk)
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
if request.method == 'POST':
form = TransactionForm(request.POST, instance=transaction)
if form.is_valid():
@@ -42,24 +53,22 @@ def edit_transaction(request, pk):
return redirect('home')
else:
form = TransactionForm(instance=transaction)
return render(request, 'edit.html', {'form': form, 'transaction': transaction})
@login_required
def delete_transaction(request, pk):
transaction = get_object_or_404(Transaction, pk=pk)
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
if request.method == 'POST':
transaction.delete()
return redirect('home')
@login_required
def stats(request):
# 1. Figure out what time period we are looking at
time_filter = request.GET.get('filter', 'all')
now = timezone.now().date()
# Start with only expenses (numbers less than 0)
expenses = Transaction.objects.filter(amount__lt=0)
expenses = Transaction.objects.filter(user=request.user, amount__lt=0)
# 2. Filter by date based on the button clicked
if time_filter == 'week':
expenses = expenses.filter(date__gte=now - timedelta(days=7))
elif time_filter == 'month':
@@ -67,18 +76,13 @@ def stats(request):
elif time_filter == 'year':
expenses = expenses.filter(date__gte=now - timedelta(days=365))
# 3. Group by category and sum the amounts
category_data = expenses.values('category__name').annotate(total=Sum('amount'))
# 4. Format data for Chart.js
labels = []
data = []
for item in category_data:
# If category is null, call it 'Uncategorized'
cat_name = item['category__name'] or 'Uncategorized'
labels.append(cat_name)
# Make the number positive for the chart (so it draws correctly)
data.append(abs(float(item['total'])))
context = {