Don't store complex objects in Flash config that could mask existing config items, rather store the values and handle the actual list of conf differently

This commit is contained in:
Gregoo 2025-01-20 15:20:07 +01:00
parent 3712356caa
commit e232e2ab7f
37 changed files with 126 additions and 118 deletions

12
app.py
View File

@ -20,19 +20,19 @@ setup_app(app)
# Create the socket
s = BrickSocket(
app,
threaded=not app.config['NO_THREADED_SOCKET'].value,
threaded=not app.config['NO_THREADED_SOCKET'],
)
if __name__ == '__main__':
# Run the application
logger.info('Starting BrickTracker on {host}:{port}'.format(
host=app.config['HOST'].value,
port=app.config['PORT'].value,
host=app.config['HOST'],
port=app.config['PORT'],
))
s.socket.run(
app,
host=app.config['HOST'].value,
debug=app.config['DEBUG'].value,
port=app.config['PORT'].value,
host=app.config['HOST'],
debug=app.config['DEBUG'],
port=app.config['PORT'],
)

View File

@ -28,7 +28,7 @@ def setup_app(app: Flask) -> None:
BrickConfigurationList(app)
# Set the logging level
if app.config['DEBUG'].value:
if app.config['DEBUG']:
logging.basicConfig(
stream=sys.stdout,
level=logging.DEBUG,
@ -90,7 +90,7 @@ def setup_app(app: Flask) -> None:
g.request_time = request_time
# Register the timezone
g.timezone = ZoneInfo(current_app.config['TIMEZONE'].value)
g.timezone = ZoneInfo(current_app.config['TIMEZONE'])
# Version
g.version = __version__

View File

@ -69,6 +69,7 @@ class BrickConfiguration(object):
# Remove static prefix
value = value.removeprefix('static/')
# Type casting
if self.cast is not None:
self.value = self.cast(value)
else:

View File

@ -1,46 +1,60 @@
import logging
from typing import Generator
from flask import current_app, Flask
from flask import Flask
from .config import CONFIG
from .configuration import BrickConfiguration
from .exceptions import ConfigurationMissingException
logger = logging.getLogger(__name__)
# Application configuration
class BrickConfigurationList(object):
app: Flask
configurations: dict[str, BrickConfiguration]
# Load configuration
def __init__(self, app: Flask, /):
self.app = app
# Process all configuration items
for config in CONFIG:
item = BrickConfiguration(**config)
self.app.config[item.name] = item
# Load the configurations only there is none already loaded
configurations = getattr(self, 'configurations', None)
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
@staticmethod
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(
'{name} must be defined (using the {environ} environment variable)'.format( # noqa: E501
name=config.name,
environ=config.env_name
name=name,
environ=configuration.env_name
),
)
# Get all the configuration items from the app config
@staticmethod
def list() -> Generator[BrickConfiguration, None, None]:
keys = list(current_app.config.keys())
keys.sort()
keys = sorted(BrickConfigurationList.configurations.keys())
for name in keys:
config = current_app.config[name]
if isinstance(config, BrickConfiguration):
yield config
yield BrickConfigurationList.configurations[name]

View File

@ -39,7 +39,7 @@ class BrickInstructions(object):
# Store the name and extension, check if extension is allowed
self.name, self.extension = os.path.splitext(self.filename)
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
self.brickset = None
@ -67,7 +67,7 @@ class BrickInstructions(object):
# Display the time in a human format
def human_time(self) -> str:
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
@ -77,7 +77,7 @@ class BrickInstructions(object):
return os.path.join(
current_app.static_folder, # type: ignore
current_app.config['INSTRUCTIONS_FOLDER'].value,
current_app.config['INSTRUCTIONS_FOLDER'],
filename
)
@ -118,7 +118,7 @@ class BrickInstructions(object):
if not self.allowed:
return ''
folder: str = current_app.config['INSTRUCTIONS_FOLDER'].value
folder: str = current_app.config['INSTRUCTIONS_FOLDER']
# Compute the path
path = os.path.join(folder, self.filename)

View File

@ -36,7 +36,7 @@ class BrickInstructionsList(object):
# Make a folder relative to static
folder: str = os.path.join(
current_app.static_folder, # type: ignore
current_app.config['INSTRUCTIONS_FOLDER'].value,
current_app.config['INSTRUCTIONS_FOLDER'],
)
for file in os.scandir(folder):
@ -68,9 +68,8 @@ class BrickInstructionsList(object):
for brickset in BrickSetList().generic().records:
bricksets[brickset.fields.set_num] = brickset
# Return the files
# Inject the brickset if it exists
for instruction in self.all.values():
# Inject the brickset if it exists
if (
instruction.allowed and
instruction.number is not None and

View File

@ -12,7 +12,7 @@ class LoginManager(object):
def __init__(self, app: Flask, /):
# Setup basic authentication
app.secret_key = app.config['AUTHENTICATION_KEY'].value
app.secret_key = app.config['AUTHENTICATION_KEY']
manager = login_manager.LoginManager()
manager.login_view = 'login.login' # type: ignore
@ -23,11 +23,11 @@ class LoginManager(object):
def user_loader(*arg) -> LoginManager.User:
return self.User(
'admin',
app.config['AUTHENTICATION_PASSWORD'].value
app.config['AUTHENTICATION_PASSWORD']
)
# 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:
# - Authentication disabled

View File

@ -119,7 +119,7 @@ class BrickMinifigure(BrickRecord):
# Compute the url for minifigure part image
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:
file = RebrickableImage.nil_minifigure_name()
else:
@ -128,15 +128,15 @@ class BrickMinifigure(BrickRecord):
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
else:
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:
return self.fields.set_img_url
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value:
if current_app.config['REBRICKABLE_LINKS']:
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(),
)
except Exception:

View File

@ -27,7 +27,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
self.brickset = None
# 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
def all(self, /) -> Self:
@ -44,7 +44,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Last added minifigure
def last(self, /, limit: int = 6) -> Self:
# Randomize
if current_app.config['RANDOM'].value:
if current_app.config['RANDOM']:
order = 'RANDOM()'
else:
order = 'minifigures.rowid DESC'

View File

@ -190,9 +190,9 @@ class BrickPart(BrickRecord):
# Compute the url for the bricklink page
def url_for_bricklink(self, /) -> str:
if current_app.config['BRICKLINK_LINKS'].value:
if current_app.config['BRICKLINK_LINKS']:
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,
)
except Exception:
@ -202,7 +202,7 @@ class BrickPart(BrickRecord):
# Compute the url for the part image
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:
file = RebrickableImage.nil_name()
else:
@ -211,7 +211,7 @@ class BrickPart(BrickRecord):
return RebrickableImage.static_url(file, 'PARTS_FOLDER')
else:
if self.fields.part_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL'].value
return current_app.config['REBRICKABLE_IMAGE_NIL']
else:
return self.fields.part_img_url
@ -234,9 +234,9 @@ class BrickPart(BrickRecord):
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value:
if current_app.config['REBRICKABLE_LINKS']:
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,
color=self.fields.color_id,
)

View File

@ -30,7 +30,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
self.minifigure = None
# 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
def all(self, /) -> Self:
@ -63,10 +63,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
record=record,
)
if (
current_app.config['SKIP_SPARE_PARTS'].value and
part.fields.is_spare
):
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
continue
self.records.append(part)
@ -92,10 +89,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
record=record,
)
if (
current_app.config['SKIP_SPARE_PARTS'].value and
part.fields.is_spare
):
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
continue
self.records.append(part)

View File

@ -77,7 +77,7 @@ class Rebrickable(Generic[T]):
# Bootstrap a first set of parameters
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
@ -115,7 +115,7 @@ class Rebrickable(Generic[T]):
# Load from the API
def load(self, /, parameters: dict[str, Any] = {}) -> dict[str, Any]:
# 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:
return json.loads(

View File

@ -70,12 +70,12 @@ class RebrickableImage(object):
# Return the folder depending on the objects provided
def folder(self, /) -> str:
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:
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
def id(self, /) -> str:
@ -105,13 +105,13 @@ class RebrickableImage(object):
def url(self, /) -> str:
if self.part is not 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:
return self.part.fields.part_img_url
if self.minifigure is not 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:
return self.minifigure.fields.set_img_url
@ -122,7 +122,7 @@ class RebrickableImage(object):
def nil_name() -> str:
filename, _ = os.path.splitext(
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:
filename, _ = os.path.splitext(
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
@staticmethod
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
# not changing this behaviour.

View File

@ -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(
self.brickset,
minifigure=minifigure

View File

@ -76,7 +76,7 @@ class RebrickableParts(object):
for index, part in enumerate(inventory):
# Skip spare parts
if (
current_app.config['SKIP_SPARE_PARTS'].value and
current_app.config['SKIP_SPARE_PARTS'] and
part.fields.is_spare
):
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(
self.brickset,
minifigure=self.minifigure,

View File

@ -55,7 +55,7 @@ class RebrickableSet(object):
# Insert into database
brickset.insert(commit=False)
if not current_app.config['USE_REMOTE_IMAGES'].value:
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(brickset).download()
# Load the inventory
@ -210,5 +210,5 @@ class RebrickableSet(object):
# Insert into database
brickwish.insert()
if not current_app.config['USE_REMOTE_IMAGES'].value:
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(brickwish).download()

View File

@ -33,7 +33,7 @@ class BrickRetiredList(object):
# Try to read the themes from a CSV file
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)
# Ignore the header
@ -44,7 +44,7 @@ class BrickRetiredList(object):
BrickRetiredList.retired[retired.number] = retired
# 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.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
@ -79,7 +79,7 @@ class BrickRetiredList(object):
def human_time(self) -> str:
if self.mtime is not None:
return self.mtime.astimezone(g.timezone).strftime(
current_app.config['FILE_DATETIME_FORMAT'].value
current_app.config['FILE_DATETIME_FORMAT']
)
else:
return ''
@ -88,7 +88,7 @@ class BrickRetiredList(object):
@staticmethod
def update() -> None:
response = requests.get(
current_app.config['RETIRED_SETS_FILE_URL'].value,
current_app.config['RETIRED_SETS_FILE_URL'],
stream=True,
)
@ -99,7 +99,7 @@ class BrickRetiredList(object):
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)
logger.info('Retired sets list updated')

View File

@ -157,7 +157,7 @@ class BrickSet(BrickRecord):
# Compute the url for the set image
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(
self.fields.set_num,
'SETS_FOLDER'
@ -182,9 +182,9 @@ class BrickSet(BrickRecord):
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS'].value:
if current_app.config['REBRICKABLE_LINKS']:
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(),
)
except Exception:

View File

@ -26,7 +26,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
self.themes = []
# 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
def all(self, /) -> Self:
@ -60,7 +60,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Last added sets
def last(self, /, limit: int = 6) -> Self:
# Randomize
if current_app.config['RANDOM'].value:
if current_app.config['RANDOM']:
order = 'RANDOM()'
else:
order = 'sets.rowid DESC'

View File

@ -56,19 +56,19 @@ class BrickSocket(object):
# Compute the namespace
self.namespace = '/{namespace}'.format(
namespace=app.config['SOCKET_NAMESPACE'].value
namespace=app.config['SOCKET_NAMESPACE']
)
# Inject CORS if a domain is defined
if app.config['DOMAIN_NAME'].value != '':
kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME'].value
if app.config['DOMAIN_NAME'] != '':
kwargs['cors_allowed_origins'] = app.config['DOMAIN_NAME']
# Instantiate the socket
self.socket = SocketIO(
self.app,
*args,
**kwargs,
path=app.config['SOCKET_PATH'].value,
path=app.config['SOCKET_PATH'],
async_mode='eventlet',
)

View File

@ -37,7 +37,7 @@ class BrickSQL(object):
logger.debug('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
@ -249,7 +249,7 @@ class BrickSQL(object):
# Delete the database
@staticmethod
def delete() -> None:
os.remove(current_app.config['DATABASE_PATH'].value)
os.remove(current_app.config['DATABASE_PATH'])
# Info
logger.info('The database has been deleted')
@ -292,7 +292,7 @@ class BrickSQL(object):
# Replace the database with a new file
@staticmethod
def upload(file: FileStorage, /) -> None:
file.save(current_app.config['DATABASE_PATH'].value)
file.save(current_app.config['DATABASE_PATH'])
# Info
logger.info('The database has been imported using file {file}'.format(

View File

@ -33,7 +33,7 @@ class BrickThemeList(object):
# Try to read the themes from a CSV file
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)
# Ignore the header
@ -44,7 +44,7 @@ class BrickThemeList(object):
BrickThemeList.themes[theme.id] = theme
# 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.mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) # noqa: E501
@ -78,7 +78,7 @@ class BrickThemeList(object):
def human_time(self) -> str:
if self.mtime is not None:
return self.mtime.astimezone(g.timezone).strftime(
current_app.config['FILE_DATETIME_FORMAT'].value
current_app.config['FILE_DATETIME_FORMAT']
)
else:
return ''
@ -87,7 +87,7 @@ class BrickThemeList(object):
@staticmethod
def update() -> None:
response = requests.get(
current_app.config['THEMES_FILE_URL'].value,
current_app.config['THEMES_FILE_URL'],
stream=True,
)
@ -98,7 +98,7 @@ class BrickThemeList(object):
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)
logger.info('Theme list updated')

View File

@ -17,8 +17,8 @@ def add() -> str:
return render_template(
'add.html',
path=current_app.config['SOCKET_PATH'].value,
namespace=current_app.config['SOCKET_NAMESPACE'].value,
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES
)
@ -32,7 +32,7 @@ def bulk() -> str:
return render_template(
'bulk.html',
path=current_app.config['SOCKET_PATH'].value,
namespace=current_app.config['SOCKET_NAMESPACE'].value,
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES
)

View File

@ -160,19 +160,19 @@ def do_delete_database() -> Response:
def download_database() -> Response:
# Create a file name with a timestamp embedded
name, extension = os.path.splitext(
os.path.basename(current_app.config['DATABASE_PATH'].value)
os.path.basename(current_app.config['DATABASE_PATH'])
)
# Info
logger.info('The database has been downloaded')
return send_file(
current_app.config['DATABASE_PATH'].value,
current_app.config['DATABASE_PATH'],
as_attachment=True,
download_name='{name}-{timestamp}{extension}'.format(
name=name,
timestamp=datetime.now().astimezone(g.timezone).strftime(
current_app.config['DATABASE_TIMESTAMP_FORMAT'].value
current_app.config['DATABASE_TIMESTAMP_FORMAT']
),
extension=extension
)

View File

@ -114,7 +114,7 @@ def do_upload() -> Response:
file = upload_helper(
'file',
'instructions.upload',
extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value,
extensions=current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'],
)
if isinstance(file, Response):

View File

@ -18,7 +18,7 @@ class BrickWishList(BrickRecordList[BrickWish]):
def all(self, /) -> Self:
# Load the wished sets from the database
for record in self.select(
order=current_app.config['WISHES_DEFAULT_ORDER'].value
order=current_app.config['WISHES_DEFAULT_ORDER']
):
brickwish = BrickWish(record=record)

View File

@ -4,7 +4,7 @@
{% block main %}
<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">
<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>

View File

@ -21,7 +21,7 @@
{% else %}
{% include 'admin/logout.html' %}
{% include 'admin/instructions.html' %}
{% if not config['USE_REMOTE_IMAGES'].value %}
{% if not config['USE_REMOTE_IMAGES'] %}
{% include 'admin/image.html' %}
{% endif %}
{% include 'admin/theme.html' %}

View File

@ -6,7 +6,7 @@
{% if error %}<div class="alert alert-danger" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
{% if not is_init %}
<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>
<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>
@ -25,7 +25,7 @@
</form>
</div>
{% 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>
<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>

View File

@ -3,8 +3,8 @@
{{ accordion.header('Instructions', 'instructions', 'admin', expanded=open_instructions, icon='file-line') }}
<h5 class="border-bottom">Folder</h5>
<p>
The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'].value }}</code>. <br>
Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</code>.
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']) }}</code>.
</p>
<h5 class="border-bottom">Counters</h5>
<p>

View File

@ -4,7 +4,7 @@
<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 %}
<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.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ retired.human_time() }}</span>{% endif %}
</p>

View File

@ -4,7 +4,7 @@
<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 %}
<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.mtime %}<span class="badge rounded-pill text-bg-light border fw-normal"><i class="ri-calendar-line"></i> {{ theme.human_time() }}</span>{% endif %}
</p>

View File

@ -25,7 +25,7 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% 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">
<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 %}
@ -61,7 +61,7 @@
<div class="col-md-6 d-flex justify-content-center">
<small>
<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() }}
{% endif %}
</small>

View File

@ -3,8 +3,8 @@
{% block main %}
<div class="container-fluid">
<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
{% if not config['HIDE_ALL_SETS'].value %}
<i class="ri-hashtag"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} sets
{% if not config['HIDE_ALL_SETS'] %}
<a href="{{ url_for('set.list') }}" class="btn btn-sm btn-primary ms-1">All sets</a>
{% endif %}
</h2>
@ -23,8 +23,8 @@
{% endif %}
{% if minifigure_collection | length %}
<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
{% if not config['HIDE_ALL_MINIFIGURES'].value %}
<i class="ri-group-line"></i> {% if config['RANDOM'] %}Random selection of{% else %}Latest added{% endif %} minifigures
{% if not config['HIDE_ALL_MINIFIGURES'] %}
<a href="{{ url_for('minifigure.list') }}" class="btn btn-sm btn-primary ms-1">All minifigures</a>
{% endif %}
</h2>

View File

@ -26,8 +26,8 @@
<div class="mb-3">
<label for="file" class="form-label">Instructions file</label>
<div class="input-group">
<input type="file" class="form-control" id="file" name="file" accept="{{ ','.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}">
<span class="input-group-text">{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS'].value) }}</span>
<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']) }}</span>
</div>
</div>
<div class="text-end">

View File

@ -16,7 +16,7 @@
{% endif %}
</button>
</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 %}">
{% endmacro %}

View File

@ -60,7 +60,7 @@
{% if number %}{ select: [{{ number }}], type: "number", searchable: false },{% endif %}
],
pagerDelta: 1,
perPage: {{ config['DEFAULT_TABLE_PER_PAGE'].value }},
perPage: {{ config['DEFAULT_TABLE_PER_PAGE'] }},
perPageSelect: [10, 25, 50, 100, 500, 1000],
searchable: true,
searchQuerySeparator: "",