Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 146f3706a5 | |||
| 951e662113 | |||
| 1184f9bf48 | |||
| ede8d996e2 | |||
| 45f74848d2 | |||
| 417bbd178b | |||
| 349648969c | |||
| 7f9a7a2afe | |||
| 451b8e14a1 | |||
| cca5b6d88e | |||
| 678499a9f2 | |||
| 8fab57d55a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,6 +24,7 @@ static/sets/
|
||||
/local/
|
||||
run_local.sh
|
||||
settings.local.json
|
||||
/offline/
|
||||
|
||||
# Apple idiocy
|
||||
.DS_Store
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user