diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 1bdfb79ceb..c204c0befb 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -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() diff --git a/InvenTree/InvenTree/ci_mysql.py b/InvenTree/InvenTree/ci_mysql.py index aaaae2ddb0..0a61866082 100644 --- a/InvenTree/InvenTree/ci_mysql.py +++ b/InvenTree/InvenTree/ci_mysql.py @@ -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 diff --git a/InvenTree/InvenTree/ci_postgresql.py b/InvenTree/InvenTree/ci_postgresql.py index 67bb24540f..e235658b96 100644 --- a/InvenTree/InvenTree/ci_postgresql.py +++ b/InvenTree/InvenTree/ci_postgresql.py @@ -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 diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index f4ce28cf30..3fc16464c6 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -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: - with open(cfg_filename, 'r') as cfg: - CONFIG = yaml.safe_load(cfg) + print("Error: config.yaml not found") + sys.exit(-1) -# Read the autogenerated key-file -key_file = open(os.path.join(BASE_DIR, 'secret_key.txt'), 'r') - -SECRET_KEY = key_file.read().strip() +with open(cfg_filename, 'r') as cfg: + CONFIG = yaml.safe_load(cfg) # 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'] - 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'), - } + """ + 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: + 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 diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b7e0d64d18..944c6432b5 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -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\d+)/edit/?', SettingEdit.as_view(), name='setting-edit'), + url(r'^(?P\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'), diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 1e545cc68e..ed9b000f9b 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -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(): diff --git a/InvenTree/barcode/barcode.py b/InvenTree/barcode/barcode.py index c9fac02035..87235db190 100644 --- a/InvenTree/barcode/barcode.py +++ b/InvenTree/barcode/barcode.py @@ -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 diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 53803dd94e..2f27dd602d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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. diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py new file mode 100644 index 0000000000..0cd902d083 --- /dev/null +++ b/InvenTree/common/test_views.py @@ -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')) diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 4666a0a5a6..6d6e517ef5 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -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) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 3bf3769231..1f3c827532 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -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')) diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py index 5f84ce507f..2777425ab4 100644 --- a/InvenTree/company/apps.py +++ b/InvenTree/company/apps.py @@ -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): diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 310fedd70e..bb1e20fceb 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -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. diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 2f72537136..b1089cd57c 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -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): diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index c5281b30e6..68912edd98 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -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', diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8501fc82b3..4730ba1e9c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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 = [] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 4de1f0ea02..e2f03b89b8 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -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 """ diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py index 4e0b0f5cb0..8d70302021 100644 --- a/InvenTree/plugins/action/action.py +++ b/InvenTree/plugins/action/action.py @@ -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 diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index ae6e0630e5..abb167d173 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -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