mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
commit
a2ff3e3474
2
.github/workflows/coverage.yaml
vendored
2
.github/workflows/coverage.yaml
vendored
@ -16,6 +16,8 @@ jobs:
|
|||||||
INVENTREE_DB_NAME: './test_db.sqlite'
|
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
|
18
.github/workflows/docker_build.yaml
vendored
Normal file
18
.github/workflows/docker_build.yaml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Test that the docker file builds correctly
|
||||||
|
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build Server Image
|
||||||
|
run: cd docker/inventree && docker build . --tag inventree:$(date +%s)
|
||||||
|
- name: Build nginx Image
|
||||||
|
run: cd docker/nginx && docker build . --tag nxinx:$(date +%s)
|
||||||
|
|
38
.github/workflows/docker_publish.yaml
vendored
Normal file
38
.github/workflows/docker_publish.yaml
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Publish docker images to dockerhub
|
||||||
|
|
||||||
|
name: Docker Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
server_image:
|
||||||
|
name: Push InvenTree web server image to dockerhub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
repository: inventree/inventree
|
||||||
|
tag_with_ref: true
|
||||||
|
context: docker/inventree
|
||||||
|
|
||||||
|
nginx_image:
|
||||||
|
name: Push InvenTree nginx image to dockerhub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
repository: inventree/nginx
|
||||||
|
tag_with_ref: true
|
||||||
|
context: docker/nginx
|
46
.github/workflows/mariadb.yaml
vendored
46
.github/workflows/mariadb.yaml
vendored
@ -1,46 +0,0 @@
|
|||||||
name: MariaDB
|
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Database backend configuration
|
|
||||||
INVENTREE_DB_ENGINE: django.db.backends.mysql
|
|
||||||
INVENTREE_DB_NAME: inventree
|
|
||||||
INVENTREE_DB_USER: root
|
|
||||||
INVENTREE_DB_PASSWORD: password
|
|
||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
|
||||||
INVENTREE_DB_PORT: 3306
|
|
||||||
INVENTREE_DEBUG: info
|
|
||||||
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:latest
|
|
||||||
env:
|
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
|
||||||
MYSQL_DATABASE: inventree
|
|
||||||
MYSQL_USER: inventree
|
|
||||||
MYSQL_PASSWORD: password
|
|
||||||
MYSQL_ROOT_PASSWORD: password
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.7
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get install mysql-server libmysqlclient-dev
|
|
||||||
pip3 install invoke
|
|
||||||
pip3 install mysqlclient
|
|
||||||
invoke install
|
|
||||||
- name: Run Tests
|
|
||||||
run: invoke test
|
|
2
.github/workflows/mysql.yaml
vendored
2
.github/workflows/mysql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
INVENTREE_DB_HOST: '127.0.0.1'
|
||||||
INVENTREE_DB_PORT: 3306
|
INVENTREE_DB_PORT: 3306
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
|
2
.github/workflows/postgresql.yaml
vendored
2
.github/workflows/postgresql.yaml
vendored
@ -18,6 +18,8 @@ jobs:
|
|||||||
INVENTREE_DB_HOST: '127.0.0.1'
|
INVENTREE_DB_HOST: '127.0.0.1'
|
||||||
INVENTREE_DB_PORT: 5432
|
INVENTREE_DB_PORT: 5432
|
||||||
INVENTREE_DEBUG: info
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -19,11 +19,12 @@ from rest_framework.views import APIView
|
|||||||
|
|
||||||
from .views import AjaxView
|
from .views import AjaxView
|
||||||
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
|
||||||
|
from .status import is_worker_running
|
||||||
|
|
||||||
from plugins import plugins as inventree_plugins
|
from plugins import plugins as inventree_plugins
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
logger.info("Loading action plugins...")
|
logger.info("Loading action plugins...")
|
||||||
@ -44,6 +45,7 @@ class InfoView(AjaxView):
|
|||||||
'version': inventreeVersion(),
|
'version': inventreeVersion(),
|
||||||
'instance': inventreeInstanceName(),
|
'instance': inventreeInstanceName(),
|
||||||
'apiVersion': inventreeApiVersion(),
|
'apiVersion': inventreeApiVersion(),
|
||||||
|
'worker_running': is_worker_running(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
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
|
request._inventree_health_status = True
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
"system_healthy": InvenTree.status.check_system_health(),
|
'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):
|
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
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class AuthRequiredMiddleware(object):
|
class AuthRequiredMiddleware(object):
|
||||||
|
@ -13,6 +13,9 @@ database setup in this file.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -46,14 +49,31 @@ def get_setting(environment_var, backup_val, default_value=None):
|
|||||||
return default_value
|
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, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
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):
|
if not os.path.exists(cfg_filename):
|
||||||
print("Error: config.yaml not found")
|
print("InvenTree configuration file 'config.yaml' not found - creating default file")
|
||||||
sys.exit(-1)
|
|
||||||
|
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:
|
with open(cfg_filename, 'r') as cfg:
|
||||||
CONFIG = yaml.safe_load(cfg)
|
CONFIG = yaml.safe_load(cfg)
|
||||||
@ -94,7 +114,18 @@ LOGGING = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Get a logger instance for this setup file
|
# 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"):
|
if os.getenv("INVENTREE_SECRET_KEY"):
|
||||||
# Secret key passed in directly
|
# Secret key passed in directly
|
||||||
@ -105,15 +136,22 @@ else:
|
|||||||
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
|
||||||
|
|
||||||
if key_file:
|
if key_file:
|
||||||
if os.path.isfile(key_file):
|
key_file = os.path.abspath(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)
|
|
||||||
else:
|
else:
|
||||||
# default secret key location
|
# default secret key location
|
||||||
key_file = os.path.join(BASE_DIR, "secret_key.txt")
|
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:
|
try:
|
||||||
SECRET_KEY = open(key_file, "r").read().strip()
|
SECRET_KEY = open(key_file, "r").read().strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -144,7 +182,7 @@ STATIC_URL = '/static/'
|
|||||||
STATIC_ROOT = os.path.abspath(
|
STATIC_ROOT = os.path.abspath(
|
||||||
get_setting(
|
get_setting(
|
||||||
'INVENTREE_STATIC_ROOT',
|
'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(
|
MEDIA_ROOT = os.path.abspath(
|
||||||
get_setting(
|
get_setting(
|
||||||
'INVENTREE_MEDIA_ROOT',
|
'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',
|
'report.apps.ReportConfig',
|
||||||
'stock.apps.StockConfig',
|
'stock.apps.StockConfig',
|
||||||
'users.apps.UsersConfig',
|
'users.apps.UsersConfig',
|
||||||
|
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
|
||||||
|
|
||||||
# Third part add-ons
|
# Third part add-ons
|
||||||
'django_filters', # Extended filter functionality
|
'django_filters', # Extended filter functionality
|
||||||
@ -211,6 +250,7 @@ INSTALLED_APPS = [
|
|||||||
'djmoney', # django-money integration
|
'djmoney', # django-money integration
|
||||||
'djmoney.contrib.exchange', # django-money exchange rates
|
'djmoney.contrib.exchange', # django-money exchange rates
|
||||||
'error_report', # Error reporting in the admin interface
|
'error_report', # Error reporting in the admin interface
|
||||||
|
'django_q',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = CONFIG.get('middleware', [
|
MIDDLEWARE = CONFIG.get('middleware', [
|
||||||
@ -285,6 +325,18 @@ REST_FRAMEWORK = {
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
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
|
# Markdownx configuration
|
||||||
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
# Ref: https://neutronx.github.io/django-markdownx/customization/
|
||||||
MARKDOWNX_MEDIA_PATH = datetime.now().strftime('markdownx/%Y/%m/%d')
|
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
|
# Extract database configuration from the config.yaml file
|
||||||
db_config = CONFIG.get('database', {})
|
db_config = CONFIG.get('database', {})
|
||||||
|
|
||||||
|
if not db_config:
|
||||||
|
db_config = {}
|
||||||
|
|
||||||
# Environment variables take preference over config file!
|
# Environment variables take preference over config file!
|
||||||
|
|
||||||
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
|
||||||
@ -350,7 +405,7 @@ reqiured_keys = ['ENGINE', 'NAME']
|
|||||||
|
|
||||||
for key in reqiured_keys:
|
for key in reqiured_keys:
|
||||||
if key not in db_config:
|
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)
|
logger.error(error_msg)
|
||||||
|
|
||||||
print('Error: ' + error_msg)
|
print('Error: ' + error_msg)
|
||||||
@ -386,11 +441,6 @@ CACHES = {
|
|||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
},
|
},
|
||||||
'qr-code': {
|
|
||||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
||||||
'LOCATION': 'qr-code-cache',
|
|
||||||
'TIMEOUT': 3600
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
@ -449,13 +499,19 @@ LOCALE_PATHS = (
|
|||||||
os.path.join(BASE_DIR, 'locale/'),
|
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_I18N = True
|
||||||
|
|
||||||
USE_L10N = 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 = [
|
DATE_INPUT_FORMATS = [
|
||||||
"%Y-%m-%d",
|
"%Y-%m-%d",
|
||||||
|
@ -1,13 +1,46 @@
|
|||||||
"""
|
"""
|
||||||
Provides system status functionality checks.
|
Provides system status functionality checks.
|
||||||
"""
|
"""
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import logging
|
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):
|
def check_system_health(**kwargs):
|
||||||
@ -19,21 +52,11 @@ def check_system_health(**kwargs):
|
|||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
if not check_celery_worker(**kwargs):
|
if not is_worker_running(**kwargs):
|
||||||
result = False
|
result = False
|
||||||
logger.warning(_("Celery worker check failed"))
|
logger.warning(_("Background worker check failed"))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
logger.warning(_("InvenTree system health checks failed"))
|
logger.warning(_("InvenTree system health checks failed"))
|
||||||
|
|
||||||
return result
|
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 .validators import validate_overage, validate_part_name
|
||||||
from . import helpers
|
from . import helpers
|
||||||
|
from . import version
|
||||||
|
|
||||||
from mptt.exceptions import InvalidMove
|
from mptt.exceptions import InvalidMove
|
||||||
|
|
||||||
@ -269,3 +270,33 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
e("10, a, 7-70j", 4)
|
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,6 +4,7 @@ Provides information on the current InvenTree version
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import django
|
import django
|
||||||
|
import re
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
@ -23,6 +24,38 @@ def inventreeVersion():
|
|||||||
return INVENTREE_SW_VERSION
|
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():
|
def inventreeApiVersion():
|
||||||
return INVENTREE_API_VERSION
|
return INVENTREE_API_VERSION
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from stock.serializers import StockItemSerializer, LocationSerializer
|
|||||||
from part.serializers import PartSerializer
|
from part.serializers import PartSerializer
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def hash_barcode(barcode_data):
|
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.
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class CompanyConfig(AppConfig):
|
class CompanyConfig(AppConfig):
|
||||||
|
@ -7,11 +7,9 @@
|
|||||||
# with the prefix INVENTREE_DB_
|
# with the prefix INVENTREE_DB_
|
||||||
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
# e.g INVENTREE_DB_NAME / INVENTREE_DB_USER / INVENTREE_DB_PASSWORD
|
||||||
database:
|
database:
|
||||||
# Default configuration - sqlite filesystem database
|
# Uncomment (and edit) one of the database configurations below,
|
||||||
ENGINE: sqlite3
|
# or specify database options using environment variables
|
||||||
NAME: '../inventree_default_db.sqlite3'
|
|
||||||
|
|
||||||
# For more complex database installations, further parameters are required
|
|
||||||
# Refer to the django documentation for full list of options
|
# Refer to the django documentation for full list of options
|
||||||
|
|
||||||
# --- Available options: ---
|
# --- Available options: ---
|
||||||
@ -27,14 +25,22 @@ database:
|
|||||||
|
|
||||||
# --- Example Configuration - sqlite3 ---
|
# --- Example Configuration - sqlite3 ---
|
||||||
# ENGINE: sqlite3
|
# ENGINE: sqlite3
|
||||||
# NAME: '/path/to/database.sqlite3'
|
# NAME: '/home/inventree/database.sqlite3'
|
||||||
|
|
||||||
# --- Example Configuration - MySQL ---
|
# --- Example Configuration - MySQL ---
|
||||||
#ENGINE: django.db.backends.mysql
|
#ENGINE: mysql
|
||||||
#NAME: inventree
|
#NAME: inventree
|
||||||
#USER: inventree_username
|
#USER: inventree
|
||||||
#PASSWORD: inventree_password
|
#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'
|
#PORT: '5432'
|
||||||
|
|
||||||
# Select default system language (default is 'en-us')
|
# Select default system language (default is 'en-us')
|
||||||
@ -43,6 +49,7 @@ language: en-us
|
|||||||
# System time-zone (default is UTC)
|
# System time-zone (default is UTC)
|
||||||
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
# Select an option from the "TZ database name" column
|
# Select an option from the "TZ database name" column
|
||||||
|
# Use the environment variable INVENTREE_TIMEZONE
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
|
|
||||||
# List of currencies supported by default.
|
# List of currencies supported by default.
|
||||||
@ -57,6 +64,7 @@ currencies:
|
|||||||
- USD
|
- USD
|
||||||
|
|
||||||
# Set debug to False to run in production mode
|
# Set debug to False to run in production mode
|
||||||
|
# Use the environment variable INVENTREE_DEBUG
|
||||||
debug: True
|
debug: True
|
||||||
|
|
||||||
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
||||||
@ -65,6 +73,7 @@ debug: True
|
|||||||
debug_toolbar: False
|
debug_toolbar: False
|
||||||
|
|
||||||
# Configure the system logging level
|
# Configure the system logging level
|
||||||
|
# Use environment variable INVENTREE_LOG_LEVEL
|
||||||
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||||
log_level: WARNING
|
log_level: WARNING
|
||||||
|
|
||||||
@ -86,13 +95,14 @@ cors:
|
|||||||
# - https://sub.example.com
|
# - https://sub.example.com
|
||||||
|
|
||||||
# MEDIA_ROOT is the local filesystem location for storing uploaded files
|
# 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
|
# By default, it is stored under /home/inventree/data/media
|
||||||
# This should be changed for a production installation
|
# Use environment variable INVENTREE_MEDIA_ROOT
|
||||||
media_root: '../inventree_media'
|
media_root: '/home/inventree/data/media'
|
||||||
|
|
||||||
# STATIC_ROOT is the local filesystem location for storing static files
|
# 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
|
# By default, it is stored under /home/inventree
|
||||||
static_root: '../inventree_static'
|
# Use environment variable INVENTREE_STATIC_ROOT
|
||||||
|
static_root: '/home/inventree/static'
|
||||||
|
|
||||||
# Optional URL schemes to allow in URL fields
|
# Optional URL schemes to allow in URL fields
|
||||||
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
||||||
@ -105,7 +115,8 @@ static_root: '../inventree_static'
|
|||||||
# Backup options
|
# Backup options
|
||||||
# Set the backup_dir parameter to store backup files in a specific location
|
# Set the backup_dir parameter to store backup files in a specific location
|
||||||
# If unspecified, the local user's temp directory will be used
|
# 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
|
# Permit custom authentication backends
|
||||||
#authentication_backends:
|
#authentication_backends:
|
||||||
|
@ -7,7 +7,7 @@ from django.apps import AppConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def hashFile(filename):
|
def hashFile(filename):
|
||||||
|
@ -32,7 +32,7 @@ except OSError as err:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def rename_label(instance, filename):
|
def rename_label(instance, filename):
|
||||||
|
@ -37,7 +37,7 @@ from InvenTree.views import InvenTreeRoleMixin
|
|||||||
|
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
|
||||||
|
@ -9,7 +9,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PartConfig(AppConfig):
|
class PartConfig(AppConfig):
|
||||||
|
@ -52,7 +52,7 @@ import common.models
|
|||||||
import part.settings as part_settings
|
import part.settings as part_settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class PartCategory(InvenTreeTree):
|
class PartCategory(InvenTreeTree):
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
import plugins.plugin as plugin
|
import plugins.plugin as plugin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ActionPlugin(plugin.InvenTreePlugin):
|
class ActionPlugin(plugin.InvenTreePlugin):
|
||||||
|
@ -10,7 +10,7 @@ import plugins.action as action
|
|||||||
from plugins.action.action import ActionPlugin
|
from plugins.action.action import ActionPlugin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
def iter_namespace(pkg):
|
def iter_namespace(pkg):
|
||||||
|
@ -6,7 +6,7 @@ from django.apps import AppConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ReportConfig(AppConfig):
|
class ReportConfig(AppConfig):
|
||||||
|
@ -38,7 +38,7 @@ except OSError as err:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class ReportFileUpload(FileSystemStorage):
|
class ReportFileUpload(FileSystemStorage):
|
||||||
|
@ -19,11 +19,20 @@
|
|||||||
<col width='25'>
|
<col width='25'>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-hashtag'></span></td>
|
<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>
|
</tr>
|
||||||
{% inventree_commit_hash as hash %}
|
{% inventree_commit_hash as hash %}
|
||||||
{% if hash %}
|
{% if hash %}
|
||||||
@ -69,4 +78,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,7 +60,9 @@
|
|||||||
<li class='dropdown'>
|
<li class='dropdown'>
|
||||||
<a class='dropdown-toggle' data-toggle='dropdown' href="#">
|
<a class='dropdown-toggle' data-toggle='dropdown' href="#">
|
||||||
{% if not system_healthy %}
|
{% 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 %}
|
{% endif %}
|
||||||
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
|
<span class="fas fa-user"></span> <b>{{ user.get_username }}</b></a>
|
||||||
<ul class='dropdown-menu'>
|
<ul class='dropdown-menu'>
|
||||||
@ -78,11 +80,20 @@
|
|||||||
{% if system_healthy %}
|
{% if system_healthy %}
|
||||||
<span class='fas fa-server'>
|
<span class='fas fa-server'>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class='fas fa-exclamation-triangle icon-red'>
|
<span class='fas fa-server icon-red'>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span> {% trans "System Information" %}
|
</span> {% trans "System Information" %}
|
||||||
</a></li>
|
</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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -13,8 +13,9 @@
|
|||||||
<td>{% trans "Instance Name" %}</td>
|
<td>{% trans "Instance Name" %}</td>
|
||||||
<td>{% inventree_instance_name %}</td>
|
<td>{% inventree_instance_name %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if user.is_staff %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-exclamation-triangle'></span></td>
|
<td><span class='fas fa-server'></span></td>
|
||||||
<td>{% trans "Server status" %}</td>
|
<td>{% trans "Server status" %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if system_healthy %}
|
{% if system_healthy %}
|
||||||
@ -24,6 +25,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% if not system_healthy %}
|
||||||
{% for issue in system_issues %}
|
{% for issue in system_issues %}
|
||||||
|
@ -15,7 +15,7 @@ from django.db.models.signals import post_save, post_delete
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
class RuleSet(models.Model):
|
class RuleSet(models.Model):
|
||||||
@ -137,6 +137,13 @@ class RuleSet(models.Model):
|
|||||||
'error_report_error',
|
'error_report_error',
|
||||||
'exchange_rate',
|
'exchange_rate',
|
||||||
'exchange_exchangebackend',
|
'exchange_exchangebackend',
|
||||||
|
|
||||||
|
# Django-q
|
||||||
|
'django_q_ormq',
|
||||||
|
'django_q_failure',
|
||||||
|
'django_q_task',
|
||||||
|
'django_q_schedule',
|
||||||
|
'django_q_success',
|
||||||
]
|
]
|
||||||
|
|
||||||
RULE_OPTIONS = [
|
RULE_OPTIONS = [
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
||||||
|
[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/inventree/inventree)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
|
[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
|
||||||
![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
|
![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
|
||||||
|
![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker.yaml/badge.svg)
|
||||||
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
|
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
|
||||||
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
|
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)
|
||||||
![MariaDB](https://github.com/inventree/inventree/actions/workflows/mariadb.yaml/badge.svg)
|
|
||||||
![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
|
![PostgreSQL](https://github.com/inventree/inventree/actions/workflows/postgresql.yaml/badge.svg)
|
||||||
|
|
||||||
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/>
|
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/>
|
||||||
|
46
deploy/supervisord.conf
Normal file
46
deploy/supervisord.conf
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
; # Supervisor Config File
|
||||||
|
; Example configuration file for running InvenTree using supervisor
|
||||||
|
; There are two separate processes which must be managed:
|
||||||
|
;
|
||||||
|
; ## Web Server
|
||||||
|
; The InvenTree server must be launched and managed as a process
|
||||||
|
; The recommended way to handle the web server is to use gunicorn
|
||||||
|
;
|
||||||
|
; ## Background Tasks
|
||||||
|
; A background task manager processes long-running and periodic tasks
|
||||||
|
; InvenTree uses django-q for this purpose
|
||||||
|
|
||||||
|
[supervisord]
|
||||||
|
; Change this path if log files are stored elsewhere
|
||||||
|
logfile=/home/inventree/log/supervisor.log
|
||||||
|
user=inventree
|
||||||
|
|
||||||
|
[supervisorctl]
|
||||||
|
|
||||||
|
[inet_http_server]
|
||||||
|
port = 127.0.0.1:9001
|
||||||
|
|
||||||
|
; InvenTree Web Server Process
|
||||||
|
[program:inventree-server]
|
||||||
|
user=inventree
|
||||||
|
directory=/home/inventree/src/InvenTree
|
||||||
|
command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
|
||||||
|
startsecs=10
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startretries=3
|
||||||
|
; Change these paths if log files are stored elsewhere
|
||||||
|
stderr_logfile=/home/inventree/log/server.err.log
|
||||||
|
stdout_logfile=/home/inventree/log/server.out.log
|
||||||
|
|
||||||
|
; InvenTree Background Worker Process
|
||||||
|
[program:inventree-cluster]
|
||||||
|
user=inventree
|
||||||
|
directory=/home/inventree/src/InvenTree
|
||||||
|
command=/home/inventree/env/bin/python manage.py qcluster
|
||||||
|
startsecs=10
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
; Change these paths if log files are stored elsewhere
|
||||||
|
stderr_logfile=/home/inventree/log/cluster.err.log
|
||||||
|
stdout_logfile=/home/inventree/log/cluster.out.log
|
100
docker/docker-compose.yml
Normal file
100
docker/docker-compose.yml
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
# Docker compose recipe for InvenTree
|
||||||
|
# - Runs PostgreSQL as the database backend
|
||||||
|
# - Runs Gunicorn as the web server
|
||||||
|
# - Runs nginx as a reverse proxy
|
||||||
|
# - Runs the background worker process
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# IMPORTANT - READ BEFORE STARTING!
|
||||||
|
# ---------------------------------
|
||||||
|
# Before running, ensure that you change the "/path/to/data" directory,
|
||||||
|
# specified in the "volumes" section at the end of this file.
|
||||||
|
# This path determines where the InvenTree data will be stored!
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Database service
|
||||||
|
# Use PostgreSQL as the database backend
|
||||||
|
# Note: this can be changed to a different backend,
|
||||||
|
# just make sure that you change the INVENTREE_DB_xxx vars below
|
||||||
|
db:
|
||||||
|
container_name: db
|
||||||
|
image: postgres
|
||||||
|
ports:
|
||||||
|
- 5432/tcp
|
||||||
|
environment:
|
||||||
|
- PGDATA=/var/lib/postgresql/data/pgdb
|
||||||
|
- POSTGRES_USER=pguser
|
||||||
|
- POSTGRES_PASSWORD=pgpassword
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/postgresql/data/
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# InvenTree web server services
|
||||||
|
# Uses gunicorn as the web server
|
||||||
|
inventree:
|
||||||
|
container_name: server
|
||||||
|
image: inventree/inventree:latest
|
||||||
|
expose:
|
||||||
|
- 8080
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
volumes:
|
||||||
|
- data:/home/inventree/data
|
||||||
|
- static:/home/inventree/static
|
||||||
|
environment:
|
||||||
|
- INVENTREE_DB_ENGINE=postgresql
|
||||||
|
- INVENTREE_DB_NAME=inventree
|
||||||
|
- INVENTREE_DB_USER=pguser
|
||||||
|
- INVENTREE_DB_PASSWORD=pgpassword
|
||||||
|
- INVENTREE_DB_PORT=5432
|
||||||
|
- INVENTREE_DB_HOST=db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# nginx acts as a reverse proxy
|
||||||
|
# static files are served by nginx
|
||||||
|
# web requests are redirected to gunicorn
|
||||||
|
nginx:
|
||||||
|
container_name: nginx
|
||||||
|
image: inventree/nginx:latest
|
||||||
|
depends_on:
|
||||||
|
- inventree
|
||||||
|
ports:
|
||||||
|
# Change "1337" to the port where you want InvenTree web server to be available
|
||||||
|
- 1337:80
|
||||||
|
volumes:
|
||||||
|
- static:/home/inventree/static
|
||||||
|
|
||||||
|
# background worker process handles long-running or periodic tasks
|
||||||
|
worker:
|
||||||
|
container_name: worker
|
||||||
|
image: inventree/inventree:latest
|
||||||
|
entrypoint: ./start_worker.sh
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- inventree
|
||||||
|
volumes:
|
||||||
|
- data:/home/inventree/data
|
||||||
|
- static:/home/inventree/static
|
||||||
|
environment:
|
||||||
|
- INVENTREE_DB_ENGINE=postgresql
|
||||||
|
- INVENTREE_DB_NAME=inventree
|
||||||
|
- INVENTREE_DB_USER=pguser
|
||||||
|
- INVENTREE_DB_PASSWORD=pgpassword
|
||||||
|
- INVENTREE_DB_PORT=5432
|
||||||
|
- INVENTREE_DB_HOST=db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Static files, shared between containers
|
||||||
|
static:
|
||||||
|
# Persistent data, stored externally
|
||||||
|
data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
# This directory specified where InvenTree data are stored "outside" the docker containers
|
||||||
|
# Change this path to a local system path where you want InvenTree data stored
|
||||||
|
device: /path/to/data
|
95
docker/inventree/Dockerfile
Normal file
95
docker/inventree/Dockerfile
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
FROM python:alpine as production
|
||||||
|
|
||||||
|
# GitHub source
|
||||||
|
ARG repository="https://github.com/inventree/InvenTree.git"
|
||||||
|
ARG branch="master"
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# InvenTree key settings
|
||||||
|
ENV INVENTREE_HOME="/home/inventree"
|
||||||
|
|
||||||
|
# GitHub settings
|
||||||
|
ENV INVENTREE_REPO="${repository}"
|
||||||
|
ENV INVENTREE_BRANCH="${branch}"
|
||||||
|
|
||||||
|
ENV INVENTREE_LOG_LEVEL="INFO"
|
||||||
|
|
||||||
|
# InvenTree paths
|
||||||
|
ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
|
||||||
|
ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
|
||||||
|
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
||||||
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
|
||||||
|
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
||||||
|
ENV INVENTREE_BACKUP_DIR="${INVENTREE_DATA_DIR}/backup"
|
||||||
|
|
||||||
|
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||||
|
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DATA_DIR}/secret_key.txt"
|
||||||
|
|
||||||
|
# Pass DB configuration through as environment variables
|
||||||
|
ENV INVENTREE_DB_ENGINE="${INVENTREE_DB_ENGINE}"
|
||||||
|
ENV INVENTREE_DB_NAME="${INVENTREE_DB_NAME}"
|
||||||
|
ENV INVENTREE_DB_HOST="${INVENTREE_DB_HOST}"
|
||||||
|
ENV INVENTREE_DB_PORT="${INVENTREE_DB_PORT}"
|
||||||
|
ENV INVENTREE_DB_USER="${INVENTREE_DB_USER}"
|
||||||
|
ENV INVENTREE_DB_PASSWORD="${INVENTREE_DB_PASSWORD}"
|
||||||
|
|
||||||
|
LABEL org.label-schema.schema-version="1.0" \
|
||||||
|
org.label-schema.build-date=${DATE} \
|
||||||
|
org.label-schema.vendor="inventree" \
|
||||||
|
org.label-schema.name="inventree/inventree" \
|
||||||
|
org.label-schema.url="https://hub.docker.com/r/inventree/inventree" \
|
||||||
|
org.label-schema.version=${INVENTREE_VERSION} \
|
||||||
|
org.label-schema.vcs-url=${INVENTREE_REPO} \
|
||||||
|
org.label-schema.vcs-branch=${BRANCH} \
|
||||||
|
org.label-schema.vcs-ref=${COMMIT}
|
||||||
|
|
||||||
|
# Create user account
|
||||||
|
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
|
||||||
|
WORKDIR ${INVENTREE_HOME}
|
||||||
|
|
||||||
|
RUN mkdir ${INVENTREE_STATIC_ROOT}
|
||||||
|
|
||||||
|
# Install required system packages
|
||||||
|
RUN apk add --no-cache git make bash \
|
||||||
|
gcc libgcc g++ libstdc++ \
|
||||||
|
libjpeg-turbo libjpeg-turbo-dev jpeg jpeg-dev \
|
||||||
|
libffi libffi-dev \
|
||||||
|
zlib zlib-dev
|
||||||
|
RUN apk add --no-cache cairo cairo-dev pango pango-dev
|
||||||
|
RUN apk add --no-cache fontconfig ttf-droid ttf-liberation ttf-dejavu ttf-opensans ttf-ubuntu-font-family font-croscore font-noto
|
||||||
|
RUN apk add --no-cache python3
|
||||||
|
RUN apk add --no-cache postgresql-contrib postgresql-dev libpq
|
||||||
|
RUN apk add --no-cache mariadb-connector-c mariadb-dev
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
#RUN mkdir ${INVENTREE_DATA_DIR}}/media ${INVENTREE_HOME}/static ${INVENTREE_HOME}/backup
|
||||||
|
|
||||||
|
# Install required python packages
|
||||||
|
RUN pip install --upgrade pip setuptools wheel
|
||||||
|
RUN pip install --no-cache-dir -U invoke
|
||||||
|
RUN pip install --no-cache-dir -U psycopg2 mysqlclient pgcli mariadb
|
||||||
|
RUN pip install --no-cache-dir -U gunicorn
|
||||||
|
|
||||||
|
# Clone source code
|
||||||
|
RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
|
||||||
|
RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
|
||||||
|
|
||||||
|
# Install InvenTree packages
|
||||||
|
RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
|
||||||
|
|
||||||
|
# Copy gunicorn config file
|
||||||
|
COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
|
||||||
|
|
||||||
|
# Copy startup scripts
|
||||||
|
COPY start_server.sh ${INVENTREE_SRC_DIR}/start_server.sh
|
||||||
|
COPY start_worker.sh ${INVENTREE_SRC_DIR}/start_worker.sh
|
||||||
|
|
||||||
|
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_server.sh
|
||||||
|
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
|
||||||
|
|
||||||
|
# exec commands should be executed from the "src" directory
|
||||||
|
WORKDIR ${INVENTREE_SRC_DIR}
|
||||||
|
|
||||||
|
# Let us begin
|
||||||
|
CMD ["bash", "./start_server.sh"]
|
6
docker/inventree/gunicorn.conf.py
Normal file
6
docker/inventree/gunicorn.conf.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
workers = multiprocessing.cpu_count() * 2 + 1
|
||||||
|
|
||||||
|
max_requests = 1000
|
||||||
|
max_requests_jitter = 50
|
46
docker/inventree/start_server.sh
Normal file
46
docker/inventree/start_server.sh
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Create required directory structure (if it does not already exist)
|
||||||
|
if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
|
||||||
|
echo "Creating directory $INVENTREE_STATIC_ROOT"
|
||||||
|
mkdir $INVENTREE_STATIC_ROOT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
|
||||||
|
echo "Creating directory $INVENTREE_MEDIA_ROOT"
|
||||||
|
mkdir $INVENTREE_MEDIA_ROOT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$INVENTREE_BACKUP_DIR" ]]; then
|
||||||
|
echo "Creating directory $INVENTREE_BACKUP_DIR"
|
||||||
|
mkdir $INVENTREE_BACKUP_DIR
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if "config.yaml" has been copied into the correct location
|
||||||
|
if test -f "$INVENTREE_CONFIG_FILE"; then
|
||||||
|
echo "$INVENTREE_CONFIG_FILE exists - skipping"
|
||||||
|
else
|
||||||
|
echo "Copying config file to $INVENTREE_CONFIG_FILE"
|
||||||
|
cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting InvenTree server..."
|
||||||
|
|
||||||
|
# Wait for the database to be ready
|
||||||
|
cd $INVENTREE_MNG_DIR
|
||||||
|
python manage.py wait_for_db
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo "Running InvenTree database migrations and collecting static files..."
|
||||||
|
|
||||||
|
# We assume at this stage that the database is up and running
|
||||||
|
# Ensure that the database schema are up to date
|
||||||
|
python manage.py check || exit 1
|
||||||
|
python manage.py migrate --noinput || exit 1
|
||||||
|
python manage.py migrate --run-syncdb || exit 1
|
||||||
|
python manage.py collectstatic --noinput || exit 1
|
||||||
|
python manage.py clearsessions || exit 1
|
||||||
|
|
||||||
|
# Now we can launch the server
|
||||||
|
gunicorn -c $INVENTREE_HOME/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8080
|
14
docker/inventree/start_worker.sh
Normal file
14
docker/inventree/start_worker.sh
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Starting InvenTree worker..."
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Wait for the database to be ready
|
||||||
|
cd $INVENTREE_MNG_DIR
|
||||||
|
python manage.py wait_for_db
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Now we can launch the background worker process
|
||||||
|
python manage.py qcluster
|
14
docker/nginx/Dockerfile
Normal file
14
docker/nginx/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM nginx:1.19.0-alpine
|
||||||
|
|
||||||
|
# Create user account
|
||||||
|
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
|
||||||
|
|
||||||
|
ENV HOME=/home/inventree
|
||||||
|
WORKDIR $HOME
|
||||||
|
|
||||||
|
# Create the "static" volume directory
|
||||||
|
RUN mkdir $HOME/static
|
||||||
|
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d
|
||||||
|
|
21
docker/nginx/nginx.conf
Normal file
21
docker/nginx/nginx.conf
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
upstream inventree {
|
||||||
|
server inventree:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://inventree;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect off;
|
||||||
|
client_max_body_size 100M;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /home/inventree/static/;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
invoke>=1.4.0 # Invoke build tool
|
||||||
wheel>=0.34.2 # Wheel
|
wheel>=0.34.2 # Wheel
|
||||||
Django==3.0.7 # Django package
|
Django==3.0.7 # Django package
|
||||||
pillow==8.1.1 # Image manipulation
|
pillow==8.1.1 # Image manipulation
|
||||||
@ -30,5 +31,7 @@ django-error-report==0.2.0 # Error report viewer for the admin interface
|
|||||||
django-test-migrations==1.1.0 # Unit testing for database migrations
|
django-test-migrations==1.1.0 # Unit testing for database migrations
|
||||||
python-barcode[images]==0.13.1 # Barcode generator
|
python-barcode[images]==0.13.1 # Barcode generator
|
||||||
qrcode[pil]==6.1 # QR code generator
|
qrcode[pil]==6.1 # QR code generator
|
||||||
|
django-q==1.3.4 # Background task scheduling
|
||||||
|
gunicorn>=20.0.4 # Gunicorn web server
|
||||||
|
|
||||||
inventree # Install the latest version of the InvenTree API python library
|
inventree # Install the latest version of the InvenTree API python library
|
||||||
|
78
tasks.py
78
tasks.py
@ -1,13 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from invoke import task
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from invoke import ctask as task
|
||||||
|
except:
|
||||||
|
from invoke import task
|
||||||
|
|
||||||
|
|
||||||
def apps():
|
def apps():
|
||||||
"""
|
"""
|
||||||
Returns a list of installed apps
|
Returns a list of installed apps
|
||||||
@ -27,6 +29,7 @@ def apps():
|
|||||||
'users',
|
'users',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def localDir():
|
def localDir():
|
||||||
"""
|
"""
|
||||||
Returns the directory of *THIS* file.
|
Returns the directory of *THIS* file.
|
||||||
@ -35,6 +38,7 @@ def localDir():
|
|||||||
"""
|
"""
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
return os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
def managePyDir():
|
def managePyDir():
|
||||||
"""
|
"""
|
||||||
Returns the directory of the manage.py file
|
Returns the directory of the manage.py file
|
||||||
@ -42,6 +46,7 @@ def managePyDir():
|
|||||||
|
|
||||||
return os.path.join(localDir(), 'InvenTree')
|
return os.path.join(localDir(), 'InvenTree')
|
||||||
|
|
||||||
|
|
||||||
def managePyPath():
|
def managePyPath():
|
||||||
"""
|
"""
|
||||||
Return the path of the manage.py file
|
Return the path of the manage.py file
|
||||||
@ -49,6 +54,7 @@ def managePyPath():
|
|||||||
|
|
||||||
return os.path.join(managePyDir(), 'manage.py')
|
return os.path.join(managePyDir(), 'manage.py')
|
||||||
|
|
||||||
|
|
||||||
def manage(c, cmd, pty=False):
|
def manage(c, cmd, pty=False):
|
||||||
"""
|
"""
|
||||||
Runs a given command against django's "manage.py" script.
|
Runs a given command against django's "manage.py" script.
|
||||||
@ -63,32 +69,11 @@ def manage(c, cmd, pty=False):
|
|||||||
cmd=cmd
|
cmd=cmd
|
||||||
), pty=pty)
|
), pty=pty)
|
||||||
|
|
||||||
@task(help={'length': 'Length of secret key (default=50)'})
|
|
||||||
def key(c, length=50, force=False):
|
|
||||||
"""
|
|
||||||
Generates a SECRET_KEY file which InvenTree uses for generating security hashes
|
|
||||||
"""
|
|
||||||
|
|
||||||
SECRET_KEY_FILE = os.path.join(localDir(), 'InvenTree', 'secret_key.txt')
|
@task
|
||||||
|
|
||||||
# If a SECRET_KEY file does not exist, generate a new one!
|
|
||||||
if force or not os.path.exists(SECRET_KEY_FILE):
|
|
||||||
print("Generating SECRET_KEY file - " + SECRET_KEY_FILE)
|
|
||||||
with open(SECRET_KEY_FILE, 'w') as key_file:
|
|
||||||
options = string.digits + string.ascii_letters + string.punctuation
|
|
||||||
|
|
||||||
key = ''.join([random.choice(options) for i in range(length)])
|
|
||||||
|
|
||||||
key_file.write(key)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("SECRET_KEY file already exists - skipping")
|
|
||||||
|
|
||||||
|
|
||||||
@task(post=[key])
|
|
||||||
def install(c):
|
def install(c):
|
||||||
"""
|
"""
|
||||||
Installs required python packages, and runs initial setup functions.
|
Installs required python packages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Install required Python packages with PIP
|
# Install required Python packages with PIP
|
||||||
@ -111,6 +96,13 @@ def shell(c):
|
|||||||
|
|
||||||
manage(c, 'shell', pty=True)
|
manage(c, 'shell', pty=True)
|
||||||
|
|
||||||
|
@task
|
||||||
|
def worker(c):
|
||||||
|
"""
|
||||||
|
Run the InvenTree background worker process
|
||||||
|
"""
|
||||||
|
|
||||||
|
manage(c, 'qcluster', pty=True)
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def superuser(c):
|
def superuser(c):
|
||||||
@ -128,6 +120,14 @@ def check(c):
|
|||||||
|
|
||||||
manage(c, "check")
|
manage(c, "check")
|
||||||
|
|
||||||
|
@task
|
||||||
|
def wait(c):
|
||||||
|
"""
|
||||||
|
Wait until the database connection is ready
|
||||||
|
"""
|
||||||
|
|
||||||
|
manage(c, "wait_for_db")
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def migrate(c):
|
def migrate(c):
|
||||||
"""
|
"""
|
||||||
@ -154,7 +154,7 @@ def static(c):
|
|||||||
as per Django requirements.
|
as per Django requirements.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
manage(c, "collectstatic")
|
manage(c, "collectstatic --no-input")
|
||||||
|
|
||||||
|
|
||||||
@task(pre=[install, migrate, static])
|
@task(pre=[install, migrate, static])
|
||||||
@ -231,28 +231,6 @@ def coverage(c):
|
|||||||
# Generate coverage report
|
# Generate coverage report
|
||||||
c.run('coverage html')
|
c.run('coverage html')
|
||||||
|
|
||||||
@task
|
|
||||||
def mysql(c):
|
|
||||||
"""
|
|
||||||
Install packages required for using InvenTree with a MySQL database.
|
|
||||||
"""
|
|
||||||
|
|
||||||
print('Installing packages required for MySQL')
|
|
||||||
|
|
||||||
c.run('sudo apt-get install mysql-server libmysqlclient-dev')
|
|
||||||
c.run('pip3 install mysqlclient')
|
|
||||||
|
|
||||||
@task
|
|
||||||
def postgresql(c):
|
|
||||||
"""
|
|
||||||
Install packages required for using InvenTree with a PostgreSQL database
|
|
||||||
"""
|
|
||||||
|
|
||||||
print("Installing packages required for PostgreSQL")
|
|
||||||
|
|
||||||
c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev')
|
|
||||||
c.run('pip3 install psycopg2')
|
|
||||||
|
|
||||||
@task(help={'filename': "Output filename (default = 'data.json')"})
|
@task(help={'filename': "Output filename (default = 'data.json')"})
|
||||||
def export_records(c, filename='data.json'):
|
def export_records(c, filename='data.json'):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user