Compare commits

...

3 Commits

8 changed files with 252 additions and 5 deletions

17
lelcsc/core/forms.py Normal file
View File

@ -0,0 +1,17 @@
from django import forms
from django.utils.translation import gettext_lazy as _
class AddStockForm(forms.Form):
part_number = forms.CharField(label=_("Component"))
properties = forms.JSONField(initial={}, label=_("Properties"), required=False)
quantity = forms.IntegerField(min_value=1, label=_("Quantity"))
original_quantity = forms.IntegerField(min_value=1, label=_("Original quantity"))
total_value = forms.DecimalField(
min_value=0, max_digits=8, decimal_places=2, label=_("Total value")
)
location = forms.CharField(label=_("Location"), widget=forms.Textarea)
owner = forms.CharField(label=_("Owner"))
AddStockFormSet = forms.formset_factory(AddStockForm, extra=0)

View File

@ -1,3 +1,21 @@
from django.dispatch import Signal
from django.dispatch import Signal, receiver
from django.urls import reverse
from django.utils.translation import gettext as _
populate_nav = Signal()
@receiver(populate_nav, dispatch_uid="populate_nav_core")
def populate_nav_core(sender, **kwargs):
request = sender
if not request.user.is_authenticated:
return []
return [
{
"is_active": request.resolver_match.url_name == "add_stock",
"link": reverse("add_stock"),
"text": _("Add stock"),
},
]

View File

@ -0,0 +1,71 @@
let totalForms = Object.defineProperty({}, 'value', {
get() {
return +document.getElementById('id_form-TOTAL_FORMS').value;
},
set(value) {
document.getElementById('id_form-TOTAL_FORMS').value = value;
},
enumerable: false,
configurable: false,
});
const updateNames = (elem, i) => {
const replaceIndex = s => s.replaceAll('__prefix__', `${i}`);
if (elem.hasAttribute('for')) {
elem.htmlFor = replaceIndex(elem.htmlFor);
}
if (elem.hasAttribute('id')) {
elem.id = replaceIndex(elem.id);
}
if (elem.hasAttribute('name')) {
elem.name = replaceIndex(elem.name);
}
for (const child of elem.children) {
updateNames(child, i);
}
};
const autofillQuantity = i => {
const originalQuantity = document.getElementById(`id_form-${i}-original_quantity`);
const quantity = document.getElementById(`id_form-${i}-quantity`);
const autofillOnInput = (self, other) => () => {
delete self.dataset.autofilled;
if (!other.value || other.dataset.autofilled) {
other.value = self.value;
other.dataset.autofilled = true;
}
};
originalQuantity.addEventListener('input', autofillOnInput(originalQuantity, quantity));
quantity.addEventListener('input', autofillOnInput(quantity, originalQuantity));
};
const addFormButton = document.getElementById('addForm');
const addForm = () => {
const form = document.getElementById('formTemplate').cloneNode(true);
const i = totalForms.value++;
updateNames(form, i);
const heading = document.createElement('h3');
heading.innerText = `#${i+1}`;
addFormButton.before(heading, ...form.children);
autofillQuantity(i);
document.getElementById(`id_form-${i}-owner`).value = JSON.parse(document.getElementById('currentUser').textContent);
};
addFormButton.addEventListener('click', addForm);
for (let i = 0; i < totalForms.value; ++i) {
autofillQuantity(i);
}

View File

@ -13,14 +13,14 @@
</head>
<body>
{% block body %}
{% block navbar %}
{% include "partials/navbar.html" %}
{% endblock %}
{% block navbar %}{% include "partials/navbar.html" %}{% endblock %}
<div class="container-fluid">
{% block messages %}
{% bootstrap_messages %}
{% bootstrap_messages %}
{% endblock %}
{% block content %}
{% endblock %}
</div>
{% endblock %}
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
<script>
@ -28,5 +28,6 @@
e.classList.remove('show');
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load django_bootstrap5 %}
{% load i18n %}
{% load static %}
{% block page_title %}{% translate "Add stock" %}{% endblock %}
{% block content %}
<h1>{% translate "Add stock" %}</h1>
<form class="mb-3" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% bootstrap_formset_errors formset layout="floating" %}
{% for form in formset %}
<h3>#{{ forloop.counter }}</h3>
{% bootstrap_form form layout="floating" %}
{% endfor %}
{% bootstrap_button "+" button_class="btn-outline-secondary" button_type="button" id="addForm" %}
{% translate "Add" as add_button_text %}
{% bootstrap_button button_type="submit" content=add_button_text %}
</form>
<div class="d-none" id="formTemplate">
{% bootstrap_form formset.empty_form layout="floating" %}
</div>
{% endblock %}
{% block extra_js %}
{{ properties|json_script:"properties" }}
{{ request.user.username|json_script:"currentUser" }}
<script src="{% static 'core/add_stock.js' %}"></script>
{% endblock %}

View File

@ -3,3 +3,8 @@
{% load i18n %}
{% block page_title %}{% translate "Unauthorized" %}{% endblock %}
{% block content %}
<h1>{% translate "Unauthorized" %}</h1>
<p>{% translate "Please log in to use this application." %}</p>
{% endblock %}

View File

@ -4,6 +4,7 @@ from . import views
urlpatterns = [
path("", views.index, name="index"),
path("add-stock", views.add_stock, name="add_stock"),
path("unauthorized", views.unauthorized, name="unauthorized"),
path("admin/login/", views.admin_login),
]

View File

@ -1,7 +1,14 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.forms.models import model_to_dict
from django.http import QueryDict
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.translation import ngettext
from .forms import AddStockFormSet
from .models import Component, Property, Stock, User, Value
@login_required
@ -13,6 +20,101 @@ def unauthorized(request):
return render(request, "core/unauthorized.html", {})
@login_required
def add_stock(request):
property_models = {p.name: p for p in Property.objects.all()}
if request.method == "POST":
formset = AddStockFormSet(request.POST, initial=[{"owner": request.user}])
if not formset.is_valid():
return render(request, "core/add_stock.html", {"formset": formset})
try:
with transaction.atomic():
for form in formset:
part_number = form.cleaned_data["part_number"]
component, created = Component.objects.get_or_create(
part_number__iexact=part_number,
defaults={"part_number": part_number},
)
if created:
properties = form.cleaned_data["properties"]
for name in properties:
if name not in property_models:
property_kwargs = {
k: v
for k, v in properties[name].items()
if k
in (
"filterable",
"type",
"searchable",
"unit",
"scale",
)
}
property_models[name] = Property.objects.create(
name=name, **property_kwargs
)
property = property_models[name]
value_kwargs = {
k: v
for k, v in properties[name].items()
if k in ("integer", "text")
}
value_kwargs.setdefault("integer", 0)
value_kwargs.setdefault("text", "")
Value.objects.create(
property=property, component=component, **value_kwargs
)
owner_name = form.cleaned_data["owner"]
owner = User.objects.filter(username=owner_name).first()
Stock.objects.create(
component=component,
quantity=form.cleaned_data["quantity"],
original_quantity=form.cleaned_data["original_quantity"],
total_value=form.cleaned_data["total_value"],
location=form.cleaned_data["location"],
owner=owner,
owner_name=owner_name,
)
except Exception as e:
messages.add_message(request, messages.ERROR, str(e))
return render(
request,
"core/add_stock.html",
{
"formset": formset,
"properties": list(map(model_to_dict, property_models.values())),
},
)
messages.add_message(
request,
messages.SUCCESS,
ngettext(
formset.total_form_count,
"Added %(count)d stock entry",
"Added %(count)d stock entries",
)
% {"count": formset.total_form_count},
)
return render(
request,
"core/add_stock.html",
{
"formset": AddStockFormSet(initial=[{"owner": request.user}]),
"properties": list(map(model_to_dict, property_models.values())),
},
)
def admin_login(request):
if request.user.is_authenticated:
return redirect("unauthorized")