mirror of
https://github.com/Lyxminxx/jast.git
synced 2026-10-11 12:51:51 +02:00
redesign, added user system, added api endpoints
This commit is contained in:
1 parent
2c76e1df78
commit
72b8e569aa
17 files changed
+755
-72
No files matched your search
@@ -0,0 +1,5 @@
|
||||
# JAST
|
||||
Welcome to JAST, the spending tracker that simply tracks your spending. Not a bunch of extra fancy stuff. It let's you enter anything you spend or get paid and then gives you some stats.
|
||||
|
||||
## Requirements
|
||||
The only thing you really need to run this locally is docker and docker-compose. If you want to run this without docker, all you need is uv.
|
||||
+44
@@ -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
@@ -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
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+27
-17
@@ -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,6 +35,7 @@
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Title</th>
|
||||
@@ -37,6 +43,8 @@
|
||||
<th>Amount</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in transactions %}
|
||||
<tr>
|
||||
<td>{{ t.date }}</td>
|
||||
@@ -56,6 +64,8 @@
|
||||
{% empty %}
|
||||
<tr><td colspan="5">No transactions yet. Start spending!</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+42
-19
@@ -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>
|
||||
|
||||
<!-- 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
@@ -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
@@ -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 = {
|
||||
|
||||
BIN
Binary file not shown.
@@ -6,6 +6,7 @@ readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"django>=6.1.1",
|
||||
"django-ninja>=1.7.1",
|
||||
"gunicorn>=26.2.0",
|
||||
"whitenoise>=6.12.0",
|
||||
]
|
||||
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.12.1"
|
||||
@@ -25,6 +34,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/ca/c1040a4fd15754ede7df15fd72fc2e123045b473b253ab5f5284e11c651e/django-6.1.1-py3-none-any.whl", hash = "sha256:585fb82bf15053c42cf52e67c2f1b54a032dca384759315fd6678ab1870d1d72", size = 8419512, upload-time = "2026-09-02T17:20:39.153Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/0f/5c6811706676be4c5faf5a3fd0d3ad56293db7ce8f0cc62c3e2f8e23b26d/django_ninja-1.7.1.tar.gz", hash = "sha256:2183ee5426a8c95bfabae1b7f60c471a56c48022296eaaf1cc54b00010b07ee6", size = 2341814, upload-time = "2026-09-19T05:35:56.437Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/05/7bf91a79ac69632b15b878ad658371dcae58a4e2545da3709cfc55916da3/django_ninja-1.7.1-py3-none-any.whl", hash = "sha256:61137e9fbb97ca15a92cdbfdcf9f6a738464b328c6cb11090faf17042035ec69", size = 2376240, upload-time = "2026-09-19T05:35:54.89Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "26.2.0"
|
||||
@@ -40,6 +62,7 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-ninja" },
|
||||
{ name = "gunicorn" },
|
||||
{ name = "whitenoise" },
|
||||
]
|
||||
@@ -47,10 +70,67 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "django", specifier = ">=6.1.1" },
|
||||
{ name = "django-ninja", specifier = ">=1.7.1" },
|
||||
{ name = "gunicorn", specifier = ">=26.2.0" },
|
||||
{ name = "whitenoise", specifier = ">=6.12.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.6.0"
|
||||
@@ -60,6 +140,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.4"
|
||||
|
||||
Reference in new issue
Block a user