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