Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor label/report template copying #6582

Merged
merged 57 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
089b25c
[BUG] Inventree fiddles with files directly rather than using Django …
matmair Apr 24, 2022
ecba35f
PEP fix
matmair Apr 24, 2022
e714cc2
Merge branch 'inventree:master' into matmair/issue2585
matmair Apr 27, 2022
9c77698
Merge branch 'inventree:master' into matmair/issue2585
matmair Apr 30, 2022
e3be02d
Merge branch 'inventree:master' into matmair/issue2585
matmair May 4, 2022
85214ce
Merge branch 'inventree:master' into matmair/issue2585
matmair May 4, 2022
9c76659
Merge branch 'inventree:master' into matmair/issue2585
matmair May 5, 2022
d9c960f
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Feb 11, 2024
54cdb8e
clean diff
matmair Feb 11, 2024
f9399b8
move template discovery into central location
matmair Feb 12, 2024
21b7eea
more moving file operations
matmair Feb 12, 2024
6fd4a63
fix paths
matmair Feb 12, 2024
e3ef162
and another path fixing
matmair Feb 12, 2024
7e2ea77
more fixes
matmair Feb 12, 2024
6d576c1
fix typing
matmair Feb 12, 2024
0b388b5
switch config back to local
matmair Feb 12, 2024
b513a90
revert locale stats
matmair Feb 12, 2024
c6e1235
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Feb 13, 2024
4f0e503
add s3 support
matmair Feb 13, 2024
244895a
storages
matmair Feb 13, 2024
bf491ef
more adaptions
matmair Feb 15, 2024
aac4fe2
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Feb 26, 2024
03c191e
use s3 switch to set storage backend
matmair Feb 26, 2024
1668881
fix reqs
matmair Feb 26, 2024
86196e9
cleanup default_storage
matmair Feb 26, 2024
de1194f
init in storage_backend
matmair Feb 26, 2024
982fe05
move to storage classes everywhere
matmair Feb 26, 2024
b400695
fix call
matmair Feb 26, 2024
769a128
remove more S3 references
matmair Feb 26, 2024
f8d379f
move storage init
matmair Feb 26, 2024
488143e
fix startup error
matmair Feb 26, 2024
b4a5460
alsways use url
matmair Feb 26, 2024
fa91cb3
ignore FileExistsError
matmair Feb 26, 2024
a8259fe
move s3 required url in
matmair Feb 26, 2024
cf598ff
remove S3 for now
matmair Feb 26, 2024
b49eda6
use Djangos defaults
matmair Feb 26, 2024
b4506eb
fix old import
matmair Feb 26, 2024
a4f556e
remove default_storage calls
matmair Feb 26, 2024
d31a65e
make labels/reports more similar
matmair Feb 26, 2024
4fb7228
expand functions out
matmair Feb 26, 2024
033f9db
refactor to use refs where possible
matmair Feb 26, 2024
6739d40
refactor copy section to be similar
matmair Feb 26, 2024
e699790
unify db lookup
matmair Feb 26, 2024
7e2c8e8
move shared code to generic section
matmair Feb 26, 2024
6f6e807
move ready out
matmair Feb 27, 2024
ae2f930
docstrings
matmair Feb 27, 2024
88c9cce
move even more functions out
matmair Feb 27, 2024
eb31939
move references inline of the classes
matmair Feb 27, 2024
0fe2d78
clean up refs
matmair Feb 27, 2024
3bc361a
fix init
matmair Feb 27, 2024
a9e1723
fix ensure dir
matmair Feb 27, 2024
98dbf01
remove unneeded tries
matmair Feb 27, 2024
e47319f
cleanup diff
matmair Feb 27, 2024
8ab4e57
more cleanup
matmair Feb 27, 2024
feb0859
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Feb 27, 2024
7083c1d
fix tests
matmair Feb 27, 2024
b109f0c
use SUBDIR
matmair Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion InvenTree/InvenTree/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import warnings
from pathlib import Path

from django.core.files.base import ContentFile
from django.core.files.storage import Storage

logger = logging.getLogger('inventree')
CONFIG_DATA = None
CONFIG_LOOKUPS = {}
Expand Down Expand Up @@ -69,11 +72,16 @@ def get_base_dir() -> Path:
return Path(__file__).parent.parent.resolve()


def ensure_dir(path: Path) -> None:
def ensure_dir(path: Path, storage=None) -> None:
"""Ensure that a directory exists.

If it does not exist, create it.
"""
if storage and isinstance(storage, Storage):
if not storage.exists(str(path)):
storage.save(str(path / '.empty'), ContentFile(''))
return

if not path.exists():
path.mkdir(parents=True, exist_ok=True)

Expand Down
Empty file.
140 changes: 140 additions & 0 deletions InvenTree/generic/templating/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Shared templating code."""

import logging
import os
import warnings
from pathlib import Path

from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
from django.core.files.storage import default_storage
from django.db.utils import IntegrityError, OperationalError, ProgrammingError

from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode

import InvenTree.helpers
from InvenTree.config import ensure_dir

logger = logging.getLogger('inventree')


MEDIA_STORAGE_DIR = Path(settings.MEDIA_ROOT)


class TemplatingMixin:
"""Mixin that contains shared templating code."""

name: str = ''
db: str = ''

def __init__(self, *args, **kwargs):
"""Ensure that the required properties are set."""
super().__init__(*args, **kwargs)
if self.name == '':
raise NotImplementedError('ref must be set')
if self.db == '':
raise NotImplementedError('db must be set')

def create_defaults(self):
"""Function that creates all default templates for the app."""
raise NotImplementedError('create_defaults must be implemented')

def get_src_dir(self, ref_name):
"""Get the source directory for the default templates."""
raise NotImplementedError('get_src_dir must be implemented')

def get_new_obj_data(self, data, filename):
"""Get the data for a new template db object."""
raise NotImplementedError('get_new_obj_data must be implemented')

# Standardized code
def ready(self):
"""This function is called whenever the app is loaded."""
import InvenTree.ready

# skip loading if plugin registry is not loaded or we run in a background thread
if (
not InvenTree.ready.isPluginRegistryLoaded()
or not InvenTree.ready.isInMainThread()
):
return

if not InvenTree.ready.canAppAccessDatabase(allow_test=False):
return # pragma: no cover

with maintenance_mode_on():
try:
self.create_defaults()
except (
AppRegistryNotReady,
IntegrityError,
OperationalError,
ProgrammingError,
):
# Database might not yet be ready
warnings.warn(
f'Database was not ready for creating {self.name}s', stacklevel=2
)

set_maintenance_mode(False)

def create_template_dir(self, model, data):
"""Create folder and database entries for the default templates, if they do not already exist."""
ref_name = model.getSubdir()

# Create root dir for templates
src_dir = self.get_src_dir(ref_name)
dst_dir = MEDIA_STORAGE_DIR.joinpath(self.name, 'inventree', ref_name)
ensure_dir(dst_dir, default_storage)

# Copy each template across (if required)
for entry in data:
self.create_template_file(model, src_dir, entry, ref_name)

def create_template_file(self, model, src_dir, data, ref_name):
"""Ensure a label template is in place."""
# Destination filename
filename = os.path.join(self.name, 'inventree', ref_name, data['file'])

src_file = src_dir.joinpath(data['file'])
dst_file = MEDIA_STORAGE_DIR.joinpath(filename)

do_copy = False

if not dst_file.exists():
logger.info("%s template '%s' is not present", self.name, filename)
do_copy = True
else:
# Check if the file contents are different
src_hash = InvenTree.helpers.hash_file(src_file)
dst_hash = InvenTree.helpers.hash_file(dst_file)

if src_hash != dst_hash:
logger.info("Hash differs for '%s'", filename)
do_copy = True

if do_copy:
logger.info("Copying %s template '%s'", self.name, dst_file)
# Ensure destination dir exists
dst_file.parent.mkdir(parents=True, exist_ok=True)

# Copy file
default_storage.save(filename, src_file.open('rb'))

# Check if a file matching the template already exists
try:
if model.objects.filter(**{self.db: filename}).exists():
return # pragma: no cover
except Exception:
logger.exception(
"Failed to query %s for '%s' - you should run 'invoke update' first!",
self.name,
filename,
)

logger.info("Creating entry for %s '%s'", model, data.get('name'))

try:
model.objects.create(**self.get_new_obj_data(data, filename))
except Exception:
logger.warning("Failed to create %s '%s'", self.name, data['name'])
155 changes: 30 additions & 125 deletions InvenTree/label/apps.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,31 @@
"""label app specification."""
"""Config options for the label app."""

import hashlib
import logging
import os
import shutil
import warnings
from pathlib import Path

from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import AppRegistryNotReady
from django.db.utils import IntegrityError, OperationalError, ProgrammingError

from maintenance_mode.core import maintenance_mode_on, set_maintenance_mode
from generic.templating.apps import TemplatingMixin

import InvenTree.helpers
import InvenTree.ready

logger = logging.getLogger('inventree')


class LabelConfig(AppConfig):
"""App configuration class for the 'label' app."""
class LabelConfig(TemplatingMixin, AppConfig):
"""Configuration class for the "label" app."""

name = 'label'
db = 'label'

def ready(self):
"""This function is called whenever the label app is loaded."""
# skip loading if plugin registry is not loaded or we run in a background thread
if (
not InvenTree.ready.isPluginRegistryLoaded()
or not InvenTree.ready.isInMainThread()
):
return

if not InvenTree.ready.canAppAccessDatabase(allow_test=False):
return # pragma: no cover

with maintenance_mode_on():
try:
self.create_labels() # pragma: no cover
except (
AppRegistryNotReady,
IntegrityError,
OperationalError,
ProgrammingError,
):
# Database might not yet be ready
warnings.warn(
'Database was not ready for creating labels', stacklevel=2
)

set_maintenance_mode(False)

def create_labels(self):
def create_defaults(self):
"""Create all default templates."""
# Test if models are ready
import label.models

try:
import label.models
except Exception: # pragma: no cover
# Database is not ready yet
return
assert bool(label.models.StockLocationLabel is not None)

# Create the categories
self.create_labels_category(
self.create_template_dir(
label.models.StockItemLabel,
'stockitem',
[
{
'file': 'qr.html',
Expand All @@ -75,9 +37,8 @@ def create_labels(self):
],
)

self.create_labels_category(
self.create_template_dir(
label.models.StockLocationLabel,
'stocklocation',
[
{
'file': 'qr.html',
Expand All @@ -96,9 +57,8 @@ def create_labels(self):
],
)

self.create_labels_category(
self.create_template_dir(
label.models.PartLabel,
'part',
[
{
'file': 'part_label.html',
Expand All @@ -117,9 +77,8 @@ def create_labels(self):
],
)

self.create_labels_category(
self.create_template_dir(
label.models.BuildLineLabel,
'buildline',
[
{
'file': 'buildline_label.html',
Expand All @@ -131,72 +90,18 @@ def create_labels(self):
],
)

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("Creating required directory: '%s'", dst_dir)
dst_dir.mkdir(parents=True, exist_ok=True)

# Create 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('label', 'inventree', ref_name, label['file'])

src_file = src_dir.joinpath(label['file'])
dst_file = settings.MEDIA_ROOT.joinpath(filename)

to_copy = False

if dst_file.exists():
# File already exists - let's see if it is the "same"

if InvenTree.helpers.hash_file(dst_file) != InvenTree.helpers.hash_file(
src_file
): # pragma: no cover
logger.info("Hash differs for '%s'", filename)
to_copy = True

else:
logger.info("Label template '%s' is not present", filename)
to_copy = True

if to_copy:
logger.info("Copying label template '%s'", dst_file)
# Ensure destination dir exists
dst_file.parent.mkdir(parents=True, exist_ok=True)

# Copy file
shutil.copyfile(src_file, dst_file)

# Check if a label matching the template already exists
try:
if model.objects.filter(label=filename).exists():
return # pragma: no cover
except Exception:
logger.exception(
"Failed to query label for '%s' - you should run 'invoke update' first!",
filename,
)

logger.info("Creating entry for %s '%s'", model, label['name'])

try:
model.objects.create(
name=label['name'],
description=label['description'],
label=filename,
filters='',
enabled=True,
width=label['width'],
height=label['height'],
)
except Exception:
logger.warning("Failed to create label '%s'", label['name'])
def get_src_dir(self, ref_name):
"""Get the source directory."""
return Path(__file__).parent.joinpath('templates', self.name, ref_name)

def get_new_obj_data(self, data, filename):
"""Get the data for a new template db object."""
return {
'name': data['name'],
'description': data['description'],
'label': filename,
'filters': '',
'enabled': True,
'width': data['width'],
'height': data['height'],
}
7 changes: 6 additions & 1 deletion InvenTree/label/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ class Meta:

abstract = True

@classmethod
def getSubdir(cls) -> str:
"""Return the subdirectory for this label."""
return cls.SUBDIR

# Each class of label files will be stored in a separate subdirectory
SUBDIR = 'label'
SUBDIR: str = 'label'

# Object we will be printing against (will be filled out later)
object_to_print = None
Expand Down
2 changes: 1 addition & 1 deletion InvenTree/label/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class LabelTest(InvenTreeAPITestCase):
def setUpTestData(cls):
"""Ensure that some label instances exist as part of init routine."""
super().setUpTestData()
apps.get_app_config('label').create_labels()
apps.get_app_config('label').create_defaults()

def test_default_labels(self):
"""Test that the default label templates are copied across."""
Expand Down
Loading
Loading