2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 12:36:45 +00:00

Merge remote-tracking branch 'inventree/master' into partial-shipment

This commit is contained in:
Oliver 2021-11-15 10:55:17 +11:00
commit dad097a3ba
14 changed files with 11254 additions and 2033 deletions

View File

@ -27,6 +27,7 @@ jobs:
INVENTREE_DEBUG: info
INVENTREE_MEDIA_ROOT: ./media
INVENTREE_STATIC_ROOT: ./static
INVENTREE_CACHE_HOST: localhost
services:
postgres:
@ -37,6 +38,11 @@ jobs:
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- name: Checkout Code
uses: actions/checkout@v2
@ -49,6 +55,7 @@ jobs:
sudo apt-get install libpq-dev
pip3 install invoke
pip3 install psycopg2
pip3 install django-redis>=5.0.0
invoke install
- name: Run Tests
run: invoke test

View File

@ -15,6 +15,7 @@ import logging
import os
import random
import socket
import string
import shutil
import sys
@ -361,30 +362,6 @@ REST_FRAMEWORK = {
WSGI_APPLICATION = 'InvenTree.wsgi.application'
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
if background_workers is not None:
try:
background_workers = int(background_workers)
except ValueError:
background_workers = None
if background_workers is None:
# Sensible default?
background_workers = 4
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': background_workers,
'timeout': 90,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default',
'sync': False,
}
"""
Configure the database backend based on the user-specified values.
@ -562,12 +539,84 @@ DATABASES = {
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
_cache_config = CONFIG.get("cache", {})
_cache_host = _cache_config.get("host", os.getenv("INVENTREE_CACHE_HOST"))
_cache_port = _cache_config.get(
"port", os.getenv("INVENTREE_CACHE_PORT", "6379")
)
if _cache_host:
# We are going to rely upon a possibly non-localhost for our cache,
# so don't wait too long for the cache as nothing in the cache should be
# irreplacable. Django Q Cluster will just try again later.
_cache_options = {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
"SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")),
"CONNECTION_POOL_KWARGS": {
"socket_keepalive": _is_true(
os.getenv("CACHE_TCP_KEEPALIVE", "1")
),
"socket_keepalive_options": {
socket.TCP_KEEPCNT: int(
os.getenv("CACHE_KEEPALIVES_COUNT", "5")
),
socket.TCP_KEEPIDLE: int(
os.getenv("CACHE_KEEPALIVES_IDLE", "1")
),
socket.TCP_KEEPINTVL: int(
os.getenv("CACHE_KEEPALIVES_INTERVAL", "1")
),
socket.TCP_USER_TIMEOUT: int(
os.getenv("CACHE_TCP_USER_TIMEOUT", "1000")
),
},
},
}
CACHES = {
# Connection configuration for Django Q Cluster
"worker": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{_cache_host}:{_cache_port}/0",
"OPTIONS": _cache_options,
},
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{_cache_host}:{_cache_port}/1",
"OPTIONS": _cache_options,
},
}
else:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
}
try:
# 4 background workers seems like a sensible default
background_workers = int(os.environ.get('INVENTREE_BACKGROUND_WORKERS', 4))
except ValueError:
background_workers = 4
# django-q configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': background_workers,
'timeout': 90,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default',
'sync': False,
}
if _cache_host:
# If using external redis cache, make the cache the broker for Django Q
# as well
Q_CLUSTER["django_redis"] = "worker"
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@ -614,6 +663,7 @@ LANGUAGES = [
('nl', _('Dutch')),
('no', _('Norwegian')),
('pl', _('Polish')),
('pt', _('Portugese')),
('ru', _('Russian')),
('sv', _('Swedish')),
('th', _('Thai')),

File diff suppressed because one or more lines are too long

View File

@ -12,11 +12,31 @@ class SettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value')
def get_readonly_fields(self, request, obj=None):
"""
Prevent the 'key' field being edited once the setting is created
"""
if obj:
return ['key']
else:
return []
class UserSettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'user', )
def get_readonly_fields(self, request, obj=None):
"""
Prevent the 'key' field being edited once the setting is created
"""
if obj:
return ['key']
else:
return []
class NotificationEntryAdmin(admin.ModelAdmin):

View File

@ -1,10 +1,30 @@
# -*- coding: utf-8 -*-
import logging
from django.apps import AppConfig
logger = logging.getLogger('inventree')
class CommonConfig(AppConfig):
name = 'common'
def ready(self):
pass
self.clear_restart_flag()
def clear_restart_flag(self):
"""
Clear the SERVER_RESTART_REQUIRED setting
"""
try:
import common.models
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
except:
pass

View File

@ -63,13 +63,15 @@ class BaseInvenTreeSetting(models.Model):
Enforce validation and clean before saving
"""
self.key = str(self.key).upper()
self.clean()
self.validate_unique()
super().save()
@classmethod
def allValues(cls, user=None):
def allValues(cls, user=None, exclude_hidden=False):
"""
Return a dict of "all" defined global settings.
@ -94,9 +96,15 @@ class BaseInvenTreeSetting(models.Model):
for key in cls.GLOBAL_SETTINGS.keys():
if key.upper() not in settings:
settings[key.upper()] = cls.get_setting_default(key)
if exclude_hidden:
hidden = cls.GLOBAL_SETTINGS[key].get('hidden', False)
if hidden:
# Remove hidden items
del settings[key.upper()]
for key, value in settings.items():
validator = cls.get_setting_validator(key)
@ -545,6 +553,17 @@ class InvenTreeSetting(BaseInvenTreeSetting):
even if that key does not exist.
"""
def save(self, *args, **kwargs):
"""
When saving a global setting, check to see if it requires a server restart.
If so, set the "SERVER_RESTART_REQUIRED" setting to True
"""
super().save()
if self.requires_restart():
InvenTreeSetting.set_setting('SERVER_REQUIRES_RESTART', True, None)
"""
Dict of all global settings values:
@ -563,6 +582,14 @@ class InvenTreeSetting(BaseInvenTreeSetting):
GLOBAL_SETTINGS = {
'SERVER_RESTART_REQUIRED': {
'name': _('Restart required'),
'description': _('A setting has been changed which requires a server restart'),
'default': False,
'validator': bool,
'hidden': True,
},
'INVENTREE_INSTANCE': {
'name': _('InvenTree Instance Name'),
'default': 'InvenTree server',
@ -936,6 +963,18 @@ class InvenTreeSetting(BaseInvenTreeSetting):
return self.__class__.get_setting(self.key)
def requires_restart(self):
"""
Return True if this setting requires a server restart after changing
"""
options = InvenTreeSetting.GLOBAL_SETTINGS.get(self.key, None)
if options:
return options.get('requires_restart', False)
else:
return False
class InvenTreeUserSetting(BaseInvenTreeSetting):
"""
@ -1269,9 +1308,6 @@ def get_price(instance, quantity, moq=True, multiples=True, currency=None, break
class ColorTheme(models.Model):
""" Color Theme Setting """
default_color_theme = ('', _('Default'))
name = models.CharField(max_length=20,
default='',
blank=True)
@ -1291,10 +1327,7 @@ class ColorTheme(models.Model):
# Get color themes choices (CSS sheets)
choices = [(file_name.lower(), _(file_name.replace('-', ' ').title()))
for file_name, file_ext in files_list
if file_ext == '.css' and file_name.lower() != 'default']
# Add default option as empty option
choices.insert(0, cls.default_color_theme)
if file_ext == '.css']
return choices

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -251,6 +251,15 @@ def global_settings(*args, **kwargs):
return InvenTreeSetting.allValues()
@register.simple_tag()
def visible_global_settings(*args, **kwargs):
"""
Return any global settings which are not marked as 'hidden'
"""
return InvenTreeSetting.allValues(exclude_hidden=True)
@register.simple_tag()
def progress_bar(val, max, *args, **kwargs):
"""
@ -292,6 +301,19 @@ def progress_bar(val, max, *args, **kwargs):
@register.simple_tag()
def get_color_theme_css(username):
user_theme_name = get_user_color_theme(username)
# Build path to CSS sheet
inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
# Build static URL
inventree_css_static_url = os.path.join(settings.STATIC_URL, inventree_css_sheet)
return inventree_css_static_url
@register.simple_tag()
def get_user_color_theme(username):
""" Get current user color theme """
try:
user_theme = ColorTheme.objects.filter(user=username).get()
user_theme_name = user_theme.name
@ -300,13 +322,7 @@ def get_color_theme_css(username):
except ColorTheme.DoesNotExist:
user_theme_name = 'default'
# Build path to CSS sheet
inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
# Build static URL
inventree_css_static_url = os.path.join(settings.STATIC_URL, inventree_css_sheet)
return inventree_css_static_url
return user_theme_name
@register.simple_tag()

View File

@ -170,35 +170,6 @@
</div>
<div class='panel-heading'>
<h4>{% trans "Theme Settings" %}</h4>
</div>
<div class='row'>
<div class='col-sm-6'>
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<label for='theme' class=' requiredField'>
{% trans "Select theme" %}
</label>
<div class='form-group input-group mb-3'>
<select id='theme' name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% for theme in themes %}
<option value='{{ theme.key }}'>{{ theme.name }}</option>
{% endfor %}
</select>
<div class='input-group-append'>
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
</div>
<div class='panel-heading'>
<h4>{% trans "Language Settings" %}</h4>
</div>

View File

@ -21,4 +21,33 @@
</table>
</div>
<div class='panel-heading'>
<h4>{% trans "Theme Settings" %}</h4>
</div>
<div class='row'>
<div class='col-sm-6'>
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<label for='theme' class=' requiredField'>
{% trans "Select theme" %}
</label>
<div class='form-group input-group mb-3'>
<select id='theme' name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% get_user_color_theme request.user.username as user_theme %}
{% for theme in themes %}
<option value='{{ theme.key }}'{% if theme.key == user_theme %} selected{% endif%}>{{ theme.name }}</option>
{% endfor %}
</select>
<div class='input-group-append'>
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -5,6 +5,7 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
{% settings_value "REPORT_ENABLE" as report_enabled %}
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
<!DOCTYPE html>
<html lang="en">
@ -86,6 +87,21 @@
</div>
<main class='col ps-md-2 pt-2 pe-2'>
{% if server_restart_required %}
<div class='notification-area' id='restart-required'>
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
<span class='fas fa-server'></span>
<b>{% trans "Server Restart Required" %}</b>
<small>
<br>
{% trans "A configuration option has been changed which requires a server restart" %}.
<br>
{% trans "Contact your system administrator for further information" %}
</small>
</div>
</div>
{% endif %}
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->

View File

@ -13,7 +13,7 @@ const user_settings = {
{% endfor %}
};
{% global_settings as GLOBAL_SETTINGS %}
{% visible_global_settings as GLOBAL_SETTINGS %}
const global_settings = {
{% for key, value in GLOBAL_SETTINGS.items %}
{{ key }}: {% primitive_to_javascript value %},

View File

@ -11,3 +11,6 @@ psycopg2>=2.9.1
mysqlclient>=2.0.3
pgcli>=3.1.0
mariadb>=1.0.7
# Cache
django-redis>=5.0.0