mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-15 03:25:42 +00:00
Merged master and company migrations
This commit is contained in:
@ -19,11 +19,12 @@ from rest_framework.views import APIView
|
||||
|
||||
from .views import AjaxView
|
||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||
from .status import is_worker_running
|
||||
|
||||
from plugins import plugins as inventree_plugins
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
logger.info("Loading action plugins...")
|
||||
@ -44,6 +45,7 @@ class InfoView(AjaxView):
|
||||
'version': inventreeVersion(),
|
||||
'instance': inventreeInstanceName(),
|
||||
'apiVersion': inventreeApiVersion(),
|
||||
'worker_running': is_worker_running(),
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
44
InvenTree/InvenTree/apps.py
Normal file
44
InvenTree/InvenTree/apps.py
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
|
||||
import InvenTree.tasks
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class InvenTreeConfig(AppConfig):
|
||||
name = 'InvenTree'
|
||||
|
||||
def ready(self):
|
||||
|
||||
self.start_background_tasks()
|
||||
|
||||
def start_background_tasks(self):
|
||||
|
||||
try:
|
||||
from django_q.models import Schedule
|
||||
except (AppRegistryNotReady):
|
||||
return
|
||||
|
||||
logger.info("Starting background tasks...")
|
||||
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.delete_successful_tasks',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.check_for_updates',
|
||||
schedule_type=Schedule.DAILY
|
||||
)
|
||||
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.heartbeat',
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=15
|
||||
)
|
@ -30,10 +30,22 @@ def health_status(request):
|
||||
|
||||
request._inventree_health_status = True
|
||||
|
||||
return {
|
||||
"system_healthy": InvenTree.status.check_system_health(),
|
||||
status = {
|
||||
'django_q_running': InvenTree.status.is_worker_running(),
|
||||
}
|
||||
|
||||
all_healthy = True
|
||||
|
||||
for k in status.keys():
|
||||
if status[k] is not True:
|
||||
all_healthy = False
|
||||
|
||||
status['system_healthy'] = all_healthy
|
||||
|
||||
status['up_to_date'] = InvenTree.version.isInvenTreeUpToDate()
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def status_codes(request):
|
||||
"""
|
||||
|
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal file
42
InvenTree/InvenTree/management/commands/wait_for_db.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
Custom management command, wait for the database to be ready!
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django.db import connection
|
||||
from django.db.utils import OperationalError, ImproperlyConfigured
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
django command to pause execution until the database is ready
|
||||
"""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
self.stdout.write("Waiting for database...")
|
||||
|
||||
connected = False
|
||||
|
||||
while not connected:
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
try:
|
||||
connection.ensure_connection()
|
||||
|
||||
connected = True
|
||||
|
||||
except OperationalError as e:
|
||||
self.stdout.write(f"Could not connect to database: {e}")
|
||||
except ImproperlyConfigured as e:
|
||||
self.stdout.write(f"Improperly configured: {e}")
|
||||
else:
|
||||
if not connection.is_usable():
|
||||
self.stdout.write("Database configuration is not usable")
|
||||
|
||||
if connected:
|
||||
self.stdout.write("Database connection sucessful!")
|
@ -8,7 +8,7 @@ import operator
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class AuthRequiredMiddleware(object):
|
||||
|
@ -13,6 +13,9 @@ database setup in this file.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
@ -46,14 +49,31 @@ def get_setting(environment_var, backup_val, default_value=None):
|
||||
return default_value
|
||||
|
||||
|
||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||
TESTING = 'test' in sys.argv
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
|
||||
# Specify where the "config file" is located.
|
||||
# By default, this is 'config.yaml'
|
||||
|
||||
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
|
||||
|
||||
if cfg_filename:
|
||||
cfg_filename = cfg_filename.strip()
|
||||
cfg_filename = os.path.abspath(cfg_filename)
|
||||
|
||||
else:
|
||||
# Config file is *not* specified - use the default
|
||||
cfg_filename = os.path.join(BASE_DIR, 'config.yaml')
|
||||
|
||||
if not os.path.exists(cfg_filename):
|
||||
print("Error: config.yaml not found")
|
||||
sys.exit(-1)
|
||||
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||
|
||||
cfg_template = os.path.join(BASE_DIR, "config_template.yaml")
|
||||
shutil.copyfile(cfg_template, cfg_filename)
|
||||
print(f"Created config file {cfg_filename}")
|
||||
|
||||
with open(cfg_filename, 'r') as cfg:
|
||||
CONFIG = yaml.safe_load(cfg)
|
||||
@ -94,7 +114,18 @@ LOGGING = {
|
||||
}
|
||||
|
||||
# Get a logger instance for this setup file
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
"""
|
||||
Specify a secret key to be used by django.
|
||||
|
||||
Following options are tested, in descending order of preference:
|
||||
|
||||
A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
|
||||
B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
|
||||
C) Look for default key file "secret_key.txt"
|
||||
d) Create "secret_key.txt" if it does not exist
|
||||
"""
|
||||
|
||||
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||
# Secret key passed in directly
|
||||
@ -105,15 +136,22 @@ else:
|
||||
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||
|
||||
if key_file:
|
||||
if os.path.isfile(key_file):
|
||||
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY_FILE")
|
||||
else:
|
||||
logger.error(f"Secret key file {key_file} not found")
|
||||
exit(-1)
|
||||
key_file = os.path.abspath(key_file)
|
||||
else:
|
||||
# default secret key location
|
||||
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
||||
logger.info(f"SECRET_KEY loaded from {key_file}")
|
||||
key_file = os.path.abspath(key_file)
|
||||
|
||||
if not os.path.exists(key_file):
|
||||
logger.info(f"Generating random key file at '{key_file}'")
|
||||
# Create a random key file
|
||||
with open(key_file, 'w') as f:
|
||||
options = string.digits + string.ascii_letters + string.punctuation
|
||||
key = ''.join([random.choice(options) for i in range(100)])
|
||||
f.write(key)
|
||||
|
||||
logger.info(f"Loading SECRET_KEY from '{key_file}'")
|
||||
|
||||
try:
|
||||
SECRET_KEY = open(key_file, "r").read().strip()
|
||||
except Exception:
|
||||
@ -144,7 +182,7 @@ STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_STATIC_ROOT',
|
||||
CONFIG.get('static_root', os.path.join(BASE_DIR, 'static'))
|
||||
CONFIG.get('static_root', '/home/inventree/static')
|
||||
)
|
||||
)
|
||||
|
||||
@ -162,7 +200,7 @@ MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_MEDIA_ROOT',
|
||||
CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))
|
||||
CONFIG.get('media_root', '/home/inventree/data/media')
|
||||
)
|
||||
)
|
||||
|
||||
@ -194,6 +232,7 @@ INSTALLED_APPS = [
|
||||
'report.apps.ReportConfig',
|
||||
'stock.apps.StockConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||
|
||||
# Third part add-ons
|
||||
'django_filters', # Extended filter functionality
|
||||
@ -211,6 +250,7 @@ INSTALLED_APPS = [
|
||||
'djmoney', # django-money integration
|
||||
'djmoney.contrib.exchange', # django-money exchange rates
|
||||
'error_report', # Error reporting in the admin interface
|
||||
'django_q',
|
||||
]
|
||||
|
||||
MIDDLEWARE = CONFIG.get('middleware', [
|
||||
@ -285,6 +325,18 @@ REST_FRAMEWORK = {
|
||||
|
||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||
|
||||
# django-q configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'workers': 4,
|
||||
'timeout': 90,
|
||||
'retry': 120,
|
||||
'queue_limit': 50,
|
||||
'bulk': 10,
|
||||
'orm': 'default',
|
||||
'sync': False,
|
||||
}
|
||||
|
||||
# Markdownx configuration
|
||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
||||
@ -331,6 +383,9 @@ logger.info("Configuring database backend:")
|
||||
# Extract database configuration from the config.yaml file
|
||||
db_config = CONFIG.get('database', {})
|
||||
|
||||
if not db_config:
|
||||
db_config = {}
|
||||
|
||||
# Environment variables take preference over config file!
|
||||
|
||||
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
||||
@ -350,7 +405,7 @@ reqiured_keys = ['ENGINE', 'NAME']
|
||||
|
||||
for key in reqiured_keys:
|
||||
if key not in db_config:
|
||||
error_msg = f'Missing required database configuration value {key} in config.yaml'
|
||||
error_msg = f'Missing required database configuration value {key}'
|
||||
logger.error(error_msg)
|
||||
|
||||
print('Error: ' + error_msg)
|
||||
@ -386,11 +441,6 @@ CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
},
|
||||
'qr-code': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'qr-code-cache',
|
||||
'TIMEOUT': 3600
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
@ -449,13 +499,19 @@ LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale/'),
|
||||
)
|
||||
|
||||
TIME_ZONE = CONFIG.get('timezone', 'UTC')
|
||||
TIME_ZONE = get_setting(
|
||||
'INVENTREE_TIMEZONE',
|
||||
CONFIG.get('timezone', 'UTC')
|
||||
)
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
# Do not use native timezone support in "test" mode
|
||||
# It generates a *lot* of cruft in the logs
|
||||
if not TESTING:
|
||||
USE_TZ = True
|
||||
|
||||
DATE_INPUT_FORMATS = [
|
||||
"%Y-%m-%d",
|
||||
|
@ -1,13 +1,46 @@
|
||||
"""
|
||||
Provides system status functionality checks.
|
||||
"""
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django_q.models import Success
|
||||
from django_q.monitor import Stat
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
def is_worker_running(**kwargs):
|
||||
"""
|
||||
Return True if the background worker process is oprational
|
||||
"""
|
||||
|
||||
clusters = Stat.get_all()
|
||||
|
||||
if len(clusters) > 0:
|
||||
# TODO - Introspect on any cluster information
|
||||
return True
|
||||
|
||||
"""
|
||||
Sometimes Stat.get_all() returns [].
|
||||
In this case we have the 'heartbeat' task running every 15 minutes.
|
||||
Check to see if we have a result within the last 20 minutes
|
||||
"""
|
||||
|
||||
now = datetime.now()
|
||||
past = now - timedelta(minutes=20)
|
||||
|
||||
results = Success.objects.filter(
|
||||
func='InvenTree.tasks.heartbeat',
|
||||
started__gte=past
|
||||
)
|
||||
|
||||
# If any results are returned, then the background worker is running!
|
||||
return results.exists()
|
||||
|
||||
|
||||
def check_system_health(**kwargs):
|
||||
@ -19,21 +52,11 @@ def check_system_health(**kwargs):
|
||||
|
||||
result = True
|
||||
|
||||
if not check_celery_worker(**kwargs):
|
||||
if not is_worker_running(**kwargs):
|
||||
result = False
|
||||
logger.warning(_("Celery worker check failed"))
|
||||
logger.warning(_("Background worker check failed"))
|
||||
|
||||
if not result:
|
||||
logger.warning(_("InvenTree system health checks failed"))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_celery_worker(**kwargs):
|
||||
"""
|
||||
Check that a celery worker is running.
|
||||
"""
|
||||
|
||||
# TODO - Checks that the configured celery worker thing is running
|
||||
|
||||
return True
|
||||
|
143
InvenTree/InvenTree/tasks.py
Normal file
143
InvenTree/InvenTree/tasks.py
Normal file
@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def schedule_task(taskname, **kwargs):
|
||||
"""
|
||||
Create a scheduled task.
|
||||
If the task has already been scheduled, ignore!
|
||||
"""
|
||||
|
||||
# If unspecified, repeat indefinitely
|
||||
repeats = kwargs.pop('repeats', -1)
|
||||
kwargs['repeats'] = repeats
|
||||
|
||||
try:
|
||||
from django_q.models import Schedule
|
||||
except (AppRegistryNotReady):
|
||||
logger.warning("Could not start background tasks - App registry not ready")
|
||||
return
|
||||
|
||||
try:
|
||||
# If this task is already scheduled, don't schedule it again
|
||||
# Instead, update the scheduling parameters
|
||||
if Schedule.objects.filter(func=taskname).exists():
|
||||
logger.info(f"Scheduled task '{taskname}' already exists - updating!")
|
||||
|
||||
Schedule.objects.filter(func=taskname).update(**kwargs)
|
||||
else:
|
||||
logger.info(f"Creating scheduled task '{taskname}'")
|
||||
|
||||
Schedule.objects.create(
|
||||
name=taskname,
|
||||
func=taskname,
|
||||
**kwargs
|
||||
)
|
||||
except (OperationalError, ProgrammingError):
|
||||
# Required if the DB is not ready yet
|
||||
pass
|
||||
|
||||
|
||||
def heartbeat():
|
||||
"""
|
||||
Simple task which runs at 5 minute intervals,
|
||||
so we can determine that the background worker
|
||||
is actually running.
|
||||
|
||||
(There is probably a less "hacky" way of achieving this)?
|
||||
"""
|
||||
|
||||
try:
|
||||
from django_q.models import Success
|
||||
logger.warning("Could not perform heartbeat task - App registry not ready")
|
||||
except AppRegistryNotReady:
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(minutes=30)
|
||||
|
||||
# Delete heartbeat results more than half an hour old,
|
||||
# otherwise they just create extra noise
|
||||
heartbeats = Success.objects.filter(
|
||||
func='InvenTree.tasks.heartbeat',
|
||||
started__lte=threshold
|
||||
)
|
||||
|
||||
heartbeats.delete()
|
||||
|
||||
|
||||
def delete_successful_tasks():
|
||||
"""
|
||||
Delete successful task logs
|
||||
which are more than a month old.
|
||||
"""
|
||||
|
||||
try:
|
||||
from django_q.models import Success
|
||||
except AppRegistryNotReady:
|
||||
logger.warning("Could not perform 'delete_successful_tasks' - App registry not ready")
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(days=30)
|
||||
|
||||
results = Success.objects.filter(
|
||||
started__lte=threshold
|
||||
)
|
||||
|
||||
results.delete()
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
"""
|
||||
Check if there is an update for InvenTree
|
||||
"""
|
||||
|
||||
try:
|
||||
import common.models
|
||||
except AppRegistryNotReady:
|
||||
# Apps not yet loaded!
|
||||
return
|
||||
|
||||
response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest')
|
||||
|
||||
if not response.status_code == 200:
|
||||
raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}')
|
||||
|
||||
data = json.loads(response.text)
|
||||
|
||||
tag = data.get('tag_name', None)
|
||||
|
||||
if not tag:
|
||||
raise ValueError("'tag_name' missing from GitHub response")
|
||||
|
||||
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
|
||||
|
||||
if not len(match.groups()) == 3:
|
||||
logger.warning(f"Version '{tag}' did not match expected pattern")
|
||||
return
|
||||
|
||||
latest_version = [int(x) for x in match.groups()]
|
||||
|
||||
if not len(latest_version) == 3:
|
||||
raise ValueError(f"Version '{tag}' is not correct format")
|
||||
|
||||
logger.info(f"Latest InvenTree version: '{tag}'")
|
||||
|
||||
# Save the version to the database
|
||||
common.models.InvenTreeSetting.set_setting(
|
||||
'INVENTREE_LATEST_VERSION',
|
||||
tag,
|
||||
None
|
||||
)
|
43
InvenTree/InvenTree/test_tasks.py
Normal file
43
InvenTree/InvenTree/test_tasks.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""
|
||||
Unit tests for task management
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django_q.models import Schedule
|
||||
|
||||
import InvenTree.tasks
|
||||
|
||||
|
||||
class ScheduledTaskTests(TestCase):
|
||||
"""
|
||||
Unit tests for scheduled tasks
|
||||
"""
|
||||
|
||||
def get_tasks(self, name):
|
||||
|
||||
return Schedule.objects.filter(func=name)
|
||||
|
||||
def test_add_task(self):
|
||||
"""
|
||||
Ensure that duplicate tasks cannot be added.
|
||||
"""
|
||||
|
||||
task = 'InvenTree.tasks.heartbeat'
|
||||
|
||||
self.assertEqual(self.get_tasks(task).count(), 0)
|
||||
|
||||
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=10)
|
||||
|
||||
self.assertEqual(self.get_tasks(task).count(), 1)
|
||||
|
||||
t = Schedule.objects.get(func=task)
|
||||
|
||||
self.assertEqual(t.minutes, 10)
|
||||
|
||||
# Attempt to schedule the same task again
|
||||
InvenTree.tasks.schedule_task(task, schedule_type=Schedule.MINUTES, minutes=5)
|
||||
self.assertEqual(self.get_tasks(task).count(), 1)
|
||||
|
||||
# But the 'minutes' should have been updated
|
||||
t = Schedule.objects.get(func=task)
|
||||
self.assertEqual(t.minutes, 5)
|
@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError
|
||||
|
||||
from .validators import validate_overage, validate_part_name
|
||||
from . import helpers
|
||||
from . import version
|
||||
|
||||
from mptt.exceptions import InvalidMove
|
||||
|
||||
@ -269,3 +270,33 @@ class TestSerialNumberExtraction(TestCase):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
e("10, a, 7-70j", 4)
|
||||
|
||||
|
||||
class TestVersionNumber(TestCase):
|
||||
"""
|
||||
Unit tests for version number functions
|
||||
"""
|
||||
|
||||
def test_tuple(self):
|
||||
|
||||
v = version.inventreeVersionTuple()
|
||||
self.assertEqual(len(v), 3)
|
||||
|
||||
s = '.'.join([str(i) for i in v])
|
||||
|
||||
self.assertTrue(s in version.inventreeVersion())
|
||||
|
||||
def test_comparison(self):
|
||||
"""
|
||||
Test direct comparison of version numbers
|
||||
"""
|
||||
|
||||
v_a = version.inventreeVersionTuple('1.2.0')
|
||||
v_b = version.inventreeVersionTuple('1.2.3')
|
||||
v_c = version.inventreeVersionTuple('1.2.4')
|
||||
v_d = version.inventreeVersionTuple('2.0.0')
|
||||
|
||||
self.assertTrue(v_b > v_a)
|
||||
self.assertTrue(v_c > v_b)
|
||||
self.assertTrue(v_d > v_c)
|
||||
self.assertTrue(v_d > v_a)
|
||||
|
@ -4,10 +4,11 @@ Provides information on the current InvenTree version
|
||||
|
||||
import subprocess
|
||||
import django
|
||||
import re
|
||||
|
||||
import common.models
|
||||
|
||||
INVENTREE_SW_VERSION = "0.1.8 pre"
|
||||
INVENTREE_SW_VERSION = "0.2.1 pre"
|
||||
|
||||
# Increment this number whenever there is a significant change to the API that any clients need to know about
|
||||
INVENTREE_API_VERSION = 2
|
||||
@ -23,6 +24,38 @@ def inventreeVersion():
|
||||
return INVENTREE_SW_VERSION
|
||||
|
||||
|
||||
def inventreeVersionTuple(version=None):
|
||||
""" Return the InvenTree version string as (maj, min, sub) tuple """
|
||||
|
||||
if version is None:
|
||||
version = INVENTREE_SW_VERSION
|
||||
|
||||
match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version))
|
||||
|
||||
return [int(g) for g in match.groups()]
|
||||
|
||||
|
||||
def isInvenTreeUpToDate():
|
||||
"""
|
||||
Test if the InvenTree instance is "up to date" with the latest version.
|
||||
|
||||
A background task periodically queries GitHub for latest version,
|
||||
and stores it to the database as INVENTREE_LATEST_VERSION
|
||||
"""
|
||||
|
||||
latest = common.models.InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION', None)
|
||||
|
||||
# No record for "latest" version - we must assume we are up to date!
|
||||
if not latest:
|
||||
return True
|
||||
|
||||
# Extract "tuple" version (Python can directly compare version tuples)
|
||||
latest_version = inventreeVersionTuple(latest)
|
||||
inventree_version = inventreeVersionTuple()
|
||||
|
||||
return inventree_version >= latest_version
|
||||
|
||||
|
||||
def inventreeApiVersion():
|
||||
return INVENTREE_API_VERSION
|
||||
|
||||
|
@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
|
||||
from part.serializers import PartSerializer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def hash_barcode(barcode_data):
|
||||
|
@ -500,7 +500,7 @@ class InvenTreeSetting(models.Model):
|
||||
create: If True, create a new setting if the specified key does not exist.
|
||||
"""
|
||||
|
||||
if not user.is_staff:
|
||||
if user is not None and not user.is_staff:
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class CompanyConfig(AppConfig):
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 3.0.7 on 2021-04-08 17:18
|
||||
# Generated by Django 3.0.7 on 2021-04-10 05:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@ -6,7 +6,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0034_supplierpart_update'),
|
||||
('company', '0032_auto_20210403_1837'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -6,7 +6,7 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0032_auto_20210403_1837'),
|
||||
('company', '0033_auto_20210410_1528'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -71,8 +71,8 @@ def supplierpart_make_manufacturer_parts(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0032_auto_20210403_1837'),
|
||||
('company', '0033_manufacturerpart'),
|
||||
('company', '0033_auto_20210410_1528'),
|
||||
('company', '0034_manufacturerpart'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -97,7 +97,12 @@ class Company(models.Model):
|
||||
help_text=_('Company name'),
|
||||
verbose_name=_('Company name'))
|
||||
|
||||
description = models.CharField(max_length=500, blank=True, verbose_name=_('Company description'), help_text=_('Description of the company'))
|
||||
description = models.CharField(
|
||||
max_length=500,
|
||||
verbose_name=_('Company description'),
|
||||
help_text=_('Description of the company'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
website = models.URLField(blank=True, verbose_name=_('Website'), help_text=_('Company website URL'))
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
params: {
|
||||
supplier_part: {{ part.id }},
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
part_detail: false,
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: ['#stock-options'],
|
||||
|
@ -109,11 +109,11 @@ class TestManufacturerField(MigratorTestCase):
|
||||
|
||||
class TestManufacturerPart(MigratorTestCase):
|
||||
"""
|
||||
Tests for migration 0033 and 0034 which added and transitioned to the ManufacturerPart model
|
||||
Tests for migration 0034 and 0035 which added and transitioned to the ManufacturerPart model
|
||||
"""
|
||||
|
||||
migrate_from = ('company', '0032_auto_20210403_1837')
|
||||
migrate_to = ('company', '0034_supplierpart_update')
|
||||
migrate_from = ('company', '0033_auto_20210410_1528')
|
||||
migrate_to = ('company', '0035_supplierpart_update')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
|
@ -7,11 +7,9 @@
|
||||
# with the prefix INVENTREE_DB_
|
||||
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
||||
database:
|
||||
# Default configuration - sqlite filesystem database
|
||||
ENGINE: sqlite3
|
||||
NAME: '../inventree_default_db.sqlite3'
|
||||
|
||||
# For more complex database installations, further parameters are required
|
||||
# Uncomment (and edit) one of the database configurations below,
|
||||
# or specify database options using environment variables
|
||||
|
||||
# Refer to the django documentation for full list of options
|
||||
|
||||
# --- Available options: ---
|
||||
@ -27,14 +25,22 @@ database:
|
||||
|
||||
# --- Example Configuration - sqlite3 ---
|
||||
# ENGINE: sqlite3
|
||||
# NAME: '/path/to/database.sqlite3'
|
||||
# NAME: '/home/inventree/database.sqlite3'
|
||||
|
||||
# --- Example Configuration - MySQL ---
|
||||
#ENGINE: django.db.backends.mysql
|
||||
#ENGINE: mysql
|
||||
#NAME: inventree
|
||||
#USER: inventree_username
|
||||
#USER: inventree
|
||||
#PASSWORD: inventree_password
|
||||
#HOST: '127.0.0.1'
|
||||
#HOST: 'localhost'
|
||||
#PORT: '3306'
|
||||
|
||||
# --- Example Configuration - Postgresql ---
|
||||
#ENGINE: postgresql
|
||||
#NAME: inventree
|
||||
#USER: inventree
|
||||
#PASSWORD: inventree_password
|
||||
#HOST: 'localhost'
|
||||
#PORT: '5432'
|
||||
|
||||
# Select default system language (default is 'en-us')
|
||||
@ -43,6 +49,7 @@ language: en-us
|
||||
# System time-zone (default is UTC)
|
||||
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
# Select an option from the "TZ database name" column
|
||||
# Use the environment variable INVENTREE_TIMEZONE
|
||||
timezone: UTC
|
||||
|
||||
# List of currencies supported by default.
|
||||
@ -57,6 +64,7 @@ currencies:
|
||||
- USD
|
||||
|
||||
# Set debug to False to run in production mode
|
||||
# Use the environment variable INVENTREE_DEBUG
|
||||
debug: True
|
||||
|
||||
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
||||
@ -65,6 +73,7 @@ debug: True
|
||||
debug_toolbar: False
|
||||
|
||||
# Configure the system logging level
|
||||
# Use environment variable INVENTREE_LOG_LEVEL
|
||||
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
log_level: WARNING
|
||||
|
||||
@ -86,13 +95,14 @@ cors:
|
||||
# - https://sub.example.com
|
||||
|
||||
# MEDIA_ROOT is the local filesystem location for storing uploaded files
|
||||
# By default, it is stored in a directory named 'inventree_media' local to the InvenTree directory
|
||||
# This should be changed for a production installation
|
||||
media_root: '../inventree_media'
|
||||
# By default, it is stored under /home/inventree/data/media
|
||||
# Use environment variable INVENTREE_MEDIA_ROOT
|
||||
media_root: '/home/inventree/data/media'
|
||||
|
||||
# STATIC_ROOT is the local filesystem location for storing static files
|
||||
# By default it is stored in a directory named 'inventree_static' local to the InvenTree directory
|
||||
static_root: '../inventree_static'
|
||||
# By default, it is stored under /home/inventree
|
||||
# Use environment variable INVENTREE_STATIC_ROOT
|
||||
static_root: '/home/inventree/static'
|
||||
|
||||
# Optional URL schemes to allow in URL fields
|
||||
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
||||
@ -105,7 +115,8 @@ static_root: '../inventree_static'
|
||||
# Backup options
|
||||
# Set the backup_dir parameter to store backup files in a specific location
|
||||
# If unspecified, the local user's temp directory will be used
|
||||
#backup_dir: '/home/inventree/backup/'
|
||||
# Use environment variable INVENTREE_BACKUP_DIR
|
||||
backup_dir: '/home/inventree/data/backup/'
|
||||
|
||||
# Permit custom authentication backends
|
||||
#authentication_backends:
|
||||
|
@ -7,7 +7,7 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def hashFile(filename):
|
||||
|
@ -32,7 +32,7 @@ except OSError as err:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def rename_label(instance, filename):
|
||||
|
@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
|
||||
|
||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||
|
@ -16,8 +16,6 @@ from .models import PartCategoryParameterTemplate
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak
|
||||
|
||||
from InvenTree.helpers import normalize
|
||||
|
||||
from stock.models import StockLocation
|
||||
from company.models import SupplierPart
|
||||
|
||||
@ -180,7 +178,7 @@ class BomItemResource(ModelResource):
|
||||
|
||||
Ref: https://django-import-export.readthedocs.io/en/latest/getting_started.html#advanced-data-manipulation-on-export
|
||||
"""
|
||||
return normalize(item.quantity)
|
||||
return float(item.quantity)
|
||||
|
||||
def before_export(self, queryset, *args, **kwargs):
|
||||
|
||||
|
@ -735,6 +735,15 @@ class PartParameterList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
|
||||
class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a single PartParameter object
|
||||
"""
|
||||
|
||||
queryset = PartParameter.objects.all()
|
||||
serializer_class = part_serializers.PartParameterSerializer
|
||||
|
||||
|
||||
class BomList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of BomItem objects.
|
||||
|
||||
@ -942,6 +951,8 @@ part_api_urls = [
|
||||
# Base URL for PartParameter API endpoints
|
||||
url(r'^parameter/', include([
|
||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
|
||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
||||
])),
|
||||
|
||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PartConfig(AppConfig):
|
||||
|
@ -33,6 +33,27 @@
|
||||
template: 1
|
||||
data: 12
|
||||
|
||||
- model: part.PartParameter
|
||||
pk: 3
|
||||
fields:
|
||||
part: 3
|
||||
template: 1
|
||||
data: 12
|
||||
|
||||
- model: part.PartParameter
|
||||
pk: 4
|
||||
fields:
|
||||
part: 3
|
||||
template: 2
|
||||
data: 12
|
||||
|
||||
- model: part.PartParameter
|
||||
pk: 5
|
||||
fields:
|
||||
part: 3
|
||||
template: 3
|
||||
data: 12
|
||||
|
||||
# Add some template parameters to categories (requires category.yaml)
|
||||
- model: part.PartCategoryParameterTemplate
|
||||
pk: 1
|
||||
|
@ -52,7 +52,7 @@ import common.models
|
||||
import part.settings as part_settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class PartCategory(InvenTreeTree):
|
||||
|
@ -40,7 +40,7 @@
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
part_detail: false,
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: [
|
||||
|
@ -325,3 +325,106 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
|
||||
self.assertEqual(data['in_stock'], 1100)
|
||||
self.assertEqual(data['stock_item_count'], 105)
|
||||
|
||||
|
||||
class PartParameterTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the ParParameter API
|
||||
"""
|
||||
|
||||
superuser = True
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
'params',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_list_params(self):
|
||||
"""
|
||||
Test for listing part parameters
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-list')
|
||||
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
# Filter by part
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'part': 3,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
# Filter by template
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'template': 1,
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_create_param(self):
|
||||
"""
|
||||
Test that we can create a param via the API
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-list')
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
'part': '2',
|
||||
'template': '3',
|
||||
'data': 70
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
self.assertEqual(len(response.data), 6)
|
||||
|
||||
def test_param_detail(self):
|
||||
"""
|
||||
Tests for the PartParameter detail endpoint
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-detail', kwargs={'pk': 5})
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = response.data
|
||||
|
||||
self.assertEqual(data['pk'], 5)
|
||||
self.assertEqual(data['part'], 3)
|
||||
self.assertEqual(data['data'], '12')
|
||||
|
||||
# PATCH data back in
|
||||
response = self.client.patch(url, {'data': '15'}, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the data changed!
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
data = response.data
|
||||
|
||||
self.assertEqual(data['data'], '15')
|
||||
|
@ -5,7 +5,7 @@ import logging
|
||||
import plugins.plugin as plugin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ActionPlugin(plugin.InvenTreePlugin):
|
||||
|
@ -10,7 +10,7 @@ import plugins.action as action
|
||||
from plugins.action.action import ActionPlugin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def iter_namespace(pkg):
|
||||
|
@ -6,7 +6,7 @@ from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ReportConfig(AppConfig):
|
||||
|
@ -38,7 +38,7 @@ except OSError as err:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class ReportFileUpload(FileSystemStorage):
|
||||
|
@ -165,13 +165,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% if item.in_stock %}
|
||||
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
|
||||
{% endif %}
|
||||
{% if item.in_stock and item.can_adjust_location %}
|
||||
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||
{% endif %}
|
||||
{% if item.in_stock and item.part.trackable %}
|
||||
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if item.in_stock and item.can_adjust_location %}
|
||||
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
|
||||
{% endif %}
|
||||
{% if item.in_stock and item.can_adjust_location and item.part.salable and not item.customer %}
|
||||
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
|
||||
{% endif %}
|
||||
|
@ -131,6 +131,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
|
||||
|
||||
loadStockTable($('#table-recently-updated-stock'), {
|
||||
params: {
|
||||
part_detail: true,
|
||||
ordering: "-updated",
|
||||
max_results: {% settings_value "STOCK_RECENT_COUNT" %},
|
||||
},
|
||||
|
@ -19,11 +19,20 @@
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
|
||||
<td>{% trans "InvenTree Version" %}</td>
|
||||
<td>
|
||||
<a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a>
|
||||
{% if up_to_date %}
|
||||
<span class='label label-green float-right'>{% trans "Up to Date" %}</span>
|
||||
{% else %}
|
||||
<span class='label label-red float-right'>{% trans "Update Available" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Django Version" %}</td><td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
|
||||
<td>{% trans "Django Version" %}</td>
|
||||
<td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
|
||||
</tr>
|
||||
{% inventree_commit_hash as hash %}
|
||||
{% if hash %}
|
||||
@ -69,4 +78,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -241,7 +241,6 @@ function loadStockTable(table, options) {
|
||||
|
||||
// List of user-params which override the default filters
|
||||
|
||||
options.params['part_detail'] = true;
|
||||
options.params['location_detail'] = true;
|
||||
|
||||
var params = options.params || {};
|
||||
@ -524,7 +523,8 @@ function loadStockTable(table, options) {
|
||||
title: '{% trans "Part" %}',
|
||||
sortName: 'part__name',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
visible: params['part_detail'],
|
||||
switchable: params['part_detail'],
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var url = `/stock/item/${row.pk}/`;
|
||||
@ -543,6 +543,8 @@ function loadStockTable(table, options) {
|
||||
title: 'IPN',
|
||||
sortName: 'part__IPN',
|
||||
sortable: true,
|
||||
visible: params['part_detail'],
|
||||
switchable: params['part_detail'],
|
||||
formatter: function(value, row, index, field) {
|
||||
return row.part_detail.IPN;
|
||||
},
|
||||
@ -550,6 +552,8 @@ function loadStockTable(table, options) {
|
||||
{
|
||||
field: 'part_detail.description',
|
||||
title: '{% trans "Description" %}',
|
||||
visible: params['part_detail'],
|
||||
switchable: params['part_detail'],
|
||||
formatter: function(value, row, index, field) {
|
||||
return row.part_detail.description;
|
||||
}
|
||||
|
@ -60,7 +60,9 @@
|
||||
<li class='dropdown'>
|
||||
<a class='dropdown-toggle' data-toggle='dropdown' href="#">
|
||||
{% if not system_healthy %}
|
||||
<span title='{% trans "InvenTree server issues detected" %}' class='fas fa-exclamation-triangle icon-red'></span>
|
||||
<span class='fas fa-exclamation-triangle icon-red'></span>
|
||||
{% elif not up_to_date %}
|
||||
<span class='fas fa-info-circle icon-green'></span>
|
||||
{% endif %}
|
||||
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
|
||||
<ul class='dropdown-menu'>
|
||||
@ -78,11 +80,20 @@
|
||||
{% if system_healthy %}
|
||||
<span class='fas fa-server'>
|
||||
{% else %}
|
||||
<span class='fas fa-exclamation-triangle icon-red'>
|
||||
<span class='fas fa-server icon-red'>
|
||||
{% endif %}
|
||||
</span> {% trans "System Information" %}
|
||||
</a></li>
|
||||
<li id='launch-about'><a href='#'><span class="fas fa-info-circle"></span> {% trans "About InvenTree" %}</a></li>
|
||||
<li id='launch-about'>
|
||||
<a href='#'>
|
||||
{% if up_to_date %}
|
||||
<span class="fas fa-info-circle">
|
||||
{% else %}
|
||||
<span class='fas fa-info-circle icon-red'>
|
||||
{% endif %}
|
||||
</span> {% trans "About InvenTree" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -13,8 +13,9 @@
|
||||
<td>{% trans "Instance Name" %}</td>
|
||||
<td>{% inventree_instance_name %}</td>
|
||||
</tr>
|
||||
{% if user.is_staff %}
|
||||
<tr>
|
||||
<td><span class='fas fa-exclamation-triangle'></span></td>
|
||||
<td><span class='fas fa-server'></span></td>
|
||||
<td>{% trans "Server status" %}</td>
|
||||
<td>
|
||||
{% if system_healthy %}
|
||||
@ -24,6 +25,18 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-tasks'></span></td>
|
||||
<td>{% trans "Background Worker" %}</td>
|
||||
<td>
|
||||
{% if django_q_running %}
|
||||
<span class='label label-green'>{% trans "Operational" %}</span>
|
||||
{% else %}
|
||||
<span class='label label-red'>{% trans "Not running" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if not system_healthy %}
|
||||
{% for issue in system_issues %}
|
||||
|
@ -15,7 +15,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class RuleSet(models.Model):
|
||||
@ -138,6 +138,13 @@ class RuleSet(models.Model):
|
||||
'error_report_error',
|
||||
'exchange_rate',
|
||||
'exchange_exchangebackend',
|
||||
|
||||
# Django-q
|
||||
'django_q_ormq',
|
||||
'django_q_failure',
|
||||
'django_q_task',
|
||||
'django_q_schedule',
|
||||
'django_q_success',
|
||||
]
|
||||
|
||||
RULE_OPTIONS = [
|
||||
|
Reference in New Issue
Block a user