Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete part via API #3135

Merged
merged 4 commits into from
Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions InvenTree/part/migrations/0078_auto_20220606_0024.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.13 on 2022-06-06 00:24

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('part', '0077_alter_bomitem_unique_together'),
]

operations = [
migrations.AlterField(
model_name='partrelated',
name='part_1',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='related_parts_1', to='part.part', verbose_name='Part 1'),
),
migrations.AlterField(
model_name='partrelated',
name='part_2',
field=models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.CASCADE, related_name='related_parts_2', to='part.part', verbose_name='Part 2'),
),
migrations.AlterUniqueTogether(
name='partrelated',
unique_together={('part_1', 'part_2')},
),
]
60 changes: 22 additions & 38 deletions InvenTree/part/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,27 +2037,20 @@ def get_conversion_options(self):
return filtered_parts

def get_related_parts(self):
"""Return list of tuples for all related parts.

Includes:
- first value is PartRelated object
- second value is matching Part object
"""
related_parts = []
"""Return a set of all related parts for this part"""
related_parts = set()

related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk)

related_parts_2 = self.related_parts_2.filter(part_2__id=self.pk)

related_parts.append()

for related_part in related_parts_1:
# Add to related parts list
related_parts.append(related_part.part_2)
related_parts.add(related_part.part_2)

for related_part in related_parts_2:
# Add to related parts list
related_parts.append(related_part.part_1)
related_parts.add(related_part.part_1)

return related_parts

Expand Down Expand Up @@ -2829,44 +2822,35 @@ def get_api_url():
class PartRelated(models.Model):
"""Store and handle related parts (eg. mating connector, crimps, etc.)."""

class Meta:
"""Metaclass defines extra model properties"""
unique_together = ('part_1', 'part_2')

part_1 = models.ForeignKey(Part, related_name='related_parts_1',
verbose_name=_('Part 1'), on_delete=models.DO_NOTHING)
verbose_name=_('Part 1'), on_delete=models.CASCADE)

part_2 = models.ForeignKey(Part, related_name='related_parts_2',
on_delete=models.DO_NOTHING,
on_delete=models.CASCADE,
verbose_name=_('Part 2'), help_text=_('Select Related Part'))

def __str__(self):
"""Return a string representation of this Part-Part relationship"""
return f'{self.part_1} <--> {self.part_2}'

def validate(self, part_1, part_2):
"""Validate that the two parts relationship is unique."""
validate = True

parts = Part.objects.all()
related_parts = PartRelated.objects.all()

# Check if part exist and there are not the same part
if (part_1 in parts and part_2 in parts) and (part_1.pk != part_2.pk):
# Check if relation exists already
for relation in related_parts:
if (part_1 == relation.part_1 and part_2 == relation.part_2) \
or (part_1 == relation.part_2 and part_2 == relation.part_1):
validate = False
break
else:
validate = False

return validate
def save(self, *args, **kwargs):
"""Enforce a 'clean' operation when saving a PartRelated instance"""
self.clean()
self.validate_unique()
super().save(*args, **kwargs)

def clean(self):
"""Overwrite clean method to check that relation is unique."""
validate = self.validate(self.part_1, self.part_2)

if not validate:
error_message = _('Error creating relationship: check that '
'the part is not related to itself '
'and that the relationship is unique')
super().clean()

if self.part_1 == self.part_2:
raise ValidationError(_("Part relationship cannot be created between a part and itself"))

raise ValidationError(error_message)
# Check for inverse relationship
if PartRelated.objects.filter(part_1=self.part_2, part_2=self.part_1).exists():
raise ValidationError(_("Duplicate relationship already exists"))
14 changes: 7 additions & 7 deletions InvenTree/part/templates/part/part_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -559,13 +559,13 @@ <h5>

{% if roles.part.delete %}
$("#part-delete").click(function() {
launchModalForm(
"{% url 'part-delete' part.id %}",
{
redirect: {% if part.category %}"{% url 'category-detail' part.category.id %}"{% else %}"{% url 'part-index' %}"{% endif %},
no_post: {% if part.active %}true{% else %}false{% endif %},
}
);
deletePart({{ part.pk }}, {
{% if part.category %}
redirect: '{% url "category-detail" part.category.pk %}',
{% else %}
redirect: '{% url "part-index" %}',
{% endif %}
});
});
{% endif %}

Expand Down
78 changes: 0 additions & 78 deletions InvenTree/part/templates/part/partial_delete.html

This file was deleted.

51 changes: 49 additions & 2 deletions InvenTree/part/test_part.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from InvenTree import version
from InvenTree.helpers import InvenTreeTestCase

from .models import (Part, PartCategory, PartCategoryStar, PartStar,
PartTestTemplate, rename_part_image)
from .models import (Part, PartCategory, PartCategoryStar, PartRelated,
PartStar, PartTestTemplate, rename_part_image)
from .templatetags import inventree_extras


Expand Down Expand Up @@ -280,6 +280,53 @@ def test_metadata(self):

self.assertEqual(len(p.metadata.keys()), 4)

def test_related(self):
"""Unit tests for the PartRelated model"""

# Create a part relationship
PartRelated.objects.create(part_1=self.r1, part_2=self.r2)
self.assertEqual(PartRelated.objects.count(), 1)

# Creating a duplicate part relationship should fail
with self.assertRaises(ValidationError):
PartRelated.objects.create(part_1=self.r1, part_2=self.r2)

# Creating an 'inverse' duplicate relationship should also fail
with self.assertRaises(ValidationError):
PartRelated.objects.create(part_1=self.r2, part_2=self.r1)

# Try to add a self-referential relationship
with self.assertRaises(ValidationError):
PartRelated.objects.create(part_1=self.r2, part_2=self.r2)

# Test relation lookup for each part
r1_relations = self.r1.get_related_parts()
self.assertEqual(len(r1_relations), 1)
self.assertIn(self.r2, r1_relations)

r2_relations = self.r2.get_related_parts()
self.assertEqual(len(r2_relations), 1)
self.assertIn(self.r1, r2_relations)

# Delete a part, ensure the relationship also gets deleted
self.r1.delete()

self.assertEqual(PartRelated.objects.count(), 0)
self.assertEqual(len(self.r2.get_related_parts()), 0)

# Add multiple part relationships to self.r2
for p in Part.objects.all().exclude(pk=self.r2.pk):
PartRelated.objects.create(part_1=p, part_2=self.r2)

n = Part.objects.count() - 1

self.assertEqual(PartRelated.objects.count(), n)
self.assertEqual(len(self.r2.get_related_parts()), n)

# Deleting r2 should remove *all* relationships
self.r2.delete()
self.assertEqual(PartRelated.objects.count(), 0)


class TestTemplateTest(TestCase):
"""Unit test for the TestTemplate class"""
Expand Down
1 change: 0 additions & 1 deletion InvenTree/part/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from . import views

part_detail_urls = [
re_path(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
re_path(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),

re_path(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
Expand Down
17 changes: 0 additions & 17 deletions InvenTree/part/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,23 +760,6 @@ def get_data(self):
}


class PartDelete(AjaxDeleteView):
"""View to delete a Part object."""

model = Part
ajax_template_name = 'part/partial_delete.html'
ajax_form_title = _('Confirm Part Deletion')
context_object_name = 'part'

success_url = '/part/'

def get_data(self):
"""Returns custom message once the part deletion has been performed"""
return {
'danger': _('Part was deleted'),
}


class PartPricing(AjaxView):
"""View for inspecting part pricing information."""

Expand Down
45 changes: 45 additions & 0 deletions InvenTree/templates/js/translated/part.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

/* exported
deletePart,
duplicateBom,
duplicatePart,
editCategory,
Expand Down Expand Up @@ -395,6 +396,50 @@ function duplicatePart(pk, options={}) {
}


// Launch form to delete a part
function deletePart(pk, options={}) {

inventreeGet(`/api/part/${pk}/`, {}, {
success: function(part) {
if (part.active) {
showAlertDialog(
'{% trans "Active Part" %}',
'{% trans "Part cannot be deleted as it is currently active" %}',
{
alert_style: 'danger',
}
);
return;
}

var thumb = thumbnailImage(part.thumbnail || part.image);

var html = `
<div class='alert alert-block alert-danger'>
<p>${thumb} ${part.full_name} - <em>${part.description}</em></p>

{% trans "Deleting this part cannot be reversed" %}
<ul>
<li>{% trans "Any stock items for this part will be deleted" %}</li>
<li>{% trans "This part will be removed from any Bills of Material" %}</li>
<li>{% trans "All manufacturer and supplier information for this part will be deleted" %}</li>
</div>`;

constructForm(
`/api/part/${pk}/`,
{
method: 'DELETE',
title: '{% trans "Delete Part" %}',
preFormContent: html,
onSuccess: function(response) {
handleFormSuccess(response, options);
}
}
);
}
});
}

/* Toggle the 'starred' status of a part.
* Performs AJAX queries and updates the display on the button.
*
Expand Down