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
|
||||
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'],
|
||||
)
|
||||
|
@ -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__
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
# 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)
|
||||
self.app.config[item.name] = item
|
||||
|
||||
# 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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
for instruction in self.all.values():
|
||||
# Inject the brickset if it exists
|
||||
for instruction in self.all.values():
|
||||
if (
|
||||
instruction.allowed and
|
||||
instruction.number is not None and
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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' %}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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: "",
|
||||
|
Loading…
Reference in New Issue
Block a user