forked from FrederikBaerentsen/BrickTracker
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 787a376553 | |||
| 7f4be9da36 | |||
| d6f69bca9d | |||
| 3adeef086b | |||
| 40b63fff6a | |||
| 1cac17a420 | |||
| 7bfbbbf298 | |||
| 79f348178c | |||
| 07be7b6004 | |||
| cb24cfc014 | |||
| 418bd5cd9d | |||
| 9953e3921a | |||
| 2d0fa7bf89 |
+2
-1
@@ -28,7 +28,8 @@
|
|||||||
# BK_AUTHENTICATION_KEY=change-this-to-something-random
|
# BK_AUTHENTICATION_KEY=change-this-to-something-random
|
||||||
|
|
||||||
# Optional: Pattern of the link to Bricklink for a part. Will be passed to Python .format()
|
# Optional: Pattern of the link to Bricklink for a part. Will be passed to Python .format()
|
||||||
# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}
|
# Supports {part} and {color} parameters. BrickLink part numbers and color IDs are used when available.
|
||||||
|
# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}&C={color}
|
||||||
# BK_BRICKLINK_LINK_PART_PATTERN=
|
# BK_BRICKLINK_LINK_PART_PATTERN=
|
||||||
|
|
||||||
# Optional: Display Bricklink links wherever applicable
|
# Optional: Display Bricklink links wherever applicable
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ static/sets/
|
|||||||
# Temporary
|
# Temporary
|
||||||
*.csv
|
*.csv
|
||||||
/local/
|
/local/
|
||||||
|
run_local.sh
|
||||||
|
settings.local.json
|
||||||
|
|
||||||
# Apple idiocy
|
# Apple idiocy
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### 1.2.4
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> To use the new BrickLink color parameter in URLs, update your `.env` file:
|
||||||
|
> `BK_BRICKLINK_LINK_PART_PATTERN=https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}&C={color}`
|
||||||
|
|
||||||
|
- Add BrickLink color and part number support for accurate BrickLink URLs
|
||||||
|
- Database migrations to store BrickLink color ID, color name, and part number
|
||||||
|
- Updated Rebrickable API integration to extract BrickLink data from external_ids
|
||||||
|
- Enhanced BrickLink URL generation with proper part number fallback
|
||||||
|
- Extended admin set refresh to detect and track missing BrickLink data
|
||||||
|
|
||||||
|
## 1.2.3
|
||||||
|
|
||||||
|
Added search/filter/sort options to `parts` and `minifigures`.
|
||||||
|
|
||||||
|
## 1.2.2
|
||||||
|
|
||||||
|
Fix legibility of "Damaged" and "Missing" fields for tiny screen by reducing horizontal padding
|
||||||
|
Fixed instructions download from Rebrickable
|
||||||
|
|
||||||
## 1.2.2:
|
## 1.2.2:
|
||||||
|
|
||||||
This release fixes a bug where orphaned parts in the `inventory` table are blocking the database upgrade.
|
This release fixes a bug where orphaned parts in the `inventory` table are blocking the database upgrade.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Any, Final
|
|||||||
CONFIG: Final[list[dict[str, Any]]] = [
|
CONFIG: Final[list[dict[str, Any]]] = [
|
||||||
{'n': 'AUTHENTICATION_PASSWORD', 'd': ''},
|
{'n': 'AUTHENTICATION_PASSWORD', 'd': ''},
|
||||||
{'n': 'AUTHENTICATION_KEY', 'd': ''},
|
{'n': 'AUTHENTICATION_KEY', 'd': ''},
|
||||||
{'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}'}, # noqa: E501
|
{'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}&C={color}'}, # noqa: E501
|
||||||
{'n': 'BRICKLINK_LINKS', 'c': bool},
|
{'n': 'BRICKLINK_LINKS', 'c': bool},
|
||||||
{'n': 'DATABASE_PATH', 'd': './app.db'},
|
{'n': 'DATABASE_PATH', 'd': './app.db'},
|
||||||
{'n': 'DATABASE_TIMESTAMP_FORMAT', 'd': '%Y-%m-%d-%H-%M-%S'},
|
{'n': 'DATABASE_TIMESTAMP_FORMAT', 'd': '%Y-%m-%d-%H-%M-%S'},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urljoin
|
||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Tuple, TYPE_CHECKING
|
from typing import Tuple, TYPE_CHECKING
|
||||||
@@ -11,6 +12,8 @@ import humanize
|
|||||||
import requests
|
import requests
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
import re
|
||||||
|
import cloudscraper
|
||||||
|
|
||||||
from .exceptions import ErrorException, DownloadException
|
from .exceptions import ErrorException, DownloadException
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -89,91 +92,72 @@ class BrickInstructions(object):
|
|||||||
|
|
||||||
# Download an instruction file
|
# Download an instruction file
|
||||||
def download(self, path: str, /) -> None:
|
def download(self, path: str, /) -> None:
|
||||||
|
"""
|
||||||
|
Streams the PDF in chunks and uses self.socket.update_total
|
||||||
|
+ self.socket.progress_count to drive a determinate bar.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Just to make sure that the progress is initiated
|
|
||||||
self.socket.progress(
|
|
||||||
message='Downloading {file}'.format(
|
|
||||||
file=self.filename,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
target = self.path(filename=secure_filename(self.filename))
|
target = self.path(filename=secure_filename(self.filename))
|
||||||
|
|
||||||
# Skipping rather than failing here
|
# Skip if we already have it
|
||||||
if os.path.isfile(target):
|
if os.path.isfile(target):
|
||||||
self.socket.complete(
|
return self.socket.complete(
|
||||||
message='File {file} already exists, skipped'.format(
|
message=f"File {self.filename} already exists, skipped"
|
||||||
file=self.filename,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
# Fetch PDF via cloudscraper (to bypass Cloudflare)
|
||||||
url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
|
scraper = cloudscraper.create_scraper()
|
||||||
path=path
|
scraper.headers.update({
|
||||||
)
|
"User-Agent": current_app.config['REBRICKABLE_USER_AGENT']
|
||||||
trimmed_url = current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format( # noqa: E501
|
})
|
||||||
path=path.partition('/')[0]
|
resp = scraper.get(path, stream=True)
|
||||||
)
|
if not resp.ok:
|
||||||
|
raise DownloadException(f"Failed to download: HTTP {resp.status_code}")
|
||||||
|
|
||||||
# Request the file
|
# Tell the socket how many bytes in total
|
||||||
self.socket.progress(
|
total = int(resp.headers.get("Content-Length", 0))
|
||||||
message='Requesting {url}'.format(
|
self.socket.update_total(total)
|
||||||
url=trimmed_url,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.get(url, stream=True)
|
# Reset the counter and kick off at 0%
|
||||||
if response.ok:
|
self.socket.progress_count = 0
|
||||||
|
self.socket.progress(message=f"Starting download {self.filename}")
|
||||||
|
|
||||||
# Store the content header as size
|
# Write out in 8 KiB chunks and update the counter
|
||||||
try:
|
with open(target, "wb") as f:
|
||||||
self.size = int(
|
for chunk in resp.iter_content(chunk_size=8192):
|
||||||
response.headers.get('Content-length', 0)
|
if not chunk:
|
||||||
)
|
continue
|
||||||
except Exception:
|
f.write(chunk)
|
||||||
self.size = 0
|
|
||||||
|
|
||||||
# Downloading the file
|
# Bump the internal counter and emit
|
||||||
|
self.socket.progress_count += len(chunk)
|
||||||
self.socket.progress(
|
self.socket.progress(
|
||||||
message='Downloading {url} ({size})'.format(
|
message=(
|
||||||
url=trimmed_url,
|
f"Downloading {self.filename} "
|
||||||
size=self.human_size(),
|
f"({humanize.naturalsize(self.socket.progress_count)}/"
|
||||||
|
f"{humanize.naturalsize(self.socket.progress_total)})"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(target, 'wb') as f:
|
# Done!
|
||||||
copyfileobj(response.raw, f)
|
logger.info(f"Downloaded {self.filename}")
|
||||||
else:
|
self.socket.complete(
|
||||||
raise DownloadException('failed to download: {code}'.format( # noqa: E501
|
message=f"File {self.filename} downloaded ({self.human_size()})"
|
||||||
code=response.status_code
|
|
||||||
))
|
|
||||||
|
|
||||||
# Info
|
|
||||||
logger.info('The instruction file {file} has been downloaded'.format( # noqa: E501
|
|
||||||
file=self.filename
|
|
||||||
))
|
|
||||||
|
|
||||||
# Complete
|
|
||||||
self.socket.complete(
|
|
||||||
message='File {file} downloaded ({size})'.format( # noqa: E501
|
|
||||||
file=self.filename,
|
|
||||||
size=self.human_size()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.socket.fail(
|
|
||||||
message='Error while downloading instruction {file}: {error}'.format( # noqa: E501
|
|
||||||
file=self.filename,
|
|
||||||
error=e,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
|
self.socket.fail(
|
||||||
|
message=f"Error downloading {self.filename}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
# Display the size in a human format
|
# Display the size in a human format
|
||||||
def human_size(self) -> str:
|
def human_size(self) -> str:
|
||||||
return humanize.naturalsize(self.size)
|
try:
|
||||||
|
size = self.size
|
||||||
|
except AttributeError:
|
||||||
|
size = os.path.getsize(self.path())
|
||||||
|
return humanize.naturalsize(size)
|
||||||
|
|
||||||
# Display the time in a human format
|
# Display the time in a human format
|
||||||
def human_time(self) -> str:
|
def human_time(self) -> str:
|
||||||
@@ -250,40 +234,52 @@ class BrickInstructions(object):
|
|||||||
# Find the instructions for a set
|
# Find the instructions for a set
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_instructions(set: str, /) -> list[Tuple[str, str]]:
|
def find_instructions(set: str, /) -> list[Tuple[str, str]]:
|
||||||
response = requests.get(
|
"""
|
||||||
current_app.config['REBRICKABLE_LINK_INSTRUCTIONS_PATTERN'].format(
|
Scrape Rebrickable’s HTML and return a list of
|
||||||
path=set,
|
(filename_slug, download_url). Duplicate slugs get _1, _2, …
|
||||||
),
|
"""
|
||||||
headers={
|
page_url = f"https://rebrickable.com/instructions/{set}/"
|
||||||
'User-Agent': current_app.config['REBRICKABLE_USER_AGENT']
|
logger.debug(f"[find_instructions] fetching HTML from {page_url!r}")
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
# Solve Cloudflare’s challenge
|
||||||
raise ErrorException('Failed to load the Rebrickable instructions page. Status code: {code}'.format( # noqa: E501
|
scraper = cloudscraper.create_scraper()
|
||||||
code=response.status_code
|
scraper.headers.update({'User-Agent': current_app.config['REBRICKABLE_USER_AGENT']})
|
||||||
))
|
resp = scraper.get(page_url)
|
||||||
|
if not resp.ok:
|
||||||
|
raise ErrorException(f'Failed to load instructions page for {set}. HTTP {resp.status_code}')
|
||||||
|
|
||||||
# Parse the HTML content
|
soup = BeautifulSoup(resp.content, 'html.parser')
|
||||||
soup = BeautifulSoup(response.content, 'html.parser')
|
link_re = re.compile(r'^/instructions/\d+/.+/download/')
|
||||||
|
|
||||||
# Collect all <img> tags with "LEGO Building Instructions" in the
|
raw: list[tuple[str, str]] = []
|
||||||
# alt attribute
|
for a in soup.find_all('a', href=link_re):
|
||||||
found_tags: list[Tuple[str, str]] = []
|
img = a.find('img', alt=True)
|
||||||
for a_tag in soup.find_all('a', href=True):
|
if not img or set not in img['alt']:
|
||||||
img_tag = a_tag.find('img', alt=True)
|
continue
|
||||||
if img_tag and "LEGO Building Instructions" in img_tag['alt']:
|
|
||||||
found_tags.append(
|
|
||||||
(
|
|
||||||
img_tag['alt'].removeprefix('LEGO Building Instructions for '), # noqa: E501
|
|
||||||
a_tag['href']
|
|
||||||
)
|
|
||||||
) # Save alt and href
|
|
||||||
|
|
||||||
# Raise an error if nothing found
|
# Turn the alt text into a slug
|
||||||
if not len(found_tags):
|
alt_text = img['alt'].removeprefix('LEGO Building Instructions for ')
|
||||||
raise ErrorException('No instruction found for set {set}'.format(
|
slug = re.sub(r'[^A-Za-z0-9]+', '-', alt_text).strip('-')
|
||||||
set=set
|
|
||||||
))
|
|
||||||
|
|
||||||
return found_tags
|
# Build the absolute download URL
|
||||||
|
download_url = urljoin('https://rebrickable.com', a['href'])
|
||||||
|
raw.append((slug, download_url))
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
raise ErrorException(f'No download links found on instructions page for {set}')
|
||||||
|
|
||||||
|
# Disambiguate duplicate slugs by appending _1, _2, …
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
counts = Counter(name for name, _ in raw)
|
||||||
|
seen: dict[str, int] = defaultdict(int)
|
||||||
|
unique: list[tuple[str, str]] = []
|
||||||
|
for name, url in raw:
|
||||||
|
idx = seen[name]
|
||||||
|
if counts[name] > 1 and idx > 0:
|
||||||
|
final_name = f"{name}_{idx}"
|
||||||
|
else:
|
||||||
|
final_name = name
|
||||||
|
seen[name] += 1
|
||||||
|
unique.append((final_name, url))
|
||||||
|
|
||||||
|
return unique
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
all_query: str = 'minifigure/list/all'
|
all_query: str = 'minifigure/list/all'
|
||||||
|
all_by_owner_query: str = 'minifigure/list/all_by_owner'
|
||||||
damaged_part_query: str = 'minifigure/list/damaged_part'
|
damaged_part_query: str = 'minifigure/list/damaged_part'
|
||||||
last_query: str = 'minifigure/list/last'
|
last_query: str = 'minifigure/list/last'
|
||||||
missing_part_query: str = 'minifigure/list/missing_part'
|
missing_part_query: str = 'minifigure/list/missing_part'
|
||||||
@@ -42,6 +43,16 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Load all minifigures by owner
|
||||||
|
def all_by_owner(self, owner_id: str | None = None, /) -> Self:
|
||||||
|
# Save the owner_id parameter
|
||||||
|
self.fields.owner_id = owner_id
|
||||||
|
|
||||||
|
# Load the minifigures from the database
|
||||||
|
self.list(override_query=self.all_by_owner_query)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
# Minifigures with a part damaged part
|
# Minifigures with a part damaged part
|
||||||
def damaged_part(self, part: str, color: int, /) -> Self:
|
def damaged_part(self, part: str, color: int, /) -> Self:
|
||||||
# Save the parameters to the fields
|
# Save the parameters to the fields
|
||||||
@@ -83,11 +94,17 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
else:
|
else:
|
||||||
brickset = None
|
brickset = None
|
||||||
|
|
||||||
|
# Prepare template context for owner filtering
|
||||||
|
context = {}
|
||||||
|
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
|
||||||
|
context['owner_id'] = self.fields.owner_id
|
||||||
|
|
||||||
# Load the sets from the database
|
# Load the sets from the database
|
||||||
for record in super().select(
|
for record in super().select(
|
||||||
override_query=override_query,
|
override_query=override_query,
|
||||||
order=order,
|
order=order,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
**context
|
||||||
):
|
):
|
||||||
minifigure = BrickMinifigure(brickset=brickset, record=record)
|
minifigure = BrickMinifigure(brickset=brickset, record=record)
|
||||||
|
|
||||||
@@ -132,6 +149,10 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
if self.brickset is not None:
|
if self.brickset is not None:
|
||||||
parameters['id'] = self.brickset.fields.id
|
parameters['id'] = self.brickset.fields.id
|
||||||
|
|
||||||
|
# Add owner_id parameter for owner filtering
|
||||||
|
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
|
||||||
|
parameters['owner_id'] = self.fields.owner_id
|
||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
# Import the minifigures from Rebrickable
|
# Import the minifigures from Rebrickable
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
all_query: str = 'part/list/all'
|
all_query: str = 'part/list/all'
|
||||||
|
all_by_owner_query: str = 'part/list/all_by_owner'
|
||||||
different_color_query = 'part/list/with_different_color'
|
different_color_query = 'part/list/with_different_color'
|
||||||
last_query: str = 'part/list/last'
|
last_query: str = 'part/list/last'
|
||||||
minifigure_query: str = 'part/list/from_minifigure'
|
minifigure_query: str = 'part/list/from_minifigure'
|
||||||
@@ -46,6 +47,35 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Load all parts by owner
|
||||||
|
def all_by_owner(self, owner_id: str | None = None, /) -> Self:
|
||||||
|
# Save the owner_id parameter
|
||||||
|
self.fields.owner_id = owner_id
|
||||||
|
|
||||||
|
# Load the parts from the database
|
||||||
|
self.list(override_query=self.all_by_owner_query)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
# Load all parts with filters (owner and/or color)
|
||||||
|
def all_filtered(self, owner_id: str | None = None, color_id: str | None = None, /) -> Self:
|
||||||
|
# Save the filter parameters
|
||||||
|
if owner_id is not None:
|
||||||
|
self.fields.owner_id = owner_id
|
||||||
|
if color_id is not None:
|
||||||
|
self.fields.color_id = color_id
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Load the parts from the database
|
||||||
|
self.list(override_query=query)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
# Base part list
|
# Base part list
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
@@ -69,11 +99,19 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
else:
|
else:
|
||||||
minifigure = None
|
minifigure = None
|
||||||
|
|
||||||
|
# Prepare template context for filtering
|
||||||
|
context_vars = {}
|
||||||
|
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
|
||||||
|
context_vars['owner_id'] = self.fields.owner_id
|
||||||
|
if hasattr(self.fields, 'color_id') and self.fields.color_id is not None:
|
||||||
|
context_vars['color_id'] = self.fields.color_id
|
||||||
|
|
||||||
# Load the sets from the database
|
# Load the sets from the database
|
||||||
for record in super().select(
|
for record in super().select(
|
||||||
override_query=override_query,
|
override_query=override_query,
|
||||||
order=order,
|
order=order,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
**context_vars
|
||||||
):
|
):
|
||||||
part = BrickPart(
|
part = BrickPart(
|
||||||
brickset=brickset,
|
brickset=brickset,
|
||||||
|
|||||||
@@ -91,8 +91,17 @@ class RebrickablePart(BrickRecord):
|
|||||||
def url_for_bricklink(self, /) -> str:
|
def url_for_bricklink(self, /) -> str:
|
||||||
if current_app.config['BRICKLINK_LINKS']:
|
if current_app.config['BRICKLINK_LINKS']:
|
||||||
try:
|
try:
|
||||||
|
# Use BrickLink part number if available and not None/empty, otherwise fall back to Rebrickable part
|
||||||
|
bricklink_part = getattr(self.fields, 'bricklink_part_num', None)
|
||||||
|
part_param = bricklink_part if bricklink_part else self.fields.part
|
||||||
|
|
||||||
|
# Use BrickLink color ID if available and not None, otherwise fall back to Rebrickable color
|
||||||
|
bricklink_color = getattr(self.fields, 'bricklink_color_id', None)
|
||||||
|
color_param = bricklink_color if bricklink_color is not None else self.fields.color
|
||||||
|
print(f'BrickLink URL parameters: part={part_param}, color={color_param}') # Debugging line, can be removed later
|
||||||
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
|
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
|
||||||
part=self.fields.part,
|
part=part_param,
|
||||||
|
color=color_param,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -168,6 +177,9 @@ class RebrickablePart(BrickRecord):
|
|||||||
'color_name': data['color']['name'],
|
'color_name': data['color']['name'],
|
||||||
'color_rgb': data['color']['rgb'],
|
'color_rgb': data['color']['rgb'],
|
||||||
'color_transparent': data['color']['is_trans'],
|
'color_transparent': data['color']['is_trans'],
|
||||||
|
'bricklink_color_id': None,
|
||||||
|
'bricklink_color_name': None,
|
||||||
|
'bricklink_part_num': None,
|
||||||
'name': data['part']['name'],
|
'name': data['part']['name'],
|
||||||
'category': data['part']['part_cat_id'],
|
'category': data['part']['part_cat_id'],
|
||||||
'image': data['part']['part_img_url'],
|
'image': data['part']['part_img_url'],
|
||||||
@@ -176,6 +188,30 @@ class RebrickablePart(BrickRecord):
|
|||||||
'print': data['part']['print_of']
|
'print': data['part']['print_of']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extract BrickLink color info if available in external_ids
|
||||||
|
if 'color' in data and 'external_ids' in data['color']:
|
||||||
|
external_ids = data['color']['external_ids']
|
||||||
|
if 'BrickLink' in external_ids and external_ids['BrickLink']:
|
||||||
|
bricklink_data = external_ids['BrickLink']
|
||||||
|
|
||||||
|
# Extract BrickLink color ID and name from the nested structure
|
||||||
|
if isinstance(bricklink_data, dict):
|
||||||
|
if 'ext_ids' in bricklink_data and bricklink_data['ext_ids']:
|
||||||
|
record['bricklink_color_id'] = bricklink_data['ext_ids'][0]
|
||||||
|
|
||||||
|
if 'ext_descrs' in bricklink_data and bricklink_data['ext_descrs']:
|
||||||
|
# ext_descrs is a list of lists, get the first description from the first list
|
||||||
|
if len(bricklink_data['ext_descrs']) > 0 and len(bricklink_data['ext_descrs'][0]) > 0:
|
||||||
|
record['bricklink_color_name'] = bricklink_data['ext_descrs'][0][0]
|
||||||
|
|
||||||
|
# Extract BrickLink part number if available
|
||||||
|
if 'part' in data and 'external_ids' in data['part']:
|
||||||
|
part_external_ids = data['part']['external_ids']
|
||||||
|
if 'BrickLink' in part_external_ids and part_external_ids['BrickLink']:
|
||||||
|
bricklink_parts = part_external_ids['BrickLink']
|
||||||
|
if isinstance(bricklink_parts, list) and len(bricklink_parts) > 0:
|
||||||
|
record['bricklink_part_num'] = bricklink_parts[0]
|
||||||
|
|
||||||
if brickset is not None:
|
if brickset is not None:
|
||||||
record['id'] = brickset.fields.id
|
record['id'] = brickset.fields.id
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- description: Add BrickLink color fields to rebrickable_parts table
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Add BrickLink color fields to the rebrickable_parts table
|
||||||
|
ALTER TABLE "rebrickable_parts" ADD COLUMN "bricklink_color_id" INTEGER;
|
||||||
|
ALTER TABLE "rebrickable_parts" ADD COLUMN "bricklink_color_name" TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- description: Add BrickLink part number field to rebrickable_parts table
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Add BrickLink part number field to the rebrickable_parts table
|
||||||
|
ALTER TABLE "rebrickable_parts" ADD COLUMN "bricklink_part_num" TEXT;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
|
{% block total_missing %}
|
||||||
|
SUM(IFNULL("problem_join"."total_missing", 0)) AS "total_missing",
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_damaged %}
|
||||||
|
SUM(IFNULL("problem_join"."total_damaged", 0)) AS "total_damaged",
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_quantity %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0) ELSE 0 END) AS "total_quantity",
|
||||||
|
{% else %}
|
||||||
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_sets %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
COUNT(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_minifigures"."id" ELSE NULL END) AS "total_sets"
|
||||||
|
{% else %}
|
||||||
|
COUNT("bricktracker_minifigures"."id") AS "total_sets"
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block join %}
|
||||||
|
-- Join with sets to get owner information
|
||||||
|
INNER JOIN "bricktracker_sets"
|
||||||
|
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||||
|
|
||||||
|
-- Left join with set owners (using dynamic columns)
|
||||||
|
LEFT JOIN "bricktracker_set_owners"
|
||||||
|
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||||
|
|
||||||
|
-- LEFT JOIN + SELECT to avoid messing the total
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
"bricktracker_parts"."id",
|
||||||
|
"bricktracker_parts"."figure",
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "owner_parts"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."missing" ELSE 0 END) AS "total_missing",
|
||||||
|
SUM(CASE WHEN "owner_parts"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."damaged" ELSE 0 END) AS "total_damaged"
|
||||||
|
{% else %}
|
||||||
|
SUM("bricktracker_parts"."missing") AS "total_missing",
|
||||||
|
SUM("bricktracker_parts"."damaged") AS "total_damaged"
|
||||||
|
{% endif %}
|
||||||
|
FROM "bricktracker_parts"
|
||||||
|
INNER JOIN "bricktracker_sets" AS "parts_sets"
|
||||||
|
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "parts_sets"."id"
|
||||||
|
LEFT JOIN "bricktracker_set_owners" AS "owner_parts"
|
||||||
|
ON "parts_sets"."id" IS NOT DISTINCT FROM "owner_parts"."id"
|
||||||
|
WHERE "bricktracker_parts"."figure" IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
"bricktracker_parts"."id",
|
||||||
|
"bricktracker_parts"."figure"
|
||||||
|
) "problem_join"
|
||||||
|
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "problem_join"."id"
|
||||||
|
AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block where %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block group %}
|
||||||
|
GROUP BY
|
||||||
|
"rebrickable_minifigures"."figure"
|
||||||
|
{% endblock %}
|
||||||
@@ -14,6 +14,9 @@ SELECT
|
|||||||
"rebrickable_parts"."color_name",
|
"rebrickable_parts"."color_name",
|
||||||
"rebrickable_parts"."color_rgb",
|
"rebrickable_parts"."color_rgb",
|
||||||
"rebrickable_parts"."color_transparent",
|
"rebrickable_parts"."color_transparent",
|
||||||
|
"rebrickable_parts"."bricklink_color_id",
|
||||||
|
"rebrickable_parts"."bricklink_color_name",
|
||||||
|
"rebrickable_parts"."bricklink_part_num",
|
||||||
"rebrickable_parts"."name",
|
"rebrickable_parts"."name",
|
||||||
--"rebrickable_parts"."category",
|
--"rebrickable_parts"."category",
|
||||||
"rebrickable_parts"."image",
|
"rebrickable_parts"."image",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
SELECT DISTINCT
|
||||||
|
"rebrickable_parts"."color_id" AS "color_id",
|
||||||
|
"rebrickable_parts"."color_name" AS "color_name",
|
||||||
|
"rebrickable_parts"."color_rgb" AS "color_rgb"
|
||||||
|
FROM "rebrickable_parts"
|
||||||
|
INNER JOIN "bricktracker_parts"
|
||||||
|
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
|
||||||
|
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
INNER JOIN "bricktracker_sets"
|
||||||
|
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||||
|
INNER JOIN "bricktracker_set_owners"
|
||||||
|
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||||
|
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||||
|
{% endif %}
|
||||||
|
ORDER BY "rebrickable_parts"."color_name" ASC
|
||||||
@@ -26,6 +26,12 @@ ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id
|
|||||||
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block where %}
|
||||||
|
{% if color_id and color_id != 'all' %}
|
||||||
|
WHERE "bricktracker_parts"."color" = {{ color_id }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"bricktracker_parts"."part",
|
"bricktracker_parts"."part",
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
{% extends 'part/base/base.sql' %}
|
||||||
|
|
||||||
|
{% block total_missing %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."missing" ELSE 0 END) AS "total_missing",
|
||||||
|
{% else %}
|
||||||
|
SUM("bricktracker_parts"."missing") AS "total_missing",
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_damaged %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."damaged" ELSE 0 END) AS "total_damaged",
|
||||||
|
{% else %}
|
||||||
|
SUM("bricktracker_parts"."damaged") AS "total_damaged",
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_quantity %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1) ELSE 0 END) AS "total_quantity",
|
||||||
|
{% else %}
|
||||||
|
SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_sets %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
COUNT(DISTINCT CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."id" ELSE NULL END) AS "total_sets",
|
||||||
|
{% else %}
|
||||||
|
COUNT(DISTINCT "bricktracker_parts"."id") AS "total_sets",
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block total_minifigures %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0) ELSE 0 END) AS "total_minifigures"
|
||||||
|
{% else %}
|
||||||
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block join %}
|
||||||
|
-- Join with sets to get owner information
|
||||||
|
INNER JOIN "bricktracker_sets"
|
||||||
|
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||||
|
|
||||||
|
-- Left join with set owners (using dynamic columns)
|
||||||
|
LEFT JOIN "bricktracker_set_owners"
|
||||||
|
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||||
|
|
||||||
|
-- Left join with minifigures
|
||||||
|
LEFT JOIN "bricktracker_minifigures"
|
||||||
|
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||||
|
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block where %}
|
||||||
|
{% set has_where = false %}
|
||||||
|
{% if owner_id and owner_id != 'all' %}
|
||||||
|
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||||
|
{% set has_where = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% if color_id and color_id != 'all' %}
|
||||||
|
{% if has_where %}
|
||||||
|
AND "bricktracker_parts"."color" = {{ color_id }}
|
||||||
|
{% else %}
|
||||||
|
WHERE "bricktracker_parts"."color" = {{ color_id }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block group %}
|
||||||
|
GROUP BY
|
||||||
|
"bricktracker_parts"."part",
|
||||||
|
"bricktracker_parts"."color",
|
||||||
|
"bricktracker_parts"."spare"
|
||||||
|
{% endblock %}
|
||||||
@@ -4,6 +4,9 @@ INSERT OR IGNORE INTO "rebrickable_parts" (
|
|||||||
"color_name",
|
"color_name",
|
||||||
"color_rgb",
|
"color_rgb",
|
||||||
"color_transparent",
|
"color_transparent",
|
||||||
|
"bricklink_color_id",
|
||||||
|
"bricklink_color_name",
|
||||||
|
"bricklink_part_num",
|
||||||
"name",
|
"name",
|
||||||
"category",
|
"category",
|
||||||
"image",
|
"image",
|
||||||
@@ -16,6 +19,9 @@ INSERT OR IGNORE INTO "rebrickable_parts" (
|
|||||||
:color_name,
|
:color_name,
|
||||||
:color_rgb,
|
:color_rgb,
|
||||||
:color_transparent,
|
:color_transparent,
|
||||||
|
:bricklink_color_id,
|
||||||
|
:bricklink_color_name,
|
||||||
|
:bricklink_part_num,
|
||||||
:name,
|
:name,
|
||||||
:category,
|
:category,
|
||||||
:image,
|
:image,
|
||||||
@@ -28,6 +34,9 @@ DO UPDATE SET
|
|||||||
"color_name" = :color_name,
|
"color_name" = :color_name,
|
||||||
"color_rgb" = :color_rgb,
|
"color_rgb" = :color_rgb,
|
||||||
"color_transparent" = :color_transparent,
|
"color_transparent" = :color_transparent,
|
||||||
|
"bricklink_color_id" = :bricklink_color_id,
|
||||||
|
"bricklink_color_name" = :bricklink_color_name,
|
||||||
|
"bricklink_part_num" = :bricklink_part_num,
|
||||||
"name" = :name,
|
"name" = :name,
|
||||||
"category" = :category,
|
"category" = :category,
|
||||||
"image" = :image,
|
"image" = :image,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ SELECT
|
|||||||
"rebrickable_parts"."color_name",
|
"rebrickable_parts"."color_name",
|
||||||
"rebrickable_parts"."color_rgb",
|
"rebrickable_parts"."color_rgb",
|
||||||
"rebrickable_parts"."color_transparent",
|
"rebrickable_parts"."color_transparent",
|
||||||
|
"rebrickable_parts"."bricklink_color_id",
|
||||||
|
"rebrickable_parts"."bricklink_color_name",
|
||||||
|
"rebrickable_parts"."bricklink_part_num",
|
||||||
"rebrickable_parts"."name",
|
"rebrickable_parts"."name",
|
||||||
"rebrickable_parts"."category",
|
"rebrickable_parts"."category",
|
||||||
"rebrickable_parts"."image",
|
"rebrickable_parts"."image",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ SELECT
|
|||||||
"rebrickable_parts"."color_name",
|
"rebrickable_parts"."color_name",
|
||||||
"rebrickable_parts"."color_rgb",
|
"rebrickable_parts"."color_rgb",
|
||||||
"rebrickable_parts"."color_transparent",
|
"rebrickable_parts"."color_transparent",
|
||||||
|
"rebrickable_parts"."bricklink_color_id",
|
||||||
|
"rebrickable_parts"."bricklink_color_name",
|
||||||
|
"rebrickable_parts"."bricklink_part_num",
|
||||||
"rebrickable_parts"."name",
|
"rebrickable_parts"."name",
|
||||||
"rebrickable_parts"."category",
|
"rebrickable_parts"."category",
|
||||||
"rebrickable_parts"."image",
|
"rebrickable_parts"."image",
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ SELECT
|
|||||||
"rebrickable_sets"."url",
|
"rebrickable_sets"."url",
|
||||||
"null_join"."null_rgb",
|
"null_join"."null_rgb",
|
||||||
"null_join"."null_transparent",
|
"null_join"."null_transparent",
|
||||||
"null_join"."null_url"
|
"null_join"."null_url",
|
||||||
|
"null_join"."null_bricklink_color_id",
|
||||||
|
"null_join"."null_bricklink_color_name",
|
||||||
|
"null_join"."null_bricklink_part_num"
|
||||||
FROM "rebrickable_sets"
|
FROM "rebrickable_sets"
|
||||||
|
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
@@ -14,19 +17,28 @@ INNER JOIN (
|
|||||||
"null_sums"."set",
|
"null_sums"."set",
|
||||||
"null_sums"."null_rgb",
|
"null_sums"."null_rgb",
|
||||||
"null_sums"."null_transparent",
|
"null_sums"."null_transparent",
|
||||||
"null_sums"."null_url"
|
"null_sums"."null_url",
|
||||||
|
"null_sums"."null_bricklink_color_id",
|
||||||
|
"null_sums"."null_bricklink_color_name",
|
||||||
|
"null_sums"."null_bricklink_part_num"
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
"unique_set_parts"."set",
|
"unique_set_parts"."set",
|
||||||
SUM(CASE WHEN "unique_set_parts"."color_rgb" IS NULL THEN 1 ELSE 0 END) AS "null_rgb",
|
SUM(CASE WHEN "unique_set_parts"."color_rgb" IS NULL THEN 1 ELSE 0 END) AS "null_rgb",
|
||||||
SUM(CASE WHEN "unique_set_parts"."color_transparent" IS NULL THEN 1 ELSE 0 END) AS "null_transparent",
|
SUM(CASE WHEN "unique_set_parts"."color_transparent" IS NULL THEN 1 ELSE 0 END) AS "null_transparent",
|
||||||
SUM(CASE WHEN "unique_set_parts"."url" IS NULL THEN 1 ELSE 0 END) AS "null_url"
|
SUM(CASE WHEN "unique_set_parts"."url" IS NULL THEN 1 ELSE 0 END) AS "null_url",
|
||||||
|
SUM(CASE WHEN "unique_set_parts"."bricklink_color_id" IS NULL THEN 1 ELSE 0 END) AS "null_bricklink_color_id",
|
||||||
|
SUM(CASE WHEN "unique_set_parts"."bricklink_color_name" IS NULL THEN 1 ELSE 0 END) AS "null_bricklink_color_name",
|
||||||
|
SUM(CASE WHEN "unique_set_parts"."bricklink_part_num" IS NULL THEN 1 ELSE 0 END) AS "null_bricklink_part_num"
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
"bricktracker_sets"."set",
|
"bricktracker_sets"."set",
|
||||||
"rebrickable_parts"."color_rgb",
|
"rebrickable_parts"."color_rgb",
|
||||||
"rebrickable_parts"."color_transparent",
|
"rebrickable_parts"."color_transparent",
|
||||||
"rebrickable_parts"."url"
|
"rebrickable_parts"."url",
|
||||||
|
"rebrickable_parts"."bricklink_color_id",
|
||||||
|
"rebrickable_parts"."bricklink_color_name",
|
||||||
|
"rebrickable_parts"."bricklink_part_num"
|
||||||
FROM "bricktracker_sets"
|
FROM "bricktracker_sets"
|
||||||
|
|
||||||
INNER JOIN "bricktracker_parts"
|
INNER JOIN "bricktracker_parts"
|
||||||
@@ -49,5 +61,8 @@ INNER JOIN (
|
|||||||
WHERE "null_rgb" > 0
|
WHERE "null_rgb" > 0
|
||||||
OR "null_transparent" > 0
|
OR "null_transparent" > 0
|
||||||
OR "null_url" > 0
|
OR "null_url" > 0
|
||||||
|
OR "null_bricklink_color_id" > 0
|
||||||
|
OR "null_bricklink_color_name" > 0
|
||||||
|
OR "null_bricklink_part_num" > 0
|
||||||
) "null_join"
|
) "null_join"
|
||||||
ON "rebrickable_sets"."set" IS NOT DISTINCT FROM "null_join"."set"
|
ON "rebrickable_sets"."set" IS NOT DISTINCT FROM "null_join"."set"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[str] = '1.2.2'
|
__version__: Final[str] = '1.2.4'
|
||||||
__database_version__: Final[int] = 15
|
__database_version__: Final[int] = 17
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from ...rebrickable_set_list import RebrickableSetList
|
|||||||
admin_set_page = Blueprint('admin_set', __name__, url_prefix='/admin/set')
|
admin_set_page = Blueprint('admin_set', __name__, url_prefix='/admin/set')
|
||||||
|
|
||||||
|
|
||||||
# Sets that need o be refreshed
|
# Sets that need to be refreshed
|
||||||
@admin_set_page.route('/refresh', methods=['GET'])
|
@admin_set_page.route('/refresh', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@exception_handler(__file__)
|
@exception_handler(__file__)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, request
|
||||||
|
|
||||||
from .exceptions import exception_handler
|
from .exceptions import exception_handler
|
||||||
from ..minifigure import BrickMinifigure
|
from ..minifigure import BrickMinifigure
|
||||||
from ..minifigure_list import BrickMinifigureList
|
from ..minifigure_list import BrickMinifigureList
|
||||||
from ..set_list import BrickSetList, set_metadata_lists
|
from ..set_list import BrickSetList, set_metadata_lists
|
||||||
|
from ..set_owner_list import BrickSetOwnerList
|
||||||
|
|
||||||
minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures')
|
minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures')
|
||||||
|
|
||||||
@@ -12,9 +13,23 @@ minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures')
|
|||||||
@minifigure_page.route('/', methods=['GET'])
|
@minifigure_page.route('/', methods=['GET'])
|
||||||
@exception_handler(__file__)
|
@exception_handler(__file__)
|
||||||
def list() -> str:
|
def list() -> str:
|
||||||
|
# Get owner filter from request
|
||||||
|
owner_id = request.args.get('owner', 'all')
|
||||||
|
|
||||||
|
# Get minifigures filtered by owner
|
||||||
|
if owner_id == 'all' or owner_id is None or owner_id == '':
|
||||||
|
minifigures = BrickMinifigureList().all()
|
||||||
|
else:
|
||||||
|
minifigures = BrickMinifigureList().all_by_owner(owner_id)
|
||||||
|
|
||||||
|
# Get list of owners for filter dropdown
|
||||||
|
owners = BrickSetOwnerList.list()
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'minifigures.html',
|
'minifigures.html',
|
||||||
table_collection=BrickMinifigureList().all(),
|
table_collection=minifigures,
|
||||||
|
owners=owners,
|
||||||
|
selected_owner=owner_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, request
|
||||||
|
|
||||||
from .exceptions import exception_handler
|
from .exceptions import exception_handler
|
||||||
from ..minifigure_list import BrickMinifigureList
|
from ..minifigure_list import BrickMinifigureList
|
||||||
from ..part import BrickPart
|
from ..part import BrickPart
|
||||||
from ..part_list import BrickPartList
|
from ..part_list import BrickPartList
|
||||||
from ..set_list import BrickSetList, set_metadata_lists
|
from ..set_list import BrickSetList, set_metadata_lists
|
||||||
|
from ..set_owner_list import BrickSetOwnerList
|
||||||
|
from ..sql import BrickSQL
|
||||||
|
|
||||||
part_page = Blueprint('part', __name__, url_prefix='/parts')
|
part_page = Blueprint('part', __name__, url_prefix='/parts')
|
||||||
|
|
||||||
@@ -13,9 +15,32 @@ part_page = Blueprint('part', __name__, url_prefix='/parts')
|
|||||||
@part_page.route('/', methods=['GET'])
|
@part_page.route('/', methods=['GET'])
|
||||||
@exception_handler(__file__)
|
@exception_handler(__file__)
|
||||||
def list() -> str:
|
def list() -> str:
|
||||||
|
|
||||||
|
# Get filter parameters from request
|
||||||
|
owner_id = request.args.get('owner', 'all')
|
||||||
|
color_id = request.args.get('color', 'all')
|
||||||
|
|
||||||
|
# Get parts with filters applied
|
||||||
|
parts = BrickPartList().all_filtered(owner_id, color_id)
|
||||||
|
|
||||||
|
# Get list of owners for filter dropdown
|
||||||
|
owners = BrickSetOwnerList.list()
|
||||||
|
|
||||||
|
# Get list of colors for filter dropdown
|
||||||
|
# Prepare context for color query (filter by owner if selected)
|
||||||
|
color_context = {}
|
||||||
|
if owner_id != 'all' and owner_id:
|
||||||
|
color_context['owner_id'] = owner_id
|
||||||
|
|
||||||
|
colors = BrickSQL().fetchall('part/colors/list', **color_context)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'parts.html',
|
'parts.html',
|
||||||
table_collection=BrickPartList().all()
|
table_collection=parts,
|
||||||
|
owners=owners,
|
||||||
|
selected_owner=owner_id,
|
||||||
|
colors=colors,
|
||||||
|
selected_color=color_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -61,9 +61,9 @@ docker compose up -d
|
|||||||
|
|
||||||
2. Access BrickTracker at `http://localhost:3333`
|
2. Access BrickTracker at `http://localhost:3333`
|
||||||
|
|
||||||
Please refer to [Environment Variables Reference](docs/env.md) for a list of available variables.
|
Please refer to [Environment Variables Reference](env.md) for a list of available variables.
|
||||||
|
|
||||||
3. Read more in [First steps](docs/first-steps.md)
|
3. Read more in [First steps](first-steps.md)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -85,6 +85,6 @@ Please refer to [Environment Variables Reference](docs/env.md) for a list of ava
|
|||||||
- Check for any syntax errors in `.env` file
|
- Check for any syntax errors in `.env` file
|
||||||
- Verify no conflicting environment variables are set in the shell
|
- Verify no conflicting environment variables are set in the shell
|
||||||
|
|
||||||
For more troubleshooting, take a look at [Common Errors](docs/common-errors.md)
|
For more troubleshooting, take a look at [Common Errors](common-errors.md)
|
||||||
|
|
||||||
Please refer to [Setup](docs/setup.md) for more information.
|
Please refer to [Setup](setup.md) for more information.
|
||||||
+1
-1
@@ -53,7 +53,7 @@ services:
|
|||||||
The [.env.sample](../.env.sample) file provides ample documentation on all the configurable options. Have a look at it.
|
The [.env.sample](../.env.sample) file provides ample documentation on all the configurable options. Have a look at it.
|
||||||
You can make a copy of `.env.sample` as `.env` with your options or create an `.env` file from scratch.
|
You can make a copy of `.env.sample` as `.env` with your options or create an `.env` file from scratch.
|
||||||
|
|
||||||
[Environment Variables Reference](docs/env.md) contains a table of the available variables.
|
[Environment Variables Reference](env.md) contains a table of the available variables.
|
||||||
|
|
||||||
## Database file
|
## Database file
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ rebrick
|
|||||||
requests
|
requests
|
||||||
tzdata
|
tzdata
|
||||||
bs4
|
bs4
|
||||||
|
cloudscraper
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// Minifigures page functionality
|
||||||
|
function filterByOwner() {
|
||||||
|
const select = document.getElementById('filter-owner');
|
||||||
|
const selectedOwner = select.value;
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
|
||||||
|
if (selectedOwner === 'all') {
|
||||||
|
currentUrl.searchParams.delete('owner');
|
||||||
|
} else {
|
||||||
|
currentUrl.searchParams.set('owner', selectedOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = currentUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep filters expanded after selection
|
||||||
|
function filterByOwnerAndKeepOpen() {
|
||||||
|
// Remember if filters were open
|
||||||
|
const filterSection = document.getElementById('table-filter');
|
||||||
|
const wasOpen = filterSection && filterSection.classList.contains('show');
|
||||||
|
|
||||||
|
filterByOwner();
|
||||||
|
|
||||||
|
// Store the state to restore after page reload
|
||||||
|
if (wasOpen) {
|
||||||
|
sessionStorage.setItem('keepFiltersOpen', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup table search and sort functionality
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const searchInput = document.getElementById('table-search');
|
||||||
|
const searchClear = document.getElementById('table-search-clear');
|
||||||
|
|
||||||
|
// Restore filter state after page load
|
||||||
|
if (sessionStorage.getItem('keepFiltersOpen') === 'true') {
|
||||||
|
const filterSection = document.getElementById('table-filter');
|
||||||
|
const filterButton = document.querySelector('[data-bs-target="#table-filter"]');
|
||||||
|
|
||||||
|
if (filterSection && filterButton) {
|
||||||
|
filterSection.classList.add('show');
|
||||||
|
filterButton.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.removeItem('keepFiltersOpen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput && searchClear) {
|
||||||
|
// Wait for table to be initialized by setup_tables
|
||||||
|
setTimeout(() => {
|
||||||
|
const tableElement = document.querySelector('table[data-table="true"]');
|
||||||
|
if (tableElement && window.brickTableInstance) {
|
||||||
|
// Enable custom search for minifigures table
|
||||||
|
window.brickTableInstance.table.searchable = true;
|
||||||
|
|
||||||
|
// Connect search input to table
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
window.brickTableInstance.table.search(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
searchClear.addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
window.brickTableInstance.table.search('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup sort buttons
|
||||||
|
setupSortButtons();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupSortButtons() {
|
||||||
|
// Sort button functionality
|
||||||
|
const sortButtons = document.querySelectorAll('[data-sort-attribute]');
|
||||||
|
const clearButton = document.querySelector('[data-sort-clear]');
|
||||||
|
|
||||||
|
sortButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const attribute = button.dataset.sortAttribute;
|
||||||
|
const isDesc = button.dataset.sortDesc === 'true';
|
||||||
|
|
||||||
|
// Get column index based on attribute
|
||||||
|
const columnMap = {
|
||||||
|
'name': 1,
|
||||||
|
'parts': 2,
|
||||||
|
'quantity': 3,
|
||||||
|
'missing': 4,
|
||||||
|
'damaged': 5,
|
||||||
|
'sets': 6
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnIndex = columnMap[attribute];
|
||||||
|
if (columnIndex !== undefined && window.brickTableInstance) {
|
||||||
|
// Determine sort direction
|
||||||
|
const isCurrentlyActive = button.classList.contains('btn-primary');
|
||||||
|
const currentDirection = button.dataset.currentDirection || (isDesc ? 'desc' : 'asc');
|
||||||
|
const newDirection = isCurrentlyActive ?
|
||||||
|
(currentDirection === 'asc' ? 'desc' : 'asc') :
|
||||||
|
(isDesc ? 'desc' : 'asc');
|
||||||
|
|
||||||
|
// Clear other active buttons
|
||||||
|
sortButtons.forEach(btn => {
|
||||||
|
btn.classList.remove('btn-primary');
|
||||||
|
btn.classList.add('btn-outline-primary');
|
||||||
|
btn.removeAttribute('data-current-direction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark this button as active
|
||||||
|
button.classList.remove('btn-outline-primary');
|
||||||
|
button.classList.add('btn-primary');
|
||||||
|
button.dataset.currentDirection = newDirection;
|
||||||
|
|
||||||
|
// Apply sort using Simple DataTables API
|
||||||
|
window.brickTableInstance.table.columns.sort(columnIndex, newDirection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clearButton) {
|
||||||
|
clearButton.addEventListener('click', () => {
|
||||||
|
// Clear all sort buttons
|
||||||
|
sortButtons.forEach(btn => {
|
||||||
|
btn.classList.remove('btn-primary');
|
||||||
|
btn.classList.add('btn-outline-primary');
|
||||||
|
btn.removeAttribute('data-current-direction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset table sort - remove all sorting
|
||||||
|
if (window.brickTableInstance) {
|
||||||
|
// Destroy and recreate to clear sorting
|
||||||
|
const tableElement = document.querySelector('#minifigures');
|
||||||
|
const currentPerPage = window.brickTableInstance.table.options.perPage;
|
||||||
|
window.brickTableInstance.table.destroy();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Create new instance using the globally available BrickTable class
|
||||||
|
const newInstance = new window.BrickTable(tableElement, currentPerPage);
|
||||||
|
window.brickTableInstance = newInstance;
|
||||||
|
|
||||||
|
// Re-enable search functionality
|
||||||
|
newInstance.table.searchable = true;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Parts page functionality
|
||||||
|
function applyFilters() {
|
||||||
|
const ownerSelect = document.getElementById('filter-owner');
|
||||||
|
const colorSelect = document.getElementById('filter-color');
|
||||||
|
const currentUrl = new URL(window.location);
|
||||||
|
|
||||||
|
// Handle owner filter
|
||||||
|
if (ownerSelect) {
|
||||||
|
const selectedOwner = ownerSelect.value;
|
||||||
|
if (selectedOwner === 'all') {
|
||||||
|
currentUrl.searchParams.delete('owner');
|
||||||
|
} else {
|
||||||
|
currentUrl.searchParams.set('owner', selectedOwner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle color filter
|
||||||
|
if (colorSelect) {
|
||||||
|
const selectedColor = colorSelect.value;
|
||||||
|
if (selectedColor === 'all') {
|
||||||
|
currentUrl.searchParams.delete('color');
|
||||||
|
} else {
|
||||||
|
currentUrl.searchParams.set('color', selectedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = currentUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupColorDropdown() {
|
||||||
|
const colorSelect = document.getElementById('filter-color');
|
||||||
|
if (!colorSelect) return;
|
||||||
|
|
||||||
|
// Add color squares to option text
|
||||||
|
const options = colorSelect.querySelectorAll('option[data-color-rgb]');
|
||||||
|
options.forEach(option => {
|
||||||
|
const colorRgb = option.dataset.colorRgb;
|
||||||
|
const colorId = option.dataset.colorId;
|
||||||
|
const colorName = option.textContent.trim();
|
||||||
|
|
||||||
|
if (colorRgb && colorId !== '9999') {
|
||||||
|
// Create a visual indicator (using Unicode square)
|
||||||
|
option.textContent = `${colorName}`; //■
|
||||||
|
//option.style.color = `#${colorRgb}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep filters expanded after selection
|
||||||
|
function applyFiltersAndKeepOpen() {
|
||||||
|
// Remember if filters were open
|
||||||
|
const filterSection = document.getElementById('table-filter');
|
||||||
|
const wasOpen = filterSection && filterSection.classList.contains('show');
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
// Store the state to restore after page reload
|
||||||
|
if (wasOpen) {
|
||||||
|
sessionStorage.setItem('keepFiltersOpen', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSortButtons() {
|
||||||
|
// Sort button functionality
|
||||||
|
const sortButtons = document.querySelectorAll('[data-sort-attribute]');
|
||||||
|
const clearButton = document.querySelector('[data-sort-clear]');
|
||||||
|
|
||||||
|
sortButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const attribute = button.dataset.sortAttribute;
|
||||||
|
const isDesc = button.dataset.sortDesc === 'true';
|
||||||
|
|
||||||
|
// Get column index based on attribute
|
||||||
|
const columnMap = {
|
||||||
|
'name': 1,
|
||||||
|
'color': 2,
|
||||||
|
'quantity': 3,
|
||||||
|
'missing': 4,
|
||||||
|
'damaged': 5,
|
||||||
|
'sets': 6,
|
||||||
|
'minifigures': 7
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnIndex = columnMap[attribute];
|
||||||
|
if (columnIndex !== undefined && window.partsTableInstance) {
|
||||||
|
// Determine sort direction
|
||||||
|
const isCurrentlyActive = button.classList.contains('btn-primary');
|
||||||
|
const currentDirection = button.dataset.currentDirection || (isDesc ? 'desc' : 'asc');
|
||||||
|
const newDirection = isCurrentlyActive ?
|
||||||
|
(currentDirection === 'asc' ? 'desc' : 'asc') :
|
||||||
|
(isDesc ? 'desc' : 'asc');
|
||||||
|
|
||||||
|
// Clear other active buttons
|
||||||
|
sortButtons.forEach(btn => {
|
||||||
|
btn.classList.remove('btn-primary');
|
||||||
|
btn.classList.add('btn-outline-primary');
|
||||||
|
btn.removeAttribute('data-current-direction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark this button as active
|
||||||
|
button.classList.remove('btn-outline-primary');
|
||||||
|
button.classList.add('btn-primary');
|
||||||
|
button.dataset.currentDirection = newDirection;
|
||||||
|
|
||||||
|
// Apply sort using Simple DataTables API
|
||||||
|
window.partsTableInstance.table.columns.sort(columnIndex, newDirection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clearButton) {
|
||||||
|
clearButton.addEventListener('click', () => {
|
||||||
|
// Clear all sort buttons
|
||||||
|
sortButtons.forEach(btn => {
|
||||||
|
btn.classList.remove('btn-primary');
|
||||||
|
btn.classList.add('btn-outline-primary');
|
||||||
|
btn.removeAttribute('data-current-direction');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset table sort - remove all sorting
|
||||||
|
if (window.partsTableInstance) {
|
||||||
|
// Destroy and recreate to clear sorting
|
||||||
|
const tableElement = document.querySelector('#parts');
|
||||||
|
const currentPerPage = window.partsTableInstance.table.options.perPage;
|
||||||
|
window.partsTableInstance.table.destroy();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// Create new instance using the globally available BrickTable class
|
||||||
|
const newInstance = new window.BrickTable(tableElement, currentPerPage);
|
||||||
|
window.partsTableInstance = newInstance;
|
||||||
|
|
||||||
|
// Re-enable search functionality
|
||||||
|
newInstance.table.searchable = true;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup table search and sort functionality
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const searchInput = document.getElementById('table-search');
|
||||||
|
const searchClear = document.getElementById('table-search-clear');
|
||||||
|
|
||||||
|
// Setup color dropdown with color squares
|
||||||
|
setupColorDropdown();
|
||||||
|
|
||||||
|
// Restore filter state after page load
|
||||||
|
if (sessionStorage.getItem('keepFiltersOpen') === 'true') {
|
||||||
|
const filterSection = document.getElementById('table-filter');
|
||||||
|
const filterButton = document.querySelector('[data-bs-target="#table-filter"]');
|
||||||
|
|
||||||
|
if (filterSection && filterButton) {
|
||||||
|
filterSection.classList.add('show');
|
||||||
|
filterButton.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.removeItem('keepFiltersOpen');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchInput && searchClear) {
|
||||||
|
// Wait for table to be initialized by setup_tables
|
||||||
|
const setupSearch = () => {
|
||||||
|
const tableElement = document.querySelector('table[data-table="true"]');
|
||||||
|
if (tableElement && window.partsTableInstance) {
|
||||||
|
// Enable custom search for parts table
|
||||||
|
window.partsTableInstance.table.searchable = true;
|
||||||
|
|
||||||
|
// Connect search input to table
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
window.partsTableInstance.table.search(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
searchClear.addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
window.partsTableInstance.table.search('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup sort buttons
|
||||||
|
setupSortButtons();
|
||||||
|
} else {
|
||||||
|
// If table instance not ready, try again
|
||||||
|
setTimeout(setupSearch, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(setupSearch, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
+17
-3
@@ -1,4 +1,5 @@
|
|||||||
class BrickTable {
|
// Make BrickTable globally accessible
|
||||||
|
window.BrickTable = class BrickTable {
|
||||||
constructor(table, per_page) {
|
constructor(table, per_page) {
|
||||||
const columns = [];
|
const columns = [];
|
||||||
const no_sort_and_filter = [];
|
const no_sort_and_filter = [];
|
||||||
@@ -32,12 +33,17 @@ class BrickTable {
|
|||||||
columns.push({ select: number, type: "number", searchable: false });
|
columns.push({ select: number, type: "number", searchable: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special configuration for tables with custom search/sort
|
||||||
|
const isMinifiguresTable = table.id === 'minifigures';
|
||||||
|
const isPartsTable = table.id === 'parts';
|
||||||
|
const hasCustomInterface = isMinifiguresTable || isPartsTable;
|
||||||
|
|
||||||
this.table = new simpleDatatables.DataTable(`#${table.id}`, {
|
this.table = new simpleDatatables.DataTable(`#${table.id}`, {
|
||||||
columns: columns,
|
columns: columns,
|
||||||
pagerDelta: 1,
|
pagerDelta: 1,
|
||||||
perPage: per_page,
|
perPage: per_page,
|
||||||
perPageSelect: [10, 25, 50, 100, 500, 1000],
|
perPageSelect: [10, 25, 50, 100, 500, 1000],
|
||||||
searchable: true,
|
searchable: !hasCustomInterface, // Disable built-in search for tables with custom interface
|
||||||
searchMethod: (table => (terms, cell, row, column, source) => table.search(terms, cell, row, column, source))(this),
|
searchMethod: (table => (terms, cell, row, column, source) => table.search(terms, cell, row, column, source))(this),
|
||||||
searchQuerySeparator: "",
|
searchQuerySeparator: "",
|
||||||
tableRender: () => {
|
tableRender: () => {
|
||||||
@@ -92,5 +98,13 @@ class BrickTable {
|
|||||||
|
|
||||||
// Helper to setup the tables
|
// Helper to setup the tables
|
||||||
const setup_tables = (per_page) => document.querySelectorAll('table[data-table="true"]').forEach(
|
const setup_tables = (per_page) => document.querySelectorAll('table[data-table="true"]').forEach(
|
||||||
el => new BrickTable(el, per_page)
|
el => {
|
||||||
|
const brickTable = new window.BrickTable(el, per_page);
|
||||||
|
// Store the instance globally for external access
|
||||||
|
if (el.id === 'minifigures') {
|
||||||
|
window.brickTableInstance = brickTable;
|
||||||
|
} else if (el.id === 'parts') {
|
||||||
|
window.partsTableInstance = brickTable;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% import 'macro/table.html' as table %}
|
{% import 'macro/table.html' as table %}
|
||||||
{% import 'macro/badge.html' as badge %}
|
{% import 'macro/badge.html' as badge %}
|
||||||
|
{% import 'macro/form.html' as form %}
|
||||||
|
|
||||||
<div class="alert alert-info m-2" role="alert">This page lists the sets that may need a refresh because they have some of their newer fields containing empty values.</div>
|
<div class="alert alert-info m-2" role="alert">This page lists the sets that may need a refresh because they have some of their newer fields containing empty values.</div>
|
||||||
<div class="table-responsive-sm">
|
<div class="table-responsive-sm">
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty RGB</th>
|
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty RGB</th>
|
||||||
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty transparent</th>
|
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty transparent</th>
|
||||||
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty URL</th>
|
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty URL</th>
|
||||||
|
<th data-table-number="true" scope="col"><i class="ri-palette-line fw-normal"></i>BrickLink Issues</th>
|
||||||
<th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
|
<th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
<td>{{ item.fields.null_rgb }}</td>
|
<td>{{ item.fields.null_rgb }}</td>
|
||||||
<td>{{ item.fields.null_transparent }}</td>
|
<td>{{ item.fields.null_transparent }}</td>
|
||||||
<td>{{ item.fields.null_url }}</td>
|
<td>{{ item.fields.null_url }}</td>
|
||||||
|
<td>{{ item.fields.null_bricklink_color_id + item.fields.null_bricklink_color_name + item.fields.null_bricklink_part_num }}</td>
|
||||||
<td><a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh</a></td>
|
<td><a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -91,6 +91,12 @@
|
|||||||
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
|
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
|
||||||
|
{% if request.endpoint == 'minifigure.list' %}
|
||||||
|
<script src="{{ url_for('static', filename='scripts/minifigures.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.endpoint == 'part.list' %}
|
||||||
|
<script src="{{ url_for('static', filename='scripts/parts.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
setup_grids();
|
setup_grids();
|
||||||
|
|||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{% macro checkbox(name, id, prefix, url, checked, parent=none, delete=false) %}
|
{% macro checkbox(name, id, prefix, url, checked, parent=none, delete=false) %}
|
||||||
{% if g.login.is_authenticated() %}
|
{% if g.login.is_authenticated() %}
|
||||||
<input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ id }}" {% if checked %}checked{% endif %}
|
<input class="form-check-input px-1" type="checkbox" id="{{ prefix }}-{{ id }}" {% if checked %}checked{% endif %}
|
||||||
{% if not delete %}
|
{% if not delete %}
|
||||||
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" {% if parent %}data-changer-parent="{{ parent }}"{% endif %}
|
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" {% if parent %}data-changer-parent="{{ parent }}"{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
|
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
|
{% if icon %}<span class="input-group-text px-1"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
|
||||||
<input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
|
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
|
||||||
{% if g.login.is_authenticated() %}
|
{% if g.login.is_authenticated() %}
|
||||||
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
|
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
|
||||||
{% if date %}data-changer-date="true"{% endif %}
|
{% if date %}data-changer-date="true"{% endif %}
|
||||||
@@ -31,12 +31,12 @@
|
|||||||
disabled
|
disabled
|
||||||
{% endif %}
|
{% endif %}
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
{% if suffix %}<span class="input-group-text d-none d-md-inline">{{ suffix }}</span>{% endif %}
|
{% if suffix %}<span class="input-group-text d-none d-md-inline px-1">{{ suffix }}</span>{% endif %}
|
||||||
{% if g.login.is_authenticated() %}
|
{% if g.login.is_authenticated() %}
|
||||||
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
|
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line px-1"></span>
|
||||||
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border px-1"><i class="ri-eraser-line"></i></button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="input-group-text ri-prohibited-line"></span>
|
<span class="input-group-text ri-prohibited-line px-1"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -46,8 +46,8 @@
|
|||||||
{% if g.login.is_authenticated() %}
|
{% if g.login.is_authenticated() %}
|
||||||
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
|
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
|
{% if icon %}<span class="input-group-text px-1"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
|
||||||
<select id="{{ prefix }}-{{ id }}" class="form-select"
|
<select id="{{ prefix }}-{{ id }}" class="form-select px-1"
|
||||||
{% if not delete %}
|
{% if not delete %}
|
||||||
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
|
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
<option value="{{ metadata.fields.id }}" {% if metadata.fields.id == value %}selected{% endif %}>{{ metadata.fields.name }}</option>
|
<option value="{{ metadata.fields.id }}" {% if metadata.fields.id == value %}selected{% endif %}>{{ metadata.fields.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
|
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line px-1"></span>
|
||||||
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border px-1"><i class="ri-eraser-line"></i></button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div id="table-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
|
||||||
|
{% if owners | length %}
|
||||||
|
<div class="col-12 flex-grow-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-md-inline"> Owner</span></span>
|
||||||
|
<select id="filter-owner" class="form-select" onchange="filterByOwnerAndKeepOpen()" autocomplete="off">
|
||||||
|
<option value="all" {% if selected_owner == 'all' %}selected{% endif %}>All owners</option>
|
||||||
|
{% for owner in owners %}
|
||||||
|
<option value="{{ owner.fields.id }}" {% if selected_owner == owner.fields.id %}selected{% endif %}>{{ owner.fields.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<div id="table-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
|
||||||
|
<div class="col-12 flex-grow-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text mb-2"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-md-inline"> Sort</span></span>
|
||||||
|
<button id="sort-name" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-md-inline"> Name</span></button>
|
||||||
|
<button id="sort-parts" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xl-inline"> Parts</span></button>
|
||||||
|
<button id="sort-quantity" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="quantity" data-sort-desc="true"><i class="ri-functions"></i><span class="d-none d-xl-inline"> Quantity</span></button>
|
||||||
|
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||||
|
<button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
|
||||||
|
{% endif %}
|
||||||
|
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
|
||||||
|
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
|
||||||
|
{% endif %}
|
||||||
|
<button id="sort-sets" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="sets" data-sort-desc="true"><i class="ri-hashtag"></i><span class="d-none d-xl-inline"> Sets</span></button>
|
||||||
|
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
|
||||||
|
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -7,17 +7,17 @@
|
|||||||
{% for item in table_collection %}
|
{% for item in table_collection %}
|
||||||
<tr>
|
<tr>
|
||||||
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.figure) }}
|
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.figure) }}
|
||||||
<td >
|
<td data-sort="name">
|
||||||
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
|
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
|
||||||
{% if all %}
|
{% if all %}
|
||||||
{{ table.rebrickable(item) }}
|
{{ table.rebrickable(item) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.fields.number_of_parts }}</td>
|
<td data-sort="parts">{{ item.fields.number_of_parts }}</td>
|
||||||
<td>{{ item.fields.total_quantity }}</td>
|
<td data-sort="quantity">{{ item.fields.total_quantity }}</td>
|
||||||
<td>{{ item.fields.total_missing }}</td>
|
<td data-sort="missing">{{ item.fields.total_missing }}</td>
|
||||||
<td>{{ item.fields.total_damaged }}</td>
|
<td data-sort="damaged">{{ item.fields.total_damaged }}</td>
|
||||||
<td>{{ item.fields.total_sets }}</td>
|
<td data-sort="sets">{{ item.fields.total_sets }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -3,9 +3,53 @@
|
|||||||
{% block title %} - All minifigures{% endblock %}
|
{% block title %} - All minifigures{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container-fluid px-0">
|
{% if table_collection | length %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
|
||||||
|
<div class="col-12 flex-grow-1">
|
||||||
|
<label class="visually-hidden" for="table-search">Search</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
|
||||||
|
<input id="table-search" class="form-control form-control-sm px-1" type="text" placeholder="Figure name, parts count, sets" value="">
|
||||||
|
<button id="table-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-sort" aria-expanded="{% if config['SHOW_GRID_SORT'] %}true{% else %}false{% endif %}" aria-controls="table-sort">
|
||||||
|
<i class="ri-sort-asc"></i> Sort
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if owners | length > 1 %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
|
||||||
|
<i class="ri-filter-line"></i> Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% include 'minifigure/sort.html' %}
|
||||||
|
{% include 'minifigure/filter.html' %}
|
||||||
|
|
||||||
{% with all=true %}
|
{% with all=true %}
|
||||||
{% include 'minifigure/table.html' %}
|
{% include 'minifigure/table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="ri-group-line" style="font-size: 4rem; color: #6c757d;"></i>
|
||||||
|
<h3 class="mt-3">No minifigures found</h3>
|
||||||
|
<p class="text-muted">No minifigures are available for the selected owner.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div id="table-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
|
||||||
|
{% if owners | length %}
|
||||||
|
<div class="col-12 col-md-6 flex-grow-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-md-inline"> Owner</span></span>
|
||||||
|
<select id="filter-owner" class="form-select" onchange="applyFiltersAndKeepOpen()" autocomplete="off">
|
||||||
|
<option value="all" {% if selected_owner == 'all' %}selected{% endif %}>All owners</option>
|
||||||
|
{% for owner in owners %}
|
||||||
|
<option value="{{ owner.fields.id }}" {% if selected_owner == owner.fields.id %}selected{% endif %}>{{ owner.fields.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if colors | length %}
|
||||||
|
<div class="col-12 col-md-6 flex-grow-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="ri-palette-line"></i><span class="ms-1 d-none d-md-inline"> Color</span></span>
|
||||||
|
<select id="filter-color" class="form-select" onchange="applyFiltersAndKeepOpen()" autocomplete="off">
|
||||||
|
<option value="all" {% if selected_color == 'all' %}selected{% endif %}>All colors</option>
|
||||||
|
{% for color in colors %}
|
||||||
|
<option value="{{ color.color_id }}" {% if selected_color == color.color_id|string %}selected{% endif %} data-color-rgb="{{ color.color_rgb }}" data-color-id="{{ color.color_id }}">
|
||||||
|
{{ color.color_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="table-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
|
||||||
|
<div class="col-12 flex-grow-1">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text mb-2"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-md-inline"> Sort</span></span>
|
||||||
|
<button id="sort-name" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-md-inline"> Name</span></button>
|
||||||
|
<button id="sort-color" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="color"><i class="ri-palette-line"></i><span class="d-none d-xl-inline"> Color</span></button>
|
||||||
|
<button id="sort-quantity" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="quantity" data-sort-desc="true"><i class="ri-functions"></i><span class="d-none d-xl-inline"> Quantity</span></button>
|
||||||
|
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||||
|
<button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
|
||||||
|
{% endif %}
|
||||||
|
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
|
||||||
|
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
|
||||||
|
{% endif %}
|
||||||
|
<button id="sort-sets" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="sets" data-sort-desc="true"><i class="ri-hashtag"></i><span class="d-none d-xl-inline"> Sets</span></button>
|
||||||
|
<button id="sort-minifigures" type="button" class="btn btn-outline-primary mb-2"
|
||||||
|
data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xl-inline"> Figures</span></button>
|
||||||
|
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
|
||||||
|
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+43
-1
@@ -3,9 +3,51 @@
|
|||||||
{% block title %} - All parts{% endblock %}
|
{% block title %} - All parts{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container-fluid px-0">
|
{% if table_collection | length %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
|
||||||
|
<div class="col-12 flex-grow-1">
|
||||||
|
<label class="visually-hidden" for="table-search">Search</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
|
||||||
|
<input id="table-search" class="form-control form-control-sm px-1" type="text" placeholder="Part name, color, quantity, sets" value="">
|
||||||
|
<button id="table-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-sort" aria-expanded="{% if config['SHOW_GRID_SORT'] %}true{% else %}false{% endif %}" aria-controls="table-sort">
|
||||||
|
<i class="ri-sort-asc"></i> Sort
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
|
||||||
|
<i class="ri-filter-line"></i> Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'part/sort.html' %}
|
||||||
|
{% include 'part/filter.html' %}
|
||||||
|
|
||||||
{% with all=true %}
|
{% with all=true %}
|
||||||
{% include 'part/table.html' %}
|
{% include 'part/table.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="ri-shapes-line" style="font-size: 4rem; color: #6c757d;"></i>
|
||||||
|
<h3 class="mt-3">No parts found</h3>
|
||||||
|
<p class="text-muted">No parts are available for the selected owner.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<label class="visually-hidden" for="grid-search">Search</label>
|
<label class="visually-hidden" for="grid-search">Search</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
|
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
|
||||||
<input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
|
<input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm px-1" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
|
||||||
<button id="grid-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
<button id="grid-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user