Home

mint @main - refs - log -
-
https://git.jolheiser.com/mint.git
Budget
tree log patch
copy and import Signed-off-by: jolheiser <git@jolheiser.com>
Signature
-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgBTEvCQk6VqUAdN2RuH6bj1dNkY oOpbPWj+jw4ua1B1cAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQOZdTUKy0VgHrMVBQxCmrYjeFKUzEcc5UcS2Zm94/PPupDP+J/+d0ENC2TbvowaNU0 zHfEtmVEL0nZXpgJUW5wQ= -----END SSH SIGNATURE-----
jolheiser <git@jolheiser.com>
1 month ago
8 changed files, 168 additions(+), 115 deletions(-)
budget/management/commands/import.pybudget/migrations/0001_initial.pybudget/models.pybudget/templates/budget/index.htmlbudget/utils.pybudget/views.pymint/settings.pymint/urls.py
M budget/management/commands/import.py -> budget/management/commands/import.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
diff --git a/budget/management/commands/import.py b/budget/management/commands/import.py
index 7530e8c227147f3b02eccf3b060257256029d186..9ac262aa729eacc1498f13c21ff7745514586581 100644
--- a/budget/management/commands/import.py
+++ b/budget/management/commands/import.py
@@ -1,7 +1,7 @@
-from django.core.management.base import BaseCommand, CommandError
+from django.core.management.base import BaseCommand
 import json
 from datetime import datetime
-from budget.models import Transaction
+from budget.models import Transaction, Recurrence
 
 DT_FMT: str = "%Y-%m-%dT%I:%M:%S.%fZ"
 
@@ -26,15 +26,13 @@             # 2023-01-01T06:00:00.000Z
             dt = datetime.strptime(b["date"], DT_FMT)
             amount = b["amount"]
             if not amount:
-                self.stdout.write(
-                    self.style.WARNING(f'Zero-amount found on {dt} for "{b["title"]}"')
-                )
-                continue
+                amount = 0
             Transaction.objects.create(
                 title=b["title"],
                 cents=float(amount) * 100,
                 date=dt,
-                recurrance="",
+                recurrence=Recurrence.MONTH,
+                week=0,
                 is_income=False,
             )
         self.stdout.write(
M budget/migrations/0001_initial.py -> budget/migrations/0001_initial.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
diff --git a/budget/migrations/0001_initial.py b/budget/migrations/0001_initial.py
index fe24a2f286716c41f92bf6d9db69549d15ab1669..ccf99605b734a01fe8580cff7801522b8277fc37 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-14 21:09
+# Generated by Django 5.2.6 on 2025-09-15 01:38
 
 from django.db import migrations, models
 
@@ -7,26 +7,20 @@ 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()),
-                ("recurrance", models.CharField(max_length=10)),
-                ("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()),
+                ('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)),
             ],
         ),
     ]
M budget/models.py -> budget/models.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
diff --git a/budget/models.py b/budget/models.py
index 70c1c839bda611ae337dc4fa0f40488071ab286d..57ecf52b8360a7e988b42509bdbe0ee3fddd4a1c 100644
--- a/budget/models.py
+++ b/budget/models.py
@@ -1,11 +1,21 @@
 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()
-    recurrance = models.CharField(max_length=10)
+    recurrence = models.CharField(max_length=1, choices=Recurrence.choices, db_comment=Recurrence.comment())
+    week = models.IntegerField()
     is_income = models.BooleanField(default=False)
 
     @property
M budget/templates/budget/index.html -> budget/templates/budget/index.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
diff --git a/budget/templates/budget/index.html b/budget/templates/budget/index.html
index 27c25897aa232234d0acaf6cf0669c3c92720811..079818016b8729aa463be58e4e2f8504d9ab9599 100644
--- a/budget/templates/budget/index.html
+++ b/budget/templates/budget/index.html
@@ -7,28 +7,55 @@   <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.18/index.global.min.js'></script>
   <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
   <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
   <script>
-    let MINT_DEBUG = true;
-    const now = new Date();
+    const MINT_DEBUG = "{{ debug }}" === "True";
     let calendar;
 
     document.addEventListener('DOMContentLoaded', function () {
-      var calendarEl = document.getElementById('calendar');
+      const calendarEl = document.getElementById('calendar');
       calendar = new FullCalendar.Calendar(calendarEl, {
         initialView: 'dayGridMonth',
         dayMaxEventRows: true,
+        customButtons: {
+          admin: {
+            text: "admin",
+            hint: "Django Admin",
+            click: () => {
+              location.href = "{% url 'admin:index' %}";
+            }
+          },
+          copy: {
+            text: "copy",
+            hint: "Copy budget month-to-month",
+            click: () => {
+              Swal.fire({
+                title: "Are you sure?",
+                html: "This will copy <strong>all</strong> budget items from last month.",
+                icon: "warning",
+                allowOutsideClick: false,
+                showDenyButton: true,
+                showCloseButton: true,
+                confirmButtonText: "Yes",
+                reverseButtons: true,
+                showLoaderOnConfirm: true,
+                preConfirm: () => {
+                  fetch("/copy/", {
+                    method: "POST",
+                    headers: {"X-CSRFToken": '{{ csrf_token }}'},
+                    body: JSON.stringify({"month": calendar.getDate().getMonth() + 1}),
+                  }).then(() => calendar.refetchEvents());
+                }
+              });
+            }
+          }
+        },
         headerToolbar: {
-          left: 'prev,next today',
+          left: 'prev,next today copy {% if user.is_staff %}admin{% endif %}',
           center: 'title',
           right: 'dayGridWeek,dayGridMonth,dayGridYear'
         },
         selectable: true,
         select: (info) => {
           if (MINT_DEBUG) console.log(info);
-          const days = Math.floor(Math.abs(info.end - info.start) / (1000 * 60 * 60 * 24));
-          if (days == 1) {
-            input(info.start);
-            return;
-          }
           const events = calendar.getEvents().filter((event) => {
             const start = event.start;
             const end = event.end || event.start;
@@ -51,7 +78,7 @@               start: event.start,
               extendedProps: {
                 title: props.title,
                 amount: props.amount,
-                recurrance: props.recurrance,
+                recurrence: props.recurrence,
                 week: props.week,
                 income: props.income
               }
@@ -61,21 +88,19 @@         },
         eventClick: (info) => {
           const event = info.event;
           const props = event.extendedProps;
-          input(event.start, event.id, props.title, props.amount / 100, props.recurrance, props.income);
-        },
-        eventClassNames: (info) => {
-          const classes = ['cursor-pointer'];
-          if (toDate(info.event.start) < toDate(now)) classes.push("bg-gray-500!", "border-gray-500!");
-          return classes;
+          input(event.start, event.id, props.title, props.amount / 100, props.recurrence, props.week, props.income);
         },
+        navLinks: true,
+        navLinkDayClick: (date) => input(date),
         events: "/events/",
       });
       calendar.render();
     });
 
-    function input(time, id = '', title = 'New Transaction', amount = 0, recurrance = "month", income = false) {
+    function input(time, id = '', title = 'New Transaction', amount = 0, recurrence = "M", week = 1, income = false) {
       const weekDay = time.getDay();
       const monthDay = time.getDate();
+      title = title.replace(/"/g, "&quot;")
       Swal.fire({
         title: `<input type="text" class="font-semibold" name="title" value="${title}"/>`,
         html: `
@@ -83,11 +108,11 @@           <div class="grid grid-cols-1 gap-x-4 gap-y-3 items-center text-left">
             <input type="hidden" name="id" value="${id}"/>
             <input type="hidden" name="date" value="${time.toISOString()}"/>
             <label class="font-semibold">Amount: $<input class="border rounded w-30 mx-px p-1" type="number" name="amount" value="${amount}"/></label>
-            <label class="font-semibold">Recurring? <input class="border rounded w-4 h-4" type="checkbox" name="recurring" ${recurrance !== "" ? "checked" : ""}/></label>
-            <div id="recurringOptions" class="${recurrance !== "" ? "" : "hidden"}">
-              <label><input type="radio" name="recurrance" ${recurrance === "month" ? "checked" : ""} value="month"/> Monthly on the ${ordinal(monthDay)}</label>
+            <label class="font-semibold">Recurring? <input class="border rounded w-4 h-4" type="checkbox" name="recurring" ${recurrence !== "" ? "checked" : ""}/></label>
+            <div id="recurringOptions" class="${recurrence !== "" ? "" : "hidden"}">
+              <label><input type="radio" name="recurrence" ${recurrence === "M" ? "checked" : ""} value="M"/> Monthly on the ${ordinal(monthDay)}</label>
               <br/>
-              <label><input type="radio" name="recurrance" ${recurrance.endsWith("week") ? "checked" : ""} value="week"/> Every <input name="week" class="border rounded w-15 p-1" type="number" min="1" max="4" value="${recurrance.endsWith("week") ? recurrance.substring(0, 1) : "1"}"/> weeks on ${dayOfWeek(weekDay)}</label>
+              <label><input type="radio" name="recurrence" ${recurrence === "W" ? "checked" : ""} value="W"/> Every <input name="week" class="border rounded w-15 p-1" type="number" min="1" max="4" value="${week}"/> weeks on ${dayOfWeek(weekDay)}</label>
             </div>
             <label class="font-semibold">Income? <input class="border rounded w-4 h-4" type="checkbox" name="income" ${income ? "checked" : ""}/></label>
           </div>
@@ -101,7 +126,7 @@         denyButtonText: 'Delete',
         customClass: {
           confirmButton: 'ml-[15rem]!'
         },
-        didRender: (swal) => {
+        didRender: () => {
           const $recurring = document.querySelector("[name='recurring']");
           const $opts = document.getElementById("recurringOptions");
           $recurring.addEventListener('change', () => {
@@ -121,7 +146,7 @@             id: $popup.querySelector("[name='id']").value,
             date: new Date($popup.querySelector("[name='date']").value),
             title: $popup.querySelector("[name='title']").value,
             amount: parseFloat($popup.querySelector("[name='amount']").value),
-            recurrance: $popup.querySelector("[name='recurring']").checked ? $popup.querySelector("[name='recurrance']:checked").value : '',
+            recurrence: $popup.querySelector("[name='recurring']").checked ? $popup.querySelector("[name='recurrence']:checked").value : '',
             week: $popup.querySelector("[name='week']").value,
             income: $popup.querySelector("[name='income']").checked
           };
@@ -146,7 +171,7 @@           allDay: true,
           extendedProps: {
             title: value.title,
             amount: value.amount * 100,
-            recurrance: value.recurrance,
+            recurrence: value.recurrence,
             week: value.week,
             income: value.income
           }
@@ -171,7 +196,6 @@     }
 
     function toast(message, level = "error", duration = 5) {
       const Toast = Swal.mixin({
-        toast: true,
         position: "top-end",
         showConfirmButton: false,
         timer: duration * 1000,
@@ -197,10 +221,6 @@     }
 
     function dayOfWeek(i = 0) {
       return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][i];
-    }
-
-    function toDate(dt) {
-      return new Date(dt.toDateString());
     }
   </script>
 </head>
I budget/utils.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
diff --git a/budget/utils.py b/budget/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..e4490956053d9985ae180772ce0eefaf27b449f8
--- /dev/null
+++ b/budget/utils.py
@@ -0,0 +1,23 @@
+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
M budget/views.py -> budget/views.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
diff --git a/budget/views.py b/budget/views.py
index f8119f1c10386f5ad4ca3474ba5acb62f1489379..5315950bfc6d0f7e93c476940891e0cd3ba881ed 100644
--- a/budget/views.py
+++ b/budget/views.py
@@ -2,37 +2,42 @@ 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
-from .models import Transaction
+from datetime import date, datetime, timedelta
+from .models import Transaction, Recurrence
 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):
+    def get(self, request: HttpRequest) -> HttpResponse:
         return render(request, "budget/index.html")
 
 
 class EventsView(LoginRequiredMixin, View):
-    def get(self, request: HttpRequest):
+    def get(self, request: HttpRequest) -> JsonResponse:
         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.recurrance == "":
+            if t.recurrence == "":
                 border = YELLOW
+            if t.date < today:
+                bg, border, text = GRAY, GREEN if t.is_income else BLUE, WHITE
             events.append(
                 {
                     "id": t.id,
@@ -42,7 +47,8 @@                     "title": t.display,
                     "extendedProps": {
                         "title": t.title,
                         "amount": t.cents,
-                        "recurrance": t.recurrance,
+                        "recurrence": t.recurrence,
+                        "week": t.week,
                         "income": t.is_income,
                     },
                     "backgroundColor": bg,
@@ -54,24 +60,30 @@         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):
+    def post(self, request: HttpRequest) -> HttpResponse:
         data = json.loads(request.body)
         props = data["extendedProps"]
         t = Transaction()
         t.title = props["title"]
         t.cents = props["amount"]
-        t.date = datetime.fromisoformat(data["start"])
-        t.recurrance = self._recurrance(props["recurrance"], props["week"])
+        t.date = datetime.fromisoformat(data["start"]).date()
+        t.recurrence = props["recurrence"]
+        t.week = int(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):
+    def patch(self, request: HttpRequest) -> HttpResponse:
         data = json.loads(request.body)
         if "id" in data:
             props = data["extendedProps"]
@@ -79,13 +91,38 @@             t = Transaction.objects.get(id=data["id"])
             t.title = props["title"]
             t.cents = props["amount"]
             t.date = datetime.fromisoformat(data["start"])
-            t.recurrance = self._recurrance(props["recurrance"], props["week"])
+            t.recurrence = props["recurrence"]
+            t.week = props["week"]
             t.is_income = props["income"]
             t.save()
         return HttpResponse()
 
-    def delete(self, request: HttpRequest):
+    def delete(self, request: HttpRequest) -> HttpResponse:
         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
M mint/settings.py -> mint/settings.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
diff --git a/mint/settings.py b/mint/settings.py
index 27ffc28074443a98f60640de9ababeca7a500d08..ae48cdb188e0ab93d2ccf62afec7302f6d082af5 100644
--- a/mint/settings.py
+++ b/mint/settings.py
@@ -5,21 +5,13 @@
 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",
@@ -53,7 +45,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"
             ],
         },
     },
@@ -61,10 +53,6 @@ ]
 
 WSGI_APPLICATION = "mint.wsgi.application"
 
-
-# Database
-# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
-
 DATABASES = {
     "default": {
         "ENGINE": "django.db.backends.sqlite3",
@@ -72,32 +60,10 @@         "NAME": env.str("DATABASE", BASE_DIR / "mint.sqlite3"),
     }
 }
 
-
-# Password validation
-# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
+AUTHENTICATION_BACKENDS = ["mint.auth.MintOIDCBackend"]
 
-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"
 
-
-# Internationalization
-# https://docs.djangoproject.com/en/5.2/topics/i18n/
-
 LANGUAGE_CODE = "en-us"
 
 TIME_ZONE = env.str("TIMEZONE", "UTC")
@@ -106,14 +72,7 @@ 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"
 
@@ -135,6 +94,8 @@ 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
 
M mint/urls.py -> mint/urls.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
diff --git a/mint/urls.py b/mint/urls.py
index a1cfd4cf8aa32388b55c19536c2cfd32ac6d99d6..e11f05a4192066bb9c2d5df3d92fe1ad140c8498 100644
--- a/mint/urls.py
+++ b/mint/urls.py
@@ -1,12 +1,22 @@
+
+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
+from budget.views import IndexView, EventsView, TransactionView, CopyView
 
 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