Database migration tool, deduplication of sets data, customizable checkboxes #44
12
app.py
12
app.py
@ -20,19 +20,19 @@ setup_app(app)
|
|||||||
# Create the socket
|
# Create the socket
|
||||||
s = BrickSocket(
|
s = BrickSocket(
|
||||||
app,
|
app,
|
||||||
threaded=not app.config['NO_THREADED_SOCKET'].value,
|
threaded=not app.config['NO_THREADED_SOCKET'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Run the application
|
# Run the application
|
||||||
logger.info('Starting BrickTracker on {host}:{port}'.format(
|
logger.info('Starting BrickTracker on {host}:{port}'.format(
|
||||||
host=app.config['HOST'].value,
|
host=app.config['HOST'],
|
||||||
port=app.config['PORT'].value,
|
port=app.config['PORT'],
|
||||||
))
|
))
|
||||||
s.socket.run(
|
s.socket.run(
|
||||||
app,
|
app,
|
||||||
host=app.config['HOST'].value,
|
host=app.config['HOST'],
|
||||||
debug=app.config['DEBUG'].value,
|
debug=app.config['DEBUG'],
|
||||||
port=app.config['PORT'].value,
|
port=app.config['PORT'],
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,7 @@ def setup_app(app: Flask) -> None:
|
|||||||
BrickConfigurationList(app)
|
BrickConfigurationList(app)
|
||||||
|
|
||||||
# Set the logging level
|
# Set the logging level
|
||||||
if app.config['DEBUG'].value:
|
if app.config['DEBUG']:
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
stream=sys.stdout,
|
stream=sys.stdout,
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
@ -90,7 +90,7 @@ def setup_app(app: Flask) -> None:
|
|||||||
g.request_time = request_time
|
g.request_time = request_time
|
||||||
|
|
||||||
# Register the timezone
|
# Register the timezone
|
||||||
g.timezone = ZoneInfo(current_app.config['TIMEZONE'].value)
|
g.timezone = ZoneInfo(current_app.config['TIMEZONE'])
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
g.version = __version__
|
g.version = __version__
|
||||||
|
@ -69,6 +69,7 @@ class BrickConfiguration(object):
|
|||||||
# Remove static prefix
|
# Remove static prefix
|
||||||
value = value.removeprefix('static/')
|
value = value.removeprefix('static/')
|
||||||
|
|
||||||
|
# Type casting
|
||||||
if self.cast is not None:
|
if self.cast is not None:
|
||||||
self.value = self.cast(value)
|
self.value = self.cast(value)
|
||||||
else:
|
else:
|
||||||
|
@ -1,46 +1,60 @@
|
|||||||
|
import logging
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from flask import current_app, Flask
|
from flask import Flask
|
||||||
|
|
||||||
from .config import CONFIG
|
from .config import CONFIG
|
||||||
from .configuration import BrickConfiguration
|
from .configuration import BrickConfiguration
|
||||||
from .exceptions import ConfigurationMissingException
|
from .exceptions import ConfigurationMissingException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Application configuration
|
# Application configuration
|
||||||
class BrickConfigurationList(object):
|
class BrickConfigurationList(object):
|
||||||
app: Flask
|
app: Flask
|
||||||
|
configurations: dict[str, BrickConfiguration]
|
||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
def __init__(self, app: Flask, /):
|
def __init__(self, app: Flask, /):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
# Process all configuration items
|
# Load the configurations only there is none already loaded
|
||||||
for config in CONFIG:
|
configurations = getattr(self, 'configurations', None)
|
||||||
item = BrickConfiguration(**config)
|
|
||||||
self.app.config[item.name] = item
|
if configurations is None:
|
||||||
|
logger.info('Loading configuration variables')
|
||||||
|
|
||||||
|
BrickConfigurationList.configurations = {}
|
||||||
|
|
||||||
|
# Process all configuration items
|
||||||
|
for config in CONFIG:
|
||||||
|
item = BrickConfiguration(**config)
|
||||||
|
|
||||||
|
# Store in the list
|
||||||
|
BrickConfigurationList.configurations[item.name] = item
|
||||||
|
|
||||||
|
# Only store the value in the app to avoid breaking any
|
||||||
|
# existing variables
|
||||||
|
self.app.config[item.name] = item.value
|
||||||
|
|
||||||
# Check whether a str configuration is set
|
# Check whether a str configuration is set
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def error_unless_is_set(name: str):
|
def error_unless_is_set(name: str):
|
||||||
config: BrickConfiguration = current_app.config[name]
|
configuration = BrickConfigurationList.configurations[name]
|
||||||
|
|
||||||
if config.value is None or config.value == '':
|
if configuration.value is None or configuration.value == '':
|
||||||
raise ConfigurationMissingException(
|
raise ConfigurationMissingException(
|
||||||
'{name} must be defined (using the {environ} environment variable)'.format( # noqa: E501
|
'{name} must be defined (using the {environ} environment variable)'.format( # noqa: E501
|
||||||
name=config.name,
|
name=name,
|
||||||
environ=config.env_name
|
environ=configuration.env_name
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all the configuration items from the app config
|
# Get all the configuration items from the app config
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list() -> Generator[BrickConfiguration, None, None]:
|
def list() -> Generator[BrickConfiguration, None, None]:
|
||||||
keys = list(current_app.config.keys())
|
keys = sorted(BrickConfigurationList.configurations.keys())
|
||||||
keys.sort()
|
|
||||||
|
|
||||||
for name in keys:
|
for name in keys:
|
||||||
config = current_app.config[name]
|
yield BrickConfigurationList.configurations[name]
|
||||||
|
|
||||||
if isinstance(config, BrickConfiguration):
|
|
||||||
yield config
|
|
||||||
|
@ -39,7 +39,7 @@ class BrickInstructions(object):
|
|||||||
# Store the name and extension, check if extension is allowed
|
# Store the name and extension, check if extension is allowed
|
||||||
self.name, self.extension = os.path.splitext(self.filename)
|
self.name, self.extension = os.path.splitext(self.filename)
|
||||||
self.extension = self.extension.lower()
|
self.extension = self.extension.lower()
|
||||||
self.allowed = self.extension in current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value # noqa: E501
|
self.allowed = self.extension in current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'] # noqa: E501
|
||||||
|
|
||||||
# Placeholder
|
# Placeholder
|
||||||
self.brickset = None
|
self.brickset = None
|
||||||
@ -67,7 +67,7 @@ class BrickInstructions(object):
|
|||||||
# Display the time in a human format
|
# Display the time in a human format
|
||||||
def human_time(self) -> str:
|
def human_time(self) -> str:
|
||||||
return self.mtime.astimezone(g.timezone).strftime(
|
return self.mtime.astimezone(g.timezone).strftime(
|
||||||
current_app.config['FILE_DATETIME_FORMAT'].value
|
current_app.config['FILE_DATETIME_FORMAT']
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute the path of an instruction file
|
# Compute the path of an instruction file
|
||||||
@ -77,7 +77,7 @@ class BrickInstructions(object):
|
|||||||
|
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
current_app.static_folder, # type: ignore
|
current_app.static_folder, # type: ignore
|
||||||
current_app.config['INSTRUCTIONS_FOLDER'].value,
|
current_app.config['INSTRUCTIONS_FOLDER'],
|
||||||
filename
|
filename
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class BrickInstructions(object):
|
|||||||
if not self.allowed:
|
if not self.allowed:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
folder: str = current_app.config['INSTRUCTIONS_FOLDER'].value
|
folder: str = current_app.config['INSTRUCTIONS_FOLDER']
|
||||||
|
|
||||||
# Compute the path
|
# Compute the path
|
||||||
path = os.path.join(folder, self.filename)
|
path = os.path.join(folder, self.filename)
|
||||||
|
@ -36,7 +36,7 @@ class BrickInstructionsList(object):
|
|||||||
# Make a folder relative to static
|
# Make a folder relative to static
|
||||||
folder: str = os.path.join(
|
folder: str = os.path.join(
|
||||||
current_app.static_folder, # type: ignore
|
current_app.static_folder, # type: ignore
|
||||||
current_app.config['INSTRUCTIONS_FOLDER'].value,
|
current_app.config['INSTRUCTIONS_FOLDER'],
|
||||||
)
|
)
|
||||||
|
|
||||||
for file in os.scandir(folder):
|
for file in os.scandir(folder):
|
||||||
@ -68,9 +68,8 @@ class BrickInstructionsList(object):
|
|||||||
for brickset in BrickSetList().generic().records:
|
for brickset in BrickSetList().generic().records:
|
||||||
bricksets[brickset.fields.set_num] = brickset
|
bricksets[brickset.fields.set_num] = brickset
|
||||||
|
|
||||||
# Return the files
|
# Inject the brickset if it exists
|
||||||
for instruction in self.all.values():
|
for instruction in self.all.values():
|
||||||
# Inject the brickset if it exists
|
|
||||||
if (
|
if (
|
||||||
instruction.allowed and
|
instruction.allowed and
|
||||||
instruction.number is not None and
|
instruction.number is not None and
|
||||||
|
@ -12,7 +12,7 @@ class LoginManager(object):
|
|||||||
|
|
||||||
def __init__(self, app: Flask, /):
|
def __init__(self, app: Flask, /):
|
||||||
# Setup basic authentication
|
# Setup basic authentication
|
||||||
app.secret_key = app.config['AUTHENTICATION_KEY'].value
|
app.secret_key = app.config['AUTHENTICATION_KEY']
|
||||||
|
|
||||||
manager = login_manager.LoginManager()
|
manager = login_manager.LoginManager()
|
||||||
manager.login_view = 'login.login' # type: ignore
|
manager.login_view = 'login.login' # type: ignore
|
||||||
@ -23,11 +23,11 @@ class LoginManager(object):
|
|||||||
def user_loader(*arg) -> LoginManager.User:
|
def user_loader(*arg) -> LoginManager.User:
|
||||||
return self.User(
|
return self.User(
|
||||||
'admin',
|
'admin',
|
||||||
app.config['AUTHENTICATION_PASSWORD'].value
|
app.config['AUTHENTICATION_PASSWORD']
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the password is unset, globally disable
|
# If the password is unset, globally disable
|
||||||
app.config['LOGIN_DISABLED'] = app.config['AUTHENTICATION_PASSWORD'].value == '' # noqa: E501
|
app.config['LOGIN_DISABLED'] = app.config['AUTHENTICATION_PASSWORD'] == '' # noqa: E501
|
||||||
|
|
||||||
# Tells whether the user is authenticated, meaning:
|
# Tells whether the user is authenticated, meaning:
|
||||||
# - Authentication disabled
|
# - Authentication disabled
|
||||||
|
@ -119,7 +119,7 @@ class BrickMinifigure(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for minifigure part image
|
# Compute the url for minifigure part image
|
||||||
def url_for_image(self, /) -> str:
|
def url_for_image(self, /) -> str:
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
if self.fields.set_img_url is None:
|
if self.fields.set_img_url is None:
|
||||||
file = RebrickableImage.nil_minifigure_name()
|
file = RebrickableImage.nil_minifigure_name()
|
||||||
else:
|
else:
|
||||||
@ -128,15 +128,15 @@ class BrickMinifigure(BrickRecord):
|
|||||||
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
|
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
|
||||||
else:
|
else:
|
||||||
if self.fields.set_img_url is None:
|
if self.fields.set_img_url is None:
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value # noqa: E501
|
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
||||||
else:
|
else:
|
||||||
return self.fields.set_img_url
|
return self.fields.set_img_url
|
||||||
|
|
||||||
# Compute the url for the rebrickable page
|
# Compute the url for the rebrickable page
|
||||||
def url_for_rebrickable(self, /) -> str:
|
def url_for_rebrickable(self, /) -> str:
|
||||||
if current_app.config['REBRICKABLE_LINKS'].value:
|
if current_app.config['REBRICKABLE_LINKS']:
|
||||||
try:
|
try:
|
||||||
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].value.format( # noqa: E501
|
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
|
||||||
number=self.fields.fig_num.lower(),
|
number=self.fields.fig_num.lower(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -27,7 +27,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
self.brickset = None
|
self.brickset = None
|
||||||
|
|
||||||
# Store the order for this list
|
# Store the order for this list
|
||||||
self.order = current_app.config['MINIFIGURES_DEFAULT_ORDER'].value
|
self.order = current_app.config['MINIFIGURES_DEFAULT_ORDER']
|
||||||
|
|
||||||
# Load all minifigures
|
# Load all minifigures
|
||||||
def all(self, /) -> Self:
|
def all(self, /) -> Self:
|
||||||
@ -44,7 +44,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
# Last added minifigure
|
# Last added minifigure
|
||||||
def last(self, /, limit: int = 6) -> Self:
|
def last(self, /, limit: int = 6) -> Self:
|
||||||
# Randomize
|
# Randomize
|
||||||
if current_app.config['RANDOM'].value:
|
if current_app.config['RANDOM']:
|
||||||
order = 'RANDOM()'
|
order = 'RANDOM()'
|
||||||
else:
|
else:
|
||||||
order = 'minifigures.rowid DESC'
|
order = 'minifigures.rowid DESC'
|
||||||
|
@ -190,9 +190,9 @@ class BrickPart(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for the bricklink page
|
# Compute the url for the bricklink page
|
||||||
def url_for_bricklink(self, /) -> str:
|
def url_for_bricklink(self, /) -> str:
|
||||||
if current_app.config['BRICKLINK_LINKS'].value:
|
if current_app.config['BRICKLINK_LINKS']:
|
||||||
try:
|
try:
|
||||||
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].value.format( # noqa: E501
|
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
|
||||||
number=self.fields.part_num,
|
number=self.fields.part_num,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -202,7 +202,7 @@ class BrickPart(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for the part image
|
# Compute the url for the part image
|
||||||
def url_for_image(self, /) -> str:
|
def url_for_image(self, /) -> str:
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
if self.fields.part_img_url is None:
|
if self.fields.part_img_url is None:
|
||||||
file = RebrickableImage.nil_name()
|
file = RebrickableImage.nil_name()
|
||||||
else:
|
else:
|
||||||
@ -211,7 +211,7 @@ class BrickPart(BrickRecord):
|
|||||||
return RebrickableImage.static_url(file, 'PARTS_FOLDER')
|
return RebrickableImage.static_url(file, 'PARTS_FOLDER')
|
||||||
else:
|
else:
|
||||||
if self.fields.part_img_url is None:
|
if self.fields.part_img_url is None:
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL'].value
|
return current_app.config['REBRICKABLE_IMAGE_NIL']
|
||||||
else:
|
else:
|
||||||
return self.fields.part_img_url
|
return self.fields.part_img_url
|
||||||
|
|
||||||
@ -234,9 +234,9 @@ class BrickPart(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for the rebrickable page
|
# Compute the url for the rebrickable page
|
||||||
def url_for_rebrickable(self, /) -> str:
|
def url_for_rebrickable(self, /) -> str:
|
||||||
if current_app.config['REBRICKABLE_LINKS'].value:
|
if current_app.config['REBRICKABLE_LINKS']:
|
||||||
try:
|
try:
|
||||||
return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].value.format( # noqa: E501
|
return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501
|
||||||
number=self.fields.part_num,
|
number=self.fields.part_num,
|
||||||
color=self.fields.color_id,
|
color=self.fields.color_id,
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
self.minifigure = None
|
self.minifigure = None
|
||||||
|
|
||||||
# Store the order for this list
|
# Store the order for this list
|
||||||
self.order = current_app.config['PARTS_DEFAULT_ORDER'].value
|
self.order = current_app.config['PARTS_DEFAULT_ORDER']
|
||||||
|
|
||||||
# Load all parts
|
# Load all parts
|
||||||
def all(self, /) -> Self:
|
def all(self, /) -> Self:
|
||||||
@ -63,10 +63,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
record=record,
|
record=record,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
|
||||||
current_app.config['SKIP_SPARE_PARTS'].value and
|
|
||||||
part.fields.is_spare
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.records.append(part)
|
self.records.append(part)
|
||||||
@ -92,10 +89,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
record=record,
|
record=record,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
|
||||||
current_app.config['SKIP_SPARE_PARTS'].value and
|
|
||||||
part.fields.is_spare
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.records.append(part)
|
self.records.append(part)
|
||||||
|
@ -77,7 +77,7 @@ class Rebrickable(Generic[T]):
|
|||||||
|
|
||||||
# Bootstrap a first set of parameters
|
# Bootstrap a first set of parameters
|
||||||
parameters: dict[str, Any] | None = {
|
parameters: dict[str, Any] | None = {
|
||||||
'page_size': current_app.config['REBRICKABLE_PAGE_SIZE'].value,
|
'page_size': current_app.config['REBRICKABLE_PAGE_SIZE'],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read all pages
|
# Read all pages
|
||||||
@ -115,7 +115,7 @@ class Rebrickable(Generic[T]):
|
|||||||
# Load from the API
|
# Load from the API
|
||||||
def load(self, /, parameters: dict[str, Any] = {}) -> dict[str, Any]:
|
def load(self, /, parameters: dict[str, Any] = {}) -> dict[str, Any]:
|
||||||
# Inject the API key
|
# Inject the API key
|
||||||
parameters['api_key'] = current_app.config['REBRICKABLE_API_KEY'].value, # noqa: E501
|
parameters['api_key'] = current_app.config['REBRICKABLE_API_KEY']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.loads(
|
return json.loads(
|
||||||
|
@ -70,12 +70,12 @@ class RebrickableImage(object):
|
|||||||
# Return the folder depending on the objects provided
|
# Return the folder depending on the objects provided
|
||||||
def folder(self, /) -> str:
|
def folder(self, /) -> str:
|
||||||
if self.part is not None:
|
if self.part is not None:
|
||||||
return current_app.config['PARTS_FOLDER'].value
|
return current_app.config['PARTS_FOLDER']
|
||||||
|
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
return current_app.config['MINIFIGURES_FOLDER'].value
|
return current_app.config['MINIFIGURES_FOLDER']
|
||||||
|
|
||||||
return current_app.config['SETS_FOLDER'].value
|
return current_app.config['SETS_FOLDER']
|
||||||
|
|
||||||
# Return the id depending on the objects provided
|
# Return the id depending on the objects provided
|
||||||
def id(self, /) -> str:
|
def id(self, /) -> str:
|
||||||
@ -105,13 +105,13 @@ class RebrickableImage(object):
|
|||||||
def url(self, /) -> str:
|
def url(self, /) -> str:
|
||||||
if self.part is not None:
|
if self.part is not None:
|
||||||
if self.part.fields.part_img_url is None:
|
if self.part.fields.part_img_url is None:
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL'].value
|
return current_app.config['REBRICKABLE_IMAGE_NIL']
|
||||||
else:
|
else:
|
||||||
return self.part.fields.part_img_url
|
return self.part.fields.part_img_url
|
||||||
|
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
if self.minifigure.fields.set_img_url is None:
|
if self.minifigure.fields.set_img_url is None:
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value # noqa: E501
|
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
||||||
else:
|
else:
|
||||||
return self.minifigure.fields.set_img_url
|
return self.minifigure.fields.set_img_url
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class RebrickableImage(object):
|
|||||||
def nil_name() -> str:
|
def nil_name() -> str:
|
||||||
filename, _ = os.path.splitext(
|
filename, _ = os.path.splitext(
|
||||||
os.path.basename(
|
os.path.basename(
|
||||||
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL'].value).path # noqa: E501
|
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL']).path
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ class RebrickableImage(object):
|
|||||||
def nil_minifigure_name() -> str:
|
def nil_minifigure_name() -> str:
|
||||||
filename, _ = os.path.splitext(
|
filename, _ = os.path.splitext(
|
||||||
os.path.basename(
|
os.path.basename(
|
||||||
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'].value).path # noqa: E501
|
urlparse(current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']).path # noqa: E501
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ class RebrickableImage(object):
|
|||||||
# Return the static URL for an image given a name and folder
|
# Return the static URL for an image given a name and folder
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def static_url(name: str, folder_name: str) -> str:
|
def static_url(name: str, folder_name: str) -> str:
|
||||||
folder: str = current_app.config[folder_name].value
|
folder: str = current_app.config[folder_name]
|
||||||
|
|
||||||
# /!\ Everything is saved as .jpg, even if it came from a .png
|
# /!\ Everything is saved as .jpg, even if it came from a .png
|
||||||
# not changing this behaviour.
|
# not changing this behaviour.
|
||||||
|
@ -71,7 +71,7 @@ class RebrickableMinifigures(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
RebrickableImage(
|
RebrickableImage(
|
||||||
self.brickset,
|
self.brickset,
|
||||||
minifigure=minifigure
|
minifigure=minifigure
|
||||||
|
@ -76,7 +76,7 @@ class RebrickableParts(object):
|
|||||||
for index, part in enumerate(inventory):
|
for index, part in enumerate(inventory):
|
||||||
# Skip spare parts
|
# Skip spare parts
|
||||||
if (
|
if (
|
||||||
current_app.config['SKIP_SPARE_PARTS'].value and
|
current_app.config['SKIP_SPARE_PARTS'] and
|
||||||
part.fields.is_spare
|
part.fields.is_spare
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
@ -104,7 +104,7 @@ class RebrickableParts(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
RebrickableImage(
|
RebrickableImage(
|
||||||
self.brickset,
|
self.brickset,
|
||||||
minifigure=self.minifigure,
|
minifigure=self.minifigure,
|
||||||
|
@ -55,7 +55,7 @@ class RebrickableSet(object):
|
|||||||
# Insert into database
|
# Insert into database
|
||||||
brickset.insert(commit=False)
|
brickset.insert(commit=False)
|
||||||
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
RebrickableImage(brickset).download()
|
RebrickableImage(brickset).download()
|
||||||
|
|
||||||
# Load the inventory
|
# Load the inventory
|
||||||
@ -210,5 +210,5 @@ class RebrickableSet(object):
|
|||||||
# Insert into database
|
# Insert into database
|
||||||
brickwish.insert()
|
brickwish.insert()
|
||||||
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
RebrickableImage(brickwish).download()
|
RebrickableImage(brickwish).download()
|
||||||
|
@ -33,7 +33,7 @@ class BrickRetiredList(object):
|
|||||||
|
|
||||||
# Try to read the themes from a CSV file
|
# Try to read the themes from a CSV file
|
||||||
try:
|
try:
|
||||||
with open(current_app.config['RETIRED_SETS_PATH'].value, newline='') as themes_file: # noqa: E501
|
with open(current_app.config['RETIRED_SETS_PATH'], newline='') as themes_file: # noqa: E501
|
||||||
themes_reader = csv.reader(themes_file)
|
themes_reader = csv.reader(themes_file)
|
||||||
|
|
||||||
# Ignore the header
|
# Ignore the header
|
||||||
@ -44,7 +44,7 @@ class BrickRetiredList(object):
|
|||||||
BrickRetiredList.retired[retired.number] = retired
|
BrickRetiredList.retired[retired.number] = retired
|
||||||
|
|
||||||
# File stats
|
# File stats
|
||||||
stat = os.stat(current_app.config['RETIRED_SETS_PATH'].value)
|
stat = os.stat(current_app.config['RETIRED_SETS_PATH'])
|
||||||
BrickRetiredList.size = stat.st_size
|
BrickRetiredList.size = stat.st_size
|
||||||
BrickRetiredList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
|
BrickRetiredList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ class BrickRetiredList(object):
|
|||||||
def human_time(self) -> str:
|
def human_time(self) -> str:
|
||||||
if self.mtime is not None:
|
if self.mtime is not None:
|
||||||
return self.mtime.astimezone(g.timezone).strftime(
|
return self.mtime.astimezone(g.timezone).strftime(
|
||||||
current_app.config['FILE_DATETIME_FORMAT'].value
|
current_app.config['FILE_DATETIME_FORMAT']
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
@ -88,7 +88,7 @@ class BrickRetiredList(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def update() -> None:
|
def update() -> None:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
current_app.config['RETIRED_SETS_FILE_URL'].value,
|
current_app.config['RETIRED_SETS_FILE_URL'],
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ class BrickRetiredList(object):
|
|||||||
|
|
||||||
content = gzip.GzipFile(fileobj=response.raw)
|
content = gzip.GzipFile(fileobj=response.raw)
|
||||||
|
|
||||||
with open(current_app.config['RETIRED_SETS_PATH'].value, 'wb') as f:
|
with open(current_app.config['RETIRED_SETS_PATH'], 'wb') as f:
|
||||||
copyfileobj(content, f)
|
copyfileobj(content, f)
|
||||||
|
|
||||||
logger.info('Retired sets list updated')
|
logger.info('Retired sets list updated')
|
||||||
|
@ -157,7 +157,7 @@ class BrickSet(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for the set image
|
# Compute the url for the set image
|
||||||
def url_for_image(self, /) -> str:
|
def url_for_image(self, /) -> str:
|
||||||
if not current_app.config['USE_REMOTE_IMAGES'].value:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
return RebrickableImage.static_url(
|
return RebrickableImage.static_url(
|
||||||
self.fields.set_num,
|
self.fields.set_num,
|
||||||
'SETS_FOLDER'
|
'SETS_FOLDER'
|
||||||
@ -182,9 +182,9 @@ class BrickSet(BrickRecord):
|
|||||||
|
|
||||||
# Compute the url for the rebrickable page
|
# Compute the url for the rebrickable page
|
||||||
def url_for_rebrickable(self, /) -> str:
|
def url_for_rebrickable(self, /) -> str:
|
||||||
if current_app.config['REBRICKABLE_LINKS'].value:
|
if current_app.config['REBRICKABLE_LINKS']:
|
||||||
try:
|
try:
|
||||||
return current_app.config['REBRICKABLE_LINK_SET_PATTERN'].value.format( # noqa: E501
|
return current_app.config['REBRICKABLE_LINK_SET_PATTERN'].format( # noqa: E501
|
||||||
number=self.fields.set_num.lower(),
|
number=self.fields.set_num.lower(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -26,7 +26,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
self.themes = []
|
self.themes = []
|
||||||
|
|
||||||
# Store the order for this list
|
# Store the order for this list
|
||||||
self.order = current_app.config['SETS_DEFAULT_ORDER'].value
|
self.order = current_app.config['SETS_DEFAULT_ORDER']
|
||||||
|
|
||||||
# All the sets
|
# All the sets
|
||||||
def all(self, /) -> Self:
|
def all(self, /) -> Self:
|
||||||
@ -60,7 +60,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
# Last added sets
|
# Last added sets
|
||||||
def last(self, /, limit: int = 6) -> Self:
|
def last(self, /, limit: int = 6) -> Self:
|
||||||
# Randomize
|
# Randomize
|
||||||
if current_app.config['RANDOM'].value:
|
if current_app.config['RANDOM']:
|
||||||
order = 'RANDOM()'
|
order = 'RANDOM()'
|
||||||
else:
|
else:
|
||||||
order = 'sets.rowid DESC'
|
order = 'sets.rowid DESC'
|
||||||
|
@ -56,19 +56,19 @@ class BrickSocket(object):
|
|||||||
|
|
||||||
# Compute the namespace
|
# Compute the namespace
|
||||||
self.namespace = '/{namespace}'.format(
|
self.namespace = '/{namespace}'.format(
|
||||||
namespace=app.config['SOCKET_NAMESPACE'].value
|
namespace=app.config['SOCKET_NAMESPACE']
|
||||||
)
|
)
|
||||||
|
|
||||||
# Inject CORS if a domain is defined
|
# Inject CORS if a domain is defined
|
||||||
if app.config['DOMAIN_NAME'].value != '':
|
if app.config['DOMAIN_NAME'] != '':
|
||||||
kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME'].value
|
kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME']
|
||||||
|
|
||||||
# Instantiate the socket
|
# Instantiate the socket
|
||||||
self.socket = SocketIO(
|
self.socket = SocketIO(
|
||||||
self.app,
|
self.app,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
path=app.config['SOCKET_PATH'].value,
|
path=app.config['SOCKET_PATH'],
|
||||||
async_mode='eventlet',
|
async_mode='eventlet',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class BrickSQL(object):
|
|||||||
|
|
||||||
logger.debug('SQLite3: connect')
|
logger.debug('SQLite3: connect')
|
||||||
self.connection = sqlite3.connect(
|
self.connection = sqlite3.connect(
|
||||||
current_app.config['DATABASE_PATH'].value
|
current_app.config['DATABASE_PATH']
|
||||||
)
|
)
|
||||||
|
|
||||||
# Setup the row factory to get pseudo-dicts rather than tuples
|
# Setup the row factory to get pseudo-dicts rather than tuples
|
||||||
@ -249,7 +249,7 @@ class BrickSQL(object):
|
|||||||
# Delete the database
|
# Delete the database
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete() -> None:
|
def delete() -> None:
|
||||||
os.remove(current_app.config['DATABASE_PATH'].value)
|
os.remove(current_app.config['DATABASE_PATH'])
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('The database has been deleted')
|
logger.info('The database has been deleted')
|
||||||
@ -292,7 +292,7 @@ class BrickSQL(object):
|
|||||||
# Replace the database with a new file
|
# Replace the database with a new file
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def upload(file: FileStorage, /) -> None:
|
def upload(file: FileStorage, /) -> None:
|
||||||
file.save(current_app.config['DATABASE_PATH'].value)
|
file.save(current_app.config['DATABASE_PATH'])
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('The database has been imported using file {file}'.format(
|
logger.info('The database has been imported using file {file}'.format(
|
||||||
|
@ -33,7 +33,7 @@ class BrickThemeList(object):
|
|||||||
|
|
||||||
# Try to read the themes from a CSV file
|
# Try to read the themes from a CSV file
|
||||||
try:
|
try:
|
||||||
with open(current_app.config['THEMES_PATH'].value, newline='') as themes_file: # noqa: E501
|
with open(current_app.config['THEMES_PATH'], newline='') as themes_file: # noqa: E501
|
||||||
themes_reader = csv.reader(themes_file)
|
themes_reader = csv.reader(themes_file)
|
||||||
|
|
||||||
# Ignore the header
|
# Ignore the header
|
||||||
@ -44,7 +44,7 @@ class BrickThemeList(object):
|
|||||||
BrickThemeList.themes[theme.id] = theme
|
BrickThemeList.themes[theme.id] = theme
|
||||||
|
|
||||||
# File stats
|
# File stats
|
||||||
stat = os.stat(current_app.config['THEMES_PATH'].value)
|
stat = os.stat(current_app.config['THEMES_PATH'])
|
||||||
BrickThemeList.size = stat.st_size
|
BrickThemeList.size = stat.st_size
|
||||||
BrickThemeList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
|
BrickThemeList.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class BrickThemeList(object):
|
|||||||
def human_time(self) -> str:
|
def human_time(self) -> str:
|
||||||
if self.mtime is not None:
|
if self.mtime is not None:
|
||||||
return self.mtime.astimezone(g.timezone).strftime(
|
return self.mtime.astimezone(g.timezone).strftime(
|
||||||
current_app.config['FILE_DATETIME_FORMAT'].value
|
current_app.config['FILE_DATETIME_FORMAT']
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
@ -87,7 +87,7 @@ class BrickThemeList(object):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def update() -> None:
|
def update() -> None:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
current_app.config['THEMES_FILE_URL'].value,
|
current_app.config['THEMES_FILE_URL'],
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class BrickThemeList(object):
|
|||||||
|
|
||||||
content = gzip.GzipFile(fileobj=response.raw)
|
content = gzip.GzipFile(fileobj=response.raw)
|
||||||
|
|
||||||
with open(current_app.config['THEMES_PATH'].value, 'wb') as f:
|
with open(current_app.config['THEMES_PATH'], 'wb') as f:
|
||||||
copyfileobj(content, f)
|
copyfileobj(content, f)
|
||||||
|
|
||||||
logger.info('Theme list updated')
|
logger.info('Theme list updated')
|
||||||
|
@ -17,8 +17,8 @@ def add() -> str:
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'add.html',
|
'add.html',
|
||||||
path=current_app.config['SOCKET_PATH'].value,
|
path=current_app.config['SOCKET_PATH'],
|
||||||
namespace=current_app.config['SOCKET_NAMESPACE'].value,
|
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||||
messages=MESSAGES
|
messages=MESSAGES
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ def bulk() -> str:
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'bulk.html',
|
'bulk.html',
|
||||||
path=current_app.config['SOCKET_PATH'].value,
|
path=current_app.config['SOCKET_PATH'],
|
||||||
namespace=current_app.config['SOCKET_NAMESPACE'].value,
|
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||||
messages=MESSAGES
|
messages=MESSAGES
|
||||||
)
|
)
|
||||||
|
@ -160,19 +160,19 @@ def do_delete_database() -> Response:
|
|||||||
def download_database() -> Response:
|
def download_database() -> Response:
|
||||||
# Create a file name with a timestamp embedded
|
# Create a file name with a timestamp embedded
|
||||||
name, extension = os.path.splitext(
|
name, extension = os.path.splitext(
|
||||||
os.path.basename(current_app.config['DATABASE_PATH'].value)
|
os.path.basename(current_app.config['DATABASE_PATH'])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('The database has been downloaded')
|
logger.info('The database has been downloaded')
|
||||||
|
|
||||||
return send_file(
|
return send_file(
|
||||||
current_app.config['DATABASE_PATH'].value,
|
current_app.config['DATABASE_PATH'],
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
download_name='{name}-{timestamp}{extension}'.format(
|
download_name='{name}-{timestamp}{extension}'.format(
|
||||||
name=name,
|
name=name,
|
||||||
timestamp=datetime.now().astimezone(g.timezone).strftime(
|
timestamp=datetime.now().astimezone(g.timezone).strftime(
|
||||||
current_app.config['DATABASE_TIMESTAMP_FORMAT'].value
|
current_app.config['DATABASE_TIMESTAMP_FORMAT']
|
||||||
),
|
),
|
||||||
extension=extension
|
extension=extension
|
||||||
)
|
)
|
||||||
|
@ -114,7 +114,7 @@ def do_upload() -> Response:
|
|||||||
file = upload_helper(
|
file = upload_helper(
|
||||||
'file',
|
'file',
|
||||||
'instructions.upload',
|
'instructions.upload',
|
||||||
extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value,
|
extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'],
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(file, Response):
|
if isinstance(file, Response):
|
||||||
|
@ -18,7 +18,7 @@ class BrickWishList(BrickRecordList[BrickWish]):
|
|||||||
def all(self, /) -> Self:
|
def all(self, /) -> Self:
|
||||||
# Load the wished sets from the database
|
# Load the wished sets from the database
|
||||||
for record in self.select(
|
for record in self.select(
|
||||||
order=current_app.config['WISHES_DEFAULT_ORDER'].value
|
order=current_app.config['WISHES_DEFAULT_ORDER']
|
||||||
):
|
):
|
||||||
brickwish = BrickWish(record=record)
|
brickwish = BrickWish(record=record)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% if not config['HIDE_ADD_BULK_SET'].value %}
|
{% if not config['HIDE_ADD_BULK_SET'] %}
|
||||||
<div class="alert alert-primary" role="alert">
|
<div class="alert alert-primary" role="alert">
|
||||||
<h4 class="alert-heading">Too many to add?</h4>
|
<h4 class="alert-heading">Too many to add?</h4>
|
||||||
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
|
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% include 'admin/logout.html' %}
|
{% include 'admin/logout.html' %}
|
||||||
{% include 'admin/instructions.html' %}
|
{% include 'admin/instructions.html' %}
|
||||||
{% if not config['USE_REMOTE_IMAGES'].value %}
|
{% if not config['USE_REMOTE_IMAGES'] %}
|
||||||
{% include 'admin/image.html' %}
|
{% include 'admin/image.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'admin/theme.html' %}
|
{% include 'admin/theme.html' %}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
|
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
|
||||||
{% if not is_init %}
|
{% if not is_init %}
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<p>The database file is: <code>{{ config['DATABASE_PATH'].value }}</code>. The database is not initialized.</p>
|
<p>The database file is: <code>{{ config['DATABASE_PATH'] }}</code>. The database is not initialized.</p>
|
||||||
<hr>
|
<hr>
|
||||||
<form action="{{ url_for('admin.init_database') }}" method="post" class="text-end">
|
<form action="{{ url_for('admin.init_database') }}" method="post" class="text-end">
|
||||||
<button type="submit" class="btn btn-warning"><i class="ri-reset-right-fill"></i> Initialize the database</button>
|
<button type="submit" class="btn btn-warning"><i class="ri-reset-right-fill"></i> Initialize the database</button>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>The database file is: <code>{{ config['DATABASE_PATH'].value }}</code>. <i class="ri-checkbox-circle-line"></i> The database is initialized.</p>
|
<p>The database file is: <code>{{ config['DATABASE_PATH'] }}</code>. <i class="ri-checkbox-circle-line"></i> The database is initialized.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('admin.download_database') }}" class="btn btn-primary" role="button"><i class="ri-download-line"></i> Download the database file</a>
|
<a href="{{ url_for('admin.download_database') }}" class="btn btn-primary" role="button"><i class="ri-download-line"></i> Download the database file</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
{{ accordion.header('Instructions', 'instructions', 'admin', expanded=open_instructions, icon='file-line') }}
|
{{ accordion.header('Instructions', 'instructions', 'admin', expanded=open_instructions, icon='file-line') }}
|
||||||
<h5 class="border-bottom">Folder</h5>
|
<h5 class="border-bottom">Folder</h5>
|
||||||
<p>
|
<p>
|
||||||
The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'].value }}</code>. <br>
|
The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'] }}</code>. <br>
|
||||||
Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</code>.
|
Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}</code>.
|
||||||
</p>
|
</p>
|
||||||
<h5 class="border-bottom">Counters</h5>
|
<h5 class="border-bottom">Counters</h5>
|
||||||
<p>
|
<p>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h5 class="border-bottom">File</h5>
|
<h5 class="border-bottom">File</h5>
|
||||||
{% if retired.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the retired sets: {{ retired.exception }}</div>{% endif %}
|
{% if retired.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the retired sets: {{ retired.exception }}</div>{% endif %}
|
||||||
<p>
|
<p>
|
||||||
The retired sets file is: <code>{{ config['RETIRED_SETS_PATH'].value }}</code>.
|
The retired sets file is: <code>{{ config['RETIRED_SETS_PATH'] }}</code>.
|
||||||
{% if retired.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ retired.human_size() }}</span>{% endif %}
|
{% if retired.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ retired.human_size() }}</span>{% endif %}
|
||||||
{% if retired.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ retired.human_time() }}</span>{% endif %}
|
{% if retired.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ retired.human_time() }}</span>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h5 class="border-bottom">File</h5>
|
<h5 class="border-bottom">File</h5>
|
||||||
{% if theme.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the themes: {{ theme.exception }}</div>{% endif %}
|
{% if theme.exception %}<div class="alert alert-danger" role="alert">An exception occured while loading processing the themes: {{ theme.exception }}</div>{% endif %}
|
||||||
<p>
|
<p>
|
||||||
The themes file is: <code>{{ config['THEMES_PATH'].value }}</code>.
|
The themes file is: <code>{{ config['THEMES_PATH'] }}</code>.
|
||||||
{% if theme.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ theme.human_size() }}</span>{% endif %}
|
{% if theme.size %}<span class="badge rounded-pill text-bg-info fw-normal"><i class="ri-hard-drive-line"></i> {{ theme.human_size() }}</span>{% endif %}
|
||||||
{% if theme.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ theme.human_time() }}</span>{% endif %}
|
{% if theme.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ theme.human_time() }}</span>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
{% for item in config['_NAVBAR'] %}
|
{% for item in config['_NAVBAR'] %}
|
||||||
{% if item.flag and not config[item.flag].value %}
|
{% if item.flag and not config[item.flag] %}
|
||||||
<li class="nav-item px-1">
|
<li class="nav-item px-1">
|
||||||
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
||||||
{% if item.icon %}
|
{% if item.icon %}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<div class="col-md-6 d-flex justify-content-center">
|
<div class="col-md-6 d-flex justify-content-center">
|
||||||
<small>
|
<small>
|
||||||
<i class="ri-timer-2-line"></i> {{ g.request_time() }}
|
<i class="ri-timer-2-line"></i> {{ g.request_time() }}
|
||||||
{%if config['DEBUG'].value and g.database_stats %}
|
{%if config['DEBUG'] and g.database_stats %}
|
||||||
| <i class="ri-database-2-line"></i> {{g.database_stats.print() }}
|
| <i class="ri-database-2-line"></i> {{g.database_stats.print() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h2 class="border-bottom lh-base pb-1">
|
<h2 class="border-bottom lh-base pb-1">
|
||||||
<i class="ri-hashtag"></i> {% if config['RANDOM'].value %}Random selection of{% else %}Latest added{% endif %} sets
|
<i class="ri-hashtag"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} sets
|
||||||
{% if not config['HIDE_ALL_SETS'].value %}
|
{% if not config['HIDE_ALL_SETS'] %}
|
||||||
<a href="{{ url_for('set.list') }}" class="btn btn-sm btn-primary ms-1">All sets</a>
|
<a href="{{ url_for('set.list') }}" class="btn btn-sm btn-primary ms-1">All sets</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
@ -23,8 +23,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if minifigure_collection | length %}
|
{% if minifigure_collection | length %}
|
||||||
<h2 class="border-bottom lh-base pb-1">
|
<h2 class="border-bottom lh-base pb-1">
|
||||||
<i class="ri-group-line"></i> {% if config['RANDOM'].value %}Random selection of{% else %}Latest added{% endif %} minifigures
|
<i class="ri-group-line"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} minifigures
|
||||||
{% if not config['HIDE_ALL_MINIFIGURES'].value %}
|
{% if not config['HIDE_ALL_MINIFIGURES'] %}
|
||||||
<a href="{{ url_for('minifigure.list') }}" class="btn btn-sm btn-primary ms-1">All minifigures</a>
|
<a href="{{ url_for('minifigure.list') }}" class="btn btn-sm btn-primary ms-1">All minifigures</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -26,8 +26,8 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="file" class="form-label">Instructions file</label>
|
<label for="file" class="form-label">Instructions file</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="file" class="form-control" id="file" name="file" accept="{{ ','.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}">
|
<input type="file" class="form-control" id="file" name="file" accept="{{ ','.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}">
|
||||||
<span class="input-group-text">{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</span>
|
<span class="input-group-text">{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="{{ id }}" class="accordion-collapse collapse {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'].value %}data-bs-parent="#{{ parent }}"{% endif %}>
|
<div id="{{ id }}" class="accordion-collapse collapse {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'] %}data-bs-parent="#{{ parent }}"{% endif %}>
|
||||||
<div class="accordion-body {% if class %}{{ class }}{% endif %}">
|
<div class="accordion-body {% if class %}{{ class }}{% endif %}">
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
{% if number %}{ select: [{{ number }}], type: "number", searchable: false },{% endif %}
|
{% if number %}{ select: [{{ number }}], type: "number", searchable: false },{% endif %}
|
||||||
],
|
],
|
||||||
pagerDelta: 1,
|
pagerDelta: 1,
|
||||||
perPage: {{ config['DEFAULT_TABLE_PER_PAGE'].value }},
|
perPage: {{ config['DEFAULT_TABLE_PER_PAGE'] }},
|
||||||
perPageSelect: [10, 25, 50, 100, 500, 1000],
|
perPageSelect: [10, 25, 50, 100, 500, 1000],
|
||||||
searchable: true,
|
searchable: true,
|
||||||
searchQuerySeparator: "",
|
searchQuerySeparator: "",
|
||||||
|
Loading…
Reference in New Issue
Block a user