Merge pull request #318 from SchrodingersGat/deep-part-copy

Deep part copy
This commit is contained in:
Oliver 2019-05-13 22:03:18 +10:00 committed by GitHub
commit 4860b88927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 188 additions and 23 deletions

View File

@ -116,7 +116,7 @@ class AjaxMixin(object):
""" """
return {} 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. """ Render a JSON response based on specific class context.
Args: Args:
@ -129,6 +129,12 @@ class AjaxMixin(object):
JSON response object JSON response object
""" """
if context is None:
try:
context = self.get_context_data()
except AttributeError:
context = {}
if form: if form:
context['form'] = form context['form'] = form

View File

@ -75,11 +75,20 @@ class EditPartAttachmentForm(HelperForm):
class EditPartForm(HelperForm): class EditPartForm(HelperForm):
""" Form for editing a Part object """ """ 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: class Meta:
model = Part model = Part
fields = [ fields = [
'deep_copy',
'confirm_creation', 'confirm_creation',
'category', 'category',
'name', 'name',

View File

@ -16,6 +16,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models, transaction from django.db import models, transaction
from django.core.validators import MinValueValidator 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 the number of supplier parts available for this part """
return self.supplier_parts.count() 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): def export_bom(self, **kwargs):
data = tablib.Dataset(headers=[ data = tablib.Dataset(headers=[

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> <span class="caret"></span></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if part.active %} {% 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='edit-part' title='Edit part'>Edit</a></li>
<li><a href='#' id='duplicate-part' title='Duplicate Part'>Duplicate</a></li>
<hr> <hr>
<li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li> <li><a href="#" id='deactivate-part' title='Deactivate part'>Deactivate</a></li>
{% else %} {% else %}
@ -131,12 +131,9 @@
$("#duplicate-part").click(function() { $("#duplicate-part").click(function() {
launchModalForm( launchModalForm(
"{% url 'part-create' %}", "{% url 'part-duplicate' part.id %}",
{ {
follow: true, follow: true,
data: {
copy: {{ part.id }},
},
} }
); );
}); });

View File

@ -36,6 +36,7 @@ part_detail_urls = [
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'), 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'^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'), 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): class PartCreate(AjaxCreateView):
""" View for creating a new Part object. """ View for creating a new Part object.
Options for providing initial conditions: Options for providing initial conditions:
- Provide a category object as initial data - Provide a category object as initial data
- Copy an existing Part
""" """
model = Part model = Part
form_class = part_forms.EditPartForm form_class = part_forms.EditPartForm
@ -224,22 +333,9 @@ class PartCreate(AjaxCreateView):
""" Get initial data for the new Part object: """ Get initial data for the new Part object:
- If a category is provided, pre-fill the Category field - 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? initials = super(PartCreate, self).get_initial()
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()
if self.get_category_id(): if self.get_category_id():
try: try:

View File

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

View File

@ -1,6 +1,6 @@
<div> <div>
{% if form.pre_form_info %} {% 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 }} {{ form.pre_form_info }}
</div> </div>
{% endif %} {% endif %}