Enable file drag and drop on transaction details page

Close 
This commit is contained in:
Edgar P. Burkhart 2025-01-04 12:37:09 +01:00
parent cfb2ceb2c3
commit 805c7d3dc0
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
10 changed files with 151 additions and 15 deletions
nummi
main
forms.py
static/main/css
templatetags
statement
transaction
forms.py
static/transaction/js
templates/transaction
urls.pyviews.py

View file

@ -12,7 +12,8 @@ class NummiForm(forms.ModelForm):
template_name = "main/form/form_base.html"
meta_fieldsets = []
def __init__(self, *args, user, **kwargs):
def __init__(self, *args, **kwargs):
kwargs.pop("user", None)
super().__init__(*args, **kwargs)
@property

View file

@ -1,8 +1,32 @@
@keyframes border-pulse {
from {
border-color: var(--green);
.drop-zone {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
align-items: center;
text-align: center;
color: transparent;
display: grid;
transition-property: backdrop-filter;
transition-duration: 750ms;
z-index: -1;
> span {
font-weight: 650;
font-size: 2rem;
transition-property: color;
transition-duration: inherit;
}
to {
main.highlight > & {
z-index: 100;
backdrop-filter: blur(0.1rem);
> span {
color: var(--green);
}
}
}
@ -14,6 +38,10 @@ form {
grid-template-columns: 1fr;
}
&.hidden {
display: none;
}
.column {
display: grid;
gap: 0.5rem;

View file

@ -85,6 +85,7 @@ footer {
@media (width > 720px) {
padding: 2rem;
}
background: var(--bg);
}
main {
position: relative;

View file

@ -59,7 +59,7 @@ def messageicon(level):
@register.filter
def extension(file):
return file.name.split(".")[-1].upper()
return file.name.split(".", 1)[1].upper()
@register.filter

View file

@ -25,7 +25,7 @@ class StatementForm(NummiForm):
]
def __init__(self, *args, **kwargs):
_user = kwargs.get("user")
_user = kwargs.pop("user")
_disable_account = kwargs.pop("disable_account", False)
super().__init__(*args, **kwargs)
self.fields["account"].queryset = _user.account_set.exclude(archived=True)

View file

@ -1,6 +1,8 @@
import json
from category.forms import CategorySelect
from django import forms
from django.forms import formset_factory
from main.forms import DatalistInput, NummiFileInput, NummiForm
from statement.forms import StatementSelect
@ -46,7 +48,7 @@ class TransactionForm(NummiForm):
]
def __init__(self, *args, **kwargs):
_user = kwargs.get("user")
_user = kwargs.pop("user")
_disable_statement = kwargs.pop("disable_statement", False)
_autocomplete = kwargs.pop("autocomplete", False)
super().__init__(*args, **kwargs)
@ -117,3 +119,29 @@ class InvoiceForm(NummiForm):
widgets = {
"file": NummiFileInput,
}
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = single_file_clean(data, initial)
return result
class MultipleInvoicesForm(forms.Form):
prefix = "invoices"
invoices = MultipleFileField()
InvoicesFormSet = formset_factory(MultipleInvoicesForm)

View file

@ -0,0 +1,22 @@
const dropArea = document.querySelector("main");
const form = document.querySelector("form.invoices");
dropArea.addEventListener("dragover", (event) => {
event.preventDefault();
dropArea.classList.add("highlight");
});
dropArea.addEventListener("dragleave", () => {
dropArea.classList.remove("highlight");
});
dropArea.addEventListener("drop", (event) => {
console.log(event);
event.preventDefault();
dropArea.classList.remove("highlight");
const files = event.dataTransfer.files;
console.log(files);
const input = form.querySelector("input[type=file]");
input.files = files;
form.submit();
});

View file

@ -10,6 +10,7 @@
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% js "transaction/js/invoice_form.js" %}
{% endblock link %}
{% block body %}
<h2>{{ transaction }}</h2>
@ -44,5 +45,15 @@
<section>
<h3>{% translate "Invoices" %}</h3>
{% invoice_table transaction %}
<form class="hidden invoices"
method="post"
action="{% url "multiple_invoice" transaction=transaction.pk %}"
enctype="multipart/form-data">
{% csrf_token %}
{{ invoices_form }}
</form>
</section>
<div class="drop-zone">
<span class="wi">{{ "file-add"|remix }}{% translate "Add invoice" %}</span>
</div>
{% endblock body %}

View file

@ -31,6 +31,11 @@ urlpatterns = [
views.InvoiceCreateView.as_view(),
name="new_invoice",
),
path(
"<transaction>/invoice/multiple",
views.MultipleInvoiceCreateView.as_view(),
name="multiple_invoice",
),
path(
"<transaction>/invoice/<invoice>",
views.InvoiceUpdateView.as_view(),

View file

@ -1,8 +1,13 @@
from account.models import Account
from category.models import Category
from django.contrib import messages
from django.forms import ValidationError
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext as _
from django.views.generic.dates import MonthArchiveView, YearArchiveView
from django.views.generic.edit import FormView
from history.utils import history
from main.views import (
NummiCreateView,
@ -13,7 +18,7 @@ from main.views import (
UserMixin,
)
from .forms import InvoiceForm, TransactionForm
from .forms import InvoiceForm, MultipleInvoicesForm, TransactionForm
from .models import Invoice, Transaction
@ -57,6 +62,45 @@ class InvoiceCreateView(NummiCreateView):
return reverse_lazy("transaction", args=(self.object.transaction.pk,))
class MultipleInvoiceCreateView(FormView):
form_class = MultipleInvoicesForm
def form_valid(self, form):
transaction = get_object_or_404(
self.request.user.transaction_set, pk=self.kwargs["transaction"]
)
invoices = []
for file in form.cleaned_data["invoices"]:
invoice = Invoice(
transaction=transaction,
user=self.request.user,
file=file,
name=file.name.split(".", 1)[0],
)
try:
invoice.full_clean()
except ValidationError as err:
for msg in err.messages:
messages.error(
self.request,
format_html(
"{msg} {file}. {err}",
msg=_("Error processing file"),
file=file.name,
err=msg,
),
)
else:
invoices.append(invoice)
Invoice.objects.bulk_create(invoices)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("transaction", args=(self.kwargs["transaction"],))
class TransactionUpdateView(NummiUpdateView):
model = Transaction
form_class = TransactionForm
@ -70,12 +114,8 @@ class TransactionDetailView(NummiDetailView):
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
transaction = data.get("transaction")
return data | {
"statement": transaction.statement,
"category": transaction.category,
}
data["invoices_form"] = MultipleInvoicesForm()
return data
class InvoiceUpdateView(NummiUpdateView):