Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-11-15 17:03:28 +11:00
commit af55af6b8f
19 changed files with 385 additions and 95 deletions

View File

@ -5,6 +5,8 @@ Main JSON interface views
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from django.utils.translation import ugettext as _
from django.http import JsonResponse
@ -21,7 +23,10 @@ from .version import inventreeVersion, inventreeInstanceName
from plugins import plugins as inventree_plugins
print("Loading action plugins")
logger = logging.getLogger(__name__)
logger.info("Loading action plugins...")
action_plugins = inventree_plugins.load_action_plugins()

View File

@ -6,7 +6,7 @@ from InvenTree.settings import *
# Override the 'test' database
if 'test' in sys.argv:
eprint('InvenTree: Running tests - Using MySQL test database')
print('InvenTree: Running tests - Using MySQL test database')
DATABASES['default'] = {
# Ensure mysql backend is being used

View File

@ -6,7 +6,7 @@ from InvenTree.settings import *
# Override the 'test' database
if 'test' in sys.argv:
eprint('InvenTree: Running tests - Using PostGreSQL test database')
print('InvenTree: Running tests - Using PostGreSQL test database')
DATABASES['default'] = {
# Ensure postgresql backend is being used

View File

@ -22,32 +22,58 @@ from datetime import datetime
from django.utils.translation import gettext_lazy as _
def eprint(*args, **kwargs):
""" Print a warning message to stderr """
print(*args, file=sys.stderr, **kwargs)
# 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')
if not os.path.exists(cfg_filename):
CONFIG = {}
eprint("Warning: config.yaml not found - using default settings")
else:
print("Error: config.yaml not found")
sys.exit(-1)
with open(cfg_filename, 'r') as cfg:
CONFIG = yaml.safe_load(cfg)
# Read the autogenerated key-file
key_file = open(os.path.join(BASE_DIR, 'secret_key.txt'), 'r')
SECRET_KEY = key_file.read().strip()
# Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('debug', True)
# Configure logging settings
log_level = CONFIG.get('log_level', 'DEBUG').upper()
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
log_level = 'WARNING'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': log_level,
},
}
logging.basicConfig(
level=log_level,
format='%(asctime)s %(levelname)s %(message)s',
)
# Get a logger instance for this setup file
logger = logging.getLogger(__name__)
# Read the autogenerated key-file
key_file_name = os.path.join(BASE_DIR, 'secret_key.txt')
logger.info(f'Loading SERCRET_KEY from {key_file_name}')
key_file = open(key_file_name, 'r')
SECRET_KEY = key_file.read().strip()
# List of allowed hosts (default = allow all)
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
@ -65,13 +91,6 @@ if cors_opt:
if not CORS_ORIGIN_ALLOW_ALL:
CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', [])
if DEBUG:
# will output to your console
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s %(levelname)s %(message)s',
)
# Web URL endpoint for served static files
STATIC_URL = '/static/'
@ -92,14 +111,18 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media')))
if DEBUG:
print("InvenTree running in DEBUG mode")
print("MEDIA_ROOT:", MEDIA_ROOT)
print("STATIC_ROOT:", STATIC_ROOT)
logger.info("InvenTree running in DEBUG mode")
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
# Does the user wish to use the sentry.io integration?
sentry_opts = CONFIG.get('sentry', {})
if sentry_opts.get('enabled', False):
logger.info("Configuring sentry.io integration")
dsn = sentry_opts.get('dsn', None)
if dsn is not None:
@ -111,11 +134,11 @@ if sentry_opts.get('enabled', False):
sentry_sdk.init(dsn=dsn, integrations=[DjangoIntegration()], send_default_pii=True)
except ModuleNotFoundError:
print("sentry_sdk module not found. Install using 'pip install sentry-sdk'")
logger.error("sentry_sdk module not found. Install using 'pip install sentry-sdk'")
sys.exit(-1)
else:
print("Warning: Sentry.io DSN not specified")
logger.warning("Sentry.io DSN not specified in config file")
# Application definition
@ -160,17 +183,6 @@ INSTALLED_APPS = [
'error_report', # Error reporting in the admin interface
]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
},
}
MIDDLEWARE = CONFIG.get('middleware', [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@ -193,7 +205,7 @@ AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
# If the debug toolbar is enabled, add the modules
if DEBUG and CONFIG.get('debug_toolbar', False):
print("Running with DEBUG_TOOLBAR enabled")
logger.info("Running with DEBUG_TOOLBAR enabled")
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
@ -285,7 +297,7 @@ When running unit tests, enforce usage of sqlite3 database,
so that the tests can be run in RAM without any setup requirements
"""
if 'test' in sys.argv:
eprint('InvenTree: Running tests - Using sqlite3 memory database')
logger.info('InvenTree: Running tests - Using sqlite3 memory database')
DATABASES['default'] = {
# Ensure sqlite3 backend is being used
'ENGINE': 'django.db.backends.sqlite3',
@ -295,14 +307,69 @@ if 'test' in sys.argv:
# Database backend selection
else:
if 'database' in CONFIG:
DATABASES['default'] = CONFIG['database']
"""
Configure the database backend based on the user-specified values.
- Primarily this configuration happens in the config.yaml file
- However there may be reason to configure the DB via environmental variables
- The following code lets the user "mix and match" database configuration
"""
logger.info("Configuring database backend:")
# Extract database configuration from the config.yaml file
db_config = CONFIG.get('database', {})
# If a particular database option is not specified in the config file,
# look for it in the environmental variables
# e.g. INVENTREE_DB_NAME / INVENTREE_DB_USER / etc
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
for key in db_keys:
if key not in db_config:
logger.debug(f" - Missing {key} value: Looking for environment variable INVENTREE_DB_{key}")
env_key = f'INVENTREE_DB_{key}'
env_var = os.environ.get(env_key, None)
if env_var is not None:
logger.info(f'Using environment variable INVENTREE_DB_{key}')
db_config[key] = env_var
else:
eprint("Warning: Database backend not specified - using default (sqlite)")
DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'inventree_db.sqlite3'),
}
logger.debug(f' INVENTREE_DB_{key} not found in environment variables')
# Check that required database configuration options are specified
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'
logger.error(error_msg)
print('Error: ' + error_msg)
sys.exit(-1)
"""
Special considerations for the database 'ENGINE' setting.
It can be specified in config.yaml (or envvar) as either (for example):
- sqlite3
- django.db.backends.sqlite3
- django.db.backends.postgresql
"""
db_engine = db_config['ENGINE']
if db_engine.lower() in ['sqlite3', 'postgresql', 'mysql']:
# Prepend the required python module string
db_engine = f'django.db.backends.{db_engine.lower()}'
db_config['ENGINE'] = db_engine
db_name = db_config['NAME']
logger.info(f"Database ENGINE: '{db_engine}'")
logger.info(f"Database NAME: '{db_name}'")
DATABASES['default'] = db_config
CACHES = {
'default': {
@ -341,7 +408,7 @@ AUTH_PASSWORD_VALIDATORS = [
EXTRA_URL_SCHEMES = CONFIG.get('extra_url_schemes', [])
if not type(EXTRA_URL_SCHEMES) in [list]:
eprint("Warning: extra_url_schemes not correctly formatted")
logger.warning("extra_url_schemes not correctly formatted")
EXTRA_URL_SCHEMES = []
# Internationalization

View File

@ -82,7 +82,7 @@ settings_urls = [
url(r'^purchase-order/?', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'),
url(r'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'),
url(r'^(?P<pk>\d+)/edit/?', SettingEdit.as_view(), name='setting-edit'),
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
# Catch any other urls
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),

View File

@ -7,7 +7,7 @@ import django
import common.models
INVENTREE_SW_VERSION = "0.1.4 pre"
INVENTREE_SW_VERSION = "0.1.5 pre"
def inventreeInstanceName():

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import hashlib
import logging
from InvenTree import plugins as InvenTreePlugins
from barcode import plugins as BarcodePlugins
@ -10,6 +11,9 @@ from stock.serializers import StockItemSerializer, LocationSerializer
from part.serializers import PartSerializer
logger = logging.getLogger(__name__)
def hash_barcode(barcode_data):
"""
Calculate an MD5 hash of barcode data
@ -130,18 +134,17 @@ def load_barcode_plugins(debug=False):
Function to load all barcode plugins
"""
if debug:
print("Loading barcode plugins")
logger.debug("Loading barcode plugins")
plugins = InvenTreePlugins.get_plugins(BarcodePlugins, BarcodePlugin)
if debug:
if len(plugins) > 0:
print("Discovered {n} plugins:".format(n=len(plugins)))
logger.info(f"Discovered {len(plugins)} barcode plugins")
for p in plugins:
print(" - {p}".format(p=p.PLUGIN_NAME))
logger.debug(" - {p}".format(p=p.PLUGIN_NAME))
else:
print("No barcode plugins found")
logger.debug("No barcode plugins found")
return plugins

View File

@ -8,7 +8,8 @@ from __future__ import unicode_literals
import os
from django.db import models
from django.db import models, transaction
from django.db.utils import IntegrityError, OperationalError
from django.conf import settings
import djmoney.settings
@ -16,7 +17,6 @@ from djmoney.models.fields import MoneyField
from djmoney.contrib.exchange.models import convert_money
from djmoney.contrib.exchange.exceptions import MissingRate
from django.db.utils import OperationalError
from django.utils.translation import ugettext as _
from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
@ -230,7 +230,7 @@ class InvenTreeSetting(models.Model):
return None
@classmethod
def get_default_value(cls, key):
def get_setting_default(cls, key):
"""
Return the default value for a particular setting.
@ -281,20 +281,23 @@ class InvenTreeSetting(models.Model):
try:
setting = InvenTreeSetting.objects.filter(key__iexact=key).first()
except OperationalError:
# Settings table has not been created yet!
return None
except (ValueError, InvenTreeSetting.DoesNotExist):
setting = None
except (IntegrityError, OperationalError):
setting = None
# Setting does not exist! (Try to create it)
if not setting:
setting = InvenTreeSetting(key=key, value=InvenTreeSetting.get_setting_default(key))
try:
# Attempt Create the setting if it does not exist
setting = InvenTreeSetting.create(
key=key,
value=InvenTreeSetting.get_default_value(key)
)
except OperationalError:
# Settings table has not been created yet
setting = None
# Wrap this statement in "atomic", so it can be rolled back if it fails
with transaction.atomic():
setting.save()
except (IntegrityError, OperationalError):
# It might be the case that the database isn't created yet
pass
return setting
@ -322,7 +325,7 @@ class InvenTreeSetting(models.Model):
# If no backup value is specified, atttempt to retrieve a "default" value
if backup_value is None:
backup_value = cls.get_default_value(key)
backup_value = cls.get_setting_default(key)
setting = InvenTreeSetting.get_setting_object(key)
@ -380,7 +383,7 @@ class InvenTreeSetting(models.Model):
@property
def default_value(self):
return InvenTreeSetting.get_default_value(self.key)
return InvenTreeSetting.get_setting_default(self.key)
@property
def description(self):
@ -403,6 +406,9 @@ class InvenTreeSetting(models.Model):
if validator is not None:
self.run_validator(validator)
if self.is_bool():
self.value = InvenTree.helpers.str2bool(self.value)
def run_validator(self, validator):
"""
Run a validator against the 'value' field for this InvenTreeSetting object.

View File

@ -0,0 +1,143 @@
"""
Unit tests for the views associated with the 'common' app
"""
# -*- coding: utf-8 -*-
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('INVENTREE_DEFAULT_CURRENCY')
# Default value!
self.assertEqual(setting.value, 'USD')
url = self.get_url(setting.pk)
# Try posting an invalid currency option
data, errors = self.post(url, {'value': 'XPQaaa'}, valid=False)
self.assertIsNotNone(errors.get('value'), None)
# Try posting a valid currency option
data, errors = self.post(url, {'value': 'AUD'}, 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'))

View File

@ -70,7 +70,7 @@ class SettingsTest(TestCase):
for key in InvenTreeSetting.GLOBAL_SETTINGS.keys():
value = InvenTreeSetting.get_default_value(key)
value = InvenTreeSetting.get_setting_default(key)
InvenTreeSetting.set_setting(key, value, self.user)

View File

@ -72,3 +72,32 @@ class SettingEdit(AjaxUpdateView):
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'))

View File

@ -1,12 +1,16 @@
from __future__ import unicode_literals
import os
import logging
from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings
logger = logging.getLogger(__name__)
class CompanyConfig(AppConfig):
name = 'company'
@ -21,7 +25,7 @@ class CompanyConfig(AppConfig):
from .models import Company
print("InvenTree: Checking Company image thumbnails")
logger.debug("Checking Company image thumbnails")
try:
for company in Company.objects.all():
@ -30,11 +34,11 @@ class CompanyConfig(AppConfig):
loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc):
print("InvenTree: Generating thumbnail for Company '{c}'".format(c=company.name))
logger.info("InvenTree: Generating thumbnail for Company '{c}'".format(c=company.name))
try:
company.image.render_variations(replace=False)
except FileNotFoundError:
print("Image file missing")
logger.warning("Image file missing")
company.image = None
company.save()
except (OperationalError, ProgrammingError):

View File

@ -1,22 +1,41 @@
# Database backend selection - Configure backend database settings
# Ref: https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-DATABASES
# Ref: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-DATABASES
# Specify database parameters below as they appear in the Django docs
# Note: Database configuration options can also be specified from environmental variables,
# with the prefix INVENTREE_DB_
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
database:
# Example configuration - sqlite (default)
ENGINE: django.db.backends.sqlite3
# Default configuration - sqlite filesystem database
ENGINE: sqlite3
NAME: '../inventree_default_db.sqlite3'
# For more complex database installations, further parameters are required
# Refer to the django documentation for full list of options
# Example Configuration - MySQL
# --- Available options: ---
# ENGINE: Database engine. Selection from:
# - sqlite3
# - mysql
# - postgresql
# NAME: Database name
# USER: Database username (if required)
# PASSWORD: Database password (if required)
# HOST: Database host address (if required)
# PORT: Database host port (if required)
# --- Example Configuration - sqlite3 ---
# ENGINE: sqlite3
# NAME: '/path/to/database.sqlite3'
# --- Example Configuration - MySQL ---
#ENGINE: django.db.backends.mysql
#NAME: inventree
#USER: inventree_username
#PASSWORD: inventree_password
#HOST: ''
#PORT: ''
#HOST: '127.0.0.1'
#PORT: '5432'
# Select default system language (default is 'en-us')
language: en-us
@ -45,6 +64,9 @@ debug: True
# and only if InvenTree is accessed from a local IP (127.0.0.1)
debug_toolbar: False
# Configure the system logging level
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: WARNING
# Allowed hosts (see ALLOWED_HOSTS in Django settings documentation)
# A list of strings representing the host/domain names that this Django site can serve.

View File

@ -1,12 +1,16 @@
from __future__ import unicode_literals
import os
import logging
from django.db.utils import OperationalError, ProgrammingError
from django.apps import AppConfig
from django.conf import settings
logger = logging.getLogger(__name__)
class PartConfig(AppConfig):
name = 'part'
@ -27,7 +31,7 @@ class PartConfig(AppConfig):
from .models import Part
print("InvenTree: Checking Part image thumbnails")
logger.debug("InvenTree: Checking Part image thumbnails")
try:
for part in Part.objects.all():
@ -36,11 +40,11 @@ class PartConfig(AppConfig):
loc = os.path.join(settings.MEDIA_ROOT, url)
if not os.path.exists(loc):
print("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
logger.info("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
try:
part.image.render_variations(replace=False)
except FileNotFoundError:
print("Image file missing")
logger.warning("Image file missing")
part.image = None
part.save()
except (OperationalError, ProgrammingError):

View File

@ -213,6 +213,7 @@ class EditPartForm(HelperForm):
class Meta:
model = Part
fields = [
'confirm_creation',
'category',
'selected_category_templates',
'parent_category_templates',
@ -222,7 +223,6 @@ class EditPartForm(HelperForm):
'revision',
'bom_copy',
'parameters_copy',
'confirm_creation',
'keywords',
'variant_of',
'link',

View File

@ -350,7 +350,7 @@ class Part(MPTTModel):
# Get part category
category = self.category
if add_category_templates:
if category and add_category_templates:
# Store templates added to part
template_list = []

View File

@ -1320,8 +1320,6 @@ class BomUpload(InvenTreeRoleMixin, FormView):
except KeyError:
pass
print(row, row['part_match'], len(row['part_options']))
def extractDataFromFile(self, bom):
""" Read data from the BOM file """

View File

@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
import logging
import plugins.plugin as plugin
logger = logging.getLogger(__name__)
class ActionPlugin(plugin.InvenTreePlugin):
"""
The ActionPlugin class is used to perform custom actions

View File

@ -3,12 +3,16 @@
import inspect
import importlib
import pkgutil
import logging
# Action plugins
import plugins.action as action
from plugins.action.action import ActionPlugin
logger = logging.getLogger(__name__)
def iter_namespace(pkg):
return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".")
@ -52,14 +56,14 @@ def load_action_plugins():
Return a list of all registered action plugins
"""
print("Loading action plugins")
logger.debug("Loading action plugins")
plugins = get_plugins(action, ActionPlugin)
if len(plugins) > 0:
print("Discovered {n} action plugins:".format(n=len(plugins)))
logger.info("Discovered {n} action plugins:".format(n=len(plugins)))
for ap in plugins:
print(" - {ap}".format(ap=ap.PLUGIN_NAME))
logger.debug(" - {ap}".format(ap=ap.PLUGIN_NAME))
return plugins