Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-13 23:07:12 +10:00
commit 787ab0a2e4
29 changed files with 302 additions and 116 deletions

View File

@ -17,3 +17,19 @@ class HelperForm(forms.ModelForm):
self.helper = FormHelper()
self.helper.form_tag = False
class DeleteForm(forms.Form):
""" Generic deletion form which provides simple user confirmation
"""
confirm_delete = forms.BooleanField(
required=False,
initial=False,
help_text='Confirm item deletion'
)
class Meta:
fields = [
'confirm_delete'
]

View File

@ -12,11 +12,14 @@ from django.template.loader import render_to_string
from django.http import JsonResponse
from django.views import View
from django.views.generic import UpdateView, CreateView, DeleteView
from django.views.generic import UpdateView, CreateView
from django.views.generic.base import TemplateView
from part.models import Part
from .forms import DeleteForm
from .helpers import str2bool
from rest_framework import views
@ -116,7 +119,7 @@ class AjaxMixin(object):
"""
return {}
def renderJsonResponse(self, request, form=None, data={}, context={}):
def renderJsonResponse(self, request, form=None, data={}, context=None):
""" Render a JSON response based on specific class context.
Args:
@ -129,6 +132,12 @@ class AjaxMixin(object):
JSON response object
"""
if context is None:
try:
context = self.get_context_data()
except AttributeError:
context = {}
if form:
context['form'] = form
@ -294,13 +303,28 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, form, data)
class AjaxDeleteView(AjaxMixin, DeleteView):
class AjaxDeleteView(AjaxMixin, UpdateView):
""" An 'AJAXified DeleteView for removing an object from the DB
- Returns a HTML object (not a form!) in JSON format (for delivery to a modal window)
- Handles deletion
"""
form_class = DeleteForm
ajax_form_title = "Delete Item"
ajax_template_name = "modal_delete_form.html"
context_object_name = 'item'
def get_object(self):
try:
self.object = self.model.objects.get(pk=self.kwargs['pk'])
except:
return None
return self.object
def get_form(self):
return self.form_class(self.get_form_kwargs())
def get(self, request, *args, **kwargs):
""" Respond to GET request
@ -308,19 +332,15 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
- Return rendered form to client
"""
super(DeleteView, self).get(request, *args, **kwargs)
super(UpdateView, self).get(request, *args, **kwargs)
data = {
'id': self.get_object().id,
'delete': False,
'title': self.ajax_form_title,
'html_data': render_to_string(
self.ajax_template_name,
self.get_context_data(),
request=request)
}
form = self.get_form()
return JsonResponse(data)
context = self.get_context_data()
context[self.context_object_name] = self.get_object()
return self.renderJsonResponse(request, form, context=context)
def post(self, request, *args, **kwargs):
""" Respond to POST request
@ -331,14 +351,24 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
obj = self.get_object()
pk = obj.id
obj.delete()
form = self.get_form()
confirmed = str2bool(request.POST.get('confirm_delete', False))
context = self.get_context_data()
if confirmed:
obj.delete()
else:
form.errors['confirm_delete'] = ['Check box to confirm item deletion']
context[self.context_object_name] = self.get_object()
data = {
'id': pk,
'delete': True
'form_valid': confirmed
}
return self.renderJsonResponse(request, data=data)
return self.renderJsonResponse(request, form, data=data, context=context)
class IndexView(TemplateView):

View File

@ -1,3 +1,7 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to unallocate these parts?
<br>
This will remove {{ item.quantity }} parts from build '{{ item.build.title }}'.
{% endblock %}

View File

@ -1,3 +1,7 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to delete company '{{ company.name }}'?
<br>
@ -11,3 +15,5 @@ If this supplier is deleted, these supplier part entries will also be deleted.</
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -54,7 +54,7 @@
});
$('#delete-company').click(function() {
launchDeleteForm(
launchModalForm(
"{% url 'company-delete' company.id %}",
{
redirect: "{% url 'company-index' %}"

View File

@ -1 +1,5 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to delete this supplier part?
{% endblock %}

View File

@ -109,7 +109,7 @@ InvenTree | {{ company.name }} - Parts
});
$('#delete-part').click(function() {
launchDeleteForm(
launchModalForm(
"{% url 'supplier-part-delete' part.id %}",
{
redirect: "{% url 'company-index' %}"

View File

@ -93,10 +93,12 @@ class CompanyCreate(AjaxCreateView):
class CompanyDelete(AjaxDeleteView):
""" View for deleting a Company object """
model = Company
success_url = '/company/'
ajax_template_name = 'company/delete.html'
ajax_form_title = 'Delete Company'
context_object_name = 'company'
def get_data(self):
return {

View File

@ -75,11 +75,20 @@ class EditPartAttachmentForm(HelperForm):
class EditPartForm(HelperForm):
""" Form for editing a Part object """
confirm_creation = forms.BooleanField(required=False, initial=False, help_text='Confirm part creation', widget=forms.HiddenInput())
deep_copy = forms.BooleanField(required=False,
initial=True,
help_text="Perform 'deep copy' which will duplicate all BOM data for this part",
widget=forms.HiddenInput())
confirm_creation = forms.BooleanField(required=False,
initial=False,
help_text='Confirm part creation',
widget=forms.HiddenInput())
class Meta:
model = Part
fields = [
'deep_copy',
'confirm_creation',
'category',
'name',

View File

@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models, transaction
from django.core.validators import MinValueValidator
@ -533,6 +534,33 @@ class Part(models.Model):
""" Return the number of supplier parts available for this part """
return self.supplier_parts.count()
def deepCopy(self, other, **kwargs):
""" Duplicates non-field data from another part.
Does not alter the normal fields of this part,
but can be used to copy other data linked by ForeignKey refernce.
Keyword Args:
image: If True, copies Part image (default = True)
bom: If True, copies BOM data (default = False)
"""
# Copy the part image
if kwargs.get('image', True):
image_file = ContentFile(other.image.read())
image_file.name = rename_part_image(self, 'test.png')
self.image = image_file
# Copy the BOM data
if kwargs.get('bom', False):
for item in other.bom_items.all():
# Point the item to THIS part
item.part = self
item.pk = None
item.save()
self.save()
def export_bom(self, **kwargs):
data = tablib.Dataset(headers=[

View File

@ -1,3 +1,7 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you wish to delete this attachment?
<br>
This will remove the file '{{ attachment.basename }}'.
{% endblock %}

View File

@ -62,7 +62,7 @@
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
launchDeleteForm(button.attr('url'), {
launchModalForm(button.attr('url'), {
success: function() {
location.reload();
}

View File

@ -1,3 +1,7 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to delete this BOM item?
<br>
Deleting this entry will remove the BOM row from the following part:
@ -7,3 +11,5 @@ Deleting this entry will remove the BOM row from the following part:
<b>{{ item.part.full_name }}</b> - <i>{{ item.part.description }}</i>
</li>
</ul>
{% endblock %}

View File

@ -111,7 +111,7 @@
{% endif %}
$('#cat-delete').click(function() {
launchDeleteForm("{% url 'category-delete' category.id %}",
launchModalForm("{% url 'category-delete' category.id %}",
{
redirect: redirect
});

View File

@ -1,3 +1,6 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to delete category '{{ category.name }}'?
{% if category.children.all|length > 0 %}
@ -31,3 +34,5 @@ the top level 'Parts' category.
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "modal_form.html" %}
{% block pre_form_content %}
{{ block.super }}
<div class='alert alert-info alert-block'>
<b>Duplicate Part</b><br>
Make a copy of part '{{ part.full_name }}'.
</div>
{% if matches %}
<b>Possible Matching Parts</b>
<p>The new part may be a duplicate of these existing parts:</p>
<ul class='list-group'>
{% for match in matches %}
<li class='list-group-item list-group-item-condensed'>
{{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%)
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -14,8 +14,8 @@
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% if part.active %}
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<li><a href="#" id='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %}
@ -131,12 +131,9 @@
$("#duplicate-part").click(function() {
launchModalForm(
"{% url 'part-create' %}",
"{% url 'part-duplicate' part.id %}",
{
follow: true,
data: {
copy: {{ part.id }},
},
}
);
});
@ -196,7 +193,7 @@
$('#delete-part').click(function() {
launchDeleteForm(
launchModalForm(
"{% url 'part-delete' part.id %}",
{
redirect: {% if part.category %}"{% url 'category-detail' part.category.id %}"{% else %}"{% url 'part-index' %}"{% endif %}

View File

@ -36,6 +36,7 @@ part_detail_urls = [
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),

View File

@ -124,13 +124,122 @@ class PartAttachmentDelete(AjaxDeleteView):
}
class PartDuplicate(AjaxCreateView):
""" View for duplicating an existing Part object.
- Part <pk> is provided in the URL '/part/<pk>/copy/'
- Option for 'deep-copy' which will duplicate all BOM items (default = True)
"""
model = Part
form_class = part_forms.EditPartForm
ajax_form_title = "Duplicate Part"
ajax_template_name = "part/copy_part.html"
def get_data(self):
return {
'success': 'Copied part'
}
def get_part_to_copy(self):
try:
return Part.objects.get(id=self.kwargs['pk'])
except Part.DoesNotExist:
return None
def get_context_data(self):
return {
'part': self.get_part_to_copy()
}
def get_form(self):
form = super(AjaxCreateView, self).get_form()
# Force display of the 'deep_copy' widget
form.fields['deep_copy'].widget = CheckboxInput()
return form
def post(self, request, *args, **kwargs):
""" Capture the POST request for part duplication
- If the deep_copy object is set, copy all the BOM items too!
"""
form = self.get_form()
context = self.get_context_data()
valid = form.is_valid()
name = request.POST.get('name', None)
if name:
matches = match_part_names(name)
if len(matches) > 0:
context['matches'] = matches
# Enforce display of the checkbox
form.fields['confirm_creation'].widget = CheckboxInput()
# Check if the user has checked the 'confirm_creation' input
confirmed = str2bool(request.POST.get('confirm_creation', False))
if not confirmed:
form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part']
form.pre_form_warning = 'Possible matches exist - confirm creation of new part'
valid = False
data = {
'form_valid': valid
}
if valid:
# Create the new Part
part = form.save()
data['pk'] = part.pk
deep_copy = str2bool(request.POST.get('deep_copy', False))
original = self.get_part_to_copy()
if original:
part.deepCopy(original, bom=deep_copy)
try:
data['url'] = part.get_absolute_url()
except AttributeError:
pass
if valid:
pass
return self.renderJsonResponse(request, form, data, context=context)
def get_initial(self):
""" Get initial data based on the Part to be copied from.
"""
part = self.get_part_to_copy()
if part:
initials = model_to_dict(part)
else:
initials = super(AjaxCreateView, self).get_initial()
return initials
class PartCreate(AjaxCreateView):
""" View for creating a new Part object.
Options for providing initial conditions:
- Provide a category object as initial data
- Copy an existing Part
"""
model = Part
form_class = part_forms.EditPartForm
@ -224,22 +333,9 @@ class PartCreate(AjaxCreateView):
""" Get initial data for the new Part object:
- If a category is provided, pre-fill the Category field
- If 'copy' parameter is provided, copy from referenced Part
"""
# Is the client attempting to copy an existing part?
part_to_copy = self.request.GET.get('copy', None)
if part_to_copy:
try:
original = Part.objects.get(pk=part_to_copy)
initials = model_to_dict(original)
self.ajax_form_title = "Copy Part '{p}'".format(p=original.name)
except Part.DoesNotExist:
initials = super(PartCreate, self).get_initial()
else:
initials = super(PartCreate, self).get_initial()
initials = super(PartCreate, self).get_initial()
if self.get_category_id():
try:
@ -708,3 +804,4 @@ class SupplierPartDelete(AjaxDeleteView):
success_url = '/supplier/'
ajax_template_name = 'company/partdelete.html'
ajax_form_title = 'Delete Supplier Part'
context_object_name = 'supplier_part'

View File

@ -162,6 +162,10 @@
z-index: 99999999;
}
.js-modal-form .checkbox {
margin-left: 0px;
}
.modal-dialog {
width: 45%;
}

View File

@ -184,7 +184,7 @@ function loadBomTable(table, options) {
table.on('click', '.bom-delete-button', function() {
var button = $(this);
launchDeleteForm(button.attr('url'), {
launchModalForm(button.attr('url'), {
success: function() {
reloadBomTable(table);
}

View File

@ -87,7 +87,7 @@ function loadAllocationTable(table, part_id, part, url, required, button) {
table.on('click', '.item-del-button', function() {
var button = $(this);
launchDeleteForm(button.attr('url'), {
launchModalForm(button.attr('url'), {
success: function() {
table.bootstrapTable('refresh');
}

View File

@ -345,68 +345,6 @@ function openModal(options) {
}
function launchDeleteForm(url, options = {}) {
/* Launch a modal form to delete an object
*/
var modal = options.modal || '#modal-delete';
$(modal).on('shown.bs.modal', function() {
$(modal + ' .modal-form-content').scrollTop(0);
});
// Un-bind any attached click listeners
$(modal).off('click', '#modal-form-delete');
// Request delete form data
$.ajax({
url: url,
type: 'get',
dataType: 'json',
beforeSend: function() {
openModal({modal: modal});
},
success: function (response) {
if (response.title) {
modalSetTitle(modal, response.title);
}
if (response.html_data) {
modalSetContent(modal, response.html_data);
}
else {
$(modal).modal('hide');
showAlertDialog('Invalid form response', 'JSON response missing HTML data');
}
},
error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide');
showAlertDialog('Error requesting form data', renderErrorMessage(xhr));
}
});
$(modal).on('click', '#modal-form-delete', function() {
var form = $(modal).find('#delete-form');
$.ajax({
url: url,
data: form.serialize(),
type: 'post',
dataType: 'json',
success: function (response) {
$(modal).modal('hide');
afterForm(response, options);
},
error: function (xhr, ajaxOptions, thrownError) {
$(modal).modal('hide');
showAlertDialog('Error deleting item', renderErrorMessage(xhr));
}
});
});
}
function injectModalForm(modal, form_html) {
/* Inject form content into the modal.
* Updates the HTML of the form content, and then applies some other updates

View File

@ -194,7 +194,7 @@
{% endif %}
$("#stock-delete").click(function () {
launchDeleteForm(
launchModalForm(
"{% url 'stock-item-delete' item.id %}",
{
redirect: "{% url 'part-stock' item.part.id %}"

View File

@ -1,5 +1,11 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
<div class='alert alert-danger alert-block'>
Are you sure you want to delete this stock item?
<br>
This will remove <b>{{ item.quantity }}</b> units of <b>{{ item.part.full_name }}</b> from stock.
</div>
{% endblock %}

View File

@ -97,7 +97,7 @@
});
$('#location-delete').click(function() {
launchDeleteForm("{% url 'stock-location-delete' location.id %}",
launchModalForm("{% url 'stock-location-delete' location.id %}",
{
redirect: "{% url 'stock-index' %}"
});

View File

@ -1,3 +1,6 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to delete stock location '{{ location.name }}'?
<br>
@ -34,3 +37,4 @@ If this location is deleted, these items will be moved to the top level 'Stock'
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@ -0,0 +1 @@
{% extends "modal_form.html" %}

View File

@ -1,6 +1,6 @@
<div>
{% if form.pre_form_info %}
<div class='alert alert-info' role='alert' style='display: block;'>
<div class='alert alert-info alert-block' role='alert'>
{{ form.pre_form_info }}
</div>
{% endif %}