mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
merge
This commit is contained in:
commit
2835022330
1
.github/workflows/html.yaml
vendored
1
.github/workflows/html.yaml
vendored
@ -43,7 +43,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm install markuplint
|
npm install markuplint
|
||||||
npx markuplint InvenTree/build/templates/build/*.html
|
npx markuplint InvenTree/build/templates/build/*.html
|
||||||
npx markuplint InvenTree/common/templates/common/*.html
|
|
||||||
npx markuplint InvenTree/company/templates/company/*.html
|
npx markuplint InvenTree/company/templates/company/*.html
|
||||||
npx markuplint InvenTree/order/templates/order/*.html
|
npx markuplint InvenTree/order/templates/order/*.html
|
||||||
npx markuplint InvenTree/part/templates/part/*.html
|
npx markuplint InvenTree/part/templates/part/*.html
|
||||||
|
@ -108,6 +108,13 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
model_fields = model_meta.get_field_info(model_class)
|
model_fields = model_meta.get_field_info(model_class)
|
||||||
|
|
||||||
|
model_default_func = getattr(model_class, 'api_defaults', None)
|
||||||
|
|
||||||
|
if model_default_func:
|
||||||
|
model_default_values = model_class.api_defaults(self.request)
|
||||||
|
else:
|
||||||
|
model_default_values = {}
|
||||||
|
|
||||||
# Iterate through simple fields
|
# Iterate through simple fields
|
||||||
for name, field in model_fields.fields.items():
|
for name, field in model_fields.fields.items():
|
||||||
|
|
||||||
@ -123,6 +130,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
serializer_info[name]['default'] = default
|
serializer_info[name]['default'] = default
|
||||||
|
|
||||||
|
elif name in model_default_values:
|
||||||
|
serializer_info[name]['default'] = model_default_values[name]
|
||||||
|
|
||||||
# Iterate through relations
|
# Iterate through relations
|
||||||
for name, relation in model_fields.relations.items():
|
for name, relation in model_fields.relations.items():
|
||||||
|
|
||||||
@ -141,6 +151,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
|
if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'):
|
||||||
serializer_info[name]['help_text'] = relation.model_field.help_text
|
serializer_info[name]['help_text'] = relation.model_field.help_text
|
||||||
|
|
||||||
|
if name in model_default_values:
|
||||||
|
serializer_info[name]['default'] = model_default_values[name]
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -34,8 +34,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
padding-right: 30px;
|
margin-right: 5px;
|
||||||
margin-right: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-container input {
|
.login-container input {
|
||||||
@ -125,15 +124,14 @@
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-container {
|
|
||||||
width: 100%;
|
|
||||||
align-content: center;
|
|
||||||
object-fit: fill;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
|
box-shadow: 0px 5px 5px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventree-navbar-menu {
|
||||||
|
position: absolute !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@ -544,6 +542,7 @@
|
|||||||
.inventree-body {
|
.inventree-body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventree-pre-content {
|
.inventree-pre-content {
|
||||||
@ -829,11 +828,12 @@ input[type="submit"] {
|
|||||||
color: var(--bs-body-color);
|
color: var(--bs-body-color);
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0px 5px 5px rgb(0 0 0 / 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
box-shadow: 2px 2px #DDD;
|
box-shadow: 2px 2px #DDD;
|
||||||
margin-bottom: 20px;
|
margin-bottom: .75rem;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,6 @@ from .views import CurrencyRefreshView
|
|||||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
|
|
||||||
from common.views import SettingEdit, UserSettingEdit
|
|
||||||
|
|
||||||
from .api import InfoView, NotFoundView
|
from .api import InfoView, NotFoundView
|
||||||
from .api import ActionPluginView
|
from .api import ActionPluginView
|
||||||
|
|
||||||
@ -53,7 +51,7 @@ admin.site.site_header = "InvenTree Admin"
|
|||||||
|
|
||||||
apipatterns = [
|
apipatterns = [
|
||||||
url(r'^barcode/', include(barcode_api_urls)),
|
url(r'^barcode/', include(barcode_api_urls)),
|
||||||
url(r'^common/', include(common_api_urls)),
|
url(r'^settings/', include(common_api_urls)),
|
||||||
url(r'^part/', include(part_api_urls)),
|
url(r'^part/', include(part_api_urls)),
|
||||||
url(r'^bom/', include(bom_api_urls)),
|
url(r'^bom/', include(bom_api_urls)),
|
||||||
url(r'^company/', include(company_api_urls)),
|
url(r'^company/', include(company_api_urls)),
|
||||||
@ -85,9 +83,6 @@ settings_urls = [
|
|||||||
|
|
||||||
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
|
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
|
|
||||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
|
||||||
|
|
||||||
# Catch any other urls
|
# Catch any other urls
|
||||||
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
|
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
|
||||||
]
|
]
|
||||||
|
@ -12,11 +12,15 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 16
|
INVENTREE_API_VERSION = 17
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v17 -> 2021-11-09
|
||||||
|
- Adds API endpoints for GLOBAL and USER settings objects
|
||||||
|
- Ref: https://github.com/inventree/InvenTree/pull/2275
|
||||||
|
|
||||||
v16 -> 2021-10-17
|
v16 -> 2021-10-17
|
||||||
- Adds API endpoint for completing build order outputs
|
- Adds API endpoint for completing build order outputs
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ def get_next_build_number():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if Build.objects.count() == 0:
|
if Build.objects.count() == 0:
|
||||||
return
|
return '0001'
|
||||||
|
|
||||||
build = Build.objects.exclude(reference=None).last()
|
build = Build.objects.exclude(reference=None).last()
|
||||||
|
|
||||||
@ -107,6 +107,21 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def api_defaults(cls, request):
|
||||||
|
"""
|
||||||
|
Return default values for this model when issuing an API OPTIONS request
|
||||||
|
"""
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'reference': get_next_build_number(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if request and request.user:
|
||||||
|
defaults['issued_by'] = request.user.pk
|
||||||
|
|
||||||
|
return defaults
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
self.rebuild_reference_field()
|
self.rebuild_reference_field()
|
||||||
|
@ -5,5 +5,149 @@ Provides a JSON API for common components.
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
common_api_urls = [
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework import filters, generics, permissions
|
||||||
|
|
||||||
|
import common.models
|
||||||
|
import common.serializers
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsList(generics.ListAPIView):
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
filters.SearchFilter,
|
||||||
|
filters.OrderingFilter,
|
||||||
|
]
|
||||||
|
|
||||||
|
ordering_fields = [
|
||||||
|
'pk',
|
||||||
|
'key',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'key',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalSettingsList(SettingsList):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing a list of global settings objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = common.models.InvenTreeSetting.objects.all()
|
||||||
|
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalSettingsPermissions(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Special permission class to determine if the user is "staff"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
"""
|
||||||
|
Check that the requesting user is 'admin'
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
return user.is_staff
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
Detail view for an individual "global setting" object.
|
||||||
|
|
||||||
|
- User must have 'staff' status to view / edit
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = common.models.InvenTreeSetting.objects.all()
|
||||||
|
serializer_class = common.serializers.GlobalSettingsSerializer
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
GlobalSettingsPermissions,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsList(SettingsList):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing a list of user settings objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||||
|
serializer_class = common.serializers.UserSettingsSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Only list settings which apply to the current user
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = self.request.user
|
||||||
|
except AttributeError:
|
||||||
|
return common.models.InvenTreeUserSetting.objects.none()
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
queryset = queryset.filter(user=user)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsPermissions(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Special permission class to determine if the user can view / edit a particular setting
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return user == obj.user
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
||||||
|
"""
|
||||||
|
Detail view for an individual "user setting" object
|
||||||
|
|
||||||
|
- User can only view / edit settings their own settings objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||||
|
serializer_class = common.serializers.UserSettingsSerializer
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
UserSettingsPermissions,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
common_api_urls = [
|
||||||
|
|
||||||
|
# User settings
|
||||||
|
url(r'^user/', include([
|
||||||
|
# User Settings Detail
|
||||||
|
url(r'^(?P<pk>\d+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
|
||||||
|
|
||||||
|
# User Settings List
|
||||||
|
url(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
|
# Global settings
|
||||||
|
url(r'^global/', include([
|
||||||
|
# Global Settings Detail
|
||||||
|
url(r'^(?P<pk>\d+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
|
||||||
|
|
||||||
|
# Global Settings List
|
||||||
|
url(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
|
||||||
|
]))
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -34,6 +34,19 @@ import logging
|
|||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyURLValidator(URLValidator):
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
|
||||||
|
value = str(value).strip()
|
||||||
|
|
||||||
|
if len(value) == 0:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
super().__call__(value)
|
||||||
|
|
||||||
|
|
||||||
class BaseInvenTreeSetting(models.Model):
|
class BaseInvenTreeSetting(models.Model):
|
||||||
"""
|
"""
|
||||||
An base InvenTreeSetting object is a key:value pair used for storing
|
An base InvenTreeSetting object is a key:value pair used for storing
|
||||||
@ -45,6 +58,16 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Enforce validation and clean before saving
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.clean()
|
||||||
|
self.validate_unique()
|
||||||
|
|
||||||
|
super().save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allValues(cls, user=None):
|
def allValues(cls, user=None):
|
||||||
"""
|
"""
|
||||||
@ -343,6 +366,11 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
except (ValueError):
|
except (ValueError):
|
||||||
raise ValidationError(_('Must be an integer value'))
|
raise ValidationError(_('Must be an integer value'))
|
||||||
|
|
||||||
|
options = self.valid_options()
|
||||||
|
|
||||||
|
if options and self.value not in options:
|
||||||
|
raise ValidationError(_("Chosen value is not a valid option"))
|
||||||
|
|
||||||
if validator is not None:
|
if validator is not None:
|
||||||
self.run_validator(validator)
|
self.run_validator(validator)
|
||||||
|
|
||||||
@ -409,6 +437,18 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return self.__class__.get_setting_choices(self.key)
|
return self.__class__.get_setting_choices(self.key)
|
||||||
|
|
||||||
|
def valid_options(self):
|
||||||
|
"""
|
||||||
|
Return a list of valid options for this setting
|
||||||
|
"""
|
||||||
|
|
||||||
|
choices = self.choices()
|
||||||
|
|
||||||
|
if not choices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return [opt[0] for opt in choices]
|
||||||
|
|
||||||
def is_bool(self):
|
def is_bool(self):
|
||||||
"""
|
"""
|
||||||
Check if this setting is required to be a boolean value
|
Check if this setting is required to be a boolean value
|
||||||
@ -427,6 +467,20 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return InvenTree.helpers.str2bool(self.value)
|
return InvenTree.helpers.str2bool(self.value)
|
||||||
|
|
||||||
|
def setting_type(self):
|
||||||
|
"""
|
||||||
|
Return the field type identifier for this setting object
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.is_bool():
|
||||||
|
return 'boolean'
|
||||||
|
|
||||||
|
elif self.is_int():
|
||||||
|
return 'integer'
|
||||||
|
|
||||||
|
else:
|
||||||
|
return 'string'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validator_is_bool(cls, validator):
|
def validator_is_bool(cls, validator):
|
||||||
|
|
||||||
@ -531,7 +585,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'INVENTREE_BASE_URL': {
|
'INVENTREE_BASE_URL': {
|
||||||
'name': _('Base URL'),
|
'name': _('Base URL'),
|
||||||
'description': _('Base URL for server instance'),
|
'description': _('Base URL for server instance'),
|
||||||
'validator': URLValidator(),
|
'validator': EmptyURLValidator(),
|
||||||
'default': '',
|
'default': '',
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -850,7 +904,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
},
|
},
|
||||||
'SIGNUP_GROUP': {
|
'SIGNUP_GROUP': {
|
||||||
'name': _('Group on signup'),
|
'name': _('Group on signup'),
|
||||||
'description': _('Group new user are asigned on registration'),
|
'description': _('Group to which new users are assigned on registration'),
|
||||||
'default': '',
|
'default': '',
|
||||||
'choices': settings_group_options
|
'choices': settings_group_options
|
||||||
},
|
},
|
||||||
@ -873,6 +927,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
help_text=_('Settings key (must be unique - case insensitive'),
|
help_text=_('Settings key (must be unique - case insensitive'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_native_value(self):
|
||||||
|
"""
|
||||||
|
Return the "pythonic" value,
|
||||||
|
e.g. convert "True" to True, and "1" to 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__class__.get_setting(self.key)
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeUserSetting(BaseInvenTreeSetting):
|
class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||||
"""
|
"""
|
||||||
@ -1083,6 +1145,14 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'user__id': kwargs['user'].id
|
'user__id': kwargs['user'].id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_native_value(self):
|
||||||
|
"""
|
||||||
|
Return the "pythonic" value,
|
||||||
|
e.g. convert "True" to True, and "1" to 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.__class__.get_setting(self.key, user=self.user)
|
||||||
|
|
||||||
|
|
||||||
class PriceBreak(models.Model):
|
class PriceBreak(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,85 @@
|
|||||||
"""
|
"""
|
||||||
JSON serializers for common components
|
JSON serializers for common components
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from InvenTree.serializers import InvenTreeModelSerializer
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Base serializer for a settings object
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
name = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
description = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
type = serializers.CharField(source='setting_type', read_only=True)
|
||||||
|
|
||||||
|
choices = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_choices(self, obj):
|
||||||
|
"""
|
||||||
|
Returns the choices available for a given item
|
||||||
|
"""
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
choices = obj.choices()
|
||||||
|
|
||||||
|
if choices:
|
||||||
|
for choice in choices:
|
||||||
|
results.append({
|
||||||
|
'value': choice[0],
|
||||||
|
'display_name': choice[1],
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalSettingsSerializer(SettingsSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for the InvenTreeSetting model
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InvenTreeSetting
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'type',
|
||||||
|
'choices',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettingsSerializer(SettingsSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for the InvenTreeUserSetting model
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InvenTreeUserSetting
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'user',
|
||||||
|
'type',
|
||||||
|
'choices',
|
||||||
|
]
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
{% extends "modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block pre_form_content %}
|
|
||||||
|
|
||||||
{{ block.super }}
|
|
||||||
<!--
|
|
||||||
<p>
|
|
||||||
<strong>{{ name }}</strong><br>
|
|
||||||
{{ description }}<br>
|
|
||||||
<em>{% trans "Current value" %}: {{ value }}</em>
|
|
||||||
</p>
|
|
||||||
-->
|
|
||||||
{% endblock %}
|
|
@ -4,156 +4,3 @@ Unit tests for the views associated with the 'common' app
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsViewTest(TestCase):
|
|
||||||
"""
|
|
||||||
Tests for the settings management views
|
|
||||||
"""
|
|
||||||
|
|
||||||
fixtures = [
|
|
||||||
'settings',
|
|
||||||
]
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
# Create a user (required to access the views / forms)
|
|
||||||
self.user = get_user_model().objects.create_user(
|
|
||||||
username='username',
|
|
||||||
email='me@email.com',
|
|
||||||
password='password',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.login(username='username', password='password')
|
|
||||||
|
|
||||||
def get_url(self, pk):
|
|
||||||
return reverse('setting-edit', args=(pk,))
|
|
||||||
|
|
||||||
def get_setting(self, title):
|
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting_object(title)
|
|
||||||
|
|
||||||
def get(self, url, status=200):
|
|
||||||
|
|
||||||
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status)
|
|
||||||
|
|
||||||
data = json.loads(response.content)
|
|
||||||
|
|
||||||
return response, data
|
|
||||||
|
|
||||||
def post(self, url, data, valid=None):
|
|
||||||
|
|
||||||
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
|
|
||||||
json_data = json.loads(response.content)
|
|
||||||
|
|
||||||
# If a particular status code is required
|
|
||||||
if valid is not None:
|
|
||||||
if valid:
|
|
||||||
self.assertEqual(json_data['form_valid'], True)
|
|
||||||
else:
|
|
||||||
self.assertEqual(json_data['form_valid'], False)
|
|
||||||
|
|
||||||
form_errors = json.loads(json_data['form_errors'])
|
|
||||||
|
|
||||||
return json_data, form_errors
|
|
||||||
|
|
||||||
def test_instance_name(self):
|
|
||||||
"""
|
|
||||||
Test that we can get the settings view for particular setting objects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Start with something basic - load the settings view for INVENTREE_INSTANCE
|
|
||||||
setting = self.get_setting('INVENTREE_INSTANCE')
|
|
||||||
|
|
||||||
self.assertIsNotNone(setting)
|
|
||||||
self.assertEqual(setting.value, 'My very first InvenTree Instance')
|
|
||||||
|
|
||||||
url = self.get_url(setting.pk)
|
|
||||||
|
|
||||||
self.get(url)
|
|
||||||
|
|
||||||
new_name = 'A new instance name!'
|
|
||||||
|
|
||||||
# Change the instance name via the form
|
|
||||||
data, errors = self.post(url, {'value': new_name}, valid=True)
|
|
||||||
|
|
||||||
name = InvenTreeSetting.get_setting('INVENTREE_INSTANCE')
|
|
||||||
|
|
||||||
self.assertEqual(name, new_name)
|
|
||||||
|
|
||||||
def test_choices(self):
|
|
||||||
"""
|
|
||||||
Tests for a setting which has choices
|
|
||||||
"""
|
|
||||||
|
|
||||||
setting = InvenTreeSetting.get_setting_object('PURCHASEORDER_REFERENCE_PREFIX')
|
|
||||||
|
|
||||||
# Default value!
|
|
||||||
self.assertEqual(setting.value, 'PO')
|
|
||||||
|
|
||||||
url = self.get_url(setting.pk)
|
|
||||||
|
|
||||||
# Try posting an invalid currency option
|
|
||||||
data, errors = self.post(url, {'value': 'Purchase Order'}, valid=True)
|
|
||||||
|
|
||||||
def test_binary_values(self):
|
|
||||||
"""
|
|
||||||
Test for binary value
|
|
||||||
"""
|
|
||||||
|
|
||||||
setting = InvenTreeSetting.get_setting_object('PART_COMPONENT')
|
|
||||||
|
|
||||||
self.assertTrue(setting.as_bool())
|
|
||||||
|
|
||||||
url = self.get_url(setting.pk)
|
|
||||||
|
|
||||||
setting.value = True
|
|
||||||
setting.save()
|
|
||||||
|
|
||||||
# Try posting some invalid values
|
|
||||||
# The value should be "cleaned" and stay the same
|
|
||||||
for value in ['', 'abc', 'cat', 'TRUETRUETRUE']:
|
|
||||||
self.post(url, {'value': value}, valid=True)
|
|
||||||
|
|
||||||
# Try posting some valid (True) values
|
|
||||||
for value in [True, 'True', '1', 'yes']:
|
|
||||||
self.post(url, {'value': value}, valid=True)
|
|
||||||
self.assertTrue(InvenTreeSetting.get_setting('PART_COMPONENT'))
|
|
||||||
|
|
||||||
# Try posting some valid (False) values
|
|
||||||
for value in [False, 'False']:
|
|
||||||
self.post(url, {'value': value}, valid=True)
|
|
||||||
self.assertFalse(InvenTreeSetting.get_setting('PART_COMPONENT'))
|
|
||||||
|
|
||||||
def test_part_name_format(self):
|
|
||||||
"""
|
|
||||||
Try posting some valid and invalid name formats for PART_NAME_FORMAT
|
|
||||||
"""
|
|
||||||
setting = InvenTreeSetting.get_setting_object('PART_NAME_FORMAT')
|
|
||||||
|
|
||||||
# test default value
|
|
||||||
self.assertEqual(setting.value, "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}"
|
|
||||||
"{{ ' | ' if part.revision }}{{ part.revision if part.revision }}")
|
|
||||||
|
|
||||||
url = self.get_url(setting.pk)
|
|
||||||
|
|
||||||
# Try posting an invalid part name format
|
|
||||||
invalid_values = ['{{asset.IPN}}', '{{part}}', '{{"|"}}', '{{part.falcon}}']
|
|
||||||
for invalid_value in invalid_values:
|
|
||||||
self.post(url, {'value': invalid_value}, valid=False)
|
|
||||||
|
|
||||||
# try posting valid value
|
|
||||||
new_format = "{{ part.name if part.name }} {{ ' with revision ' if part.revision }} {{ part.revision }}"
|
|
||||||
self.post(url, {'value': new_format}, valid=True)
|
|
||||||
|
@ -8,138 +8,18 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.forms import CheckboxInput, Select
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
from formtools.wizard.views import SessionWizardView
|
from formtools.wizard.views import SessionWizardView
|
||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
|
|
||||||
from InvenTree.views import AjaxUpdateView, AjaxView
|
from InvenTree.views import AjaxView
|
||||||
from InvenTree.helpers import str2bool
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
from . import forms
|
from . import forms
|
||||||
from .files import FileManager
|
from .files import FileManager
|
||||||
|
|
||||||
|
|
||||||
class SettingEdit(AjaxUpdateView):
|
|
||||||
"""
|
|
||||||
View for editing an InvenTree key:value settings object,
|
|
||||||
(or creating it if the key does not already exist)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = models.InvenTreeSetting
|
|
||||||
ajax_form_title = _('Change Setting')
|
|
||||||
form_class = forms.SettingEditForm
|
|
||||||
ajax_template_name = "common/edit_setting.html"
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Add extra context information about the particular setting object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ctx = super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
setting = self.get_object()
|
|
||||||
|
|
||||||
ctx['key'] = setting.key
|
|
||||||
ctx['value'] = setting.value
|
|
||||||
ctx['name'] = self.model.get_setting_name(setting.key)
|
|
||||||
ctx['description'] = self.model.get_setting_description(setting.key)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
"""
|
|
||||||
Custom data to return to the client after POST success
|
|
||||||
"""
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
setting = self.get_object()
|
|
||||||
|
|
||||||
data['pk'] = setting.pk
|
|
||||||
data['key'] = setting.key
|
|
||||||
data['value'] = setting.value
|
|
||||||
data['is_bool'] = setting.is_bool()
|
|
||||||
data['is_int'] = setting.is_int()
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
"""
|
|
||||||
Override default get_form behaviour
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = super().get_form()
|
|
||||||
|
|
||||||
setting = self.get_object()
|
|
||||||
|
|
||||||
choices = setting.choices()
|
|
||||||
|
|
||||||
if choices is not None:
|
|
||||||
form.fields['value'].widget = Select(choices=choices)
|
|
||||||
elif setting.is_bool():
|
|
||||||
form.fields['value'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
self.object.value = str2bool(setting.value)
|
|
||||||
form.fields['value'].value = str2bool(setting.value)
|
|
||||||
|
|
||||||
name = self.model.get_setting_name(setting.key)
|
|
||||||
|
|
||||||
if name:
|
|
||||||
form.fields['value'].label = name
|
|
||||||
|
|
||||||
description = self.model.get_setting_description(setting.key)
|
|
||||||
|
|
||||||
if description:
|
|
||||||
form.fields['value'].help_text = description
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def validate(self, setting, form):
|
|
||||||
"""
|
|
||||||
Perform custom validation checks on the form data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
data = form.cleaned_data
|
|
||||||
|
|
||||||
value = data.get('value', None)
|
|
||||||
|
|
||||||
if setting.choices():
|
|
||||||
"""
|
|
||||||
If a set of choices are provided for a given setting,
|
|
||||||
the provided value must be one of those choices.
|
|
||||||
"""
|
|
||||||
|
|
||||||
choices = [choice[0] for choice in setting.choices()]
|
|
||||||
|
|
||||||
if value not in choices:
|
|
||||||
form.add_error('value', _('Supplied value is not allowed'))
|
|
||||||
|
|
||||||
if setting.is_bool():
|
|
||||||
"""
|
|
||||||
If a setting is defined as a boolean setting,
|
|
||||||
the provided value must look somewhat like a boolean value!
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not str2bool(value, test=True) and not str2bool(value, test=False):
|
|
||||||
form.add_error('value', _('Supplied value must be a boolean'))
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettingEdit(SettingEdit):
|
|
||||||
"""
|
|
||||||
View for editing an InvenTree key:value user settings object,
|
|
||||||
(or creating it if the key does not already exist)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = models.InvenTreeUserSetting
|
|
||||||
ajax_form_title = _('Change User Setting')
|
|
||||||
form_class = forms.SettingEditForm
|
|
||||||
ajax_template_name = "common/edit_setting.html"
|
|
||||||
|
|
||||||
|
|
||||||
class MultiStepFormView(SessionWizardView):
|
class MultiStepFormView(SessionWizardView):
|
||||||
""" Setup basic methods of multi-step form
|
""" Setup basic methods of multi-step form
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ def get_next_po_number():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if PurchaseOrder.objects.count() == 0:
|
if PurchaseOrder.objects.count() == 0:
|
||||||
return
|
return '0001'
|
||||||
|
|
||||||
order = PurchaseOrder.objects.exclude(reference=None).last()
|
order = PurchaseOrder.objects.exclude(reference=None).last()
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ def get_next_so_number():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if SalesOrder.objects.count() == 0:
|
if SalesOrder.objects.count() == 0:
|
||||||
return
|
return '0001'
|
||||||
|
|
||||||
order = SalesOrder.objects.exclude(reference=None).last()
|
order = SalesOrder.objects.exclude(reference=None).last()
|
||||||
|
|
||||||
|
@ -242,6 +242,7 @@ class POLineItemReceiveSerializer(serializers.Serializer):
|
|||||||
help_text=_('Unique identifier field'),
|
help_text=_('Unique identifier field'),
|
||||||
default='',
|
default='',
|
||||||
required=False,
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
{% block thumbnail %}
|
{% block thumbnail %}
|
||||||
<img class='part-thumb'
|
<img class='part-thumb'
|
||||||
{% if order.customer.image %}
|
{% if order.customer and order.customer.image %}
|
||||||
src="{{ order.customer.image.url }}"
|
src="{{ order.customer.image.url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
src="{% static 'img/blank_image.png' %}"
|
src="{% static 'img/blank_image.png' %}"
|
||||||
@ -106,11 +106,13 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if order.customer %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-building'></span></td>
|
<td><span class='fas fa-building'></span></td>
|
||||||
<td>{% trans "Customer" %}</td>
|
<td>{% trans "Customer" %}</td>
|
||||||
<td><a href="{% url 'company-detail' order.customer.id %}">{{ order.customer.name }}</a>{% include "clip.html"%}</td>
|
<td><a href="{% url 'company-detail' order.customer.id %}">{{ order.customer.name }}</a>{% include "clip.html"%}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if order.customer_reference %}
|
{% if order.customer_reference %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
|
@ -349,6 +349,31 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
# No new stock items have been created
|
# No new stock items have been created
|
||||||
self.assertEqual(self.n, StockItem.objects.count())
|
self.assertEqual(self.n, StockItem.objects.count())
|
||||||
|
|
||||||
|
def test_null_barcode(self):
|
||||||
|
"""
|
||||||
|
Test than a "null" barcode field can be provided
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set stock item barcode
|
||||||
|
item = StockItem.objects.get(pk=1)
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Test with "null" value
|
||||||
|
self.post(
|
||||||
|
self.url,
|
||||||
|
{
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'line_item': 1,
|
||||||
|
'quantity': 50,
|
||||||
|
'barcode': None,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'location': 1,
|
||||||
|
},
|
||||||
|
expected_code=201
|
||||||
|
)
|
||||||
|
|
||||||
def test_invalid_barcodes(self):
|
def test_invalid_barcodes(self):
|
||||||
"""
|
"""
|
||||||
Tests for checking in items with invalid barcodes:
|
Tests for checking in items with invalid barcodes:
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<span class='fas fa-bell icon-green'></span>
|
<span class='fas fa-bell icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "Subscribe to nofications for this category" %}'>
|
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "Subscribe to notifications for this category" %}'>
|
||||||
<span id='category-star-icon' class='fa fa-bell-slash'/>
|
<span id='category-star-icon' class='fa fa-bell-slash'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if starred_directly %}
|
{% if starred_directly %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "You are subscribed to nofications for this part" %}'>
|
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "You are subscribed to notifications for this part" %}'>
|
||||||
<span id='part-star-icon' class='fas fa-bell icon-green'/>
|
<span id='part-star-icon' class='fas fa-bell icon-green'/>
|
||||||
</button>
|
</button>
|
||||||
{% elif starred %}
|
{% elif starred %}
|
||||||
@ -32,7 +32,7 @@
|
|||||||
<span class='fas fa-bell icon-green'></span>
|
<span class='fas fa-bell icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "Subscribe to nofications for this part" %}'>
|
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "Subscribe to notifications for this part" %}'>
|
||||||
<span id='part-star-icon' class='fa fa-bell-slash'/>
|
<span id='part-star-icon' class='fa fa-bell-slash'/>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -257,7 +257,6 @@ class ReportPrintMixin:
|
|||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
|
||||||
|
|
||||||
if len(outputs) > 1:
|
if len(outputs) > 1:
|
||||||
# If more than one output is generated, merge them into a single file
|
# If more than one output is generated, merge them into a single file
|
||||||
@ -265,6 +264,8 @@ class ReportPrintMixin:
|
|||||||
doc = output.get_document()
|
doc = output.get_document()
|
||||||
for page in doc.pages:
|
for page in doc.pages:
|
||||||
pages.append(page)
|
pages.append(page)
|
||||||
|
|
||||||
|
pdf = outputs[0].get_document().copy(pages).write_pdf()
|
||||||
else:
|
else:
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
@ -53,6 +53,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Stock adjustment menu -->
|
<!-- Stock adjustment menu -->
|
||||||
<!-- Check permissions and owner -->
|
<!-- Check permissions and owner -->
|
||||||
|
|
||||||
|
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||||
|
{% if owner_control.value == "True" %}
|
||||||
|
{% authorized_owners item.owner as owners %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
{% if owner_control.value == "False" or owner_control.value == "True" and user in owners or user.is_superuser %}
|
||||||
{% if roles.stock.change and not item.is_building %}
|
{% if roles.stock.change and not item.is_building %}
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
|
@ -50,26 +50,17 @@
|
|||||||
$('table').find('.btn-edit-setting').click(function() {
|
$('table').find('.btn-edit-setting').click(function() {
|
||||||
var setting = $(this).attr('setting');
|
var setting = $(this).attr('setting');
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
var url = `/settings/${pk}/edit/`;
|
|
||||||
|
var is_global = true;
|
||||||
|
|
||||||
if ($(this).attr('user')){
|
if ($(this).attr('user')){
|
||||||
url += `user/`;
|
is_global = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
launchModalForm(
|
editSetting(pk, {
|
||||||
url,
|
global: is_global,
|
||||||
{
|
title: is_global ? '{% trans "Edit Global Setting" %}' : '{% trans "Edit User Setting" %}',
|
||||||
success: function(response) {
|
});
|
||||||
|
|
||||||
if (response.is_bool) {
|
|
||||||
var enabled = response.value.toLowerCase() == 'true';
|
|
||||||
$(`#setting-value-${setting}`).prop('checked', enabled);
|
|
||||||
} else {
|
|
||||||
$(`#setting-value-${setting}`).html(response.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#edit-user").on('click', function() {
|
$("#edit-user").on('click', function() {
|
||||||
|
@ -250,26 +250,37 @@
|
|||||||
{% trans "Select language" %}
|
{% trans "Select language" %}
|
||||||
</label>
|
</label>
|
||||||
<div class='form-group input-group mb-3'>
|
<div class='form-group input-group mb-3'>
|
||||||
<select name="language" class="select form-control">
|
<select name="language" class="select form-control w-25">
|
||||||
{% get_current_language as LANGUAGE_CODE %}
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
{% get_available_languages as LANGUAGES %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
{% if 'alllang' in request.GET %}{% define True as ALL_LANG %}{% endif %}
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
{% define language.code as lang_code %}
|
{% define language.code as lang_code %}
|
||||||
{% define locale_stats|keyvalue:lang_code as lang_translated %}
|
{% define locale_stats|keyvalue:lang_code as lang_translated %}
|
||||||
|
{% if lang_translated > 10 or lang_code == 'en' or lang_code == LANGUAGE_CODE %}{% define True as use_lang %}{% else %}{% define False as use_lang %}{% endif %}
|
||||||
|
{% if ALL_LANG or use_lang %}
|
||||||
<option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
<option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
{{ language.name_local }} ({{ lang_code }})
|
{{ language.name_local }} ({{ lang_code }})
|
||||||
{% if lang_translated %}
|
{% if lang_translated %}
|
||||||
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
|
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans 'No translations available' %}
|
{% if lang_code == 'en' %}-{% else %}{% trans 'No translations available' %}{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</option>
|
</option>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class='input-group-append'>
|
<div class='input-group-append'>
|
||||||
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
|
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
|
||||||
</div>
|
</div>
|
||||||
|
<p>{% trans "Some languages are not complete" %}
|
||||||
|
{% if ALL_LANG %}
|
||||||
|
. <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a>
|
||||||
|
{% else %}
|
||||||
|
and hidden. <a href="?alllang">{% trans "Show them too" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,12 +22,12 @@
|
|||||||
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %}
|
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>{% include "clip.html" %}
|
||||||
{% inventree_is_development as dev %}
|
{% inventree_is_development as dev %}
|
||||||
{% if dev %}
|
{% if dev %}
|
||||||
<span class='badge rounded-pill bg-primary'>{% trans "Development Version" %}</span>
|
<span class='badge badge-right rounded-pill bg-primary'>{% trans "Development Version" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if up_to_date %}
|
{% if up_to_date %}
|
||||||
<span class='badge rounded-pill bg-success'>{% trans "Up to Date" %}</span>
|
<span class='badge badge-right rounded-pill bg-success'>{% trans "Up to Date" %}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class='badge rounded-pill bg-info'>{% trans "Update Available" %}</span>
|
<span class='badge badge-right rounded-pill bg-info'>{% trans "Update Available" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -71,8 +71,11 @@
|
|||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<span class='float-right'><h3>{% inventree_title %}</h3></span>
|
<span class='float-right'><h3>{% inventree_title %}</h3></span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='container-fluid'>
|
||||||
<hr>
|
<hr>
|
||||||
<div class='container-fluid'>{% block content %}{% endblock %}</div>
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,7 @@ for a account and sign in below:{% endblocktrans %}</p>
|
|||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
<div class="btn-group float-right" role="group">
|
<div class="btn-group float-right" role="group">
|
||||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
{% if redirect_field_value %}
|
{% if redirect_field_value %}
|
||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
|
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<hr>
|
||||||
<div class='btn-group float-right' role='group'>
|
<div class='btn-group float-right' role='group'>
|
||||||
<a type='button' class='btn btn-secondary' href='{% url "index" %}'><span class='fas fa-undo-alt'></span> {% trans "Back to Site" %}</a>
|
<a type='button' class='btn btn-secondary' href='{% url "index" %}'><span class='fas fa-undo-alt'></span> {% trans "Back to Site" %}</a>
|
||||||
<button type="submit" class="btn btn-danger btn-block">{% trans 'Sign Out' %}</button>
|
<button type="submit" class="btn btn-danger btn-block">{% trans 'Sign Out' %}</button>
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main class='col ps-md-2 pt-2'>
|
<main class='col ps-md-2 pt-2 pe-2'>
|
||||||
|
|
||||||
{% block alerts %}
|
{% block alerts %}
|
||||||
<div class='notification-area' id='alerts'>
|
<div class='notification-area' id='alerts'>
|
||||||
@ -190,6 +190,18 @@ $(document).ready(function () {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
moment.locale('{{ request.LANGUAGE_CODE }}');
|
moment.locale('{{ request.LANGUAGE_CODE }}');
|
||||||
|
|
||||||
|
// Account notifications
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
showMessage(
|
||||||
|
'{{ message }}',
|
||||||
|
{
|
||||||
|
style: 'info',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
editSetting,
|
||||||
user_settings,
|
user_settings,
|
||||||
global_settings,
|
global_settings,
|
||||||
*/
|
*/
|
||||||
@ -18,3 +19,83 @@ const global_settings = {
|
|||||||
{{ key }}: {% primitive_to_javascript value %},
|
{{ key }}: {% primitive_to_javascript value %},
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Edit a setting value
|
||||||
|
*/
|
||||||
|
function editSetting(pk, options={}) {
|
||||||
|
|
||||||
|
// Is this a global setting or a user setting?
|
||||||
|
var global = options.global || false;
|
||||||
|
|
||||||
|
var url = '';
|
||||||
|
|
||||||
|
if (global) {
|
||||||
|
url = `/api/settings/global/${pk}/`;
|
||||||
|
} else {
|
||||||
|
url = `/api/settings/user/${pk}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, read the settings object from the server
|
||||||
|
inventreeGet(url, {}, {
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
if (response.choices && response.choices.length > 0) {
|
||||||
|
response.type = 'choice';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the field
|
||||||
|
var fields = {
|
||||||
|
value: {
|
||||||
|
label: response.name,
|
||||||
|
help_text: response.description,
|
||||||
|
type: response.type,
|
||||||
|
choices: response.choices,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructChangeForm(fields, {
|
||||||
|
url: url,
|
||||||
|
method: 'PATCH',
|
||||||
|
title: options.title,
|
||||||
|
processResults: function(data, fields, opts) {
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'boolean':
|
||||||
|
// Convert to boolean value
|
||||||
|
data.value = data.value.toString().toLowerCase() == 'true';
|
||||||
|
break;
|
||||||
|
case 'integer':
|
||||||
|
// Convert to integer value
|
||||||
|
data.value = parseInt(data.value.toString());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
processBeforeUpload: function(data) {
|
||||||
|
// Convert value to string
|
||||||
|
data.value = data.value.toString();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: function(response) {
|
||||||
|
|
||||||
|
var setting = response.key;
|
||||||
|
|
||||||
|
if (response.type == 'boolean') {
|
||||||
|
var enabled = response.value.toString().toLowerCase() == 'true';
|
||||||
|
$(`#setting-value-${setting}`).prop('checked', enabled);
|
||||||
|
} else {
|
||||||
|
$(`#setting-value-${setting}`).html(response.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
showApiError(xhr, url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -61,7 +61,11 @@ function inventreeGet(url, filters={}, options={}) {
|
|||||||
},
|
},
|
||||||
error: function(xhr, ajaxOptions, thrownError) {
|
error: function(xhr, ajaxOptions, thrownError) {
|
||||||
console.error('Error on GET at ' + url);
|
console.error('Error on GET at ' + url);
|
||||||
console.error(thrownError);
|
|
||||||
|
if (thrownError) {
|
||||||
|
console.error('Error: ' + thrownError);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.error) {
|
if (options.error) {
|
||||||
options.error({
|
options.error({
|
||||||
error: thrownError
|
error: thrownError
|
||||||
@ -174,7 +178,7 @@ function showApiError(xhr, url) {
|
|||||||
var title = null;
|
var title = null;
|
||||||
var message = null;
|
var message = null;
|
||||||
|
|
||||||
switch (xhr.status) {
|
switch (xhr.status || 0) {
|
||||||
// No response
|
// No response
|
||||||
case 0:
|
case 0:
|
||||||
title = '{% trans "No Response" %}';
|
title = '{% trans "No Response" %}';
|
||||||
|
@ -257,7 +257,7 @@ function barcodeDialog(title, options={}) {
|
|||||||
|
|
||||||
$(modal).modal({
|
$(modal).modal({
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
keyboard: false,
|
keyboard: user_settings.FORMS_CLOSE_USING_ESCAPE,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.preShow) {
|
if (options.preShow) {
|
||||||
|
@ -43,11 +43,18 @@ function buildFormFields() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
sales_order: {
|
sales_order: {
|
||||||
|
icon: 'fa-truck',
|
||||||
},
|
},
|
||||||
batch: {},
|
batch: {},
|
||||||
target_date: {},
|
target_date: {
|
||||||
take_from: {},
|
icon: 'fa-calendar-alt',
|
||||||
destination: {},
|
},
|
||||||
|
take_from: {
|
||||||
|
icon: 'fa-sitemap',
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
icon: 'fa-sitemap',
|
||||||
|
},
|
||||||
link: {
|
link: {
|
||||||
icon: 'fa-link',
|
icon: 'fa-link',
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
renderStockLocation,
|
renderStockLocation,
|
||||||
renderSupplierPart,
|
renderSupplierPart,
|
||||||
renderUser,
|
renderUser,
|
||||||
showAlertDialog,
|
|
||||||
showAlertOrCache,
|
showAlertOrCache,
|
||||||
showApiError,
|
showApiError,
|
||||||
*/
|
*/
|
||||||
@ -200,14 +199,6 @@ function constructChangeForm(fields, options) {
|
|||||||
},
|
},
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
|
|
||||||
// Push existing 'value' to each field
|
|
||||||
for (const field in data) {
|
|
||||||
|
|
||||||
if (field in fields) {
|
|
||||||
fields[field].value = data[field];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An optional function can be provided to process the returned results,
|
// An optional function can be provided to process the returned results,
|
||||||
// before they are rendered to the form
|
// before they are rendered to the form
|
||||||
if (options.processResults) {
|
if (options.processResults) {
|
||||||
@ -219,6 +210,14 @@ function constructChangeForm(fields, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Push existing 'value' to each field
|
||||||
|
for (const field in data) {
|
||||||
|
|
||||||
|
if (field in fields) {
|
||||||
|
fields[field].value = data[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the entire data object
|
// Store the entire data object
|
||||||
options.instance = data;
|
options.instance = data;
|
||||||
|
|
||||||
@ -347,10 +346,12 @@ function constructForm(url, options) {
|
|||||||
constructCreateForm(OPTIONS.actions.POST, options);
|
constructCreateForm(OPTIONS.actions.POST, options);
|
||||||
} else {
|
} else {
|
||||||
// User does not have permission to POST to the endpoint
|
// User does not have permission to POST to the endpoint
|
||||||
showAlertDialog(
|
showMessage('{% trans "Action Prohibited" %}', {
|
||||||
'{% trans "Action Prohibited" %}',
|
style: 'danger',
|
||||||
'{% trans "Create operation not allowed" %}'
|
details: '{% trans "Create operation not allowed" %}',
|
||||||
);
|
icon: 'fas fa-user-times',
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`'POST action unavailable at ${url}`);
|
console.log(`'POST action unavailable at ${url}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -360,10 +361,12 @@ function constructForm(url, options) {
|
|||||||
constructChangeForm(OPTIONS.actions.PUT, options);
|
constructChangeForm(OPTIONS.actions.PUT, options);
|
||||||
} else {
|
} else {
|
||||||
// User does not have permission to PUT/PATCH to the endpoint
|
// User does not have permission to PUT/PATCH to the endpoint
|
||||||
showAlertDialog(
|
showMessage('{% trans "Action Prohibited" %}', {
|
||||||
'{% trans "Action Prohibited" %}',
|
style: 'danger',
|
||||||
'{% trans "Update operation not allowed" %}'
|
details: '{% trans "Update operation not allowed" %}',
|
||||||
);
|
icon: 'fas fa-user-times',
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`${options.method} action unavailable at ${url}`);
|
console.log(`${options.method} action unavailable at ${url}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -372,10 +375,12 @@ function constructForm(url, options) {
|
|||||||
constructDeleteForm(OPTIONS.actions.DELETE, options);
|
constructDeleteForm(OPTIONS.actions.DELETE, options);
|
||||||
} else {
|
} else {
|
||||||
// User does not have permission to DELETE to the endpoint
|
// User does not have permission to DELETE to the endpoint
|
||||||
showAlertDialog(
|
showMessage('{% trans "Action Prohibited" %}', {
|
||||||
'{% trans "Action Prohibited" %}',
|
style: 'danger',
|
||||||
'{% trans "Delete operation not allowed" %}'
|
details: '{% trans "Delete operation not allowed" %}',
|
||||||
);
|
icon: 'fas fa-user-times',
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`DELETE action unavailable at ${url}`);
|
console.log(`DELETE action unavailable at ${url}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -384,10 +389,12 @@ function constructForm(url, options) {
|
|||||||
// TODO?
|
// TODO?
|
||||||
} else {
|
} else {
|
||||||
// User does not have permission to GET to the endpoint
|
// User does not have permission to GET to the endpoint
|
||||||
showAlertDialog(
|
showMessage('{% trans "Action Prohibited" %}', {
|
||||||
'{% trans "Action Prohibited" %}',
|
style: 'danger',
|
||||||
'{% trans "View operation not allowed" %}'
|
details: '{% trans "View operation not allowed" %}',
|
||||||
);
|
icon: 'fas fa-user-times',
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`GET action unavailable at ${url}`);
|
console.log(`GET action unavailable at ${url}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -717,6 +724,11 @@ function submitFormData(fields, options) {
|
|||||||
data = form_data;
|
data = form_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optionally pre-process the data before uploading to the server
|
||||||
|
if (options.processBeforeUpload) {
|
||||||
|
data = options.processBeforeUpload(data);
|
||||||
|
}
|
||||||
|
|
||||||
// Submit data
|
// Submit data
|
||||||
upload_func(
|
upload_func(
|
||||||
options.url,
|
options.url,
|
||||||
|
@ -992,7 +992,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var grid_view = inventreeLoad('part-grid-view') == 1;
|
var grid_view = options.gridView && inventreeLoad('part-grid-view') == 1;
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
url: url,
|
url: url,
|
||||||
@ -1020,7 +1020,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
buttons: [
|
buttons: options.gridView ? [
|
||||||
{
|
{
|
||||||
icon: 'fas fa-bars',
|
icon: 'fas fa-bars',
|
||||||
attributes: {
|
attributes: {
|
||||||
@ -1053,7 +1053,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
] : [],
|
||||||
customView: function(data) {
|
customView: function(data) {
|
||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
|
@ -10,9 +10,6 @@
|
|||||||
<div class="navbar-header clearfix content-heading">
|
<div class="navbar-header clearfix content-heading">
|
||||||
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
|
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
|
||||||
</div>
|
</div>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-objects" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="navbar-collapse collapse" id="navbar-objects">
|
<div class="navbar-collapse collapse" id="navbar-objects">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
{% if roles.part.view %}
|
{% if roles.part.view %}
|
||||||
@ -62,19 +59,24 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% include "search_form.html" %}
|
{% include "search_form.html" %}
|
||||||
<ul class='navbar-nav'>
|
<ul class='navbar-nav flex-row'>
|
||||||
{% if barcodes %}
|
{% if barcodes %}
|
||||||
<li id='navbar-barcode-li'>
|
<li class='nav-item' id='navbar-barcode-li'>
|
||||||
<button id='barcode-scan' class='btn btn-secondary' title='{% trans "Scan Barcode" %}'>
|
<button id='barcode-scan' class='btn btn-secondary' title='{% trans "Scan Barcode" %}'>
|
||||||
<span class='fas fa-qrcode'></span>
|
<span class='fas fa-qrcode'></span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li class='nav-item' id='navbar-barcode-li'>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-objects" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li class='nav-item dropdown'>
|
<li class='nav-item dropdown'>
|
||||||
<a class='nav-link dropdown-toggle' href='#' id='userMenuDropdown' role='button' data-bs-toggle='dropdown'>
|
<a class='nav-link dropdown-toggle' href='#' id='userMenuDropdown' role='button' data-bs-toggle='dropdown'>
|
||||||
<span class='fas fa-user'></span> <strong>{{ user.get_username }}</strong>
|
<span class='fas fa-user'></span> <strong>{{ user.get_username }}</strong>
|
||||||
</a>
|
</a>
|
||||||
<ul class='dropdown-menu dropdown-menu-end'>
|
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
|
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
|
||||||
@ -108,7 +110,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class='container' style='width: 80%;'>
|
<div class='container' style='width: 80%;'>
|
||||||
{% if qr_data %}
|
{% if qr_data %}
|
||||||
<div class='qr-container'>
|
<div class='d-flex justify-content-center'>
|
||||||
<img src="{% qrcode qr_data %}">
|
<img src="{% qrcode qr_data %}">
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -7,15 +7,16 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from rest_framework import generics, permissions
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
|
from rest_framework import filters, generics, permissions
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from .serializers import UserSerializer, OwnerSerializer
|
from users.models import RuleSet, Owner, check_user_role
|
||||||
|
from users.serializers import UserSerializer, OwnerSerializer
|
||||||
from .models import RuleSet, Owner, check_user_role
|
|
||||||
|
|
||||||
|
|
||||||
class OwnerList(generics.ListAPIView):
|
class OwnerList(generics.ListAPIView):
|
||||||
@ -26,6 +27,37 @@ class OwnerList(generics.ListAPIView):
|
|||||||
queryset = Owner.objects.all()
|
queryset = Owner.objects.all()
|
||||||
serializer_class = OwnerSerializer
|
serializer_class = OwnerSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Implement text search for the "owner" model.
|
||||||
|
|
||||||
|
Note that an "owner" can be either a group, or a user,
|
||||||
|
so we cannot do a direct text search.
|
||||||
|
|
||||||
|
A "hack" here is to post-process the queryset and simply
|
||||||
|
remove any values which do not match.
|
||||||
|
|
||||||
|
It is not necessarily "efficient" to do it this way,
|
||||||
|
but until we determine a better way, this is what we have...
|
||||||
|
"""
|
||||||
|
|
||||||
|
search_term = str(self.request.query_params.get('search', '')).lower()
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
if not search_term:
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Extract search term f
|
||||||
|
|
||||||
|
for result in queryset.all():
|
||||||
|
if search_term in result.name().lower():
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class OwnerDetail(generics.RetrieveAPIView):
|
class OwnerDetail(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
@ -96,6 +128,17 @@ class UserList(generics.ListAPIView):
|
|||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
filters.SearchFilter,
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'username',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class GetAuthToken(APIView):
|
class GetAuthToken(APIView):
|
||||||
""" Return authentication token for an authenticated user. """
|
""" Return authentication token for an authenticated user. """
|
||||||
|
Loading…
Reference in New Issue
Block a user