Compare commits

...

12 Commits
v1.3 ... master

13 changed files with 64 additions and 48 deletions

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ static/sets/
/local/
run_local.sh
settings.local.json
/offline/
# Apple idiocy
.DS_Store

View File

@@ -1,5 +1,24 @@
# Changelog
## 1.3.1
### Bug Fixes
- **Fixed foreign key constraint errors during set imports**: Resolved `FOREIGN KEY constraint failed` errors when importing sets with parts and minifigures
- Fixed insertion order in `bricktracker/part.py`: Parent records (`rebrickable_parts`) now inserted before child records (`bricktracker_parts`)
- Fixed insertion order in `bricktracker/minifigure.py`: Parent records (`rebrickable_minifigures`) now inserted before child records (`bricktracker_minifigures`)
- Ensures foreign key references are valid when SQLite checks constraints
- **Fixed set metadata updates**: Owner, status, and tag checkboxes now properly persist changes on set details page
- Fixed `update_set_state()` method to commit database transactions (was using deferred execution without commit)
- All metadata updates (owner, status, tags, storage, purchase info) now work consistently
- **Fixed nil image downloads**: Placeholder images for parts and minifigures without images now download correctly
- Removed early returns that prevented nil image downloads
- Nil images now properly saved to configured folders (e.g., `/app/data/parts/nil.jpg`)
- **Fixed error logging for missing files**: File not found errors now show actual configured folder paths instead of just URL paths
- Added detailed logging showing both file path and configured folder for easier debugging
- **Fixed minifigure filters in client-side pagination mode**: Owner and other filters now work correctly when server-side pagination is disabled
- Aligned filter behavior with parts page (applies filters server-side, then loads filtered data for client-side search)
## 1.3
### Breaking Changes

View File

@@ -18,8 +18,6 @@ A web application for organizing and tracking LEGO sets, parts, and minifigures.
## Prefered setup: pre-build docker image
Use the provided [compose.yaml](compose.yaml) file.
See [Quick Start](https://bricktracker.baerentsen.space/quick-start) to get up and running right away.
See [Walk Through](https://bricktracker.baerentsen.space/tutorial-first-steps) for a more detailed guide.

View File

@@ -104,12 +104,14 @@ def setup_app(app: Flask) -> None:
level=logging.DEBUG,
format='[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s', # noqa: E501
)
logging.getLogger().setLevel(logging.DEBUG)
else:
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='[%(asctime)s] %(levelname)s - %(message)s',
)
logging.getLogger().setLevel(logging.INFO)
# Load the navbar
Navbar(app)

View File

@@ -191,15 +191,18 @@ class BrickMetadata(BrickRecord):
parameters['set_id'] = brickset.fields.id
parameters['state'] = state
rows, _ = BrickSQL().execute(
rows, _ = BrickSQL().execute_and_commit(
self.update_set_state_query,
parameters=parameters,
defer=True,
name=self.as_column(),
)
# Note: rows will be -1 when deferred, so we can't validate here
# Validation will happen at final commit in set.py
if rows != 1:
raise DatabaseException('Could not update the {kind} state for set {set} ({id})'.format(
kind=self.kind,
set=brickset.fields.set,
id=brickset.fields.id,
))
# Info
logger.info('{kind} "{name}" state changed to "{state}" for set {set} ({id})'.format( # noqa: E501

View File

@@ -33,11 +33,7 @@ class BrickMinifigure(RebrickableMinifigure):
)
)
if not refresh:
# Insert into database
self.insert(commit=False)
# Load the inventory
# Load the inventory (needed to count parts for rebrickable record)
if not BrickPartList.download(
socket,
self.brickset,
@@ -46,9 +42,14 @@ class BrickMinifigure(RebrickableMinifigure):
):
return False
# Insert the rebrickable set into database (after counting parts)
# Insert the rebrickable minifigure into database first (parent record)
# This must happen before inserting into bricktracker_minifigures due to FK constraint
self.insert_rebrickable()
if not refresh:
# Insert into bricktracker_minifigures database (child record)
self.insert(commit=False)
except Exception as e:
socket.fail(
message='Error while importing minifigure {figure} from {set}: {error}'.format( # noqa: E501

View File

@@ -44,7 +44,11 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
return self
# Load all minifigures with problems filter
def all_filtered(self, /, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all') -> Self:
def all_filtered(self, /, owner_id: str | None = None, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all') -> Self:
# Save the owner_id parameter
if owner_id is not None:
self.fields.owner_id = owner_id
context = {}
if problems_filter and problems_filter != 'all':
context['problems_filter'] = problems_filter
@@ -53,7 +57,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
if year and year != 'all':
context['year'] = year
self.list(override_query=self.all_query, **context)
# Choose query based on whether owner filtering is needed
if owner_id and owner_id != 'all':
query = self.all_by_owner_query
else:
query = self.all_query
self.list(override_query=query, **context)
return self
# Load all minifigures by owner

View File

@@ -62,13 +62,14 @@ class BrickPart(RebrickablePart):
)
)
if not refresh:
# Insert into database
self.insert(commit=False)
# Insert the rebrickable set into database
# Insert the rebrickable part into database first (parent record)
# This must happen before inserting into bricktracker_parts due to FK constraint
self.insert_rebrickable()
if not refresh:
# Insert into bricktracker_parts database (child record)
self.insert(commit=False)
except Exception as e:
socket.fail(
message='Error while importing part {part} from {kind} {identifier}: {error}'.format( # noqa: E501

View File

@@ -53,23 +53,7 @@ class RebrickableImage(object):
if os.path.exists(path):
return
# Check if the original image field is null - copy nil placeholder instead
if self.part is not None and self.part.fields.image is None:
return
if self.minifigure is not None and self.minifigure.fields.image is None:
return
if self.set.fields.image is None:
# Copy nil.png from parts folder to sets folder with set number as filename
parts_folder = current_app.config['PARTS_FOLDER']
if not os.path.isabs(parts_folder):
parts_folder = os.path.join(current_app.root_path, parts_folder)
nil_source = os.path.join(parts_folder, f"{RebrickableImage.nil_name()}.{self.extension}")
if os.path.exists(nil_source):
import shutil
shutil.copy2(nil_source, path)
return
# Get the URL (this handles nil images via url() method)
url = self.url()
if url is None:
return

View File

@@ -1,4 +1,4 @@
from typing import Final
__version__: Final[str] = '1.3.0'
__version__: Final[str] = '1.3.1'
__database_version__: Final[int] = 20

View File

@@ -49,7 +49,7 @@ def serve_data_file(folder: str, filename: str):
# Check if file exists
file_path = os.path.join(folder_path, safe_filename)
if not os.path.isfile(file_path):
logger.debug(f"File not found: {file_path}")
logger.warning(f"File not found: {file_path} (configured folder: {folder_path})")
abort(404)
# Verify the resolved path is still within the allowed folder (security check)

View File

@@ -42,10 +42,7 @@ def list() -> str:
pagination_context = build_pagination_context(page, per_page, total_count, is_mobile)
else:
# ORIGINAL MODE - Single page with all data for client-side search
if owner_id == 'all' or owner_id is None or owner_id == '':
minifigures = BrickMinifigureList().all_filtered(problems_filter=problems_filter, theme_id=theme_id, year=year)
else:
minifigures = BrickMinifigureList().all_by_owner_filtered(owner_id=owner_id, problems_filter=problems_filter, theme_id=theme_id, year=year)
minifigures = BrickMinifigureList().all_filtered(owner_id=owner_id, problems_filter=problems_filter, theme_id=theme_id, year=year)
pagination_context = None

View File

@@ -53,15 +53,15 @@ function applyFilters() {
}
}
// Only reset to page 1 when filtering in server-side pagination mode
// Reset to page 1 when filtering in server-side pagination mode
if (isPaginationMode()) {
currentUrl.searchParams.set('page', '1');
// Navigate to updated URL (server-side pagination)
window.location.href = currentUrl.toString();
} else {
// Client-side mode: Update URL without page reload
window.history.replaceState({}, '', currentUrl.toString());
}
// Navigate to updated URL (reload page with new filters)
// This works for both pagination and client-side modes
// because backend applies filters even in client-side mode
window.location.href = currentUrl.toString();
}
// Legacy function for compatibility