diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py
index 7ec4401101..c186ff8589 100644
--- a/InvenTree/InvenTree/forms.py
+++ b/InvenTree/InvenTree/forms.py
@@ -7,6 +7,7 @@ from __future__ import unicode_literals
from django import forms
from crispy_forms.helper import FormHelper
+from django.contrib.auth.models import User
class HelperForm(forms.ModelForm):
@@ -33,3 +34,42 @@ class DeleteForm(forms.Form):
fields = [
'confirm_delete'
]
+
+
+class EditUserForm(HelperForm):
+ """ Form for editing user information
+ """
+
+ class Meta:
+ model = User
+ fields = [
+ 'first_name',
+ 'last_name',
+ 'email'
+ ]
+
+
+class SetPasswordForm(HelperForm):
+ """ Form for setting user password
+ """
+
+ enter_password = forms.CharField(max_length=100,
+ min_length=8,
+ required=True,
+ initial='',
+ widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
+ help_text='Enter new password')
+
+ confirm_password = forms.CharField(max_length=100,
+ min_length=8,
+ required=True,
+ initial='',
+ widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
+ help_text='Confirm new password')
+
+ class Meta:
+ model = User
+ fields = [
+ 'enter_password',
+ 'confirm_password'
+ ]
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index fe8a624458..2872096c08 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -30,7 +30,7 @@ from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from rest_framework.documentation import include_docs_urls
-from .views import IndexView, SearchView
+from .views import IndexView, SearchView, SettingsView, EditUserView, SetPasswordView
from users.urls import user_urls
@@ -61,6 +61,11 @@ urlpatterns = [
url(r'^login/', auth_views.LoginView.as_view(), name='login'),
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logout.html'), name='logout'),
+
+ url(r'^settings/', SettingsView.as_view(), name='settings'),
+
+ url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
+ url(r'^set-password/', SetPasswordView.as_view(), name='set-password'),
url(r'^admin/', admin.site.urls, name='inventree-admin'),
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index 88da882e10..f3fd9ef306 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
def validate_part_name(value):
# Prevent some illegal characters in part names
- for c in ['/', '\\', '|', '#', '$']:
+ for c in ['|', '#', '$']:
if c in str(value):
raise ValidationError(
_('Invalid character in part name')
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 08ca9e0eb5..041603f7fb 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -17,7 +17,7 @@ from django.views.generic.base import TemplateView
from part.models import Part
-from .forms import DeleteForm
+from .forms import DeleteForm, EditUserForm, SetPasswordForm
from .helpers import str2bool
from rest_framework import views
@@ -371,6 +371,59 @@ class AjaxDeleteView(AjaxMixin, UpdateView):
return self.renderJsonResponse(request, form, data=data, context=context)
+class EditUserView(AjaxUpdateView):
+ """ View for editing user information """
+
+ ajax_template_name = "modal_form.html"
+ ajax_form_title = "Edit User Information"
+ form_class = EditUserForm
+
+ def get_object(self):
+ return self.request.user
+
+
+class SetPasswordView(AjaxUpdateView):
+ """ View for setting user password """
+
+ ajax_template_name = "InvenTree/password.html"
+ ajax_form_title = "Set Password"
+ form_class = SetPasswordForm
+
+ def get_object(self):
+ return self.request.user
+
+ def post(self, request, *args, **kwargs):
+
+ form = self.get_form()
+
+ valid = form.is_valid()
+
+ p1 = request.POST.get('enter_password', '')
+ p2 = request.POST.get('confirm_password', '')
+
+ if valid:
+ # Passwords must match
+
+ if not p1 == p2:
+ error = 'Password fields must match'
+ form.errors['enter_password'] = [error]
+ form.errors['confirm_password'] = [error]
+
+ valid = False
+
+ data = {
+ 'form_valid': valid
+ }
+
+ if valid:
+ user = self.request.user
+
+ user.set_password(p1)
+ user.save()
+
+ return self.renderJsonResponse(request, form, data=data)
+
+
class IndexView(TemplateView):
""" View for InvenTree index page """
@@ -414,3 +467,10 @@ class SearchView(TemplateView):
context['query'] = query
return super(TemplateView, self).render_to_response(context)
+
+
+class SettingsView(TemplateView):
+ """ View for configuring User settings
+ """
+
+ template_name = "InvenTree/settings.html"
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index 66ec98ac77..01024230f4 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -58,6 +58,18 @@ class CompleteBuildForm(HelperForm):
]
+class CancelBuildForm(HelperForm):
+ """ Form for cancelling a build """
+
+ confirm_cancel = forms.BooleanField(required=False, help_text='Confirm build cancellation')
+
+ class Meta:
+ model = Build
+ fields = [
+ 'confirm_cancel'
+ ]
+
+
class EditBuildItemForm(HelperForm):
""" Form for adding a new BuildItem to a Build """
diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html
index f850d094b2..dc2160a006 100644
--- a/InvenTree/build/templates/build/auto_allocate.html
+++ b/InvenTree/build/templates/build/auto_allocate.html
@@ -22,8 +22,8 @@ Automatically allocate stock to this build?
-
-
+
+
|
diff --git a/InvenTree/build/templates/build/cancel.html b/InvenTree/build/templates/build/cancel.html
index d273a14ff5..d7e4d51b10 100644
--- a/InvenTree/build/templates/build/cancel.html
+++ b/InvenTree/build/templates/build/cancel.html
@@ -1,3 +1,7 @@
+{% extends "modal_form.html" %}
+
+{% block pre_form_content %}
+
Are you sure you wish to cancel this build?
-{% include "modal_csrf.html" %}
\ No newline at end of file
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 85d07858fb..7cef486d55 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -5,8 +5,6 @@ Django views for interacting with Build objects
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.shortcuts import get_object_or_404
-
from django.views.generic import DetailView, ListView
from django.forms import HiddenInput
@@ -15,7 +13,8 @@ from .models import Build, BuildItem
from . import forms
from stock.models import StockLocation, StockItem
-from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView
+from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
+from InvenTree.helpers import str2bool
class BuildIndex(ListView):
@@ -41,31 +40,41 @@ class BuildIndex(ListView):
return context
-class BuildCancel(AjaxView):
+class BuildCancel(AjaxUpdateView):
""" View to cancel a Build.
Provides a cancellation information dialog
"""
+
model = Build
ajax_template_name = 'build/cancel.html'
ajax_form_title = 'Cancel Build'
context_object_name = 'build'
- fields = []
+ form_class = forms.CancelBuildForm
def post(self, request, *args, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """
- build = get_object_or_404(Build, pk=self.kwargs['pk'])
+ build = self.get_object()
- build.cancelBuild(request.user)
+ form = self.get_form()
- return self.renderJsonResponse(request, None)
+ valid = form.is_valid()
- def get_data(self):
- """ Provide JSON context data. """
- return {
+ confirm = str2bool(request.POST.get('confirm_cancel', False))
+
+ if confirm:
+ build.cancelBuild(request.user)
+ else:
+ form.errors['confirm_cancel'] = ['Confirm build cancellation']
+ valid = False
+
+ data = {
+ 'form_valid': valid,
'danger': 'Build was cancelled'
}
+ return self.renderJsonResponse(request, form, data=data)
+
class BuildAutoAllocate(AjaxUpdateView):
""" View to auto-allocate parts for a build.
@@ -90,7 +99,7 @@ class BuildAutoAllocate(AjaxUpdateView):
context['build'] = build
context['allocations'] = build.getAutoAllocations()
except Build.DoesNotExist:
- context['error'] = 'No matching buidl found'
+ context['error'] = 'No matching build found'
return context
@@ -217,7 +226,7 @@ class BuildComplete(AjaxUpdateView):
form = self.get_form()
- confirm = request.POST.get('confirm', False)
+ confirm = str2bool(request.POST.get('confirm', False))
loc_id = request.POST.get('location', None)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 0a2bdbfef6..e671b49e4f 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -157,6 +157,7 @@ class PartList(generics.ListCreateAPIView):
'$name',
'description',
'$IPN',
+ 'keywords',
]
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 580ed737a4..88c6c11385 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -92,9 +92,10 @@ class EditPartForm(HelperForm):
'confirm_creation',
'category',
'name',
+ 'IPN',
'variant',
'description',
- 'IPN',
+ 'keywords',
'URL',
'default_location',
'default_supplier',
@@ -118,7 +119,8 @@ class EditCategoryForm(HelperForm):
'parent',
'name',
'description',
- 'default_location'
+ 'default_location',
+ 'default_keywords',
]
diff --git a/InvenTree/part/migrations/0023_part_keywords.py b/InvenTree/part/migrations/0023_part_keywords.py
new file mode 100644
index 0000000000..4752d80740
--- /dev/null
+++ b/InvenTree/part/migrations/0023_part_keywords.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-05-14 07:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0022_auto_20190512_1246'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='part',
+ name='keywords',
+ field=models.CharField(blank=True, help_text='Part keywords to improve visibility in search results', max_length=250),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0024_partcategory_default_keywords.py b/InvenTree/part/migrations/0024_partcategory_default_keywords.py
new file mode 100644
index 0000000000..317d982f7d
--- /dev/null
+++ b/InvenTree/part/migrations/0024_partcategory_default_keywords.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-05-14 07:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0023_part_keywords'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='partcategory',
+ name='default_keywords',
+ field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 5a609c0e59..3aeb94700a 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -37,6 +37,12 @@ from company.models import Company
class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects.
+
+ Attributes:
+ name: Name of this category
+ parent: Parent category
+ default_location: Default storage location for parts in this category or child categories
+ default_keywords: Default keywords for parts created in this category
"""
default_location = models.ForeignKey(
@@ -46,6 +52,8 @@ class PartCategory(InvenTreeTree):
help_text='Default location for parts in this category'
)
+ default_keywords = models.CharField(blank=True, max_length=250, help_text='Default keywords for parts in this category')
+
def get_absolute_url(self):
return reverse('category-detail', kwargs={'pk': self.id})
@@ -179,8 +187,9 @@ class Part(models.Model):
Attributes:
name: Brief name for this part
variant: Optional variant number for this part - Must be unique for the part name
- description: Longer form description of the part
category: The PartCategory to which this part belongs
+ description: Longer form description of the part
+ keywords: Optional keywords for improving part search results
IPN: Internal part number (optional)
URL: Link to an external page with more information about this part (e.g. internal Wiki)
image: Image of this part
@@ -250,6 +259,8 @@ class Part(models.Model):
description = models.CharField(max_length=250, blank=False, help_text='Part description')
+ keywords = models.CharField(max_length=250, blank=True, help_text='Part keywords to improve visibility in search results')
+
category = models.ForeignKey(PartCategory, related_name='parts',
null=True, blank=True,
on_delete=models.DO_NOTHING,
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 957bfa5951..37ccb639a0 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -62,15 +62,16 @@ class PartSerializer(serializers.ModelSerializer):
fields = [
'pk',
'url', # Link to the part detail page
- 'full_name',
- 'name',
- 'variant',
- 'image_url',
- 'IPN',
- 'URL', # Link to an external URL (optional)
- 'description',
'category',
'category_name',
+ 'image_url',
+ 'full_name',
+ 'name',
+ 'IPN',
+ 'variant',
+ 'description',
+ 'keywords',
+ 'URL',
'total_stock',
'available_stock',
'units',
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index c0f823ce13..a7bad6cae4 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -35,16 +35,22 @@
| Part name |
{{ part.full_name }} |
-
- Description |
- {{ part.description }} |
-
{% if part.IPN %}
IPN |
{{ part.IPN }} |
{% endif %}
+
+ Description |
+ {{ part.description }} |
+
+ {% if part.keywords %}
+
+ Keywords |
+ {{ part.keywords }} |
+
+ {% endif %}
{% if part.URL %}
URL |
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 8650b85fb4..fef88197f7 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -339,7 +339,9 @@ class PartCreate(AjaxCreateView):
if self.get_category_id():
try:
- initials['category'] = PartCategory.objects.get(pk=self.get_category_id())
+ category = PartCategory.objects.get(pk=self.get_category_id())
+ initials['category'] = category
+ initials['keywords'] = category.default_keywords
except PartCategory.DoesNotExist:
pass
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js
index eaa1d40d3d..46bdc249ef 100644
--- a/InvenTree/static/script/inventree/bom.js
+++ b/InvenTree/static/script/inventree/bom.js
@@ -89,11 +89,11 @@ function loadBomTable(table, options) {
// Part column
cols.push(
{
- field: 'sub_part_detail',
+ field: 'sub_part_detail.full_name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
- return imageHoverIcon(value.image_url) + renderLink(value.full_name, value.url);
+ return imageHoverIcon(row.sub_part_detail.image_url) + renderLink(row.sub_part_detail.full_name, row.sub_part_detail.url);
}
}
);
@@ -115,6 +115,34 @@ function loadBomTable(table, options) {
sortable: true,
}
);
+
+ if (!options.editable) {
+ cols.push(
+ {
+ field: 'sub_part_detail.available_stock',
+ title: 'Available',
+ searchable: false,
+ sortable: true,
+ formatter: function(value, row, index, field) {
+ var text = "";
+
+ if (row.quantity < row.sub_part_detail.available_stock)
+ {
+ text = "" + value + "";
+ }
+ else
+ {
+ if (!value) {
+ value = 'No Stock';
+ }
+ text = "" + value + "";
+ }
+
+ return renderLink(text, row.sub_part_detail.url + "stock/");
+ }
+ }
+ );
+ }
// Part notes
cols.push(
@@ -137,31 +165,6 @@ function loadBomTable(table, options) {
});
}
- else {
- cols.push(
- {
- field: 'sub_part_detail.available_stock',
- title: 'Available',
- searchable: false,
- sortable: true,
- formatter: function(value, row, index, field) {
- var text = "";
-
- if (row.quantity < row.sub_part_detail.available_stock)
- {
- text = "" + value + "";
- }
- else
- {
- text = "" + value + "";
- }
-
- return renderLink(text, row.sub_part.url + "stock/");
- }
- }
- );
- }
-
// Configure the table (bootstrap-table)
table.bootstrapTable({
@@ -172,6 +175,7 @@ function loadBomTable(table, options) {
queryParams: function(p) {
return {
part: options.parent_id,
+ ordering: 'name',
}
},
columns: cols,
diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js
index c66fe405b2..51d86f1c7a 100644
--- a/InvenTree/static/script/inventree/part.js
+++ b/InvenTree/static/script/inventree/part.js
@@ -119,13 +119,12 @@ function loadPartTable(table, url, options={}) {
visible: false,
},
{
- field: 'name',
+ field: 'full_name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
- var name = row.full_name;
- var display = imageHoverIcon(row.image_url) + renderLink(name, row.url);
+ var display = imageHoverIcon(row.image_url) + renderLink(value, row.url);
if (!row.active) {
display = display + "INACTIVE";
}
@@ -160,7 +159,7 @@ function loadPartTable(table, url, options={}) {
return renderLink(value, row.url + 'stock/');
}
else {
- return "No stock";
+ return "No Stock";
}
}
}
diff --git a/InvenTree/templates/InvenTree/password.html b/InvenTree/templates/InvenTree/password.html
new file mode 100644
index 0000000000..d109a09a7a
--- /dev/null
+++ b/InvenTree/templates/InvenTree/password.html
@@ -0,0 +1,7 @@
+{% extends "modal_form.html" %}
+
+{% block pre_form_content %}
+
+{{ block.super }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/InvenTree/settings.html b/InvenTree/templates/InvenTree/settings.html
new file mode 100644
index 0000000000..2a2bbcc144
--- /dev/null
+++ b/InvenTree/templates/InvenTree/settings.html
@@ -0,0 +1,66 @@
+{% extends "base.html" %}
+
+{% block page_title %}
+InvenTree | Settings
+{% endblock %}
+
+{% block content %}
+InvenTree Settings
+
+
+
+
+
User Information
+
+
+
+
+
+
+
+ First Name |
+ {{ user.first_name }} |
+
+
+ Last Name |
+ {{ user.last_name }} |
+
+
+ Email Address |
+ {{ user.email }} |
+
+
+
+{% endblock %}
+
+{% block js_load %}
+{{ block.super }}
+{% endblock %}
+
+{% block js_ready %}
+{{ block.super }}
+
+ $("#edit-user").on('click', function() {
+ launchModalForm(
+ "{% url 'edit-user' %}",
+ {
+ reload: true,
+ }
+ );
+ });
+
+ $("#edit-password").on('click', function() {
+ launchModalForm(
+ "{% url 'set-password' %}",
+ {
+ reload: true,
+ }
+ );
+ });
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html
index 53a7bb4894..5cb71d13ac 100644
--- a/InvenTree/templates/navbar.html
+++ b/InvenTree/templates/navbar.html
@@ -19,7 +19,9 @@
{% if user.is_authenticated %}
{% if user.is_staff %}
Admin
+
{% endif %}
+ Settings
Logout
{% else %}
Login