Implement edit componant form and adapt UI

This commit is contained in:
Mario Hüttel 2021-11-11 20:51:02 +01:00
parent e2aba765d4
commit 69ed1092e0
15 changed files with 276 additions and 41 deletions

View File

@ -61,3 +61,13 @@ class ManufacturerSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = parts_models.Manufacturer model = parts_models.Manufacturer
fields = '__all__' fields = '__all__'
class ComponentTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = parts_models.ComponentType
fields = '__all__'
class ComponentParameterTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = parts_models.ComponentParameterType
fields = '__all__'

View File

@ -12,6 +12,8 @@ router.register(r'parts/stocks', PartsStockViewSet)
router.register(r'parts/packages', PartsPackageViewSet) router.register(r'parts/packages', PartsPackageViewSet)
router.register(r'parts/distributors', PartsDistributorviewSet) router.register(r'parts/distributors', PartsDistributorviewSet)
router.register(r'parts/manufacturers', PartsManufacturerViewSet) router.register(r'parts/manufacturers', PartsManufacturerViewSet)
router.register(r'parts/component-types', PartsComponentTypeViewSet)
router.register(r'parts/component-param-types', PartsComponentParameterTypeViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),

View File

@ -49,6 +49,20 @@ class PartsComponentViewSet(viewsets.ModelViewSet):
filter_backends = [filters.SearchFilter] filter_backends = [filters.SearchFilter]
search_fields = ['id', 'name', 'package__name', 'manufacturer__name'] search_fields = ['id', 'name', 'package__name', 'manufacturer__name']
class PartsComponentTypeViewSet(viewsets.ModelViewSet):
queryset = parts_models.ComponentType.objects.all()
serializer_class = ComponentTypeSerializer
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter]
search_fields = ['class_name']
class PartsComponentParameterTypeViewSet(viewsets.ModelViewSet):
queryset = parts_models.ComponentParameterType.objects.all()
serializer_class = ComponentParameterTypeSerializer
permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter]
search_fields = ['name']
class PartsManufacturerViewSet(viewsets.ModelViewSet): class PartsManufacturerViewSet(viewsets.ModelViewSet):
queryset = parts_models.Manufacturer.objects.all() queryset = parts_models.Manufacturer.objects.all()
serializer_class = ManufacturerSerializer serializer_class = ManufacturerSerializer
@ -78,11 +92,15 @@ class PartsPackageViewSet(viewsets.ModelViewSet):
queryset = parts_models.Package.objects.all() queryset = parts_models.Package.objects.all()
serializer_class = PackageSerializer serializer_class = PackageSerializer
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter]
search_fields = ['name']
class PartsDistributorviewSet(viewsets.ModelViewSet): class PartsDistributorviewSet(viewsets.ModelViewSet):
queryset = parts_models.Distributor.objects.all() queryset = parts_models.Distributor.objects.all()
serializer_class = DistributorSerializer serializer_class = DistributorSerializer
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
filter_backends = [filters.SearchFilter]
search_fields = ['name']
## Token Authentication views ## Token Authentication views

View File

@ -111,10 +111,10 @@ class EditComponentForm(forms.Form):
description = forms.CharField(required=False, widget=forms.Textarea) description = forms.CharField(required=False, widget=forms.Textarea)
# Look up these fields later. Will be autocompleted in UI # Look up these fields later. Will be autocompleted in UI
manufacturer = forms.CharField(required=False) manufacturer = forms.CharField(required=False, initial='')
component_type = forms.CharField(required=False, label='Component Type') component_type = forms.CharField(required=False, label='Component Type', initial='')
pref_distri = forms.CharField(required=False, label='Preferred Distributor') pref_distri = forms.CharField(required=False, label='Preferred Distributor', initial='')
package = forms.CharField(required=False) package = forms.CharField(required=False, initial='')
image = forms.ImageField(required=False) image = forms.ImageField(required=False)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-11-11 19:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('parts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='componenttype',
name='possible_parameter',
field=models.ManyToManyField(blank=True, to='parts.ComponentParameterType'),
),
]

View File

@ -32,7 +32,7 @@ class ComponentType(models.Model):
ordering = ['class_name'] ordering = ['class_name']
class_name = models.CharField(max_length=50, unique=True) class_name = models.CharField(max_length=50, unique=True)
passive = models.BooleanField() passive = models.BooleanField()
possible_parameter = models.ManyToManyField(ComponentParameterType) possible_parameter = models.ManyToManyField(ComponentParameterType, blank=True)
def __str__(self): def __str__(self):
return '[' + self.class_name + ']' return '[' + self.class_name + ']'

View File

@ -15,6 +15,7 @@ from .models import Storage, Stock, Component, Distributor, Manufacturer, Packag
from .qr_parser import QrCodeValidator from .qr_parser import QrCodeValidator
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError
from .forms import MyTestForm, AddSubStorageForm, DeleteStockForm, EditWatermarkForm, EditStockAmountForm, AddStockForm, EditComponentForm from .forms import MyTestForm, AddSubStorageForm, DeleteStockForm, EditWatermarkForm, EditStockAmountForm, AddStockForm, EditComponentForm
from django.db.models import Q from django.db.models import Q
from django.db.models.functions import Lower from django.db.models.functions import Lower
@ -403,9 +404,15 @@ class ComponentDetailView(LoginRequiredMixin, BaseTemplateMixin, DetailView):
form = EditComponentForm(instance=self.object, data=request.POST, files=request.FILES) form = EditComponentForm(instance=self.object, data=request.POST, files=request.FILES)
if form.is_valid(): if form.is_valid():
try:
form.save() form.save()
except IntegrityError as ie:
form.add_error('name', 'Component name, package, and manufacturer are not unique!')
form.add_error('package', 'Component name, package, and manufacturer are not unique!')
form.add_error('manufacturer', 'Component name, package, and manufacturer are not unique!')
form_error = True
self.object = self.get_object()
else: else:
print("Error")
form_error = True form_error = True
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)

View File

@ -36,23 +36,8 @@ function(search, autocomplete_obj) {
var test = []; var test = [];
for (var i = 0; i < components.length; i++) { for (var i = 0; i < components.length; i++) {
var c = components[i]; var c = components[i];
var node = document.createElement('div');
node.setAttribute('class', 'd-flex align-items-center');
var img_container = document.createElement('div');
img_container.setAttribute('class', 'flex-shrink-0');
var text_container = document.createElement('div');
text_container.setAttribute('class', 'flex-grow-1 ms-1');
var img = document.createElement('img');
var img_path = fallback_img_path;
var style = "width:64px;max-height:64px;";
if (c.ro_image != null) {
img_path = c.ro_image;
style = "max-width:64px;max-height:64px;";
}
img.setAttribute('src', img_path);
img.setAttribute('style', style)
img_container.appendChild(img);
var text_container = document.createElement('div');
var name_text = document.createTextNode(c.name); var name_text = document.createTextNode(c.name);
var heading = document.createElement('h6'); var heading = document.createElement('h6');
heading.appendChild(name_text); heading.appendChild(name_text);
@ -64,8 +49,7 @@ function(search, autocomplete_obj) {
text_container.appendChild(document.createTextNode(' by '+c.ro_manufacturer_name)); text_container.appendChild(document.createTextNode(' by '+c.ro_manufacturer_name));
} }
node.appendChild(img_container); node = AutocompleteCustomUi.create_media_div(c.ro_image, [text_container])
node.appendChild(text_container);
test.push({'ui': node, 'data': c}) test.push({'ui': node, 'data': c})
} }

View File

@ -1,6 +1,36 @@
const autocomplete_query_delay_ms = 60; const autocomplete_query_delay_ms = 60;
class AutocompleteCustomUi { class AutocompleteCustomUi {
static create_media_div(img_src, text_nodes) {
var ui = document.createElement('div');
ui.setAttribute('class', 'd-flex align-items-center');
var img_div = document.createElement('div');
img_div.setAttribute('class', 'flex-shrink-0');
var img = document.createElement('img');
if (img_src != null && img_src != undefined && img_src != '') {
img.setAttribute('src', img_src);
img.setAttribute('class', 'component-img-small');
} else {
img.setAttribute('src', fallback_img_path);
img.setAttribute('class', 'component-img-def-small');
}
img_div.appendChild(img);
ui.appendChild(img_div);
var text_div = document.createElement('div');
text_div.setAttribute('class', 'flex-grow-1 ms-3');
if (text_nodes != undefined) {
for (var i = 0; i < text_nodes.length; i++) {
text_div.appendChild(text_nodes[i]);
}
}
ui.appendChild(text_div)
return ui
}
constructor(text_id, dropdown_id, query_function) constructor(text_id, dropdown_id, query_function)
{ {
this.text_id = text_id; this.text_id = text_id;
@ -17,7 +47,7 @@ class AutocompleteCustomUi {
/** /**
* *
* @param {*} nodes_to_add A liust of dictionaries containing the shown objects and the data called when clicked. * @param {*} nodes_to_add A list of dictionaries containing the shown objects and the data called when clicked.
* *
* The list: {{'ui': <nodes>, 'data': 'my_data'}, {}, ...} * The list: {{'ui': <nodes>, 'data': 'my_data'}, {}, ...}
* *

View File

@ -0,0 +1,81 @@
document.addEventListener("DOMContentLoaded", function(){
// Create the autocompletion stuff
new AutocompleteText(edit_comp_modal_ids['component_type'], edit_comp_modal_ids['component_type']+'-ac-ul',
function(search, autocomplete) {
api_search_component_types(search, function(result) {
type_names = [];
for(var i = 0; i < result.results.length; i++) {
var r = result.results[i];
type_names.push(r.class_name);
}
autocomplete.show_results(type_names)
}, function() {
// Nothing to do---
})
});
new AutocompleteCustomUi(edit_comp_modal_ids['manufacturer'], edit_comp_modal_ids['manufacturer']+'-ac-ul',
function(search, autocomplete) {
api_search_manufacturer(search, function(result) {
nodes = [];
for (var i = 0; i < result.results.length; i++) {
var manu = result.results[i];
// Construct the ui:
ui = AutocompleteCustomUi.create_media_div(manu.image, [document.createTextNode(manu.name)])
nodes.push({'ui': ui, 'data': manu.name});
}
autocomplete.show_results(nodes, function(data) {
document.getElementById(edit_comp_modal_ids['manufacturer']).value = data;
})
}, function() {
// Nothing to do---
})
});
new AutocompleteCustomUi(edit_comp_modal_ids['pref_distri'], edit_comp_modal_ids['pref_distri']+'-ac-ul',
function(search, autocomplete) {
api_search_distributor(search, function(result) {
nodes = [];
for (var i = 0; i < result.results.length; i++) {
var distri = result.results[i];
// Construct the ui:
ui = AutocompleteCustomUi.create_media_div(distri.image, [document.createTextNode(distri.name)])
nodes.push({'ui': ui, 'data': distri.name});
}
autocomplete.show_results(nodes, function(data) {
document.getElementById(edit_comp_modal_ids['pref_distri']).value = data;
})
}, function() {
// Nothing to do---
})
});
new AutocompleteCustomUi(edit_comp_modal_ids['package'], edit_comp_modal_ids['package']+'-ac-ul',
function(search, autocomplete) {
api_search_package(search, function(result) {
nodes = [];
for (var i = 0; i < result.results.length; i++) {
var pkg = result.results[i];
// Construct the ui:
ui = AutocompleteCustomUi.create_media_div(pkg.image, [document.createTextNode(pkg.name)])
nodes.push({'ui': ui, 'data': pkg.name});
}
autocomplete.show_results(nodes, function(data) {
document.getElementById(edit_comp_modal_ids['package']).value = data;
})
}, function() {
// Nothing to do---
})
});
});

View File

@ -36,3 +36,19 @@ function api_search_component(search, onSuccess, onFail) {
function api_get_component_from_id(id, onSuccess, onFail) { function api_get_component_from_id(id, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(id)}`, function(method, url, json) {onSuccess(json.results[0]);}, onFail); return api_ajax_request_without_send('GET', api_urls_v1['component-list']+`?search=${encodeURIComponent(id)}`, function(method, url, json) {onSuccess(json.results[0]);}, onFail);
} }
function api_search_component_types(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['component-type-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_package(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['package-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_manufacturer(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['manufacturer-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}
function api_search_distributor(search, onSuccess, onFail) {
return api_ajax_request_without_send('GET', api_urls_v1['distributor-list']+`?search=${encodeURIComponent(search)}`, function(method, url, json) {onSuccess(json);}, onFail);
}

View File

@ -8,6 +8,9 @@
<link href="{% static 'css/icons/bootstrap-icons.css' %}" rel="stylesheet"> <link href="{% static 'css/icons/bootstrap-icons.css' %}" rel="stylesheet">
<link href="{% static 'css/shimatta-kenkyusho-base.css' %}" rel="stylesheet"> <link href="{% static 'css/shimatta-kenkyusho-base.css' %}" rel="stylesheet">
<title>{{ base.title }}</title> <title>{{ base.title }}</title>
<script type="text/javascript">
const fallback_img_path = "{% static 'css/icons/card-image.svg' %}";
</script>
{% block customhead %} {% block customhead %}
{% endblock customhead %} {% endblock customhead %}
</head> </head>
@ -66,6 +69,9 @@
'package-list': '{% url 'package-list' %}', 'package-list': '{% url 'package-list' %}',
'stock-list': '{% url 'stock-list' %}', 'stock-list': '{% url 'stock-list' %}',
'distributor-list': '{% url 'distributor-list' %}', 'distributor-list': '{% url 'distributor-list' %}',
'manufacturer-list': '{% url 'manufacturer-list' %}',
'component-type-list': '{% url 'componenttype-list' %}',
'component-parameter-type-list': '{% url 'componentparametertype-list' %}',
}; };
</script> </script>
<script type="text/javascript" src="{% static 'js/kenyusho-api-v1.js' %}"></script> <script type="text/javascript" src="{% static 'js/kenyusho-api-v1.js' %}"></script>

View File

@ -36,17 +36,37 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th scope="row">{{component.name}}</th> <td class="align-middle" scope="row">
<th>{% if component.package %}<a href="{% url 'parts-packages-detail' uuid=component.package.id %}" class="link-primary text-decoration-none">{{component.package.name}}</a>{% endif %}</th> {{component.name}}
<th>{% if component.manufacturer %}{{component.manufacturer.name}}{% endif %}</th> </td>
<th>{% if component.component_type %}{{component.component_type.class_name}}{% endif %} <td class="align-middle" >
<th>{{component.get_total_amount}}</th> {% if component.package %}
<a href="{% url 'parts-packages-detail' uuid=component.package.id %}" class="link-primary text-decoration-none">{{component.package.name}}</a>
{% if component.package.image %}
<img src="{{component.package.image.url}}" class="component-img-small">
{% endif %}
{% endif %}
</td>
<td class="align-middle" >
{% if component.manufacturer %}
{{component.manufacturer.name}}
{% if component.manufacturer.image %}
<img src="{{component.manufacturer.image.url}}" class="component-img-small">
{% endif %}
{% endif %}
</td>
<td class="align-middle">
{% if component.component_type %}{{component.component_type.class_name}}{% endif %}
</td>
<td class="align-middle">
{{component.get_total_amount}}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h2>Description</h2> <h2>Description</h2>
{% if component.description %} {% if component.description %}
{{component.description}} {{component.description|linebreaks}}
{% else %} {% else %}
<div class="alert alert-secondary" role="alert"> <div class="alert alert-secondary" role="alert">
No description available No description available
@ -80,7 +100,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% include 'parts/modals/edit-component-modal.html' with heading="Edit "|add:component.name edit_form=edit_form %} {% include 'parts/modals/edit-component-modal.html' with heading="Edit "|add:component.name form=edit_form %}
{% endblock content %} {% endblock content %}
{% block custom_scripts %} {% block custom_scripts %}

View File

@ -1,9 +1,10 @@
{% comment "" %} {% comment "" %}
Needs following context: Needs following context:
- heading - heading
- edit_form EditComponentForm - form EditComponentForm
{% endcomment %} {% endcomment %}
{% load static %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<div class="modal fade" id="comp-edit-modal" tabindex="-1"> <div class="modal fade" id="comp-edit-modal" tabindex="-1">
@ -16,7 +17,44 @@ Needs following context:
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
{{edit_form|crispy}} {{form.name|as_crispy_field}}
{{form.datasheet_link|as_crispy_field}}
{{form.description|as_crispy_field}}
<div class="mb-3">
<label class="form-label" for="{{form.manufacturer.id_for_label}}">Manufacturer</label>
<div class="dropdown">
<input autocomplete="off" data-bs-toggle="dropdown" class="form-control{% if form.manufacturer.errors %} is-invalid{% endif %}" type="text" id="{{form.manufacturer.id_for_label}}" name="{{form.manufacturer.name}}" value="{{form.manufacturer.value}}">
<ul id="{{form.manufacturer.id_for_label}}-ac-ul" class="dropdown-menu">
</ul>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{form.package.id_for_label}}">Package</label>
<div class="dropdown">
<input autocomplete="off" data-bs-toggle="dropdown" class="form-control{% if form.package.errors %} is-invalid{% endif %}" type="text" id="{{form.package.id_for_label}}" name="{{form.package.name}}" value="{{form.package.value}}">
<ul id="{{form.package.id_for_label}}-ac-ul" class="dropdown-menu">
</ul>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{form.component_type.id_for_label}}">Component Type</label>
<div class="dropdown">
<input autocomplete="off" data-bs-toggle="dropdown" class="form-control{% if form.component_type.errors %} is-invalid{% endif %}" type="text" id="{{form.component_type.id_for_label}}" name="{{form.component_type.name}}" value="{{form.component_type.value}}">
<ul id="{{form.component_type.id_for_label}}-ac-ul" class="dropdown-menu">
</ul>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="{{form.pref_distri.id_for_label}}">Preferred Distributor</label>
<div class="dropdown">
<input autocomplete="off" data-bs-toggle="dropdown" class="form-control{% if form.pref_distri.errors %} is-invalid{% endif %}" type="text" id="{{form.pref_distri.id_for_label}}" name="{{form.pref_distri.name}}" value="{{form.pref_distri.value}}">
<ul id="{{form.pref_distri.id_for_label}}-ac-ul" class="dropdown-menu">
</ul>
</div>
</div>
<div class="mb-3">
{{form.image|as_crispy_field}}
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<input type="submit" name="submit-edit-comp" class="btn btn-primary" value="Save"> <input type="submit" name="submit-edit-comp" class="btn btn-primary" value="Save">
@ -25,3 +63,12 @@ Needs following context:
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript">
const edit_comp_modal_ids = {
'package': '{{form.package.id_for_label}}',
'manufacturer': '{{form.manufacturer.id_for_label}}',
'component_type': '{{form.component_type.id_for_label}}',
'pref_distri': '{{form.pref_distri.id_for_label}}',
}
</script>
<script type="text/javascript" src="{% static 'js/edit-component-modal.js' %}"></script>

View File

@ -168,9 +168,5 @@ function(search, autocomplete_obj) {
}, function(){}); }, function(){});
}); });
</script> </script>
<script type="text/javascript">
const fallback_img_path = "{% static 'css/icons/card-image.svg' %}";
</script>
{% endblock custom_scripts %} {% endblock custom_scripts %}