2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 04:25:42 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2022-07-27 19:04:34 +10:00
32 changed files with 489 additions and 515 deletions

View File

@ -41,6 +41,7 @@ jobs:
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
- name: Build Docker Image - name: Build Docker Image
# Build the development docker image (using docker-compose.yml)
run: | run: |
docker-compose build docker-compose build
- name: Run Unit Tests - name: Run Unit Tests
@ -51,6 +52,18 @@ jobs:
docker-compose run inventree-dev-server invoke wait docker-compose run inventree-dev-server invoke wait
docker-compose run inventree-dev-server invoke test docker-compose run inventree-dev-server invoke test
docker-compose down docker-compose down
- name: Check Data Directory
# The following file structure should have been created by the docker image
run: |
test -d data
test -d data/env
test -d data/pgdb
test -d data/media
test -d data/static
test -d data/plugins
test -f data/config.yaml
test -f data/plugins.txt
test -f data/secret_key.txt
- name: Set up QEMU - name: Set up QEMU
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
@ -58,6 +71,7 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Set up cosign - name: Set up cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@48866aa521d8bf870604709cd43ec2f602d03ff2 uses: sigstore/cosign-installer@48866aa521d8bf870604709cd43ec2f602d03ff2
- name: Login to Dockerhub - name: Login to Dockerhub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
@ -66,6 +80,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Docker metadata - name: Extract Docker metadata
if: github.event_name != 'pull_request'
id: meta id: meta
uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a
with: with:
@ -85,12 +100,13 @@ jobs:
commit_hash=${{ env.git_commit_hash }} commit_hash=${{ env.git_commit_hash }}
commit_date=${{ env.git_commit_date }} commit_date=${{ env.git_commit_date }}
- name: Sign the published image - name: Sign the published image
if: github.event_name != 'pull_request'
env: env:
COSIGN_EXPERIMENTAL: "true" COSIGN_EXPERIMENTAL: "true"
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }} run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
- name: Push to Stable Branch - name: Push to Stable Branch
uses: ad-m/github-push-action@master uses: ad-m/github-push-action@master
if: env.stable_release == 'true' if: env.stable_release == 'true' && github.event_name != 'pull_request'
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
branch: stable branch: stable

View File

@ -7,11 +7,12 @@ tasks:
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static' export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
export PIP_USER='no' export PIP_USER='no'
sudo apt install gettext
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install invoke pip install invoke
mkdir dev mkdir dev
inv test-setup inv setup-test
gp sync-done start_server gp sync-done start_server
- name: Start server - name: Start server
@ -23,6 +24,7 @@ tasks:
export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media' export INVENTREE_MEDIA_ROOT='/workspace/InvenTree/inventree-data/media'
export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static' export INVENTREE_STATIC_ROOT='/workspace/InvenTree/dev/static'
source venv/bin/activate
inv server inv server
ports: ports:

View File

@ -2,11 +2,15 @@
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 67 INVENTREE_API_VERSION = 68
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v68 -> 2022-07-27 : https://github.com/inventree/InvenTree/pull/3417
- Allows SupplierPart list to be filtered by SKU value
- Allows SupplierPart list to be filtered by MPN value
v67 -> 2022-07-25 : https://github.com/inventree/InvenTree/pull/3395 v67 -> 2022-07-25 : https://github.com/inventree/InvenTree/pull/3395
- Adds a 'requirements' endpoint for Part instance - Adds a 'requirements' endpoint for Part instance
- Provides information on outstanding order requirements for a given part - Provides information on outstanding order requirements for a given part

View File

@ -3,16 +3,17 @@
import logging import logging
import os import os
import shutil import shutil
from pathlib import Path
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
def get_base_dir(): def get_base_dir() -> Path:
"""Returns the base (top-level) InvenTree directory.""" """Returns the base (top-level) InvenTree directory."""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return Path(__file__).parent.parent.resolve()
def get_config_file(): def get_config_file() -> Path:
"""Returns the path of the InvenTree configuration file. """Returns the path of the InvenTree configuration file.
Note: It will be created it if does not already exist! Note: It will be created it if does not already exist!
@ -22,16 +23,15 @@ def get_config_file():
cfg_filename = os.getenv('INVENTREE_CONFIG_FILE') cfg_filename = os.getenv('INVENTREE_CONFIG_FILE')
if cfg_filename: if cfg_filename:
cfg_filename = cfg_filename.strip() cfg_filename = Path(cfg_filename.strip()).resolve()
cfg_filename = os.path.abspath(cfg_filename)
else: else:
# Config file is *not* specified - use the default # Config file is *not* specified - use the default
cfg_filename = os.path.join(base_dir, 'config.yaml') cfg_filename = base_dir.joinpath('config.yaml').resolve()
if not os.path.exists(cfg_filename): if not cfg_filename.exists():
print("InvenTree configuration file 'config.yaml' not found - creating default file") print("InvenTree configuration file 'config.yaml' not found - creating default file")
cfg_template = os.path.join(base_dir, "config_template.yaml") cfg_template = base_dir.joinpath("config_template.yaml")
shutil.copyfile(cfg_template, cfg_filename) shutil.copyfile(cfg_template, cfg_filename)
print(f"Created config file {cfg_filename}") print(f"Created config file {cfg_filename}")
@ -48,18 +48,18 @@ def get_plugin_file():
if not PLUGIN_FILE: if not PLUGIN_FILE:
# If not specified, look in the same directory as the configuration file # If not specified, look in the same directory as the configuration file
config_dir = get_config_file().parent
PLUGIN_FILE = config_dir.joinpath('plugins.txt')
else:
# Make sure we are using a modern Path object
PLUGIN_FILE = Path(PLUGIN_FILE)
config_dir = os.path.dirname(get_config_file()) if not PLUGIN_FILE.exists():
PLUGIN_FILE = os.path.join(config_dir, 'plugins.txt')
if not os.path.exists(PLUGIN_FILE):
logger.warning("Plugin configuration file does not exist") logger.warning("Plugin configuration file does not exist")
logger.info(f"Creating plugin file at '{PLUGIN_FILE}'") logger.info(f"Creating plugin file at '{PLUGIN_FILE}'")
# If opening the file fails (no write permission, for example), then this will throw an error # If opening the file fails (no write permission, for example), then this will throw an error
with open(PLUGIN_FILE, 'w') as plugin_file: PLUGIN_FILE.write_text("# InvenTree Plugins (uses PIP framework to install)\n\n")
plugin_file.write("# InvenTree Plugins (uses PIP framework to install)\n\n")
return PLUGIN_FILE return PLUGIN_FILE

View File

@ -12,12 +12,21 @@ from django.utils.translation import gettext_lazy as _
from djmoney.forms.fields import MoneyField from djmoney.forms.fields import MoneyField
from djmoney.models.fields import MoneyField as ModelMoneyField from djmoney.models.fields import MoneyField as ModelMoneyField
from djmoney.models.validators import MinMoneyValidator from djmoney.models.validators import MinMoneyValidator
from rest_framework.fields import URLField as RestURLField
import InvenTree.helpers import InvenTree.helpers
from .validators import allowable_url_schemes from .validators import allowable_url_schemes
class InvenTreeRestURLField(RestURLField):
"""Custom field for DRF with custom scheme vaildators."""
def __init__(self, **kwargs):
"""Update schemes."""
super().__init__(**kwargs)
self.validators[-1].schemes = allowable_url_schemes()
class InvenTreeURLFormField(FormURLField): class InvenTreeURLFormField(FormURLField):
"""Custom URL form field with custom scheme validators.""" """Custom URL form field with custom scheme validators."""
@ -27,7 +36,7 @@ class InvenTreeURLFormField(FormURLField):
class InvenTreeURLField(models.URLField): class InvenTreeURLField(models.URLField):
"""Custom URL field which has custom scheme validators.""" """Custom URL field which has custom scheme validators."""
default_validators = [validators.URLValidator(schemes=allowable_url_schemes())] validators = [validators.URLValidator(schemes=allowable_url_schemes())]
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Return a Field instance for this field.""" """Return a Field instance for this field."""

View File

@ -7,6 +7,7 @@ import os
import os.path import os.path
import re import re
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from pathlib import Path
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
from django.conf import settings from django.conf import settings
@ -211,7 +212,7 @@ def getLogoImage(as_file=False, custom=True):
else: else:
if as_file: if as_file:
path = os.path.join(settings.STATIC_ROOT, 'img/inventree.png') path = settings.STATIC_ROOT.joinpath('img/inventree.png')
return f"file://{path}" return f"file://{path}"
else: else:
return getStaticUrl('img/inventree.png') return getStaticUrl('img/inventree.png')
@ -687,20 +688,17 @@ def addUserPermissions(user, permissions):
def getMigrationFileNames(app): def getMigrationFileNames(app):
"""Return a list of all migration filenames for provided app.""" """Return a list of all migration filenames for provided app."""
local_dir = os.path.dirname(os.path.abspath(__file__)) local_dir = Path(__file__).parent
files = local_dir.joinpath('..', app, 'migrations').iterdir()
migration_dir = os.path.join(local_dir, '..', app, 'migrations')
files = os.listdir(migration_dir)
# Regex pattern for migration files # Regex pattern for migration files
pattern = r"^[\d]+_.*\.py$" regex = re.compile(r"^[\d]+_.*\.py$")
migration_files = [] migration_files = []
for f in files: for f in files:
if re.match(pattern, f): if regex.match(f.name):
migration_files.append(f) migration_files.append(f.name)
return migration_files return migration_files

View File

@ -437,26 +437,12 @@ class InvenTreeAttachment(models.Model):
if len(fn) == 0: if len(fn) == 0:
raise ValidationError(_('Filename must not be empty')) raise ValidationError(_('Filename must not be empty'))
attachment_dir = os.path.join( attachment_dir = settings.MEDIA_ROOT.joinpath(self.getSubdir())
settings.MEDIA_ROOT, old_file = settings.MEDIA_ROOT.joinpath(self.attachment.name)
self.getSubdir() new_file = settings.MEDIA_ROOT.joinpath(self.getSubdir(), fn).resolve()
)
old_file = os.path.join(
settings.MEDIA_ROOT,
self.attachment.name
)
new_file = os.path.join(
settings.MEDIA_ROOT,
self.getSubdir(),
fn
)
new_file = os.path.abspath(new_file)
# Check that there are no directory tricks going on... # Check that there are no directory tricks going on...
if os.path.dirname(new_file) != attachment_dir: if new_file.parent != attachment_dir:
logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'") logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'")
raise ValidationError(_("Invalid attachment directory")) raise ValidationError(_("Invalid attachment directory"))
@ -473,11 +459,11 @@ class InvenTreeAttachment(models.Model):
if len(fn.split('.')) < 2: if len(fn.split('.')) < 2:
raise ValidationError(_("Filename missing extension")) raise ValidationError(_("Filename missing extension"))
if not os.path.exists(old_file): if not old_file.exists():
logger.error(f"Trying to rename attachment '{old_file}' which does not exist") logger.error(f"Trying to rename attachment '{old_file}' which does not exist")
return return
if os.path.exists(new_file): if new_file.exists():
raise ValidationError(_("Attachment with this filename already exists")) raise ValidationError(_("Attachment with this filename already exists"))
try: try:

View File

@ -7,6 +7,7 @@ from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import tablib import tablib
@ -20,6 +21,7 @@ from rest_framework.serializers import DecimalField
from rest_framework.utils import model_meta from rest_framework.utils import model_meta
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.fields import InvenTreeRestURLField
from InvenTree.helpers import download_image_from_url from InvenTree.helpers import download_image_from_url
@ -64,6 +66,12 @@ class InvenTreeMoneySerializer(MoneyField):
class InvenTreeModelSerializer(serializers.ModelSerializer): class InvenTreeModelSerializer(serializers.ModelSerializer):
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation.""" """Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
# Switch out URLField mapping
serializer_field_mapping = {
**serializers.ModelSerializer.serializer_field_mapping,
models.URLField: InvenTreeRestURLField,
}
def __init__(self, instance=None, data=empty, **kwargs): def __init__(self, instance=None, data=empty, **kwargs):
"""Custom __init__ routine to ensure that *default* values (as specified in the ORM) are used by the DRF serializers, *if* the values are not provided by the user.""" """Custom __init__ routine to ensure that *default* values (as specified in the ORM) are used by the DRF serializers, *if* the values are not provided by the user."""
# If instance is None, we are creating a new instance # If instance is None, we are creating a new instance

View File

@ -15,6 +15,7 @@ import random
import socket import socket
import string import string
import sys import sys
from pathlib import Path
import django.conf.locale import django.conf.locale
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@ -44,7 +45,7 @@ TESTING_ENV = False
# New requirement for django 3.2+ # New requirement for django 3.2+
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: BASE_DIR.joinpath(...)
BASE_DIR = get_base_dir() BASE_DIR = get_base_dir()
cfg_filename = get_config_file() cfg_filename = get_config_file()
@ -53,7 +54,7 @@ with open(cfg_filename, 'r') as cfg:
CONFIG = yaml.safe_load(cfg) CONFIG = yaml.safe_load(cfg)
# We will place any config files in the same directory as the config file # We will place any config files in the same directory as the config file
config_dir = os.path.dirname(cfg_filename) config_dir = cfg_filename.parent
# Default action is to run the system in Debug mode # Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
@ -123,19 +124,17 @@ else:
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE") key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
if key_file: if key_file:
key_file = os.path.abspath(key_file) # pragma: no cover key_file = Path(key_file).resolve() # pragma: no cover
else: else:
# default secret key location # default secret key location
key_file = os.path.join(BASE_DIR, "secret_key.txt") key_file = BASE_DIR.joinpath("secret_key.txt").resolve()
key_file = os.path.abspath(key_file)
if not os.path.exists(key_file): # pragma: no cover if not key_file.exists(): # pragma: no cover
logger.info(f"Generating random key file at '{key_file}'") logger.info(f"Generating random key file at '{key_file}'")
# Create a random key file # Create a random key file
with open(key_file, 'w') as f:
options = string.digits + string.ascii_letters + string.punctuation options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(100)]) key = ''.join([random.choice(options) for i in range(100)])
f.write(key) key_file.write_text(key)
logger.info(f"Loading SECRET_KEY from '{key_file}'") logger.info(f"Loading SECRET_KEY from '{key_file}'")
@ -146,28 +145,34 @@ else:
sys.exit(-1) sys.exit(-1)
# The filesystem location for served static files # The filesystem location for served static files
STATIC_ROOT = os.path.abspath( STATIC_ROOT = Path(
get_setting( get_setting(
'INVENTREE_STATIC_ROOT', 'INVENTREE_STATIC_ROOT',
CONFIG.get('static_root', None) CONFIG.get('static_root', None)
) )
) ).resolve()
if STATIC_ROOT is None: # pragma: no cover if STATIC_ROOT is None: # pragma: no cover
print("ERROR: INVENTREE_STATIC_ROOT directory not defined") print("ERROR: INVENTREE_STATIC_ROOT directory not defined")
sys.exit(1) sys.exit(1)
else:
# Ensure the root really is availalble
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
# The filesystem location for served static files # The filesystem location for served static files
MEDIA_ROOT = os.path.abspath( MEDIA_ROOT = Path(
get_setting( get_setting(
'INVENTREE_MEDIA_ROOT', 'INVENTREE_MEDIA_ROOT',
CONFIG.get('media_root', None) CONFIG.get('media_root', None)
) )
) ).resolve()
if MEDIA_ROOT is None: # pragma: no cover if MEDIA_ROOT is None: # pragma: no cover
print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined") print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined")
sys.exit(1) sys.exit(1)
else:
# Ensure the root really is availalble
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
# List of allowed hosts (default = allow all) # List of allowed hosts (default = allow all)
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*']) ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
@ -193,17 +198,17 @@ STATICFILES_DIRS = []
# Translated Template settings # Translated Template settings
STATICFILES_I18_PREFIX = 'i18n' STATICFILES_I18_PREFIX = 'i18n'
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated') STATICFILES_I18_SRC = BASE_DIR.joinpath('templates', 'js', 'translated')
STATICFILES_I18_TRG = os.path.join(BASE_DIR, 'InvenTree', 'static_i18n') STATICFILES_I18_TRG = BASE_DIR.joinpath('InvenTree', 'static_i18n')
STATICFILES_DIRS.append(STATICFILES_I18_TRG) STATICFILES_DIRS.append(STATICFILES_I18_TRG)
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX) STATICFILES_I18_TRG = STATICFILES_I18_TRG.joinpath(STATICFILES_I18_PREFIX)
STATFILES_I18_PROCESSORS = [ STATFILES_I18_PROCESSORS = [
'InvenTree.context.status_codes', 'InvenTree.context.status_codes',
] ]
# Color Themes Directory # Color Themes Directory
STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes') STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes')
# Web URL endpoint for served media files # Web URL endpoint for served media files
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
@ -339,10 +344,10 @@ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'DIRS': [
os.path.join(BASE_DIR, 'templates'), BASE_DIR.joinpath('templates'),
# Allow templates in the reporting directory to be accessed # Allow templates in the reporting directory to be accessed
os.path.join(MEDIA_ROOT, 'report'), MEDIA_ROOT.joinpath('report'),
os.path.join(MEDIA_ROOT, 'label'), MEDIA_ROOT.joinpath('label'),
], ],
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -809,7 +814,7 @@ EMAIL_USE_SSL = get_setting(
EMAIL_TIMEOUT = 60 EMAIL_TIMEOUT = 60
LOCALE_PATHS = ( LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'), BASE_DIR.joinpath('locale/'),
) )
TIME_ZONE = get_setting( TIME_ZONE = get_setting(
@ -935,17 +940,6 @@ PLUGINS_ENABLED = _is_true(get_setting(
PLUGIN_FILE = get_plugin_file() PLUGIN_FILE = get_plugin_file()
# Plugin Directories (local plugins will be loaded from these directories)
PLUGIN_DIRS = ['plugin.builtin', ]
if not TESTING:
# load local deploy directory in prod
PLUGIN_DIRS.append('plugins') # pragma: no cover
if DEBUG or TESTING:
# load samples in debug mode
PLUGIN_DIRS.append('plugin.samples')
# Plugin test settings # Plugin test settings
PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested? PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested?
PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing? PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing?

View File

@ -2,6 +2,7 @@
import os import os
from django.contrib.auth import get_user_model
from django.urls import reverse from django.urls import reverse
from InvenTree.helpers import InvenTreeTestCase from InvenTree.helpers import InvenTreeTestCase
@ -41,3 +42,80 @@ class ViewTests(InvenTreeTestCase):
self.assertIn("<div id='detail-panels'>", content) self.assertIn("<div id='detail-panels'>", content)
# TODO: In future, run the javascript and ensure that the panels get created! # TODO: In future, run the javascript and ensure that the panels get created!
def test_settings_page(self):
"""Test that the 'settings' page loads correctly"""
# Settings page loads
url = reverse('settings')
# Attempt without login
self.client.logout()
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
# Login with default client
self.client.login(username=self.username, password=self.password)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = response.content.decode()
user_panels = [
'account',
'user-display',
'user-home',
'user-reports',
]
staff_panels = [
'server',
'login',
'barcodes',
'currencies',
'parts',
'stock',
]
plugin_panels = [
'plugin',
]
# Default user has staff access, so all panels will be present
for panel in user_panels + staff_panels + plugin_panels:
self.assertIn(f"select-{panel}", content)
self.assertIn(f"panel-{panel}", content)
# Now create a user who does not have staff access
pleb_user = get_user_model().objects.create_user(
username='pleb',
password='notstaff',
)
pleb_user.groups.add(self.group)
pleb_user.is_superuser = False
pleb_user.is_staff = False
pleb_user.save()
self.client.logout()
result = self.client.login(
username='pleb',
password='notstaff',
)
self.assertTrue(result)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
content = response.content.decode()
# Normal user still has access to user-specific panels
for panel in user_panels:
self.assertIn(f"select-{panel}", content)
self.assertIn(f"panel-{panel}", content)
# Normal user does NOT have access to global or plugin settings
for panel in staff_panels + plugin_panels:
self.assertNotIn(f"select-{panel}", content)
self.assertNotIn(f"panel-{panel}", content)

View File

@ -745,11 +745,11 @@ class TestSettings(helpers.InvenTreeTestCase):
'inventree/data/config.yaml', 'inventree/data/config.yaml',
] ]
self.assertTrue(any([opt in config.get_config_file().lower() for opt in valid])) self.assertTrue(any([opt in str(config.get_config_file()).lower() for opt in valid]))
# with env set # with env set
with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}): with self.in_env_context({'INVENTREE_CONFIG_FILE': 'my_special_conf.yaml'}):
self.assertIn('inventree/my_special_conf.yaml', config.get_config_file().lower()) self.assertIn('inventree/my_special_conf.yaml', str(config.get_config_file()).lower())
def test_helpers_plugin_file(self): def test_helpers_plugin_file(self):
"""Test get_plugin_file.""" """Test get_plugin_file."""
@ -760,11 +760,11 @@ class TestSettings(helpers.InvenTreeTestCase):
'inventree/data/plugins.txt', 'inventree/data/plugins.txt',
] ]
self.assertTrue(any([opt in config.get_plugin_file().lower() for opt in valid])) self.assertTrue(any([opt in str(config.get_plugin_file()).lower() for opt in valid]))
# with env set # with env set
with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}): with self.in_env_context({'INVENTREE_PLUGIN_FILE': 'my_special_plugins.txt'}):
self.assertIn('my_special_plugins.txt', config.get_plugin_file()) self.assertIn('my_special_plugins.txt', str(config.get_plugin_file()))
def test_helpers_setting(self): def test_helpers_setting(self):
"""Test get_setting.""" """Test get_setting."""

View File

@ -5,7 +5,6 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
""" """
import json import json
import os
from django.conf import settings from django.conf import settings
from django.contrib.auth import password_validation from django.contrib.auth import password_validation
@ -638,7 +637,8 @@ class SettingsView(TemplateView):
ctx["rates_updated"] = None ctx["rates_updated"] = None
# load locale stats # load locale stats
STAT_FILE = os.path.abspath(os.path.join(settings.BASE_DIR, 'InvenTree/locale_stats.json')) STAT_FILE = settings.BASE_DIR.joinpath('InvenTree/locale_stats.json').absolute()
try: try:
ctx["locale_stats"] = json.load(open(STAT_FILE, 'r')) ctx["locale_stats"] = json.load(open(STAT_FILE, 'r'))
except Exception: except Exception:

View File

@ -1,7 +1,5 @@
"""Django views for interacting with common models.""" """Django views for interacting with common models."""
import os
from django.conf import settings from django.conf import settings
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -37,9 +35,9 @@ class MultiStepFormView(SessionWizardView):
def process_media_folder(self): def process_media_folder(self):
"""Process media folder.""" """Process media folder."""
if self.media_folder: if self.media_folder:
media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder) media_folder_abs = settings.MEDIA_ROOT.joinpath(self.media_folder)
if not os.path.exists(media_folder_abs): if not media_folder_abs.exists():
os.mkdir(media_folder_abs) media_folder_abs.mkdir(parents=True, exist_ok=True)
self.file_storage = FileSystemStorage(location=media_folder_abs) self.file_storage = FileSystemStorage(location=media_folder_abs)
def get_template_names(self): def get_template_names(self):

View File

@ -254,6 +254,31 @@ class ManufacturerPartParameterDetail(RetrieveUpdateDestroyAPI):
serializer_class = ManufacturerPartParameterSerializer serializer_class = ManufacturerPartParameterSerializer
class SupplierPartFilter(rest_filters.FilterSet):
"""API filters for the SupplierPartList endpoint"""
class Meta:
"""Metaclass option"""
model = SupplierPart
fields = [
'supplier',
'part',
'manufacturer_part',
'SKU',
]
# Filter by 'active' status of linked part
active = rest_filters.BooleanFilter(field_name='part__active')
# Filter by the 'MPN' of linked manufacturer part
MPN = rest_filters.CharFilter(
label='Manufacturer Part Number',
field_name='manufacturer_part__MPN',
lookup_expr='iexact'
)
class SupplierPartList(ListCreateDestroyAPIView): class SupplierPartList(ListCreateDestroyAPIView):
"""API endpoint for list view of SupplierPart object. """API endpoint for list view of SupplierPart object.
@ -262,6 +287,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
""" """
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
filterset_class = SupplierPartFilter
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
"""Return annotated queryest object for the SupplierPart list""" """Return annotated queryest object for the SupplierPart list"""
@ -282,37 +308,12 @@ class SupplierPartList(ListCreateDestroyAPIView):
if manufacturer is not None: if manufacturer is not None:
queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer) queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
# Filter by supplier
supplier = params.get('supplier', None)
if supplier is not None:
queryset = queryset.filter(supplier=supplier)
# Filter by EITHER manufacturer or supplier # Filter by EITHER manufacturer or supplier
company = params.get('company', None) company = params.get('company', None)
if company is not None: if company is not None:
queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company)) queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company))
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
# Filter by manufacturer part?
manufacturer_part = params.get('manufacturer_part', None)
if manufacturer_part is not None:
queryset = queryset.filter(manufacturer_part=manufacturer_part)
# Filter by 'active' status of the part?
active = params.get('active', None)
if active is not None:
active = str2bool(active)
queryset = queryset.filter(part__active=active)
return queryset return queryset
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):

View File

@ -5,6 +5,7 @@ import logging
import os import os
import shutil import shutil
import warnings import warnings
from pathlib import Path
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -40,40 +41,18 @@ class LabelConfig(AppConfig):
"""Create all default templates.""" """Create all default templates."""
# Test if models are ready # Test if models are ready
try: try:
from .models import StockLocationLabel from .models import PartLabel, StockItemLabel, StockLocationLabel
assert bool(StockLocationLabel is not None) assert bool(StockLocationLabel is not None)
except AppRegistryNotReady: # pragma: no cover except AppRegistryNotReady: # pragma: no cover
# Database might not yet be ready # Database might not yet be ready
warnings.warn('Database was not ready for creating labels') warnings.warn('Database was not ready for creating labels')
return return
self.create_stock_item_labels() # Create the categories
self.create_stock_location_labels() self.create_labels_category(
self.create_part_labels() StockItemLabel,
def create_stock_item_labels(self):
"""Create database entries for the default StockItemLabel templates, if they do not already exist."""
from .models import StockItemLabel
src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'templates',
'label',
'stockitem', 'stockitem',
) [
dst_dir = os.path.join(
settings.MEDIA_ROOT,
'label',
'inventree',
'stockitem',
)
if not os.path.exists(dst_dir):
logger.info(f"Creating required directory: '{dst_dir}'")
os.makedirs(dst_dir, exist_ok=True)
labels = [
{ {
'file': 'qr.html', 'file': 'qr.html',
'name': 'QR Code', 'name': 'QR Code',
@ -81,78 +60,12 @@ class LabelConfig(AppConfig):
'width': 24, 'width': 24,
'height': 24, 'height': 24,
}, },
] ],
for label in labels:
filename = os.path.join(
'label',
'inventree',
'stockitem',
label['file'],
) )
self.create_labels_category(
# Check if the file exists in the media directory StockLocationLabel,
src_file = os.path.join(src_dir, label['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename)
to_copy = False
if os.path.exists(dst_file):
# File already exists - let's see if it is the "same",
# or if we need to overwrite it with a newer copy!
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
logger.info(f"Hash differs for '{filename}'")
to_copy = True
else:
logger.info(f"Label template '{filename}' is not present")
to_copy = True
if to_copy:
logger.info(f"Copying label template '{dst_file}'")
shutil.copyfile(src_file, dst_file)
# Check if a label matching the template already exists
if StockItemLabel.objects.filter(label=filename).exists():
continue # pragma: no cover
logger.info(f"Creating entry for StockItemLabel '{label['name']}'")
StockItemLabel.objects.create(
name=label['name'],
description=label['description'],
label=filename,
filters='',
enabled=True,
width=label['width'],
height=label['height'],
)
def create_stock_location_labels(self):
"""Create database entries for the default StockItemLocation templates, if they do not already exist."""
from .models import StockLocationLabel
src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'templates',
'label',
'stocklocation', 'stocklocation',
) [
dst_dir = os.path.join(
settings.MEDIA_ROOT,
'label',
'inventree',
'stocklocation',
)
if not os.path.exists(dst_dir):
logger.info(f"Creating required directory: '{dst_dir}'")
os.makedirs(dst_dir, exist_ok=True)
labels = [
{ {
'file': 'qr.html', 'file': 'qr.html',
'name': 'QR Code', 'name': 'QR Code',
@ -168,77 +81,11 @@ class LabelConfig(AppConfig):
'height': 24, 'height': 24,
} }
] ]
for label in labels:
filename = os.path.join(
'label',
'inventree',
'stocklocation',
label['file'],
) )
self.create_labels_category(
# Check if the file exists in the media directory PartLabel,
src_file = os.path.join(src_dir, label['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename)
to_copy = False
if os.path.exists(dst_file):
# File already exists - let's see if it is the "same",
# or if we need to overwrite it with a newer copy!
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
logger.info(f"Hash differs for '{filename}'")
to_copy = True
else:
logger.info(f"Label template '{filename}' is not present")
to_copy = True
if to_copy:
logger.info(f"Copying label template '{dst_file}'")
shutil.copyfile(src_file, dst_file)
# Check if a label matching the template already exists
if StockLocationLabel.objects.filter(label=filename).exists():
continue # pragma: no cover
logger.info(f"Creating entry for StockLocationLabel '{label['name']}'")
StockLocationLabel.objects.create(
name=label['name'],
description=label['description'],
label=filename,
filters='',
enabled=True,
width=label['width'],
height=label['height'],
)
def create_part_labels(self):
"""Create database entries for the default PartLabel templates, if they do not already exist."""
from .models import PartLabel
src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'templates',
'label',
'part', 'part',
) [
dst_dir = os.path.join(
settings.MEDIA_ROOT,
'label',
'inventree',
'part',
)
if not os.path.exists(dst_dir):
logger.info(f"Creating required directory: '{dst_dir}'")
os.makedirs(dst_dir, exist_ok=True)
labels = [
{ {
'file': 'part_label.html', 'file': 'part_label.html',
'name': 'Part Label', 'name': 'Part Label',
@ -254,22 +101,46 @@ class LabelConfig(AppConfig):
'height': 24, 'height': 24,
}, },
] ]
)
def create_labels_category(self, model, ref_name, labels):
"""Create folder and database entries for the default templates, if they do not already exist."""
# Create root dir for templates
src_dir = Path(__file__).parent.joinpath(
'templates',
'label',
ref_name,
)
dst_dir = settings.MEDIA_ROOT.joinpath(
'label',
'inventree',
ref_name,
)
if not dst_dir.exists():
logger.info(f"Creating required directory: '{dst_dir}'")
dst_dir.mkdir(parents=True, exist_ok=True)
# Create lables
for label in labels: for label in labels:
self.create_template_label(model, src_dir, ref_name, label)
def create_template_label(self, model, src_dir, ref_name, label):
"""Ensure a label template is in place."""
filename = os.path.join( filename = os.path.join(
'label', 'label',
'inventree', 'inventree',
'part', ref_name,
label['file'] label['file']
) )
src_file = os.path.join(src_dir, label['file']) src_file = src_dir.joinpath(label['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename) dst_file = settings.MEDIA_ROOT.joinpath(filename)
to_copy = False to_copy = False
if os.path.exists(dst_file): if dst_file.exists():
# File already exists - let's see if it is the "same" # File already exists - let's see if it is the "same"
if hashFile(dst_file) != hashFile(src_file): # pragma: no cover if hashFile(dst_file) != hashFile(src_file): # pragma: no cover
@ -282,15 +153,19 @@ class LabelConfig(AppConfig):
if to_copy: if to_copy:
logger.info(f"Copying label template '{dst_file}'") logger.info(f"Copying label template '{dst_file}'")
# Ensure destionation dir exists
dst_file.parent.mkdir(parents=True, exist_ok=True)
# Copy file
shutil.copyfile(src_file, dst_file) shutil.copyfile(src_file, dst_file)
# Check if a label matching the template already exists # Check if a label matching the template already exists
if PartLabel.objects.filter(label=filename).exists(): if model.objects.filter(label=filename).exists():
continue # pragma: no cover return # pragma: no cover
logger.info(f"Creating entry for PartLabel '{label['name']}'") logger.info(f"Creating entry for {model} '{label['name']}'")
PartLabel.objects.create( model.objects.create(
name=label['name'], name=label['name'],
description=label['description'], description=label['description'],
label=filename, label=filename,
@ -299,3 +174,4 @@ class LabelConfig(AppConfig):
width=label['width'], width=label['width'],
height=label['height'], height=label['height'],
) )
return

View File

@ -155,7 +155,7 @@ class LabelTemplate(models.Model):
template = template.replace('/', os.path.sep) template = template.replace('/', os.path.sep)
template = template.replace('\\', os.path.sep) template = template.replace('\\', os.path.sep)
template = os.path.join(settings.MEDIA_ROOT, template) template = settings.MEDIA_ROOT.joinpath(template)
return template return template

View File

@ -1,7 +1,5 @@
"""Tests for labels""" """Tests for labels"""
import os
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -42,27 +40,17 @@ class LabelTest(InvenTreeAPITestCase):
def test_default_files(self): def test_default_files(self):
"""Test that label files exist in the MEDIA directory.""" """Test that label files exist in the MEDIA directory."""
item_dir = os.path.join( def test_subdir(ref_name):
settings.MEDIA_ROOT, item_dir = settings.MEDIA_ROOT.joinpath(
'label', 'label',
'inventree', 'inventree',
'stockitem', ref_name,
) )
self.assertTrue(len([item_dir.iterdir()]) > 0)
files = os.listdir(item_dir) test_subdir('stockitem')
test_subdir('stocklocation')
self.assertTrue(len(files) > 0) test_subdir('part')
loc_dir = os.path.join(
settings.MEDIA_ROOT,
'label',
'inventree',
'stocklocation',
)
files = os.listdir(loc_dir)
self.assertTrue(len(files) > 0)
def test_filters(self): def test_filters(self):
"""Test the label filters.""" """Test the label filters."""

View File

@ -568,7 +568,7 @@ class PartScheduling(RetrieveAPI):
class PartRequirements(RetrieveAPI): class PartRequirements(RetrieveAPI):
"""API endpoint detailing 'requirements' information for aa particular part. """API endpoint detailing 'requirements' information for a particular part.
This endpoint returns information on upcoming requirements for: This endpoint returns information on upcoming requirements for:

View File

@ -510,7 +510,7 @@ class PartImageSelect(AjaxUpdateView):
data = {} data = {}
if img: if img:
img_path = os.path.join(settings.MEDIA_ROOT, 'part_images', img) img_path = settings.MEDIA_ROOT.joinpath('part_images', img)
# Ensure that the image already exists # Ensure that the image already exists
if os.path.exists(img_path): if os.path.exists(img_path):

View File

@ -2,7 +2,6 @@
import inspect import inspect
import logging import logging
import os
import pathlib import pathlib
import pkgutil import pkgutil
import subprocess import subprocess
@ -103,10 +102,10 @@ def get_git_log(path):
output = None output = None
if registry.git_is_modern: if registry.git_is_modern:
path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] path = path.replace(str(settings.BASE_DIR.parent), '')[1:]
command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path]
try: try:
output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1] output = str(subprocess.check_output(command, cwd=settings.BASE_DIR.parent), 'utf-8')[1:-1]
if output: if output:
output = output.split('\n') output = output.split('\n')
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
@ -125,7 +124,7 @@ def check_git_version():
"""Returns if the current git version supports modern features.""" """Returns if the current git version supports modern features."""
# get version string # get version string
try: try:
output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') output = str(subprocess.check_output(['git', '--version'], cwd=settings.BASE_DIR.parent), 'utf-8')
except subprocess.CalledProcessError: # pragma: no cover except subprocess.CalledProcessError: # pragma: no cover
return False return False
except FileNotFoundError: # pragma: no cover except FileNotFoundError: # pragma: no cover
@ -172,10 +171,16 @@ class GitStatus:
# region plugin finders # region plugin finders
def get_modules(pkg): def get_modules(pkg, path=None):
"""Get all modules in a package.""" """Get all modules in a package."""
context = {} context = {}
for loader, name, _ in pkgutil.walk_packages(pkg.__path__):
if path is None:
path = pkg.__path__
elif type(path) is not list:
path = [path]
for loader, name, _ in pkgutil.walk_packages(path):
try: try:
module = loader.find_module(name).load_module(name) module = loader.find_module(name).load_module(name)
pkg_names = getattr(module, '__all__', None) pkg_names = getattr(module, '__all__', None)
@ -199,7 +204,7 @@ def get_classes(module):
return inspect.getmembers(module, inspect.isclass) return inspect.getmembers(module, inspect.isclass)
def get_plugins(pkg, baseclass): def get_plugins(pkg, baseclass, path=None):
"""Return a list of all modules under a given package. """Return a list of all modules under a given package.
- Modules must be a subclass of the provided 'baseclass' - Modules must be a subclass of the provided 'baseclass'
@ -207,7 +212,7 @@ def get_plugins(pkg, baseclass):
""" """
plugins = [] plugins = []
modules = get_modules(pkg) modules = get_modules(pkg, path=path)
# Iterate through each module in the package # Iterate through each module in the package
for mod in modules: for mod in modules:

View File

@ -275,7 +275,11 @@ class InvenTreePlugin(MixinBase, MetaBase):
"""Path to the plugin.""" """Path to the plugin."""
if self._is_package: if self._is_package:
return self.__module__ # pragma: no cover return self.__module__ # pragma: no cover
try:
return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR)
except ValueError:
return pathlib.Path(self.def_path)
@property @property
def settings_url(self): def settings_url(self):

View File

@ -7,9 +7,9 @@
import importlib import importlib
import logging import logging
import os import os
import pathlib
import subprocess import subprocess
from importlib import metadata, reload from importlib import metadata, reload
from pathlib import Path
from typing import OrderedDict from typing import OrderedDict
from django.apps import apps from django.apps import apps
@ -187,6 +187,53 @@ class PluginsRegistry:
logger.info('Finished reloading plugins') logger.info('Finished reloading plugins')
def plugin_dirs(self):
"""Construct a list of directories from where plugins can be loaded"""
dirs = ['plugin.builtin', ]
if settings.TESTING or settings.DEBUG:
# If in TEST or DEBUG mode, load plugins from the 'samples' directory
dirs.append('plugin.samples')
if settings.TESTING:
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
else:
custom_dirs = os.getenv('INVENTREE_PLUGIN_DIR', None)
# Load from user specified directories (unless in testing mode)
dirs.append('plugins')
if custom_dirs is not None:
# Allow multiple plugin directories to be specified
for pd_text in custom_dirs.split(','):
pd = Path(pd_text.strip()).absolute()
# Attempt to create the directory if it does not already exist
if not pd.exists():
try:
pd.mkdir(exist_ok=True)
except Exception:
logger.error(f"Could not create plugin directory '{pd}'")
continue
# Ensure the directory has an __init__.py file
init_filename = pd.joinpath('__init__.py')
if not init_filename.exists():
try:
init_filename.write_text("# InvenTree plugin directory\n")
except Exception:
logger.error(f"Could not create file '{init_filename}'")
continue
if pd.exists() and pd.is_dir():
# By this point, we have confirmed that the directory at least exists
logger.info(f"Added plugin directory: '{pd}'")
dirs.append(pd)
return dirs
def collect_plugins(self): def collect_plugins(self):
"""Collect plugins from all possible ways of loading.""" """Collect plugins from all possible ways of loading."""
if not settings.PLUGINS_ENABLED: if not settings.PLUGINS_ENABLED:
@ -196,8 +243,20 @@ class PluginsRegistry:
self.plugin_modules = [] # clear self.plugin_modules = [] # clear
# Collect plugins from paths # Collect plugins from paths
for plugin in settings.PLUGIN_DIRS: for plugin in self.plugin_dirs():
modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin)
logger.info(f"Loading plugins from directory '{plugin}'")
parent_path = None
parent_obj = Path(plugin)
# If a "path" is provided, some special handling is required
if parent_obj.name is not plugin and len(parent_obj.parts) > 1:
parent_path = parent_obj.parent
plugin = parent_obj.name
modules = get_plugins(importlib.import_module(plugin), InvenTreePlugin, path=parent_path)
if modules: if modules:
[self.plugin_modules.append(item) for item in modules] [self.plugin_modules.append(item) for item in modules]
@ -224,7 +283,7 @@ class PluginsRegistry:
return True return True
try: try:
output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') output = str(subprocess.check_output(['pip', 'install', '-U', '-r', settings.PLUGIN_FILE], cwd=settings.BASE_DIR.parent), 'utf-8')
except subprocess.CalledProcessError as error: # pragma: no cover except subprocess.CalledProcessError as error: # pragma: no cover
logger.error(f'Ran into error while trying to install plugins!\n{str(error)}') logger.error(f'Ran into error while trying to install plugins!\n{str(error)}')
return False return False
@ -506,7 +565,7 @@ class PluginsRegistry:
""" """
try: try:
# for local path plugins # for local path plugins
plugin_path = '.'.join(pathlib.Path(plugin.path).relative_to(settings.BASE_DIR).parts) plugin_path = '.'.join(Path(plugin.path).relative_to(settings.BASE_DIR).parts)
except ValueError: # pragma: no cover except ValueError: # pragma: no cover
# plugin is shipped as package # plugin is shipped as package
plugin_path = plugin.NAME plugin_path = plugin.NAME

View File

@ -1,6 +1,5 @@
"""JSON serializers for plugin app.""" """JSON serializers for plugin app."""
import os
import subprocess import subprocess
from django.conf import settings from django.conf import settings
@ -144,7 +143,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
success = False success = False
# execute pypi # execute pypi
try: try:
result = subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)) result = subprocess.check_output(command, cwd=settings.BASE_DIR.parent)
ret['result'] = str(result, 'utf-8') ret['result'] = str(result, 'utf-8')
ret['success'] = True ret['success'] = True
success = True success = True

View File

@ -3,6 +3,7 @@
import logging import logging
import os import os
import shutil import shutil
from pathlib import Path
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -25,23 +26,21 @@ class ReportConfig(AppConfig):
def create_default_reports(self, model, reports): def create_default_reports(self, model, reports):
"""Copy defualt report files across to the media directory.""" """Copy defualt report files across to the media directory."""
# Source directory for report templates # Source directory for report templates
src_dir = os.path.join( src_dir = Path(__file__).parent.joinpath(
os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',
'report', 'report',
) )
# Destination directory # Destination directory
dst_dir = os.path.join( dst_dir = settings.MEDIA_ROOT.joinpath(
settings.MEDIA_ROOT,
'report', 'report',
'inventree', 'inventree',
model.getSubdir(), model.getSubdir(),
) )
if not os.path.exists(dst_dir): if not dst_dir.exists():
logger.info(f"Creating missing directory: '{dst_dir}'") logger.info(f"Creating missing directory: '{dst_dir}'")
os.makedirs(dst_dir, exist_ok=True) dst_dir.mkdir(parents=True, exist_ok=True)
# Copy each report template across (if required) # Copy each report template across (if required)
for report in reports: for report in reports:
@ -54,10 +53,10 @@ class ReportConfig(AppConfig):
report['file'], report['file'],
) )
src_file = os.path.join(src_dir, report['file']) src_file = src_dir.joinpath(report['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename) dst_file = settings.MEDIA_ROOT.joinpath(filename)
if not os.path.exists(dst_file): if not dst_file.exists():
logger.info(f"Copying test report template '{dst_file}'") logger.info(f"Copying test report template '{dst_file}'")
shutil.copyfile(src_file, dst_file) shutil.copyfile(src_file, dst_file)

View File

@ -111,14 +111,13 @@ class ReportBase(models.Model):
path = os.path.join('report', 'report_template', self.getSubdir(), filename) path = os.path.join('report', 'report_template', self.getSubdir(), filename)
fullpath = os.path.join(settings.MEDIA_ROOT, path) fullpath = settings.MEDIA_ROOT.joinpath(path).resolve()
fullpath = os.path.abspath(fullpath)
# If the report file is the *same* filename as the one being uploaded, # If the report file is the *same* filename as the one being uploaded,
# remove the original one from the media directory # remove the original one from the media directory
if str(filename) == str(self.template): if str(filename) == str(self.template):
if os.path.exists(fullpath): if fullpath.exists():
logger.info(f"Deleting existing report template: '{filename}'") logger.info(f"Deleting existing report template: '{filename}'")
os.remove(fullpath) os.remove(fullpath)
@ -139,10 +138,12 @@ class ReportBase(models.Model):
Required for passing the file to an external process Required for passing the file to an external process
""" """
template = self.template.name template = self.template.name
# TODO @matmair change to using new file objects
template = template.replace('/', os.path.sep) template = template.replace('/', os.path.sep)
template = template.replace('\\', os.path.sep) template = template.replace('\\', os.path.sep)
template = os.path.join(settings.MEDIA_ROOT, template) template = settings.MEDIA_ROOT.joinpath(template)
return template return template
@ -474,14 +475,13 @@ def rename_snippet(instance, filename):
path = os.path.join('report', 'snippets', filename) path = os.path.join('report', 'snippets', filename)
fullpath = os.path.join(settings.MEDIA_ROOT, path) fullpath = settings.MEDIA_ROOT.joinpath(path).resolve()
fullpath = os.path.abspath(fullpath)
# If the snippet file is the *same* filename as the one being uploaded, # If the snippet file is the *same* filename as the one being uploaded,
# delete the original one from the media directory # delete the original one from the media directory
if str(filename) == str(instance.snippet): if str(filename) == str(instance.snippet):
if os.path.exists(fullpath): if fullpath.exists():
logger.info(f"Deleting existing snippet file: '{filename}'") logger.info(f"Deleting existing snippet file: '{filename}'")
os.remove(fullpath) os.remove(fullpath)
@ -517,10 +517,9 @@ def rename_asset(instance, filename):
# If the asset file is the *same* filename as the one being uploaded, # If the asset file is the *same* filename as the one being uploaded,
# delete the original one from the media directory # delete the original one from the media directory
if str(filename) == str(instance.asset): if str(filename) == str(instance.asset):
fullpath = os.path.join(settings.MEDIA_ROOT, path) fullpath = settings.MEDIA_ROOT.joinpath(path).resolve()
fullpath = os.path.abspath(fullpath)
if os.path.exists(fullpath): if fullpath.exists():
logger.info(f"Deleting existing asset file: '{filename}'") logger.info(f"Deleting existing asset file: '{filename}'")
os.remove(fullpath) os.remove(fullpath)

View File

@ -32,9 +32,9 @@ def asset(filename):
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Test if the file actually exists # Test if the file actually exists
full_path = os.path.join(settings.MEDIA_ROOT, 'report', 'assets', filename) full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename)
if not os.path.exists(full_path) or not os.path.isfile(full_path): if not full_path.exists() or not full_path.is_file():
raise FileNotFoundError(f"Asset file '{filename}' does not exist") raise FileNotFoundError(f"Asset file '{filename}' does not exist")
if debug_mode: if debug_mode:
@ -63,9 +63,8 @@ def uploaded_image(filename, replace_missing=True, replacement_file='blank_image
exists = False exists = False
else: else:
try: try:
full_path = os.path.join(settings.MEDIA_ROOT, filename) full_path = settings.MEDIA_ROOT.joinpath(filename).resolve()
full_path = os.path.abspath(full_path) exists = full_path.exists() and full_path.is_file()
exists = os.path.exists(full_path) and os.path.isfile(full_path)
except Exception: except Exception:
exists = False exists = False
@ -85,11 +84,9 @@ def uploaded_image(filename, replace_missing=True, replacement_file='blank_image
else: else:
# Return file path # Return file path
if exists: if exists:
path = os.path.join(settings.MEDIA_ROOT, filename) path = settings.MEDIA_ROOT.joinpath(filename).resolve()
path = os.path.abspath(path)
else: else:
path = os.path.join(settings.STATIC_ROOT, 'img', replacement_file) path = settings.STATIC_ROOT.joinpath('img', replacement_file).resolve()
path = os.path.abspath(path)
return f"file://{path}" return f"file://{path}"

View File

@ -2,6 +2,7 @@
import os import os
import shutil import shutil
from pathlib import Path
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
@ -38,12 +39,11 @@ class ReportTagTest(TestCase):
report_tags.asset("bad_file.txt") report_tags.asset("bad_file.txt")
# Create an asset file # Create an asset file
asset_dir = os.path.join(settings.MEDIA_ROOT, 'report', 'assets') asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets')
os.makedirs(asset_dir, exist_ok=True) asset_dir.mkdir(parents=True, exist_ok=True)
asset_path = os.path.join(asset_dir, 'test.txt') asset_path = asset_dir.joinpath('test.txt')
with open(asset_path, 'w') as f: asset_path.write_text("dummy data")
f.write("dummy data")
self.debug_mode(True) self.debug_mode(True)
asset = report_tags.asset('test.txt') asset = report_tags.asset('test.txt')
@ -68,13 +68,11 @@ class ReportTagTest(TestCase):
# Create a dummy image # Create a dummy image
img_path = 'part/images/' img_path = 'part/images/'
img_path = os.path.join(settings.MEDIA_ROOT, img_path) img_path = settings.MEDIA_ROOT.joinpath(img_path)
img_file = os.path.join(img_path, 'test.jpg') img_file = img_path.joinpath('test.jpg')
os.makedirs(img_path, exist_ok=True) img_path.mkdir(parents=True, exist_ok=True)
img_file.write_text("dummy data")
with open(img_file, 'w') as f:
f.write("dummy data")
# Test in debug mode. Returns blank image as dummy file is not a valid image # Test in debug mode. Returns blank image as dummy file is not a valid image
self.debug_mode(True) self.debug_mode(True)
@ -91,7 +89,7 @@ class ReportTagTest(TestCase):
self.debug_mode(False) self.debug_mode(False)
img = report_tags.uploaded_image('part/images/test.jpg') img = report_tags.uploaded_image('part/images/test.jpg')
self.assertEqual(img, f'file://{img_path}test.jpg') self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')
def test_part_image(self): def test_part_image(self):
"""Unit tests for the 'part_image' tag""" """Unit tests for the 'part_image' tag"""
@ -178,8 +176,7 @@ class ReportTest(InvenTreeAPITestCase):
def copyReportTemplate(self, filename, description): def copyReportTemplate(self, filename, description):
"""Copy the provided report template into the required media directory.""" """Copy the provided report template into the required media directory."""
src_dir = os.path.join( src_dir = Path(__file__).parent.joinpath(
os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',
'report' 'report'
) )
@ -190,18 +187,15 @@ class ReportTest(InvenTreeAPITestCase):
self.model.getSubdir(), self.model.getSubdir(),
) )
dst_dir = os.path.join( dst_dir = settings.MEDIA_ROOT.joinpath(template_dir)
settings.MEDIA_ROOT,
template_dir
)
if not os.path.exists(dst_dir): # pragma: no cover if not dst_dir.exists(): # pragma: no cover
os.makedirs(dst_dir, exist_ok=True) dst_dir.mkdir(parents=True, exist_ok=True)
src_file = os.path.join(src_dir, filename) src_file = src_dir.joinpath(filename)
dst_file = os.path.join(dst_dir, filename) dst_file = dst_dir.joinpath(filename)
if not os.path.exists(dst_file): # pragma: no cover if not dst_file.exists(): # pragma: no cover
shutil.copyfile(src_file, dst_file) shutil.copyfile(src_file, dst_file)
# Convert to an "internal" filename # Convert to an "internal" filename

View File

@ -248,7 +248,11 @@
{% for object in session_list %} {% for object in session_list %}
<tr {% if object.session_key == session_key %}class="active"{% endif %}> <tr {% if object.session_key == session_key %}class="active"{% endif %}>
<td>{{ object.ip }}</td> <td>{{ object.ip }}</td>
{% if object.user_agent or object.device %}
<td>{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }}</td> <td>{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }}</td>
{% else %}
<td>{{ unknown_on_unknown }}</td>
{% endif %}
<td> <td>
{% if object.session_key == session_key %} {% if object.session_key == session_key %}
{% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %} {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %}

View File

@ -25,17 +25,17 @@ services:
expose: expose:
- ${INVENTREE_DB_PORT:-5432}/tcp - ${INVENTREE_DB_PORT:-5432}/tcp
environment: environment:
- PGDATA=/var/lib/postgresql/data/dev/pgdb - PGDATA=/var/lib/postgresql/data/pgdb
- POSTGRES_USER=${INVENTREE_DB_USER:?You must provide the 'INVENTREE_DB_USER' variable in the .env file} - POSTGRES_USER=${INVENTREE_DB_USER:?You must provide the 'INVENTREE_DB_USER' variable in the .env file}
- POSTGRES_PASSWORD=${INVENTREE_DB_PASSWORD:?You must provide the 'INVENTREE_DB_PASSWORD' variable in the .env file} - POSTGRES_PASSWORD=${INVENTREE_DB_PASSWORD:?You must provide the 'INVENTREE_DB_PASSWORD' variable in the .env file}
- POSTGRES_DB=${INVENTREE_DB_NAME:?You must provide the 'INVENTREE_DB_NAME' variable in the .env file} - POSTGRES_DB=${INVENTREE_DB_NAME:?You must provide the 'INVENTREE_DB_NAME' variable in the .env file}
volumes: volumes:
# Map 'data' volume such that postgres database is stored externally # Map 'data' volume such that postgres database is stored externally
- inventree_src:/var/lib/postgresql/data - ./data:/var/lib/postgresql/data
restart: unless-stopped restart: unless-stopped
# InvenTree web server services # InvenTree web server service
# Uses gunicorn as the web server # Runs the django built-in webserver application
inventree-dev-server: inventree-dev-server:
container_name: inventree-dev-server container_name: inventree-dev-server
depends_on: depends_on:
@ -48,13 +48,9 @@ services:
ports: ports:
# Expose web server on port 8000 # Expose web server on port 8000
- 8000:8000 - 8000:8000
# Note: If using the inventree-dev-proxy container (see below),
# comment out the "ports" directive (above) and uncomment the "expose" directive
#expose:
# - 8000
volumes: volumes:
# Ensure you specify the location of the 'src' directory at the end of this file # Mount local source directory to /home/inventree
- inventree_src:/home/inventree - ./:/home/inventree
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped
@ -67,38 +63,8 @@ services:
depends_on: depends_on:
- inventree-dev-server - inventree-dev-server
volumes: volumes:
# Ensure you specify the location of the 'src' directory at the end of this file # Mount local source directory to /home/inventree
- inventree_src:/home/inventree - ./:/home/inventree
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped
### Optional: Serve static and media files using nginx
### Uncomment the following lines to enable nginx proxy for testing
### Note: If enabling the proxy, change "ports" to "expose" for the inventree-dev-server container (above)
#inventree-dev-proxy:
# container_name: inventree-dev-proxy
# image: nginx:stable
# depends_on:
# - inventree-dev-server
# ports:
# # Change "8000" to the port that you want InvenTree web server to be available on
# - 8000:80
# volumes:
# # Provide ./nginx.dev.conf file to the container
# # Refer to the provided example file as a starting point
# - ./nginx.dev.conf:/etc/nginx/conf.d/default.conf:ro
# # nginx proxy needs access to static and media files
# - inventree_src:/var/www
# restart: unless-stopped
volumes:
# Persistent data, stored external to the container(s)
inventree_src:
driver: local
driver_opts:
type: none
o: bind
# This directory specified where InvenTree source code is stored "outside" the docker containers
# By default, this directory is one level above the "docker" directory
device: ${INVENTREE_EXT_VOLUME:-./}

View File

@ -34,10 +34,12 @@ INVENTREE_DB_PORT=5432
#INVENTREE_DB_USER=pguser #INVENTREE_DB_USER=pguser
#INVENTREE_DB_PASSWORD=pgpassword #INVENTREE_DB_PASSWORD=pgpassword
# Redis cache setup # Redis cache setup (disabled by default)
# Un-comment the following lines to enable Redis cache
# Note that you will also have to run docker-compose with the --profile redis command
# Refer to settings.py for other cache options # Refer to settings.py for other cache options
INVENTREE_CACHE_HOST=inventree-cache #INVENTREE_CACHE_HOST=inventree-cache
INVENTREE_CACHE_PORT=6379 #INVENTREE_CACHE_PORT=6379
# Enable plugins? # Enable plugins?
INVENTREE_PLUGINS_ENABLED=False INVENTREE_PLUGINS_ENABLED=False

View File

@ -1,11 +1,11 @@
version: "3.8" version: "3.8"
# Docker compose recipe for InvenTree production server, with the following containerized processes # Docker compose recipe for a production-ready InvenTree setup, with the following containers:
# - PostgreSQL as the database backend # - PostgreSQL as the database backend
# - gunicorn as the InvenTree web server # - gunicorn as the InvenTree web server
# - django-q as the InvenTree background worker process # - django-q as the InvenTree background worker process
# - nginx as a reverse proxy # - nginx as a reverse proxy
# - redis as the cache manager # - redis as the cache manager (optional, disabled by default)
# --------------------- # ---------------------
# READ BEFORE STARTING! # READ BEFORE STARTING!
@ -18,10 +18,8 @@ version: "3.8"
# Changes made to this file are reflected across all containers! # Changes made to this file are reflected across all containers!
# #
# IMPORTANT NOTE: # IMPORTANT NOTE:
# You should not have to change *anything* within the docker-compose.yml file! # You should not have to change *anything* within this docker-compose.yml file!
# Instead, make any changes in the .env file! # Instead, make any changes in the .env file!
# The only *mandatory* change is to set the INVENTREE_EXT_VOLUME variable,
# which defines the directory (on your local machine) where persistent data are stored.
# ------------------------ # ------------------------
# InvenTree Image Versions # InvenTree Image Versions
@ -29,15 +27,12 @@ version: "3.8"
# By default, this docker-compose script targets the STABLE version of InvenTree, # By default, this docker-compose script targets the STABLE version of InvenTree,
# image: inventree/inventree:stable # image: inventree/inventree:stable
# #
# To run the LATEST (development) version of InvenTree, change the target image to: # To run the LATEST (development) version of InvenTree,
# image: inventree/inventree:latest # change the INVENTREE_TAG variable (in the .env file) to "latest"
# #
# Alternatively, you could target a specific tagged release version with (for example): # Alternatively, you could target a specific tagged release version with (for example):
# image: inventree/inventree:0.5.3 # INVENTREE_TAG=0.7.5
# #
# NOTE: If you change the target image, ensure it is the same for the following containers:
# - inventree-server
# - inventree-worker
services: services:
# Database service # Database service
@ -58,18 +53,21 @@ services:
restart: unless-stopped restart: unless-stopped
# redis acts as database cache manager # redis acts as database cache manager
# only runs under the "redis" profile : https://docs.docker.com/compose/profiles/
inventree-cache: inventree-cache:
container_name: inventree-cache container_name: inventree-cache
image: redis:7.0 image: redis:7.0
depends_on: depends_on:
- inventree-db - inventree-db
profiles:
- redis
env_file: env_file:
- .env - .env
expose: expose:
- ${INVENTREE_CACHE_PORT:-6379} - ${INVENTREE_CACHE_PORT:-6379}
restart: always restart: always
# InvenTree web server services # InvenTree web server service
# Uses gunicorn as the web server # Uses gunicorn as the web server
inventree-server: inventree-server:
container_name: inventree-server container_name: inventree-server
@ -79,13 +77,11 @@ services:
- 8000 - 8000
depends_on: depends_on:
- inventree-db - inventree-db
- inventree-cache
env_file: env_file:
- .env - .env
volumes: volumes:
# Data volume must map to /home/inventree/data # Data volume must map to /home/inventree/data
- inventree_data:/home/inventree/data - inventree_data:/home/inventree/data
- inventree_plugins:/home/inventree/InvenTree/plugins
restart: unless-stopped restart: unless-stopped
# Background worker process handles long-running or periodic tasks # Background worker process handles long-running or periodic tasks
@ -101,7 +97,6 @@ services:
volumes: volumes:
# Data volume must map to /home/inventree/data # Data volume must map to /home/inventree/data
- inventree_data:/home/inventree/data - inventree_data:/home/inventree/data
- inventree_plugins:/home/inventree/InvenTree/plugins
restart: unless-stopped restart: unless-stopped
# nginx acts as a reverse proxy # nginx acts as a reverse proxy
@ -136,10 +131,3 @@ volumes:
o: bind o: bind
# This directory specified where InvenTree data are stored "outside" the docker containers # This directory specified where InvenTree data are stored "outside" the docker containers
device: ${INVENTREE_EXT_VOLUME:?You must specify the 'INVENTREE_EXT_VOLUME' variable in the .env file!} device: ${INVENTREE_EXT_VOLUME:?You must specify the 'INVENTREE_EXT_VOLUME' variable in the .env file!}
inventree_plugins:
driver: local
driver_opts:
type: none
o: bind
# This directory specified where the optional local plugin directory is stored "outside" the docker containers
device: ${INVENTREE_EXT_PLUGINS:-./}

View File

@ -5,6 +5,7 @@ import os
import pathlib import pathlib
import re import re
import sys import sys
from pathlib import Path
from invoke import task from invoke import task
@ -52,23 +53,23 @@ def content_excludes():
return output return output
def localDir(): def localDir() -> Path:
"""Returns the directory of *THIS* file. """Returns the directory of *THIS* file.
Used to ensure that the various scripts always run Used to ensure that the various scripts always run
in the correct directory. in the correct directory.
""" """
return os.path.dirname(os.path.abspath(__file__)) return Path(__file__).parent.resolve()
def managePyDir(): def managePyDir():
"""Returns the directory of the manage.py file.""" """Returns the directory of the manage.py file."""
return os.path.join(localDir(), 'InvenTree') return localDir().joinpath('InvenTree')
def managePyPath(): def managePyPath():
"""Return the path of the manage.py file.""" """Return the path of the manage.py file."""
return os.path.join(managePyDir(), 'manage.py') return managePyDir().joinpath('manage.py')
def manage(c, cmd, pty: bool = False): def manage(c, cmd, pty: bool = False):
@ -171,7 +172,7 @@ def translate_stats(c):
The file generated from this is needed for the UI. The file generated from this is needed for the UI.
""" """
path = os.path.join('InvenTree', 'script', 'translation_stats.py') path = Path('InvenTree', 'script', 'translation_stats.py')
c.run(f'python3 {path}') c.run(f'python3 {path}')
@ -252,12 +253,11 @@ def export_records(c, filename='data.json', overwrite=False, include_permissions
""" """
# Get an absolute path to the file # Get an absolute path to the file
if not os.path.isabs(filename): if not os.path.isabs(filename):
filename = os.path.join(localDir(), filename) filename = localDir().joinpath(filename).resolve()
filename = os.path.abspath(filename)
print(f"Exporting database records to file '{filename}'") print(f"Exporting database records to file '{filename}'")
if os.path.exists(filename) and overwrite is False: if filename.exists() and overwrite is False:
response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ") response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ")
response = str(response).strip().lower() response = str(response).strip().lower()
@ -306,7 +306,7 @@ def import_records(c, filename='data.json', clear=False):
"""Import database records from a file.""" """Import database records from a file."""
# Get an absolute path to the supplied filename # Get an absolute path to the supplied filename
if not os.path.isabs(filename): if not os.path.isabs(filename):
filename = os.path.join(localDir(), filename) filename = localDir().joinpath(filename)
if not os.path.exists(filename): if not os.path.exists(filename):
print(f"Error: File '{filename}' does not exist") print(f"Error: File '{filename}' does not exist")
@ -442,8 +442,8 @@ def test_translations(c):
from django.conf import settings from django.conf import settings
# setup django # setup django
base_path = os.getcwd() base_path = Path.cwd()
new_base_path = pathlib.Path('InvenTree').absolute() new_base_path = pathlib.Path('InvenTree').resolve()
sys.path.append(str(new_base_path)) sys.path.append(str(new_base_path))
os.chdir(new_base_path) os.chdir(new_base_path)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings')
@ -487,8 +487,8 @@ def test_translations(c):
file_new.write(line) file_new.write(line)
# change out translation files # change out translation files
os.rename(file_path, str(file_path) + '_old') file_path.rename(str(file_path) + '_old')
os.rename(new_file_path, file_path) new_file_path.rename(file_path)
# compile languages # compile languages
print("Compile languages ...") print("Compile languages ...")