Skip to content

Commit

Permalink
Refactor label/report template copying (#6582)
Browse files Browse the repository at this point in the history
* [BUG] Inventree fiddles with files directly rather than using Django Storage api
Fixes #2585

* PEP fix

* clean diff

* move template discovery into central location

* more moving file operations

* fix paths

* and another path fixing

* more fixes

* fix typing

* switch config back to local

* revert locale stats

* add s3 support

* storages

* more adaptions

* use s3 switch to set storage backend

* fix reqs

* cleanup default_storage

* init in storage_backend

* move to storage classes everywhere

* fix call

* remove more S3 references

* move storage init

* fix startup error

* alsways use url

* ignore FileExistsError

* move s3 required url in

* remove S3 for now

* use Djangos defaults

* fix old import

* remove default_storage calls

* make labels/reports more similar

* expand functions out

* refactor to use refs where possible

* refactor copy section to be similar

* unify db lookup

* move shared code to generic section

* move ready out

* docstrings

* move even more functions out

* move references inline of the classes

* clean up refs

* fix init

* fix ensure dir

* remove unneeded tries

* cleanup diff

* more cleanup

* fix tests

* use SUBDIR
  • Loading branch information
matmair authored Feb 27, 2024
1 parent f6123cc commit 1199291
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 367 deletions.
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

0 comments on commit 1199291

Please sign in to comment.