diff --git a/budget/management/commands/import.py b/budget/management/commands/import.py
index 9ac262aa729eacc1498f13c21ff7745514586581..7530e8c227147f3b02eccf3b060257256029d186 100644
--- a/budget/management/commands/import.py
+++ b/budget/management/commands/import.py
@@ -1,7 +1,7 @@
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
import json
from datetime import datetime
-from budget.models import Transaction, Recurrence
+from budget.models import Transaction
DT_FMT: str = "%Y-%m-%dT%I:%M:%S.%fZ"
@@ -26,13 +26,15 @@ # 2023-01-01T06:00:00.000Z
dt = datetime.strptime(b["date"], DT_FMT)
amount = b["amount"]
if not amount:
- amount = 0
+ self.stdout.write(
+ self.style.WARNING(f'Zero-amount found on {dt} for "{b["title"]}"')
+ )
+ continue
Transaction.objects.create(
title=b["title"],
cents=float(amount) * 100,
date=dt,
- recurrence=Recurrence.MONTH,
- week=0,
+ recurrance="",
is_income=False,
)
self.stdout.write(
diff --git a/budget/migrations/0001_initial.py b/budget/migrations/0001_initial.py
index ccf99605b734a01fe8580cff7801522b8277fc37..fe24a2f286716c41f92bf6d9db69549d15ab1669 100644
--- a/budget/migrations/0001_initial.py
+++ b/budget/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.2.6 on 2025-09-15 01:38
+# Generated by Django 5.2.6 on 2025-09-14 21:09
from django.db import migrations, models
@@ -7,20 +7,26 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ]
+ dependencies = []
operations = [
migrations.CreateModel(
- name='Transaction',
+ name="Transaction",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('title', models.CharField(max_length=100)),
- ('cents', models.BigIntegerField()),
- ('date', models.DateField()),
- ('recurrence', models.CharField(choices=[('M', 'Monthly'), ('W', 'Weekly')], db_comment='M (Monthly) | W (Weekly)', max_length=1)),
- ('week', models.IntegerField()),
- ('is_income', models.BooleanField(default=False)),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("title", models.CharField(max_length=100)),
+ ("cents", models.BigIntegerField()),
+ ("date", models.DateField()),
+ ("recurrance", models.CharField(max_length=10)),
+ ("is_income", models.BooleanField(default=False)),
],
),
]
diff --git a/budget/models.py b/budget/models.py
index 57ecf52b8360a7e988b42509bdbe0ee3fddd4a1c..70c1c839bda611ae337dc4fa0f40488071ab286d 100644
--- a/budget/models.py
+++ b/budget/models.py
@@ -1,21 +1,11 @@
from django.db import models
-from django.db.models import Manager
-class Recurrence(models.TextChoices):
- MONTH = "M", "Monthly"
- WEEK = "W", "Weekly"
-
- @staticmethod
- def comment() -> str:
- return " | ".join([f"{v[0]} ({v[1]})" for v in Recurrence.choices])
-
class Transaction(models.Model):
title = models.CharField(max_length=100)
cents = models.BigIntegerField()
date = models.DateField()
- recurrence = models.CharField(max_length=1, choices=Recurrence.choices, db_comment=Recurrence.comment())
- week = models.IntegerField()
+ recurrance = models.CharField(max_length=10)
is_income = models.BooleanField(default=False)
@property
diff --git a/budget/templates/budget/index.html b/budget/templates/budget/index.html
index 079818016b8729aa463be58e4e2f8504d9ab9599..27c25897aa232234d0acaf6cf0669c3c92720811 100644
--- a/budget/templates/budget/index.html
+++ b/budget/templates/budget/index.html
@@ -7,55 +7,28 @@
diff --git a/budget/utils.py b/budget/utils.py
deleted file mode 100644
index e4490956053d9985ae180772ce0eefaf27b449f8..0000000000000000000000000000000000000000
--- a/budget/utils.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from datetime import date, timedelta
-from calendar import monthrange
-from typing import Tuple
-
-
-def prev_month_range(current_month: int) -> Tuple[date, date]:
- current_year = date.today().year
- if current_month == 1:
- month = 12
- year = current_year - 1
- else:
- month = current_month - 1
- year = current_year
- first_day = date(year, month, 1)
- last_day = date(current_year, current_month, 1) - timedelta(days=1)
- return first_day, last_day
-
-def add_months(dt: date, months: int) -> date:
- month = dt.month - 1 + months
- year = dt.year + month // 12
- month = month % 12 + 1
- day = min(dt.day, monthrange(year, month)[1])
- return date(year, month, day)
\ No newline at end of file
diff --git a/budget/views.py b/budget/views.py
index 5315950bfc6d0f7e93c476940891e0cd3ba881ed..f8119f1c10386f5ad4ca3474ba5acb62f1489379 100644
--- a/budget/views.py
+++ b/budget/views.py
@@ -2,42 +2,37 @@ from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import View
from django.http import HttpRequest, JsonResponse, HttpResponse
-from datetime import date, datetime, timedelta
-from .models import Transaction, Recurrence
+from datetime import date, datetime
+from .models import Transaction
import json
-from .utils import prev_month_range, add_months
BLUE = "#3788d8"
YELLOW = "#d4a574"
GREEN = "#a8d5ba"
WHITE = "#fff"
BLACK = "#000"
-GRAY = "#6a7282"
class IndexView(LoginRequiredMixin, View):
- def get(self, request: HttpRequest) -> HttpResponse:
+ def get(self, request: HttpRequest):
return render(request, "budget/index.html")
class EventsView(LoginRequiredMixin, View):
- def get(self, request: HttpRequest) -> JsonResponse:
+ def get(self, request: HttpRequest):
data = request.GET
# 2025-07-27T00:00:00-05:00
start = datetime.fromisoformat(data["start"]) if "start" in data else date.min
end = datetime.fromisoformat(data["end"]) if "end" in data else date.max
transactions = Transaction.objects.filter(date__range=[start, end])
events = []
- today = datetime.now().date()
for t in transactions:
bg, border, text = BLUE, BLUE, WHITE
if t.is_income:
bg, border, text = GREEN, GREEN, BLACK
- if t.recurrence == "":
+ if t.recurrance == "":
border = YELLOW
- if t.date < today:
- bg, border, text = GRAY, GREEN if t.is_income else BLUE, WHITE
events.append(
{
"id": t.id,
@@ -47,8 +42,7 @@ "title": t.display,
"extendedProps": {
"title": t.title,
"amount": t.cents,
- "recurrence": t.recurrence,
- "week": t.week,
+ "recurrance": t.recurrance,
"income": t.is_income,
},
"backgroundColor": bg,
@@ -60,30 +54,24 @@ return JsonResponse(events, safe=False)
class TransactionView(LoginRequiredMixin, View):
+ def _recurrance(self, r: str, w: int) -> str:
+ if r == "week" and w != 0:
+ return f"{w}week"
+ return r
- def post(self, request: HttpRequest) -> HttpResponse:
+ def post(self, request: HttpRequest):
data = json.loads(request.body)
props = data["extendedProps"]
t = Transaction()
t.title = props["title"]
t.cents = props["amount"]
- t.date = datetime.fromisoformat(data["start"]).date()
- t.recurrence = props["recurrence"]
- t.week = int(props["week"])
+ t.date = datetime.fromisoformat(data["start"])
+ t.recurrance = self._recurrance(props["recurrance"], props["week"])
t.is_income = props["income"]
t.save()
- if t.recurrence == Recurrence.WEEK:
- while True:
- month = t.date.month
- t.date += timedelta(weeks=t.week)
- if t.date.month == month:
- t.id = None
- t.save()
- else:
- break
return HttpResponse()
- def patch(self, request: HttpRequest) -> HttpResponse:
+ def patch(self, request: HttpRequest):
data = json.loads(request.body)
if "id" in data:
props = data["extendedProps"]
@@ -91,38 +79,13 @@ t = Transaction.objects.get(id=data["id"])
t.title = props["title"]
t.cents = props["amount"]
t.date = datetime.fromisoformat(data["start"])
- t.recurrence = props["recurrence"]
- t.week = props["week"]
+ t.recurrance = self._recurrance(props["recurrance"], props["week"])
t.is_income = props["income"]
t.save()
return HttpResponse()
- def delete(self, request: HttpRequest) -> HttpResponse:
+ def delete(self, request: HttpRequest):
data = json.loads(request.body)
if "id" in data:
Transaction.objects.get(id=data["id"]).delete()
return HttpResponse()
-
-
-class CopyView(View):
- def post(self, request: HttpRequest):
- month = json.loads(request.body)["month"]
- first, last = prev_month_range(month)
- transactions = Transaction.objects.filter(date__range=[first, last], recurrence__in=[Recurrence.WEEK, Recurrence.MONTH])
- for transaction in transactions:
- if transaction.recurrence == Recurrence.MONTH:
- transaction.id = None
- transaction.date = add_months(transaction.date, 1)
- transaction.save()
- else:
- check = transaction.date + timedelta(weeks=transaction.week)
- if check.month != month:
- continue
- while True:
- transaction.date += timedelta(weeks=transaction.week)
- if transaction.date.month == month:
- transaction.id = None
- transaction.save()
- else:
- break
- return HttpResponse()
\ No newline at end of file
diff --git a/mint/settings.py b/mint/settings.py
index ae48cdb188e0ab93d2ccf62afec7302f6d082af5..27ffc28074443a98f60640de9ababeca7a500d08 100644
--- a/mint/settings.py
+++ b/mint/settings.py
@@ -5,13 +5,21 @@
env = Env(prefix="MINT_")
env.read_env()
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
+
SECRET_KEY = env.str("SECRET_KEY")
DEBUG = env.bool("DEBUG", False)
ALLOWED_HOSTS = []
+
+
+# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
@@ -45,7 +53,7 @@ "OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
- "django.contrib.messages.context_processors.messages"
+ "django.contrib.messages.context_processors.messages",
],
},
},
@@ -53,6 +61,10 @@ ]
WSGI_APPLICATION = "mint.wsgi.application"
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
@@ -60,9 +72,31 @@ "NAME": env.str("DATABASE", BASE_DIR / "mint.sqlite3"),
}
}
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
AUTHENTICATION_BACKENDS = ["mint.auth.MintOIDCBackend"]
+LOGIN_URL = "/oidc/authenticate"
-LOGIN_URL = "/oidc/authenticate"
+
+# Internationalization
+# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = "en-us"
@@ -72,7 +106,14 @@ USE_I18N = True
USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
STATIC_URL = "static/"
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -94,8 +135,6 @@ OIDC_OP_JWKS_ENDPOINT = resp["jwks_uri"]
OIDC_RP_SCOPES = "openid email profile groups"
if DEBUG:
- INTERNAL_IPS = ["127.0.0.1"]
- TEMPLATES[0]["OPTIONS"]["context_processors"].append("django.template.context_processors.debug")
try:
import debug_toolbar
diff --git a/mint/urls.py b/mint/urls.py
index e11f05a4192066bb9c2d5df3d92fe1ad140c8498..a1cfd4cf8aa32388b55c19536c2cfd32ac6d99d6 100644
--- a/mint/urls.py
+++ b/mint/urls.py
@@ -1,22 +1,12 @@
-
-from django.conf import settings
from django.contrib import admin
+from django.contrib.auth.decorators import login_required
from django.urls import path, include
-from budget.views import IndexView, EventsView, TransactionView, CopyView
+from budget.views import IndexView, EventsView, TransactionView
urlpatterns = [
path("", IndexView.as_view(), name="index"),
path("events/", EventsView.as_view(), name="events"),
path("transaction/", TransactionView.as_view(), name="transaction"),
- path("copy/", CopyView.as_view(), name="copy"),
-
path("admin/", admin.site.urls),
path("oidc/", include("mozilla_django_oidc.urls")),
]
-
-if settings.DEBUG:
- try:
- from debug_toolbar.toolbar import debug_toolbar_urls
- urlpatterns += debug_toolbar_urls()
- except:
- pass