Merge pull request #201 from SchrodingersGat/part-attachments

Part attachments
This commit is contained in:
Oliver 2019-05-02 17:46:58 +10:00 committed by GitHub
commit 2f7c02133e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 206 additions and 51 deletions

View File

@ -177,7 +177,10 @@ class AjaxCreateView(AjaxMixin, CreateView):
# Return the PK of the newly-created object
data['pk'] = obj.pk
try:
data['url'] = obj.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data)
@ -223,7 +226,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
# Include context data about the updated object
data['pk'] = obj.id
try:
data['url'] = obj.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data)

View File

@ -9,7 +9,8 @@ from InvenTree.forms import HelperForm
from django import forms
from .models import Part, PartCategory, BomItem
from .models import Part, PartCategory, PartAttachment
from .models import BomItem
from .models import SupplierPart
@ -44,6 +45,18 @@ class BomExportForm(HelperForm):
]
class EditPartAttachmentForm(HelperForm):
""" Form for editing a PartAttachment object """
class Meta:
model = PartAttachment
fields = [
'part',
'attachment',
'comment'
]
class EditPartForm(HelperForm):
""" Form for editing a Part object """

View File

@ -14,5 +14,10 @@ class Migration(migrations.Migration):
model_name='part',
name='active',
field=models.BooleanField(default=True, help_text='Is this part active?'),
),
migrations.AddField(
model_name='partattachment',
name='comment',
field=models.CharField(blank=True, help_text='File comment', max_length=100),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2 on 2019-04-30 23:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0013_auto_20190429_2229'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='comment',
field=models.CharField(blank=True, help_text='Attachment description', max_length=100),
),
]

View File

@ -125,7 +125,7 @@ class Part(models.Model):
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
# Provide a URL for an external link
URL = models.URLField(blank=True, help_text='Link to external URL')
URL = models.URLField(blank=True, help_text='Link to extenal URL')
# Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts',
@ -307,12 +307,6 @@ class Part(models.Model):
def used_in_count(self):
return self.used_in.count()
def required_parts(self):
parts = []
for bom in self.bom_items.all():
parts.append(bom.sub_part)
return parts
@property
def supplier_count(self):
# Return the number of supplier parts available for this part
@ -366,7 +360,7 @@ class PartAttachment(models.Model):
attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
comment = models.CharField(max_length=100, blank=True, help_text="Attachment description")
comment = models.CharField(max_length=100, blank=True, help_text='File comment')
@property
def basename(self):
@ -407,11 +401,8 @@ class BomItem(models.Model):
- A part cannot refer to a part which refers to it
"""
if self.part is None or self.sub_part is None:
# Field validation will catch these None values
pass
# A part cannot refer to itself in its BOM
elif self.part == self.sub_part:
if self.part == self.sub_part:
raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
# Test for simple recursion

View File

@ -0,0 +1,3 @@
Are you sure you wish to delete this attachment?
<br>
This will remove the file '{{ attachment.basename }}'.

View File

@ -0,0 +1,62 @@
{% extends "part/part_base.html" %}
{% load static %}
{% block details %}
{% include 'part/tabs.html' with tab='attachments' %}
<h4>Attachments</h4>
<div id='toolbar' class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
</div>
<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
<tr>
<th>File</th>
<th>Comment</th>
<th></th>
</tr>
{% for attachment in part.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'>Edit</button>
<button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'>Delete</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#new-attachment").click(function() {
launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}");
});
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
success: function() {
}
});
});
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
launchDeleteForm(button.attr('url'), {
success: function() {
}
});
});
{% endblock %}

View File

@ -66,13 +66,6 @@
</tr>
{% endif %}
</table>
<h4>Attachments</h4>
<ul>
{% for attachment in part.attachments.all %}
<li><a href="/media/{{ attachment.attachment }}">{{ attachment.basename }}</a></li>
{% endfor %}
</ul>
</div>
</div>

View File

@ -1,6 +1,15 @@
<ul class="nav nav-tabs">
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
<a href="{% url 'part-detail' part.id %}">Details</a></li>
<a href="{% url 'part-detail' part.id %}">Details</a>
</li>
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
<a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a>
</li>
{% if part.allocation_count > 0 %}
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
<a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a>
</li>
{% endif %}
{% if part.buildable %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
<a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li>
@ -11,12 +20,6 @@
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
<a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}>
<a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a></li>
{% if part.allocation_count > 0 %}
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}>
<a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a></li>
{% endif %}
{% if part.purchaseable %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">Suppliers
@ -31,4 +34,7 @@
{% endif %}
</a></li>
{% endif %}
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
</li>
</ul>

View File

@ -1,5 +1,11 @@
"""
URL lookup for Part app
URL lookup for Part app. Provides URL endpoints for:
- Display / Create / Edit / Delete PartCategory
- Display / Create / Edit / Delete Part
- Create / Edit / Delete PartAttachment
- Display / Create / Edit / Delete SupplierPart
"""
from django.conf.urls import url, include
@ -19,11 +25,18 @@ supplier_part_urls = [
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
]
part_attachment_urls = [
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
]
part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
@ -69,6 +82,10 @@ part_urls = [
# Part category
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
# Part attachments
url(r'^attachment/', include(part_attachment_urls)),
# Bom Items
url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
# Top level part list (display top level parts and categories)

View File

@ -13,11 +13,13 @@ from django.forms.models import model_to_dict
from django.forms import HiddenInput
from company.models import Company
from .models import PartCategory, Part, BomItem
from .models import PartCategory, Part, PartAttachment
from .models import BomItem
from .models import SupplierPart
from .forms import PartImageForm
from .forms import EditPartForm
from .forms import EditPartAttachmentForm
from .forms import EditCategoryForm
from .forms import EditBomItemForm
from .forms import BomExportForm
@ -51,6 +53,81 @@ class PartIndex(ListView):
return context
class PartAttachmentCreate(AjaxCreateView):
""" View for creating a new PartAttachment object
- The view only makes sense if a Part object is passed to it
"""
model = PartAttachment
form_class = EditPartAttachmentForm
ajax_form_title = "Add part attachment"
ajax_template_name = "modal_form.html"
def get_data(self):
return {
'success': 'Added attachment'
}
def get_initial(self):
""" Get initial data for new PartAttachment object.
- Client should have requested this form with a parent part in mind
- e.g. ?part=<pk>
"""
initials = super(AjaxCreateView, self).get_initial()
# TODO - If the proper part was not sent, return an error message
initials['part'] = Part.objects.get(id=self.request.GET.get('part'))
return initials
def get_form(self):
""" Create a form to upload a new PartAttachment
- Hide the 'part' field
"""
form = super(AjaxCreateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentEdit(AjaxUpdateView):
""" View for editing a PartAttachment object """
model = PartAttachment
form_class = EditPartAttachmentForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit attachment'
def get_data(self):
return {
'success': 'Part attachment updated'
}
def get_form(self):
form = super(AjaxUpdateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentDelete(AjaxDeleteView):
""" View for deleting a PartAttachment """
model = PartAttachment
ajax_template_name = "part/attachment_delete.html"
context_object_name = "attachment"
def get_data(self):
return {
'danger': 'Deleted part attachment'
}
class PartCreate(AjaxCreateView):
""" View for creating a new Part object.
@ -102,7 +179,6 @@ class PartCreate(AjaxCreateView):
return form
# Pre-fill the category field if a valid category is provided
def get_initial(self):
""" Get initial data for the new Part object:

View File

@ -4,5 +4,5 @@ ignore =
W293,
# - E501 - line too long (82 characters)
E501
exclude = .git,__pycache__
exclude = .git,__pycache__,*/migrations/*
max-complexity = 20