Compare commits

..

159 Commits

Author SHA1 Message Date
fa3a321b9e Updated change log, quickstart and env docs. 2025-02-06 20:26:38 +01:00
cf4c575a4f Merge pull request 'Deduplicated minifigure and parts, set owners, set tags, damaged pieces, parts color, parts print, refresh from Rebrickable' (#57) from gregoo/BrickTracker:master into master
Reviewed-on: #57
2025-02-06 20:00:47 +01:00
a99669d9dc List of sets to be refreshed 2025-02-04 23:07:16 +01:00
b6d69e0f10 Revert the checked state of a checkbox if an error occured 2025-02-04 23:03:56 +01:00
6eb0964322 Add clear button to the grid search bar 2025-02-04 21:38:20 +01:00
64a9e063ec Wish requesters 2025-02-04 20:07:15 +01:00
ad24506ab7 Fix extra comma 2025-02-04 19:56:33 +01:00
56c1a46b37 Differentiate between no sort and no sort-and-filter in tables 2025-02-04 19:55:34 +01:00
5d6b373769 Add missing metadata when deleting a set 2025-02-04 19:48:39 +01:00
9e709039c5 Make form.checkbox generic 2025-02-04 19:35:14 +01:00
e022a6bc1e Remove unused logging 2025-02-04 19:06:36 +01:00
90a72130df Make form.select generic 2025-02-04 19:05:38 +01:00
50e6c8bf9c Merge delete.html with set.html 2025-02-04 18:16:27 +01:00
9326c06c3e Remove forced open management accordion 2025-02-04 17:39:47 +01:00
9642853d8e Sort metadata lists by name for more consistency 2025-02-04 17:38:26 +01:00
2e995b615d Configuration doc 2025-02-04 17:04:09 +01:00
f0cec23da9 Set purchase date and price 2025-02-04 17:03:39 +01:00
195f18f141 Set purchase location 2025-02-04 12:52:18 +01:00
e7bfa66512 Put set metadata in nested accordion to reduce footprint 2025-02-04 12:40:49 +01:00
7f684c5f02 Fix improper open flag 2025-02-04 12:34:46 +01:00
16e4c28516 Continue separation of state and value 2025-02-04 12:34:19 +01:00
584389e205 Typo 2025-02-04 10:55:59 +01:00
3d660c594b Make instructions failsafe in the admin 2025-02-04 10:47:22 +01:00
7ce029029d Properly separate setting state and value for metadata 2025-02-04 10:37:43 +01:00
82b744334f Add helper to produce the set metadata lists 2025-02-04 10:08:25 +01:00
b0c7cd7da5 Enforce hidden features in the card and grid filters/sort 2025-02-04 09:32:57 +01:00
bd8c52941a Move grid filters and sort to their own files (plus cosmetics) 2025-02-04 08:47:38 +01:00
48e4b59344 Make sure COUNT() does not return NULL 2025-02-03 23:46:05 +01:00
4e3ae49187 Set storage details 2025-02-03 23:45:35 +01:00
f9e9edd506 Remove debug print 2025-02-03 22:46:34 +01:00
76ccb20dfa Add a little border at the left of accordion to separate sections 2025-02-03 22:40:08 +01:00
9a9b5af7f4 Restore RebrickablePart __init__ definition 2025-02-03 22:21:26 +01:00
8e40b1fd7e Simplify BrickRecord based lists to deduplicate code 2025-02-03 22:20:43 +01:00
8ad525926a Fix metadata storage deletion 2025-02-03 18:12:31 +01:00
0e485ddb71 Collapsible sort on the grid 2025-02-03 18:07:56 +01:00
9b55fd5e33 Fix storage status filters 2025-02-03 18:07:03 +01:00
38e664b733 Don't load card dataset for tiny cards 2025-02-03 17:38:54 +01:00
d8046ac174 Add missing metadata for set loaded from minifigures or parts 2025-02-03 17:38:39 +01:00
561720343b Remove year from tiny cards 2025-02-03 17:12:03 +01:00
d45070eb74 Display metadata filters only if they have values 2025-02-03 17:10:13 +01:00
ac2d2a0b5d Storage filterable and searchable on the Grid 2025-02-03 17:09:59 +01:00
56c926a8ef Cosmetics 2025-02-03 16:47:35 +01:00
714e84ea09 Missed set storage management 2025-02-03 16:47:26 +01:00
103c3c3017 Additional socket debug messages 2025-02-03 16:47:09 +01:00
9aff7e622d Set storage 2025-02-03 16:46:45 +01:00
ec7fab2a7a Scroll confirm and progress to view when adding a set through the socket 2025-02-03 16:35:09 +01:00
187afdc2cf Add support for select to BrickChanger 2025-02-03 16:08:11 +01:00
b87ff162c1 Center not found message for metadata 2025-02-03 16:06:56 +01:00
8ea6a3d003 Remove useless format() 2025-02-03 16:03:21 +01:00
53d1603e3e Simplify the instantiation of metadata list 2025-02-03 12:25:42 +01:00
2b37934503 Make form.checkbox parent configurable 2025-02-03 12:19:49 +01:00
d0d1e53acc Fix set storages and purchase locations to be normal metadata 2025-02-03 11:03:54 +01:00
7453d97c81 Wrap form metadata in accordion for legibility 2025-02-03 10:49:23 +01:00
4cf91a6edd Compute and display number of parts for minifigures 2025-02-03 10:48:19 +01:00
34408a1bff Display same parts using a different color 2025-02-03 10:10:06 +01:00
eac9fc1793 Allow hiding the damaged and missing columns from the parts table 2025-02-03 09:52:33 +01:00
6903667946 Update changelog 2025-01-31 20:57:07 +01:00
9d6bc332cb Add missing database counters 2025-01-31 20:57:07 +01:00
1e2f9fb11a Fix database counters layout 2025-01-31 20:57:07 +01:00
b6c004c045 Remove unused html_id for sets 2025-01-31 20:57:07 +01:00
2c06ca511e Fix management always opened for sets 2025-01-31 20:57:07 +01:00
271effd5d2 Support for damaged parts 2025-01-31 20:57:07 +01:00
5ffea66de0 Leaner card dataset 2025-01-31 20:57:07 +01:00
302eafe08c Fix broken set status 2025-01-31 20:57:07 +01:00
418a332f03 Add missing set owners SQL drop 2025-01-31 20:57:07 +01:00
f34bbe0602 Set tags 2025-01-31 20:57:07 +01:00
5ad94078ed Don't toggle the no confirm button in bulk mode 2025-01-31 20:57:07 +01:00
739d933900 Fix broken list filtering on the grid 2025-01-31 20:57:07 +01:00
c02321368a Disable no confirm checkbox when toggling the form 2025-01-31 20:57:07 +01:00
030345fe6b Fix functions definition 2025-01-31 20:57:07 +01:00
b8d4f23a84 Set owners 2025-01-31 20:57:07 +01:00
ba8744befb Merge add and bulk add templates 2025-01-31 20:57:07 +01:00
d4037cd953 Fix socket always in refresh mode 2025-01-31 20:57:07 +01:00
5fcd76febb Missing quotes around SQL identifier 2025-01-31 20:57:07 +01:00
47261ed420 Display color and print for part cards not solo 2025-01-31 20:57:07 +01:00
adb2170d47 Fix print badge for elements no having this field 2025-01-31 20:57:07 +01:00
6262ac7889 Use badge macros in the card header 2025-01-31 20:57:07 +01:00
ece15e97fb Fix the similar prints icon 2025-01-31 20:57:07 +01:00
6011173c1f Make the default collapsed state of grid filters configurable through a variable 2025-01-31 20:57:07 +01:00
6ec4f160f7 Make filters collapsible 2025-01-31 20:57:06 +01:00
23515526c8 Make the grid controls normal sized 2025-01-31 20:57:06 +01:00
e9f97a6f5e Use a with block rather than set to avoid leaking variables 2025-01-31 20:57:06 +01:00
2260774a58 Rename solo and attribute to value and metadata in grid filter 2025-01-31 20:57:06 +01:00
1f73ae2323 Configure the Grid search through data- attributes 2025-01-31 20:57:06 +01:00
6fdc933c32 Cosmetics 2025-01-31 20:57:06 +01:00
0e3637e5ef Make checkbox clickable in the entire width of their container 2025-01-31 20:57:06 +01:00
069ba37e13 Fix database counters display 2025-01-31 20:57:06 +01:00
ca3d4d09d5 Make grid filters controlled through data- fields 2025-01-31 20:57:06 +01:00
8e3816e2e2 Create dedicated object for Grid filter 2025-01-31 20:57:06 +01:00
d80728d133 Create dedicated javascript object for Grid sort 2025-01-31 20:57:06 +01:00
f854a01925 Split the grid javascript code 2025-01-31 20:57:06 +01:00
2eb8ebfeca Remove sort-target attribute, handle it internally 2025-01-31 20:57:06 +01:00
cf641b3199 Separate the filters from the search and sort in the set grid 2025-01-31 20:57:06 +01:00
d6a729b5a5 Move the checkbox logic inside the macro 2025-01-31 20:57:06 +01:00
637be0d272 Fix admin status error 2025-01-31 20:57:06 +01:00
d15d7ffb61 Move from_form function about name to the base metadata class 2025-01-31 20:57:06 +01:00
fc3c92e9a3 Remove metadata prefix, it's identical to kind 2025-01-31 20:57:06 +01:00
344d4fb575 Metadata list 2025-01-31 20:57:06 +01:00
7d16e491c8 Rename checkboxes (too generic) to status (and some bug fixes) 2025-01-31 20:57:06 +01:00
050b1993da Don't rely on SQL files for migration patches as their existence is not guaranteed 2025-01-31 20:57:06 +01:00
8f5d59394c Remove the 404 code from post redirect as it will cause the browser to not redirect 2025-01-31 20:57:06 +01:00
a832ff27f7 Create a Metadata object as a base for checkboxes 2025-01-31 20:57:06 +01:00
4fc96ec38f Rename checkox_error 2025-01-31 20:57:06 +01:00
bba741b4a5 Rename database_error 2025-01-31 20:57:06 +01:00
aed7a520bd Parametrable error names 2025-01-31 20:57:06 +01:00
3893f2aa19 Theme override nobody cares actually 2025-01-31 20:57:06 +01:00
51f729a18b Fix variable type hint 2025-01-31 20:57:06 +01:00
b2d2019bfd Set theme override 2025-01-31 20:57:06 +01:00
257bccc339 Move set management to its own file 2025-01-31 20:57:06 +01:00
728e0050b3 Fix functions definition 2025-01-31 20:57:06 +01:00
56ad9fba13 url_for_missing should be part of BrickPart, not RebrickablePart 2025-01-31 20:57:06 +01:00
160ab066b2 Update container versions 2025-01-31 20:57:06 +01:00
69c7dbaefe Don't display the set management section when deleting it 2025-01-31 20:57:06 +01:00
acbd58ca71 Add missing @login_required for set deletion 2025-01-31 20:57:06 +01:00
b8d6003339 Add a tooltip with an error message on the visual status 2025-01-31 20:57:06 +01:00
130b3fa84a Fix undefined id variable used when a checkbox does not exist 2025-01-31 20:57:06 +01:00
cb58ef83cc Add a clear button for dynamic input 2025-01-31 20:57:06 +01:00
f016e65b69 Rename read_only_missing to a more generic read_only 2025-01-31 20:57:06 +01:00
b142ff5bed Fix missing logic to handle empty string from dynamic input 2025-01-31 20:57:06 +01:00
e2b8b51db8 Move dynamic input to BrickChanger 2025-01-31 20:57:06 +01:00
f44192a114 Add visually hidden label for dynamic input, move read-only logic in the macro 2025-01-31 20:57:06 +01:00
cf11e4d718 Move the dynamic input into a macro 2025-01-31 20:57:06 +01:00
468cc7ede9 Display prints based on a part 2025-01-31 20:57:06 +01:00
a2aafbf93a Visual fix for Any/No color 2025-01-31 20:57:06 +01:00
e033dec988 Use data-sort to sort colums with complex data 2025-01-31 20:57:06 +01:00
d08b7bb063 Display RGB color, transparency and prints for parts 2025-01-31 20:57:06 +01:00
d93723ab4e Use Rebrickable URL for cosmetics if available 2025-01-31 20:57:06 +01:00
fe13cfdb08 Collapsible grid controls 2025-01-31 20:57:06 +01:00
71ccfcd23d Remove leftover debug prints 2025-01-31 20:57:06 +01:00
fc6ff5dd49 Add a refresh mode for sets 2025-01-31 20:57:06 +01:00
482817fd96 Add purchase location to the database 2025-01-31 20:57:06 +01:00
c4bb3c7607 Deduplicated parts and missing parts 2025-01-31 20:57:06 +01:00
7ff1605c21 Garbage leftover from copy-paste 2025-01-31 20:57:06 +01:00
964dd90704 Remove unused socket 2025-01-31 20:57:06 +01:00
50e5981c58 Cosmetics 2025-01-31 20:57:06 +01:00
d5f66151b9 Documentation touch up 2025-01-31 20:57:06 +01:00
711c020c27 Add extra fields to set for the future while we are refactoring it 2025-01-31 20:57:06 +01:00
9878f426b1 Update versions and changelog 2025-01-31 20:57:06 +01:00
420ff7af7a Properly use the _listener variables as expected, and allow Enter key to execute the action 2025-01-31 20:57:06 +01:00
270838a549 Simplify fields name in the database 2025-01-31 20:57:06 +01:00
2e36db4d3d Allow more advanced migration action through a companion python file 2025-01-31 20:57:06 +01:00
0a129209a5 Add remixicon in the libraries 2025-01-31 20:57:06 +01:00
8b82594512 Documentation about base SQL files 2025-01-31 20:57:05 +01:00
6dd42ed52d Add missing checkboxes counter alias 2025-01-31 20:57:05 +01:00
26fd9aa3f9 Fix hide instructions block placement 2025-01-31 20:57:05 +01:00
32044dffe4 Remove confusing reference to number for sets 2025-01-31 20:57:05 +01:00
a0fd62b9d2 Deduplicate minifigures 2025-01-31 20:57:05 +01:00
1f7a984692 Rename load to from_set for clarity 2025-01-31 20:57:05 +01:00
d1325b595c Inject the socket only where necessary 2025-01-31 20:57:05 +01:00
900492ae14 Provide decorator for socket actions, for repetitive tasks like checking if authenticated or ready for Rebrickable actions 2025-01-31 20:57:05 +01:00
bdf635e427 Remove confusing reference to number for sets 2025-01-31 20:57:05 +01:00
1afb6f841c Rename routes 2025-01-31 20:57:05 +01:00
ee78457e82 Remove unused insert_rebrickable 2025-01-31 20:57:05 +01:00
25aec890a0 Rename download_rebrickable to insert_rebrickable and make it return if an insertion occured 2025-01-31 20:57:05 +01:00
0f53674d8a Grey out legacy database tables in the admin 2025-01-31 20:57:05 +01:00
4350ade65b Add a flag to hide instructions in a set card 2025-01-31 20:57:05 +01:00
ff1f02b7e3 Updated readme and various docs. Added quickstartguide and env overview. 2025-01-28 14:55:28 +01:00
53309a9502 Forgot a version change 2025-01-27 19:49:58 +01:00
4762028a39 Updated Version to 1.1.1 to match Docker version tag and Release note tag 2025-01-27 19:41:49 +01:00
258 changed files with 6713 additions and 2560 deletions

View File

@ -2,7 +2,7 @@
# If set, it will append a direct ORDER BY <whatever you set> to the SQL query # If set, it will append a direct ORDER BY <whatever you set> to the SQL query
# while listing objects. You can look at the structure of the SQLite database to # while listing objects. You can look at the structure of the SQLite database to
# see the schema and the column names. Some fields are compound and not visible # see the schema and the column names. Some fields are compound and not visible
# directly from the schema (joins). You can check the query in the */list.sql files # directly from the schema (joins). You can check the query in the */list.sql and */base/*.sql files
# in the source to see all column names. # in the source to see all column names.
# The usual syntax for those variables is "<table>"."<column>" [ASC|DESC]. # The usual syntax for those variables is "<table>"."<column>" [ASC|DESC].
# For composite fields (CASE, SUM, COUNT) the syntax is <field>, there is no <table> name. # For composite fields (CASE, SUM, COUNT) the syntax is <field>, there is no <table> name.
@ -28,7 +28,7 @@
# 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={number} # Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}
# BK_BRICKLINK_LINK_PART_PATTERN= # BK_BRICKLINK_LINK_PART_PATTERN=
# Optional: Display Bricklink links wherever applicable # Optional: Display Bricklink links wherever applicable
@ -60,6 +60,11 @@
# Legacy name: DOMAIN_NAME # Legacy name: DOMAIN_NAME
# BK_DOMAIN_NAME=http://localhost:3333 # BK_DOMAIN_NAME=http://localhost:3333
# Optional: Format of the timestamp for files on disk (instructions, themes)
# Check https://docs.python.org/3/library/time.html#time.strftime for format details
# Default: %d/%m/%Y, %H:%M:%S
# BK_FILE_DATETIME_FORMAT=%m/%d/%Y, %H:%M
# Optional: IP address the server will listen on. # Optional: IP address the server will listen on.
# Default: 0.0.0.0 # Default: 0.0.0.0
# BK_HOST=0.0.0.0 # BK_HOST=0.0.0.0
@ -103,13 +108,30 @@
# Default: false # Default: false
# BK_HIDE_ALL_PARTS=true # BK_HIDE_ALL_PARTS=true
# Optional: Hide the 'Problems' entry from the menu. Does not disable the route.
# Default: false
# Legacy name: BK_HIDE_MISSING_PARTS
# BK_HIDE_ALL_PROBLEMS_PARTS=true
# Optional: Hide the 'Sets' entry from the menu. Does not disable the route. # Optional: Hide the 'Sets' entry from the menu. Does not disable the route.
# Default: false # Default: false
# BK_HIDE_ALL_SETS=true # BK_HIDE_ALL_SETS=true
# Optional: Hide the 'Missing' entry from the menu. Does not disable the route. # Optional: Hide the 'Storages' entry from the menu. Does not disable the route.
# Default: false # Default: false
# BK_HIDE_MISSING_PARTS=true # BK_HIDE_ALL_STORAGES=true
# Optional: Hide the 'Instructions' entry in a Set card
# Default: false
# BK_HIDE_SET_INSTRUCTIONS=true
# Optional: Hide the 'Damaged' column from the parts table.
# Default: false
# BK_HIDE_TABLE_DAMAGED_PARTS=true
# Optional: Hide the 'Missing' column from the parts table.
# Default: false
# BK_HIDE_TABLE_MISSING_PARTS=true
# Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route. # Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route.
# Default: false # Default: false
@ -117,10 +139,11 @@
# Optional: Change the default order of minifigures. By default ordered by insertion order. # Optional: Change the default order of minifigures. By default ordered by insertion order.
# Useful column names for this option are: # Useful column names for this option are:
# - "minifigures"."fig_num": minifigure ID (fig-xxxxx) # - "rebrickable_minifigures"."figure": minifigure ID (fig-xxxxx)
# - "minifigures"."name": minifigure name # - "rebrickable_minifigures"."number": minifigure ID as an integer (xxxxx)
# Default: "minifigures"."name" ASC # - "rebrickable_minifigures"."name": minifigure name
# BK_MINIFIGURES_DEFAULT_ORDER="minifigures"."name" ASC # Default: "rebrickable_minifigures"."name" ASC
# BK_MINIFIGURES_DEFAULT_ORDER="rebrickable_minifigures"."name" ASC
# Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder # Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder
# Default: minifigs # Default: minifigs
@ -134,13 +157,13 @@
# Optional: Change the default order of parts. By default ordered by insertion order. # Optional: Change the default order of parts. By default ordered by insertion order.
# Useful column names for this option are: # Useful column names for this option are:
# - "inventory"."part_num": part number # - "bricktracker_parts"."part": part number
# - "inventory"."name": part name # - "bricktracker_parts"."spare": part is a spare part
# - "inventory"."color_name": part color name # - "rebrickable_parts"."name": part name
# - "inventory"."is_spare": par is a spare part # - "rebrickable_parts"."color_name": part color name
# - "total_missing": number of missing parts # - "total_missing": number of missing parts
# Default: "inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC # Default: "rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC
# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "inventory"."name" ASC # BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "rebrickable_parts"."name"."name" ASC
# Optional: Folder where to store the parts images, relative to the '/app/static/' folder # Optional: Folder where to store the parts images, relative to the '/app/static/' folder
# Default: parts # Default: parts
@ -150,6 +173,21 @@
# Default: 3333 # Default: 3333
# BK_PORT=3333 # BK_PORT=3333
# Optional: Format of the timestamp for purchase dates
# Check https://docs.python.org/3/library/time.html#time.strftime for format details
# Default: %d/%m/%Y
# BK_PURCHASE_DATE_FORMAT=%m/%d/%Y
# Optional: Currency to display for purchase prices.
# Default: €
# BK_PURCHASE_CURRENCY=£
# Optional: Change the default order of purchase locations. By default ordered by insertion order.
# Useful column names for this option are:
# - "bricktracker_metadata_purchase_locations"."name" ASC: storage name
# Default: "bricktracker_metadata_purchase_locations"."name" ASC
# BK_PURCHASE_LOCATION_DEFAULT_ORDER="bricktracker_metadata_purchase_locations"."name" ASC
# Optional: Shuffle the lists on the front page. # Optional: Shuffle the lists on the front page.
# Default: false # Default: false
# Legacy name: RANDOM # Legacy name: RANDOM
@ -171,11 +209,11 @@
# BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE= # BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE=
# Optional: Pattern of the link to Rebrickable for a minifigure. Will be passed to Python .format() # Optional: Pattern of the link to Rebrickable for a minifigure. Will be passed to Python .format()
# Default: https://rebrickable.com/minifigs/{number} # Default: https://rebrickable.com/minifigs/{figure}
# BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN= # BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN=
# Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format() # Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format()
# Default: https://rebrickable.com/parts/{number}/_/{color} # Default: https://rebrickable.com/parts/{part}/_/{color}
# BK_REBRICKABLE_LINK_PART_PATTERN= # BK_REBRICKABLE_LINK_PART_PATTERN=
# Optional: Pattern of the link to Rebrickable for instructions. Will be passed to Python .format() # Optional: Pattern of the link to Rebrickable for instructions. Will be passed to Python .format()
@ -221,6 +259,14 @@
# Default: sets # Default: sets
# BK_SETS_FOLDER=sets # BK_SETS_FOLDER=sets
# Optional: Make the grid filters displayed by default, rather than collapsed
# Default: false
# BK_SHOW_GRID_FILTERS=true
# Optional: Make the grid sort displayed by default, rather than collapsed
# Default: false
# BK_SHOW_GRID_SORT=true
# Optional: Skip saving or displaying spare parts # Optional: Skip saving or displaying spare parts
# Default: false # Default: false
# BK_SKIP_SPARE_PARTS=true # BK_SKIP_SPARE_PARTS=true
@ -233,6 +279,12 @@
# Default: /bricksocket/ # Default: /bricksocket/
# BK_SOCKET_PATH=custompath # BK_SOCKET_PATH=custompath
# Optional: Change the default order of storages. By default ordered by insertion order.
# Useful column names for this option are:
# - "bricktracker_metadata_storages"."name" ASC: storage name
# Default: "bricktracker_metadata_storages"."name" ASC
# BK_STORAGE_DEFAULT_ORDER="bricktracker_metadata_storages"."name" ASC
# Optional: URL to the themes.csv.gz on Rebrickable # Optional: URL to the themes.csv.gz on Rebrickable
# Default: https://cdn.rebrickable.com/media/downloads/themes.csv.gz # Default: https://cdn.rebrickable.com/media/downloads/themes.csv.gz
# BK_THEMES_FILE_URL= # BK_THEMES_FILE_URL=

View File

@ -1,5 +1,124 @@
# Changelog # Changelog
## 1.2.0:
> **Warning**
> "Missing" part has been renamed to "Problems" to accomodate for missing and damaged parts.
> The associated environment variables have changed named (the old names are still valid)
### Environment
- Renamed: `BK_HIDE_MISSING_PARTS` -> `BK_HIDE_ALL_PROBLEMS_PARTS`
- Added: `BK_HIDE_TABLE_MISSING_PARTS`, hide the Missing column in all tables
- Added: `BK_HIDE_TABLE_DAMAGED_PARTS`, hide the Damaged column in all tables
- Added: `BK_SHOW_GRID_SORT`, show the sort options on the grid by default
- Added: `BK_SHOW_GRID_FILTERS`, show the filter options on the grid by default
- Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry
- Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages
- Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations
- Added: `BK_PURCHASE_CURRENCY`, currency to display for purchase prices
- Added: `BK_PURCHASE_DATE_FORMAT`, date format for purchase dates
- Documented: `BK_FILE_DATETIME_FORMAT`, date format for files on disk (instructions, theme)
### Code
- Changer
- Revert the checked state of a checkbox if an error occured
- Form
- Migrate missing input fields to BrickChanger
- General cleanup
- Metadata
- Underlying class to implement more metadata-like features
- Minifigure
- Deduplicate
- Compute number of parts
- Parts
- Damaged parts
- Sets
- Refresh data from Rebrickable
- Fix missing @login_required for set deletion
- Ownership
- Tags
- Storage
- Purchase location, date, price
- Storage
- Storage content and list
- Socket
- Add decorator for rebrickable, authenticated and threaded socket actions
- SQL
- Allow for advanced migration scenarios through companion python files
- Add a bunch of the requested fields into the database for future implementation
- Wish
- Requester
### UI
- Add
- Allow adding or bulk adding by pressing Enter in the input field
- Admin
- Grey out legacy tables in the database view
- Checkboxes renamed to Set statuses
- List of sets that may need to be refreshed
- Cards
- Use macros for badge in the card header
- Form
- Add a clear button for dynamic text inputs
- Add error message in a tooltip for dynamic inputs
- Minifigure
- Display number of parts
- Parts
- Use Rebrickable URL if stored (+ color code)
- Display color and transparency
- Display if print of another part
- Display prints using the same base
- Damaged parts
- Display same parts using a different color
- Sets
- Add a flag to hide instructions in a set
- Make checkbox clickable on the whole width of the card
- Management
- Ownership
- Tags
- Refresh
- Storage
- Purchase location, date, price
- Sets grid
- Collapsible controls depending on screen size
- Manually collapsible filters (with configuration variable for default state)
- Manually collapsible sort (with configuration variable for default state)
- Clear search bar
- Storage
- Storage list
- Storage content
- Wish
- Requester
## 1.1.1: PDF Instructions Download
### Instructions
- Added buttons for instructions download from Rebrickable
## 1.1.0: Deduped sets, custom checkboxes and database upgrade ## 1.1.0: Deduped sets, custom checkboxes and database upgrade
### Database ### Database

View File

@ -18,7 +18,9 @@ A web application for organizing and tracking LEGO sets, parts, and minifigures.
Use the provided [compose.yaml](compose.yaml) file. Use the provided [compose.yaml](compose.yaml) file.
See [setup](docs/setup.md). See [Quickstart](docs/quickstart.md) to get up and running right away.
See [Setup](docs/setup.md) for a more setup guide.
## Usage ## Usage

View File

@ -13,11 +13,16 @@ from bricktracker.sql import close
from bricktracker.version import __version__ from bricktracker.version import __version__
from bricktracker.views.add import add_page from bricktracker.views.add import add_page
from bricktracker.views.admin.admin import admin_page from bricktracker.views.admin.admin import admin_page
from bricktracker.views.admin.checkbox import admin_checkbox_page
from bricktracker.views.admin.database import admin_database_page from bricktracker.views.admin.database import admin_database_page
from bricktracker.views.admin.image import admin_image_page from bricktracker.views.admin.image import admin_image_page
from bricktracker.views.admin.instructions import admin_instructions_page from bricktracker.views.admin.instructions import admin_instructions_page
from bricktracker.views.admin.owner import admin_owner_page
from bricktracker.views.admin.purchase_location import admin_purchase_location_page # noqa: E501
from bricktracker.views.admin.retired import admin_retired_page from bricktracker.views.admin.retired import admin_retired_page
from bricktracker.views.admin.set import admin_set_page
from bricktracker.views.admin.status import admin_status_page
from bricktracker.views.admin.storage import admin_storage_page
from bricktracker.views.admin.tag import admin_tag_page
from bricktracker.views.admin.theme import admin_theme_page from bricktracker.views.admin.theme import admin_theme_page
from bricktracker.views.error import error_404 from bricktracker.views.error import error_404
from bricktracker.views.index import index_page from bricktracker.views.index import index_page
@ -26,6 +31,7 @@ from bricktracker.views.login import login_page
from bricktracker.views.minifigure import minifigure_page from bricktracker.views.minifigure import minifigure_page
from bricktracker.views.part import part_page from bricktracker.views.part import part_page
from bricktracker.views.set import set_page from bricktracker.views.set import set_page
from bricktracker.views.storage import storage_page
from bricktracker.views.wish import wish_page from bricktracker.views.wish import wish_page
@ -74,15 +80,21 @@ def setup_app(app: Flask) -> None:
app.register_blueprint(minifigure_page) app.register_blueprint(minifigure_page)
app.register_blueprint(part_page) app.register_blueprint(part_page)
app.register_blueprint(set_page) app.register_blueprint(set_page)
app.register_blueprint(storage_page)
app.register_blueprint(wish_page) app.register_blueprint(wish_page)
# Register admin routes # Register admin routes
app.register_blueprint(admin_page) app.register_blueprint(admin_page)
app.register_blueprint(admin_checkbox_page)
app.register_blueprint(admin_database_page) app.register_blueprint(admin_database_page)
app.register_blueprint(admin_image_page) app.register_blueprint(admin_image_page)
app.register_blueprint(admin_instructions_page) app.register_blueprint(admin_instructions_page)
app.register_blueprint(admin_retired_page) app.register_blueprint(admin_retired_page)
app.register_blueprint(admin_owner_page)
app.register_blueprint(admin_purchase_location_page)
app.register_blueprint(admin_set_page)
app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page)
app.register_blueprint(admin_theme_page) app.register_blueprint(admin_theme_page)
# An helper to make global variables available to the # An helper to make global variables available to the

View File

@ -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={number}'}, # noqa: E501 {'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}'}, # 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'},
@ -28,21 +28,28 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'HIDE_ALL_INSTRUCTIONS', 'c': bool}, {'n': 'HIDE_ALL_INSTRUCTIONS', 'c': bool},
{'n': 'HIDE_ALL_MINIFIGURES', 'c': bool}, {'n': 'HIDE_ALL_MINIFIGURES', 'c': bool},
{'n': 'HIDE_ALL_PARTS', 'c': bool}, {'n': 'HIDE_ALL_PARTS', 'c': bool},
{'n': 'HIDE_ALL_PROBLEMS_PARTS', 'e': 'BK_HIDE_MISSING_PARTS', 'c': bool},
{'n': 'HIDE_ALL_SETS', 'c': bool}, {'n': 'HIDE_ALL_SETS', 'c': bool},
{'n': 'HIDE_MISSING_PARTS', 'c': bool}, {'n': 'HIDE_ALL_STORAGES', 'c': bool},
{'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool},
{'n': 'HIDE_TABLE_DAMAGED_PARTS', 'c': bool},
{'n': 'HIDE_TABLE_MISSING_PARTS', 'c': bool},
{'n': 'HIDE_WISHES', 'c': bool}, {'n': 'HIDE_WISHES', 'c': bool},
{'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"minifigures"."name" ASC'}, {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501
{'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True},
{'n': 'NO_THREADED_SOCKET', 'c': bool}, {'n': 'NO_THREADED_SOCKET', 'c': bool},
{'n': 'PARTS_DEFAULT_ORDER', 'd': '"inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC'}, # noqa: E501 {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501
{'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True},
{'n': 'PORT', 'd': 3333, 'c': int}, {'n': 'PORT', 'd': 3333, 'c': int},
{'n': 'PURCHASE_DATE_FORMAT', 'd': '%d/%m/%Y'},
{'n': 'PURCHASE_CURRENCY', 'd': ''},
{'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501
{'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool},
{'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
{'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501
{'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501
{'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{number}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{figure}'}, # noqa: E501
{'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{part}/_/{color}'}, # noqa: E501
{'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501
{'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501 {'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501
{'n': 'REBRICKABLE_LINKS', 'e': 'LINKS', 'c': bool}, {'n': 'REBRICKABLE_LINKS', 'e': 'LINKS', 'c': bool},
@ -51,9 +58,12 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'RETIRED_SETS_PATH', 'd': './retired_sets.csv'}, {'n': 'RETIRED_SETS_PATH', 'd': './retired_sets.csv'},
{'n': 'SETS_DEFAULT_ORDER', 'd': '"rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC'}, # noqa: E501 {'n': 'SETS_DEFAULT_ORDER', 'd': '"rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC'}, # noqa: E501
{'n': 'SETS_FOLDER', 'd': 'sets', 's': True}, {'n': 'SETS_FOLDER', 'd': 'sets', 's': True},
{'n': 'SHOW_GRID_FILTERS', 'c': bool},
{'n': 'SHOW_GRID_SORT', 'c': bool},
{'n': 'SKIP_SPARE_PARTS', 'c': bool}, {'n': 'SKIP_SPARE_PARTS', 'c': bool},
{'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'}, {'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'},
{'n': 'SOCKET_PATH', 'd': '/bricksocket/'}, {'n': 'SOCKET_PATH', 'd': '/bricksocket/'},
{'n': 'STORAGE_DEFAULT_ORDER', 'd': '"bricktracker_metadata_storages"."name" ASC'}, # noqa: E501
{'n': 'THEMES_FILE_URL', 'd': 'https://cdn.rebrickable.com/media/downloads/themes.csv.gz'}, # noqa: E501 {'n': 'THEMES_FILE_URL', 'd': 'https://cdn.rebrickable.com/media/downloads/themes.csv.gz'}, # noqa: E501
{'n': 'THEMES_PATH', 'd': './themes.csv'}, {'n': 'THEMES_PATH', 'd': './themes.csv'},
{'n': 'TIMEZONE', 'd': 'Etc/UTC'}, {'n': 'TIMEZONE', 'd': 'Etc/UTC'},

263
bricktracker/metadata.py Normal file
View File

@ -0,0 +1,263 @@
import logging
from sqlite3 import Row
from typing import Any, Self, TYPE_CHECKING
from uuid import uuid4
from flask import url_for
from .exceptions import DatabaseException, ErrorException, NotFoundException
from .record import BrickRecord
from .sql import BrickSQL
if TYPE_CHECKING:
from .set import BrickSet
logger = logging.getLogger(__name__)
# Lego set metadata (customizable list of entries that can be checked)
class BrickMetadata(BrickRecord):
kind: str
# Set state endpoint
set_state_endpoint: str
# Queries
delete_query: str
insert_query: str
select_query: str
update_field_query: str
update_set_state_query: str
update_set_value_query: str
def __init__(
self,
/,
*,
record: Row | dict[str, Any] | None = None,
):
super().__init__()
# Defined an empty ID
self.fields.id = None
# Ingest the record if it has one
if record is not None:
self.ingest(record)
# SQL column name
def as_column(self, /) -> str:
return '{kind}_{id}'.format(
id=self.fields.id,
kind=self.kind.lower().replace(' ', '-')
)
# HTML dataset name
def as_dataset(self, /) -> str:
return self.as_column().replace('_', '-')
# Delete from database
def delete(self, /) -> None:
BrickSQL().executescript(
self.delete_query,
id=self.fields.id,
)
# Grab data from a form
def from_form(self, form: dict[str, str], /) -> Self:
name = form.get('name', None)
if name is None or name == '':
raise ErrorException('Status name cannot be empty')
self.fields.name = name
return self
# Insert into database
def insert(self, /, **context) -> None:
self.safe()
# Generate an ID for the metadata (with underscores to make it
# column name friendly)
self.fields.id = str(uuid4()).replace('-', '_')
BrickSQL().executescript(
self.insert_query,
id=self.fields.id,
name=self.fields.safe_name,
**context
)
# Rename the entry
def rename(self, /) -> None:
self.update_field('name', value=self.fields.name)
# Make the name "safe"
# Security: eh.
def safe(self, /) -> None:
# Prevent self-ownage with accidental quote escape
self.fields.safe_name = self.fields.name.replace("'", "''")
# URL to change the selected state of this metadata item for a set
def url_for_set_state(self, id: str, /) -> str:
return url_for(
self.set_state_endpoint,
id=id,
metadata_id=self.fields.id
)
# Select a specific metadata (with an id)
def select_specific(self, id: str, /) -> Self:
# Save the parameters to the fields
self.fields.id = id
# Load from database
if not self.select():
raise NotFoundException(
'{kind} with ID {id} was not found in the database'.format(
kind=self.kind.capitalize(),
id=self.fields.id,
),
)
return self
# Update a field
def update_field(
self,
field: str,
/,
*,
json: Any | None = None,
value: Any | None = None
) -> Any:
if value is None and json is not None:
value = json.get('value', None)
if value is None:
raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501
field=field,
kind=self.kind
))
if field == 'id' or not hasattr(self.fields, field):
raise NotFoundException('"{field}" is not a field of a {kind}'.format( # noqa: E501
kind=self.kind,
field=field
))
parameters = self.sql_parameters()
parameters['value'] = value
# Update the status
rows, _ = BrickSQL().execute_and_commit(
self.update_field_query,
parameters=parameters,
field=field,
)
if rows != 1:
raise DatabaseException('Could not update the field "{field}" for {kind} "{name}" ({id})'.format( # noqa: E501
field=field,
kind=self.kind,
name=self.fields.name,
id=self.fields.id,
))
# Info
logger.info('{kind} "{name}" ({id}): field "{field}" changed to "{value}"'.format( # noqa: E501
kind=self.kind.capitalize(),
name=self.fields.name,
id=self.fields.id,
field=field,
value=value,
))
return value
# Update the selected state of this metadata item for a set
def update_set_state(
self,
brickset: 'BrickSet',
/,
*,
json: Any | None = None,
state: Any | None = None
) -> Any:
if state is None and json is not None:
state = json.get('value', False)
parameters = self.sql_parameters()
parameters['set_id'] = brickset.fields.id
parameters['state'] = state
rows, _ = BrickSQL().execute_and_commit(
self.update_set_state_query,
parameters=parameters,
name=self.as_column(),
)
if rows != 1:
raise DatabaseException('Could not update the {kind} "{name}" state for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
name=self.fields.name,
set=brickset.fields.set,
id=brickset.fields.id,
))
# Info
logger.info('{kind} "{name}" state changed to "{state}" for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
name=self.fields.name,
state=state,
set=brickset.fields.set,
id=brickset.fields.id,
))
return state
# Update the selected value of this metadata item for a set
def update_set_value(
self,
brickset: 'BrickSet',
/,
*,
json: Any | None = None,
value: Any | None = None,
) -> Any:
if value is None and json is not None:
value = json.get('value', '')
if value == '':
value = None
parameters = self.sql_parameters()
parameters['set_id'] = brickset.fields.id
parameters['value'] = value
rows, _ = BrickSQL().execute_and_commit(
self.update_set_value_query,
parameters=parameters,
)
# Update the status
if value is None and not hasattr(self.fields, 'name'):
self.fields.name = 'None'
if rows != 1:
raise DatabaseException('Could not update the {kind} value for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
set=brickset.fields.set,
id=brickset.fields.id,
))
# Info
logger.info('{kind} value changed to "{name}" ({value}) for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
name=self.fields.name,
value=value,
set=brickset.fields.set,
id=brickset.fields.id,
))
return value

View File

@ -0,0 +1,171 @@
import logging
from typing import List, overload, Self, Type, TypeVar
from flask import url_for
from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
from .wish_owner import BrickWishOwner
logger = logging.getLogger(__name__)
T = TypeVar(
'T',
BrickSetOwner,
BrickSetPurchaseLocation,
BrickSetStatus,
BrickSetStorage,
BrickSetTag,
BrickWishOwner
)
# Lego sets metadata list
class BrickMetadataList(BrickRecordList[T]):
kind: str
mapping: dict[str, T]
model: Type[T]
# Database
table: str
order: str
# Queries
select_query: str
# Set endpoints
set_state_endpoint: str
set_value_endpoint: str
def __init__(
self,
model: Type[T],
/,
*,
force: bool = False,
records: list[T] | None = None
):
self.model = model
# Records override (masking the class variables with instance ones)
if records is not None:
self.override()
for metadata in records:
self.records.append(metadata)
self.mapping[metadata.fields.id] = metadata
else:
# Load metadata only if there is none already loaded
records = getattr(self, 'records', None)
if records is None or force:
# Don't use super()__init__ as it would mask class variables
self.fields = BrickRecordFields()
logger.info('Loading {kind} list'.format(
kind=self.kind
))
self.__class__.records = []
self.__class__.mapping = {}
# Load the metadata from the database
for record in self.select(order=self.order):
metadata = model(record=record)
self.__class__.records.append(metadata)
self.__class__.mapping[metadata.fields.id] = metadata
# HTML prefix name
def as_prefix(self, /) -> str:
return self.kind.replace(' ', '-')
# Filter the list of records (this one does nothing)
def filter(self) -> list[T]:
return self.records
# Add a layer of override data
def override(self) -> None:
self.fields = BrickRecordFields()
self.records = []
self.mapping = {}
# Return the items as columns for a select
@classmethod
def as_columns(cls, /, **kwargs) -> str:
new = cls.new()
return ', '.join([
'"{table}"."{column}"'.format(
table=cls.table,
column=record.as_column(),
)
for record
in new.filter(**kwargs)
])
# Grab a specific status
@classmethod
def get(cls, id: str, /, *, allow_none: bool = False) -> T:
new = cls.new()
if allow_none and id == '':
return new.model()
if id not in new.mapping:
raise NotFoundException(
'{kind} with ID {id} was not found in the database'.format(
kind=new.kind.capitalize(),
id=id,
),
)
return new.mapping[id]
# Get the list of statuses depending on the context
@overload
@classmethod
def list(cls, /, **kwargs) -> List[T]: ...
@overload
@classmethod
def list(cls, /, as_class: bool = False, **kwargs) -> Self: ...
@classmethod
def list(cls, /, as_class: bool = False, **kwargs) -> List[T] | Self:
new = cls.new()
list = new.filter(**kwargs)
if as_class:
# Return a copy of the metadata list with overriden records
return cls(new.model, records=list)
else:
return list
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
raise Exception('new() is not implemented for BrickMetadataList')
# URL to change the selected state of this metadata item for a set
@classmethod
def url_for_set_state(cls, id: str, /) -> str:
return url_for(
cls.set_state_endpoint,
id=id,
)
# URL to change the selected value of this metadata item for a set
@classmethod
def url_for_set_value(cls, id: str, /) -> str:
return url_for(
cls.set_value_endpoint,
id=id,
)

View File

@ -0,0 +1,29 @@
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from ..sql import BrickSQL
# Grab the list of checkboxes to create a list of SQL columns
def migration_0007(sql: 'BrickSQL', /) -> dict[str, Any]:
# Don't realy on sql files as they could be removed in the future
sql.cursor.execute('SELECT "bricktracker_set_checkboxes"."id" FROM "bricktracker_set_checkboxes"') # noqa: E501
records = sql.cursor.fetchall()
return {
'sources': ', '.join([
'"bricktracker_set_statuses_old"."status_{id}"'.format(id=record['id']) # noqa: E501
for record
in records
]),
'targets': ', '.join([
'"status_{id}"'.format(id=record['id'])
for record
in records
]),
'structure': ', '.join([
'"status_{id}" BOOLEAN NOT NULL DEFAULT 0'.format(id=record['id'])
for record
in records
])
}

View File

View File

@ -1,48 +1,68 @@
from sqlite3 import Row import logging
from typing import Any, Self, TYPE_CHECKING import traceback
from typing import Self, TYPE_CHECKING
from flask import current_app, url_for
from .exceptions import ErrorException, NotFoundException from .exceptions import ErrorException, NotFoundException
from .part_list import BrickPartList from .part_list import BrickPartList
from .rebrickable_image import RebrickableImage from .rebrickable_minifigure import RebrickableMinifigure
from .record import BrickRecord
if TYPE_CHECKING: if TYPE_CHECKING:
from .set import BrickSet from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# Lego minifigure # Lego minifigure
class BrickMinifigure(BrickRecord): class BrickMinifigure(RebrickableMinifigure):
brickset: 'BrickSet | None'
# Queries # Queries
insert_query: str = 'minifigure/insert' insert_query: str = 'minifigure/insert'
generic_query: str = 'minifigure/select/generic' generic_query: str = 'minifigure/select/generic'
select_query: str = 'minifigure/select/specific' select_query: str = 'minifigure/select/specific'
def __init__( # Import a minifigure into the database
self, def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool:
/, if self.brickset is None:
*, raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
brickset: 'BrickSet | None' = None,
record: Row | dict[str, Any] | None = None, try:
# Insert into the database
socket.auto_progress(
message='Set {set}: inserting minifigure {figure} into database'.format( # noqa: E501
set=self.brickset.fields.set,
figure=self.fields.figure
)
)
if not refresh:
# Insert into database
self.insert(commit=False)
# Load the inventory
if not BrickPartList.download(
socket,
self.brickset,
minifigure=self,
refresh=refresh
): ):
super().__init__() return False
# Save the brickset # Insert the rebrickable set into database (after counting parts)
self.brickset = brickset self.insert_rebrickable()
# Ingest the record if it has one except Exception as e:
if record is not None: socket.fail(
self.ingest(record) message='Error while importing minifigure {figure} from {set}: {error}'.format( # noqa: E501
figure=self.fields.figure,
set=self.brickset.fields.set,
error=e,
)
)
# Return the number just in digits format logger.debug(traceback.format_exc())
def clean_number(self, /) -> str:
number: str = self.fields.fig_num
number = number.removeprefix('fig-')
number = number.lstrip('0')
return number return False
return True
# Parts # Parts
def generic_parts(self, /) -> BrickPartList: def generic_parts(self, /) -> BrickPartList:
@ -51,108 +71,38 @@ class BrickMinifigure(BrickRecord):
# Parts # Parts
def parts(self, /) -> BrickPartList: def parts(self, /) -> BrickPartList:
if self.brickset is None: if self.brickset is None:
raise ErrorException('Part list for minifigure {number} requires a brickset'.format( # noqa: E501 raise ErrorException('Part list for minifigure {figure} requires a brickset'.format( # noqa: E501
number=self.fields.fig_num, figure=self.fields.figure,
)) ))
return BrickPartList().load(self.brickset, minifigure=self) return BrickPartList().list_specific(self.brickset, minifigure=self)
# Select a generic minifigure # Select a generic minifigure
def select_generic(self, fig_num: str, /) -> Self: def select_generic(self, figure: str, /) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.fields.fig_num = fig_num self.fields.figure = figure
if not self.select(override_query=self.generic_query): if not self.select(override_query=self.generic_query):
raise NotFoundException( raise NotFoundException(
'Minifigure with number {number} was not found in the database'.format( # noqa: E501 'Minifigure with figure {figure} was not found in the database'.format( # noqa: E501
number=self.fields.fig_num, figure=self.fields.figure,
), ),
) )
return self return self
# Select a specific minifigure (with a set and an number) # Select a specific minifigure (with a set and a figure)
def select_specific(self, brickset: 'BrickSet', fig_num: str, /) -> Self: def select_specific(self, brickset: 'BrickSet', figure: str, /) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.brickset = brickset self.brickset = brickset
self.fields.fig_num = fig_num self.fields.figure = figure
if not self.select(): if not self.select():
raise NotFoundException( raise NotFoundException(
'Minifigure with number {number} from set {set} was not found in the database'.format( # noqa: E501 'Minifigure with figure {figure} from set {set} was not found in the database'.format( # noqa: E501
number=self.fields.fig_num, figure=self.fields.figure,
set=self.brickset.fields.set, set=self.brickset.fields.set,
), ),
) )
return self return self
# Return a dict with common SQL parameters for a minifigure
def sql_parameters(self, /) -> dict[str, Any]:
parameters = super().sql_parameters()
# Supplement from the brickset
if self.brickset is not None:
if 'u_id' not in parameters:
parameters['u_id'] = self.brickset.fields.id
if 'set_num' not in parameters:
parameters['set_num'] = self.brickset.fields.set
return parameters
# Self url
def url(self, /) -> str:
return url_for(
'minifigure.details',
number=self.fields.fig_num,
)
# Compute the url for minifigure part image
def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.set_img_url is None:
file = RebrickableImage.nil_minifigure_name()
else:
file = self.fields.fig_num
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
else:
if self.fields.set_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
else:
return self.fields.set_img_url
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
try:
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
number=self.fields.fig_num.lower(),
)
except Exception:
pass
return ''
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(
data: dict[str, Any],
/,
*,
brickset: 'BrickSet | None' = None,
**_,
) -> dict[str, Any]:
record = {
'fig_num': data['set_num'],
'name': data['set_name'],
'quantity': data['quantity'],
'set_img_url': data['set_img_url'],
}
if brickset is not None:
record['set_num'] = brickset.fields.set
record['u_id'] = brickset.fields.id
return record

View File

@ -1,11 +1,17 @@
import logging
import traceback
from typing import Any, Self, TYPE_CHECKING from typing import Any, Self, TYPE_CHECKING
from flask import current_app from flask import current_app
from .minifigure import BrickMinifigure from .minifigure import BrickMinifigure
from .rebrickable import Rebrickable
from .record_list import BrickRecordList from .record_list import BrickRecordList
if TYPE_CHECKING: if TYPE_CHECKING:
from .set import BrickSet from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# Lego minifigures # Lego minifigures
@ -15,10 +21,11 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Queries # Queries
all_query: str = 'minifigure/list/all' all_query: str = 'minifigure/list/all'
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'
select_query: str = 'minifigure/list/from_set' select_query: str = 'minifigure/list/from_set'
using_part_query: str = 'minifigure/list/using_part' using_part_query: str = 'minifigure/list/using_part'
missing_part_query: str = 'minifigure/list/missing_part'
def __init__(self, /): def __init__(self, /):
super().__init__() super().__init__()
@ -31,13 +38,18 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Load all minifigures # Load all minifigures
def all(self, /) -> Self: def all(self, /) -> Self:
for record in self.select( self.list(override_query=self.all_query)
override_query=self.all_query,
order=self.order
):
minifigure = BrickMinifigure(record=record)
self.records.append(minifigure) return self
# Minifigures with a part damaged part
def damaged_part(self, part: str, color: int, /) -> Self:
# Save the parameters to the fields
self.fields.part = part
self.fields.color = color
# Load the minifigures from the database
self.list(override_query=self.damaged_part_query)
return self return self
@ -47,29 +59,69 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
if current_app.config['RANDOM']: if current_app.config['RANDOM']:
order = 'RANDOM()' order = 'RANDOM()'
else: else:
order = 'minifigures.rowid DESC' order = '"bricktracker_minifigures"."rowid" DESC'
for record in self.select( self.list(override_query=self.last_query, order=order, limit=limit)
override_query=self.last_query,
order=order,
limit=limit
):
minifigure = BrickMinifigure(record=record)
self.records.append(minifigure)
return self return self
# Base minifigure list
def list(
self,
/,
*,
override_query: str | None = None,
order: str | None = None,
limit: int | None = None,
**context: Any,
) -> None:
if order is None:
order = self.order
if hasattr(self, 'brickset'):
brickset = self.brickset
else:
brickset = None
# Load the sets from the database
for record in super().select(
override_query=override_query,
order=order,
limit=limit,
):
minifigure = BrickMinifigure(brickset=brickset, record=record)
self.records.append(minifigure)
# Load minifigures from a brickset # Load minifigures from a brickset
def load(self, brickset: 'BrickSet', /) -> Self: def from_set(self, brickset: 'BrickSet', /) -> Self:
# Save the brickset # Save the brickset
self.brickset = brickset self.brickset = brickset
# Load the minifigures from the database # Load the minifigures from the database
for record in self.select(order=self.order): self.list()
minifigure = BrickMinifigure(brickset=self.brickset, record=record)
self.records.append(minifigure) return self
# Minifigures missing a part
def missing_part(self, part: str, color: int, /) -> Self:
# Save the parameters to the fields
self.fields.part = part
self.fields.color = color
# Load the minifigures from the database
self.list(override_query=self.missing_part_query)
return self
# Minifigure using a part
def using_part(self, part: str, color: int, /) -> Self:
# Save the parameters to the fields
self.fields.part = part
self.fields.color = color
# Load the minifigures from the database
self.list(override_query=self.using_part_query)
return self return self
@ -78,57 +130,54 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
parameters: dict[str, Any] = super().sql_parameters() parameters: dict[str, Any] = super().sql_parameters()
if self.brickset is not None: if self.brickset is not None:
parameters['u_id'] = self.brickset.fields.id parameters['id'] = self.brickset.fields.id
parameters['set_num'] = self.brickset.fields.set
return parameters return parameters
# Minifigures missing a part # Import the minifigures from Rebrickable
def missing_part( @staticmethod
self, def download(
part_num: str, socket: 'BrickSocket',
color_id: int, brickset: 'BrickSet',
/, /,
*, *,
element_id: int | None = None, refresh: bool = False
) -> Self: ) -> bool:
# Save the parameters to the fields try:
self.fields.part_num = part_num socket.auto_progress(
self.fields.color_id = color_id message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501
self.fields.element_id = element_id set=brickset.fields.set,
),
increment_total=True,
)
# Load the minifigures from the database logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format(
for record in self.select( set=brickset.fields.set,
override_query=self.missing_part_query, ))
order=self.order
):
minifigure = BrickMinifigure(record=record)
self.records.append(minifigure) minifigures = Rebrickable[BrickMinifigure](
'get_set_minifigs',
brickset.fields.set,
BrickMinifigure,
socket=socket,
brickset=brickset,
).list()
return self # Process each minifigure
for minifigure in minifigures:
if not minifigure.download(socket, refresh=refresh):
return False
# Minifigure using a part return True
def using_part(
self,
part_num: str,
color_id: int,
/,
*,
element_id: int | None = None,
) -> Self:
# Save the parameters to the fields
self.fields.part_num = part_num
self.fields.color_id = color_id
self.fields.element_id = element_id
# Load the minifigures from the database except Exception as e:
for record in self.select( socket.fail(
override_query=self.using_part_query, message='Error while importing set {set} minifigure list: {error}'.format( # noqa: E501
order=self.order set=brickset.fields.set,
): error=e,
minifigure = BrickMinifigure(record=record) )
)
self.records.append(minifigure) logger.debug(traceback.format_exc())
return self return False

View File

@ -11,9 +11,10 @@ NAVBAR: Final[list[dict[str, Any]]] = [
{'e': 'set.list', 't': 'Sets', 'i': 'grid-line', 'f': 'HIDE_ALL_SETS'}, # noqa: E501 {'e': 'set.list', 't': 'Sets', 'i': 'grid-line', 'f': 'HIDE_ALL_SETS'}, # noqa: E501
{'e': 'add.add', 't': 'Add', 'i': 'add-circle-line', 'f': 'HIDE_ADD_SET'}, # noqa: E501 {'e': 'add.add', 't': 'Add', 'i': 'add-circle-line', 'f': 'HIDE_ADD_SET'}, # noqa: E501
{'e': 'part.list', 't': 'Parts', 'i': 'shapes-line', 'f': 'HIDE_ALL_PARTS'}, # noqa: E501 {'e': 'part.list', 't': 'Parts', 'i': 'shapes-line', 'f': 'HIDE_ALL_PARTS'}, # noqa: E501
{'e': 'part.missing', 't': 'Missing', 'i': 'error-warning-line', 'f': 'HIDE_MISSING_PARTS'}, # noqa: E501 {'e': 'part.problem', 't': 'Problems', 'i': 'error-warning-line', 'f': 'HIDE_ALL_PROBLEMS_PARTS'}, # noqa: E501
{'e': 'minifigure.list', 't': 'Minifigures', 'i': 'group-line', 'f': 'HIDE_ALL_MINIFIGURES'}, # noqa: E501 {'e': 'minifigure.list', 't': 'Minifigures', 'i': 'group-line', 'f': 'HIDE_ALL_MINIFIGURES'}, # noqa: E501
{'e': 'instructions.list', 't': 'Instructions', 'i': 'file-line', 'f': 'HIDE_ALL_INSTRUCTIONS'}, # noqa: E501 {'e': 'instructions.list', 't': 'Instructions', 'i': 'file-line', 'f': 'HIDE_ALL_INSTRUCTIONS'}, # noqa: E501
{'e': 'storage.list', 't': 'Storages', 'i': 'archive-2-line', 'f': 'HIDE_ALL_STORAGES'}, # noqa: E501
{'e': 'wish.list', 't': 'Wishlist', 'i': 'gift-line', 'f': 'HIDE_WISHES'}, {'e': 'wish.list', 't': 'Wishlist', 'i': 'gift-line', 'f': 'HIDE_WISHES'},
{'e': 'admin.admin', 't': 'Admin', 'i': 'settings-4-line', 'f': 'HIDE_ADMIN'}, # noqa: E501 {'e': 'admin.admin', 't': 'Admin', 'i': 'settings-4-line', 'f': 'HIDE_ADMIN'}, # noqa: E501
] ]

View File

@ -1,23 +1,25 @@
import os import logging
from sqlite3 import Row from sqlite3 import Row
from typing import Any, Self, TYPE_CHECKING from typing import Any, Self, TYPE_CHECKING
from urllib.parse import urlparse import traceback
from flask import current_app, url_for from flask import url_for
from .exceptions import DatabaseException, ErrorException, NotFoundException from .exceptions import ErrorException, NotFoundException
from .rebrickable_image import RebrickableImage from .rebrickable_part import RebrickablePart
from .record import BrickRecord
from .sql import BrickSQL from .sql import BrickSQL
if TYPE_CHECKING: if TYPE_CHECKING:
from .minifigure import BrickMinifigure from .minifigure import BrickMinifigure
from .set import BrickSet from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# Lego set or minifig part # Lego set or minifig part
class BrickPart(BrickRecord): class BrickPart(RebrickablePart):
brickset: 'BrickSet | None' identifier: str
minifigure: 'BrickMinifigure | None' kind: str
# Queries # Queries
insert_query: str = 'part/insert' insert_query: str = 'part/insert'
@ -30,74 +32,91 @@ class BrickPart(BrickRecord):
*, *,
brickset: 'BrickSet | None' = None, brickset: 'BrickSet | None' = None,
minifigure: 'BrickMinifigure | None' = None, minifigure: 'BrickMinifigure | None' = None,
record: Row | dict[str, Any] | None = None, record: Row | dict[str, Any] | None = None
): ):
super().__init__() super().__init__(
brickset=brickset,
# Save the brickset and minifigure minifigure=minifigure,
self.brickset = brickset record=record
self.minifigure = minifigure
# Ingest the record if it has one
if record is not None:
self.ingest(record)
# Delete missing part
def delete_missing(self, /) -> None:
BrickSQL().execute_and_commit(
'missing/delete/from_set',
parameters=self.sql_parameters()
) )
# Set missing part if self.minifigure is not None:
def set_missing(self, quantity: int, /) -> None: self.identifier = self.minifigure.fields.figure
parameters = self.sql_parameters() self.kind = 'Minifigure'
parameters['quantity'] = quantity elif self.brickset is not None:
self.identifier = self.brickset.fields.set
self.kind = 'Set'
# Can't use UPSERT because the database has no keys # Import a part into the database
# Try to update def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool:
database = BrickSQL() if self.brickset is None:
rows, _ = database.execute( raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501
'missing/update/from_set',
parameters=parameters,
)
# Insert if no row has been affected try:
if not rows: # Insert into the database
rows, _ = database.execute( socket.auto_progress(
'missing/insert', message='{kind} {identifier}: inserting part {part} into database'.format( # noqa: E501
parameters=parameters, kind=self.kind,
) identifier=self.identifier,
part=self.fields.part
if rows != 1:
raise DatabaseException(
'Could not update the missing quantity for part {id}'.format( # noqa: E501
id=self.fields.id
) )
) )
database.commit() if not refresh:
# Insert into database
self.insert(commit=False)
# Insert the rebrickable set into database
self.insert_rebrickable()
except Exception as e:
socket.fail(
message='Error while importing part {part} from {kind} {identifier}: {error}'.format( # noqa: E501
part=self.fields.part,
kind=self.kind,
identifier=self.identifier,
error=e,
)
)
logger.debug(traceback.format_exc())
return False
return True
# A identifier for HTML component
def html_id(self, prefix: str | None = None, /) -> str:
components: list[str] = ['part']
if prefix is not None:
components.append(prefix)
if self.fields.figure is not None:
components.append(self.fields.figure)
components.append(self.fields.part)
components.append(str(self.fields.color))
components.append(str(self.fields.spare))
return '-'.join(components)
# Select a generic part # Select a generic part
def select_generic( def select_generic(
self, self,
part_num: str, part: str,
color_id: int, color: int,
/, /,
*,
element_id: int | None = None
) -> Self: ) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.fields.part_num = part_num self.fields.part = part
self.fields.color_id = color_id self.fields.color = color
self.fields.element_id = element_id
if not self.select(override_query=self.generic_query): if not self.select(override_query=self.generic_query):
raise NotFoundException( raise NotFoundException(
'Part with number {number}, color ID {color} and element ID {element} was not found in the database'.format( # noqa: E501 'Part with number {number}, color ID {color} was not found in the database'.format( # noqa: E501
number=self.fields.part_num, number=self.fields.part,
color=self.fields.color_id, color=self.fields.color,
element=self.fields.element_id,
), ),
) )
@ -107,7 +126,9 @@ class BrickPart(BrickRecord):
def select_specific( def select_specific(
self, self,
brickset: 'BrickSet', brickset: 'BrickSet',
id: str, part: str,
color: int,
spare: int,
/, /,
*, *,
minifigure: 'BrickMinifigure | None' = None, minifigure: 'BrickMinifigure | None' = None,
@ -115,168 +136,73 @@ class BrickPart(BrickRecord):
# Save the parameters to the fields # Save the parameters to the fields
self.brickset = brickset self.brickset = brickset
self.minifigure = minifigure self.minifigure = minifigure
self.fields.id = id self.fields.part = part
self.fields.color = color
self.fields.spare = spare
if not self.select(): if not self.select():
if self.minifigure is not None:
figure = self.minifigure.fields.figure
else:
figure = None
raise NotFoundException( raise NotFoundException(
'Part with ID {id} from set {set} was not found in the database'.format( # noqa: E501 'Part {part} with color {color} (spare: {spare}) from set {set} ({id}) (minifigure: {figure}) was not found in the database'.format( # noqa: E501
part=self.fields.part,
color=self.fields.color,
spare=self.fields.spare,
id=self.fields.id, id=self.fields.id,
set=self.brickset.fields.set, set=self.brickset.fields.set,
figure=figure,
), ),
) )
return self return self
# Return a dict with common SQL parameters for a part # Update a problematic part
def sql_parameters(self, /) -> dict[str, Any]: def update_problem(self, problem: str, json: Any | None, /) -> int:
parameters = super().sql_parameters() amount: str | int = json.get('value', '') # type: ignore
# Supplement from the brickset # We need a positive integer
if 'u_id' not in parameters and self.brickset is not None:
parameters['u_id'] = self.brickset.fields.id
if 'set_num' not in parameters:
if self.minifigure is not None:
parameters['set_num'] = self.minifigure.fields.fig_num
elif self.brickset is not None:
parameters['set_num'] = self.brickset.fields.set
return parameters
# Update the missing part
def update_missing(self, missing: Any, /) -> None:
# If empty, delete it
if missing == '':
self.delete_missing()
else:
# Try to understand it as a number
try: try:
missing = int(missing) if amount == '':
amount = 0
amount = int(amount)
if amount < 0:
amount = 0
except Exception: except Exception:
raise ErrorException('"{missing}" is not a valid integer'.format( # noqa: E501 raise ErrorException('"{amount}" is not a valid integer'.format(
missing=missing amount=amount
)) ))
# If 0, delete it if amount < 0:
if missing == 0: raise ErrorException('Cannot set a negative amount')
self.delete_missing()
else: setattr(self.fields, problem, amount)
# If negative, it's an error
if missing < 0:
raise ErrorException('Cannot set a negative missing value')
# Otherwise upsert it BrickSQL().execute_and_commit(
# Not checking if it is too much, you do you 'part/update/{problem}'.format(problem=problem),
self.set_missing(missing) parameters=self.sql_parameters()
# Self url
def url(self, /) -> str:
return url_for(
'part.details',
number=self.fields.part_num,
color=self.fields.color_id,
element=self.fields.element_id,
) )
# Compute the url for the bricklink page return amount
def url_for_bricklink(self, /) -> str:
if current_app.config['BRICKLINK_LINKS']:
try:
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
number=self.fields.part_num,
)
except Exception:
pass
return '' # Compute the url for problematic part
def url_for_problem(self, problem: str, /) -> str:
# Compute the url for the part image
def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.part_img_url is None:
file = RebrickableImage.nil_name()
else:
file = self.fields.part_img_url_id
return RebrickableImage.static_url(file, 'PARTS_FOLDER')
else:
if self.fields.part_img_url is None:
return current_app.config['REBRICKABLE_IMAGE_NIL']
else:
return self.fields.part_img_url
# Compute the url for missing part
def url_for_missing(self, /) -> str:
# Different URL for a minifigure part # Different URL for a minifigure part
if self.minifigure is not None: if self.minifigure is not None:
return url_for( figure = self.minifigure.fields.figure
'set.missing_minifigure_part', else:
id=self.fields.u_id, figure = None
minifigure_id=self.minifigure.fields.fig_num,
part_id=self.fields.id,
)
return url_for( return url_for(
'set.missing_part', 'set.problem_part',
id=self.fields.u_id, id=self.fields.id,
part_id=self.fields.id figure=figure,
part=self.fields.part,
color=self.fields.color,
spare=self.fields.spare,
problem=problem,
) )
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
try:
return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501
number=self.fields.part_num,
color=self.fields.color_id,
)
except Exception:
pass
return ''
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(
data: dict[str, Any],
/,
*,
brickset: 'BrickSet | None' = None,
minifigure: 'BrickMinifigure | None' = None,
**_,
) -> dict[str, Any]:
record = {
'set_num': data['set_num'],
'id': data['id'],
'part_num': data['part']['part_num'],
'name': data['part']['name'],
'part_img_url': data['part']['part_img_url'],
'part_img_url_id': None,
'color_id': data['color']['id'],
'color_name': data['color']['name'],
'quantity': data['quantity'],
'is_spare': data['is_spare'],
'element_id': data['element_id'],
}
if brickset is not None:
record['u_id'] = brickset.fields.id
if minifigure is not None:
record['set_num'] = data['fig_num']
# Extract the file name
if data['part']['part_img_url'] is not None:
part_img_url_file = os.path.basename(
urlparse(data['part']['part_img_url']).path
)
part_img_url_id, _ = os.path.splitext(part_img_url_file)
if part_img_url_id is not None or part_img_url_id != '':
record['part_img_url_id'] = part_img_url_id
return record

View File

@ -1,12 +1,18 @@
import logging
from typing import Any, Self, TYPE_CHECKING from typing import Any, Self, TYPE_CHECKING
import traceback
from flask import current_app from flask import current_app
from .part import BrickPart from .part import BrickPart
from .rebrickable import Rebrickable
from .record_list import BrickRecordList from .record_list import BrickRecordList
if TYPE_CHECKING: if TYPE_CHECKING:
from .minifigure import BrickMinifigure from .minifigure import BrickMinifigure
from .set import BrickSet from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# Lego set or minifig parts # Lego set or minifig parts
@ -17,10 +23,12 @@ class BrickPartList(BrickRecordList[BrickPart]):
# Queries # Queries
all_query: str = 'part/list/all' all_query: str = 'part/list/all'
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'
missing_query: str = 'part/list/missing' problem_query: str = 'part/list/problem'
select_query: str = 'part/list/from_set' print_query: str = 'part/list/from_print'
select_query: str = 'part/list/specific'
def __init__(self, /): def __init__(self, /):
super().__init__() super().__init__()
@ -34,18 +42,52 @@ class BrickPartList(BrickRecordList[BrickPart]):
# Load all parts # Load all parts
def all(self, /) -> Self: def all(self, /) -> Self:
for record in self.select( self.list(override_query=self.all_query)
override_query=self.all_query,
order=self.order
):
part = BrickPart(record=record)
self.records.append(part)
return self return self
# Load parts from a brickset or minifigure # Base part list
def load( def list(
self,
/,
*,
override_query: str | None = None,
order: str | None = None,
limit: int | None = None,
**context: Any,
) -> None:
if order is None:
order = self.order
if hasattr(self, 'brickset'):
brickset = self.brickset
else:
brickset = None
if hasattr(self, 'minifigure'):
minifigure = self.minifigure
else:
minifigure = None
# Load the sets from the database
for record in super().select(
override_query=override_query,
order=order,
limit=limit,
):
part = BrickPart(
brickset=brickset,
minifigure=minifigure,
record=record,
)
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare:
continue
self.records.append(part)
# List specific parts from a brickset or minifigure
def list_specific(
self, self,
brickset: 'BrickSet', brickset: 'BrickSet',
/, /,
@ -57,17 +99,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
self.minifigure = minifigure self.minifigure = minifigure
# Load the parts from the database # Load the parts from the database
for record in self.select(order=self.order): self.list()
part = BrickPart(
brickset=self.brickset,
minifigure=minifigure,
record=record,
)
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
continue
self.records.append(part)
return self return self
@ -81,47 +113,133 @@ class BrickPartList(BrickRecordList[BrickPart]):
self.minifigure = minifigure self.minifigure = minifigure
# Load the parts from the database # Load the parts from the database
for record in self.select( self.list(override_query=self.minifigure_query)
override_query=self.minifigure_query,
order=self.order
):
part = BrickPart(
minifigure=minifigure,
record=record,
)
if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare:
continue
self.records.append(part)
return self return self
# Load missing parts # Load generic parts from a print
def missing(self, /) -> Self: def from_print(
for record in self.select( self,
override_query=self.missing_query, brickpart: BrickPart,
order=self.order /,
): ) -> Self:
part = BrickPart(record=record) # Save the part and print
if brickpart.fields.print is not None:
self.fields.print = brickpart.fields.print
else:
self.fields.print = brickpart.fields.part
self.records.append(part) self.fields.part = brickpart.fields.part
self.fields.color = brickpart.fields.color
# Load the parts from the database
self.list(override_query=self.print_query)
return self
# Load problematic parts
def problem(self, /) -> Self:
self.list(override_query=self.problem_query)
return self return self
# Return a dict with common SQL parameters for a parts list # Return a dict with common SQL parameters for a parts list
def sql_parameters(self, /) -> dict[str, Any]: def sql_parameters(self, /) -> dict[str, Any]:
parameters: dict[str, Any] = {} parameters: dict[str, Any] = super().sql_parameters()
# Set id # Set id
if self.brickset is not None: if self.brickset is not None:
parameters['u_id'] = self.brickset.fields.id parameters['id'] = self.brickset.fields.id
# Use the minifigure number if present, # Use the minifigure number if present,
# otherwise use the set number
if self.minifigure is not None: if self.minifigure is not None:
parameters['set_num'] = self.minifigure.fields.fig_num parameters['figure'] = self.minifigure.fields.figure
elif self.brickset is not None: else:
parameters['set_num'] = self.brickset.fields.set parameters['figure'] = None
return parameters return parameters
# Load generic parts with same base but different color
def with_different_color(
self,
brickpart: BrickPart,
/,
) -> Self:
# Save the part
self.fields.part = brickpart.fields.part
self.fields.color = brickpart.fields.color
# Load the parts from the database
self.list(override_query=self.different_color_query)
return self
# Import the parts from Rebrickable
@staticmethod
def download(
socket: 'BrickSocket',
brickset: 'BrickSet',
/,
*,
minifigure: 'BrickMinifigure | None' = None,
refresh: bool = False
) -> bool:
if minifigure is not None:
identifier = minifigure.fields.figure
kind = 'Minifigure'
method = 'get_minifig_elements'
else:
identifier = brickset.fields.set
kind = 'Set'
method = 'get_set_elements'
try:
socket.auto_progress(
message='{kind} {identifier}: loading parts inventory from Rebrickable'.format( # noqa: E501
kind=kind,
identifier=identifier,
),
increment_total=True,
)
logger.debug('rebrick.lego.{method}("{identifier}")'.format(
method=method,
identifier=identifier,
))
inventory = Rebrickable[BrickPart](
method,
identifier,
BrickPart,
socket=socket,
brickset=brickset,
minifigure=minifigure,
).list()
# Process each part
number_of_parts: int = 0
for part in inventory:
# Count the number of parts for minifigures
if minifigure is not None:
number_of_parts += part.fields.quantity
if not part.download(socket, refresh=refresh):
return False
if minifigure is not None:
minifigure.fields.number_of_parts = number_of_parts
except Exception as e:
socket.fail(
message='Error while importing {kind} {identifier} parts list: {error}'.format( # noqa: E501
kind=kind,
identifier=identifier,
error=e,
)
)
logger.debug(traceback.format_exc())
return False
return True

View File

@ -8,16 +8,16 @@ from shutil import copyfileobj
from .exceptions import DownloadException from .exceptions import DownloadException
if TYPE_CHECKING: if TYPE_CHECKING:
from .minifigure import BrickMinifigure from .rebrickable_minifigure import RebrickableMinifigure
from .part import BrickPart from .rebrickable_part import RebrickablePart
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
# A set, part or minifigure image from Rebrickable # A set, part or minifigure image from Rebrickable
class RebrickableImage(object): class RebrickableImage(object):
set: 'RebrickableSet' set: 'RebrickableSet'
minifigure: 'BrickMinifigure | None' minifigure: 'RebrickableMinifigure | None'
part: 'BrickPart | None' part: 'RebrickablePart | None'
extension: str | None extension: str | None
@ -26,8 +26,8 @@ class RebrickableImage(object):
set: 'RebrickableSet', set: 'RebrickableSet',
/, /,
*, *,
minifigure: 'BrickMinifigure | None' = None, minifigure: 'RebrickableMinifigure | None' = None,
part: 'BrickPart | None' = None, part: 'RebrickablePart | None' = None,
): ):
# Save all objects # Save all objects
self.set = set self.set = set
@ -81,16 +81,16 @@ class RebrickableImage(object):
# Return the id depending on the objects provided # Return the id depending on the objects provided
def id(self, /) -> str: def id(self, /) -> str:
if self.part is not None: if self.part is not None:
if self.part.fields.part_img_url_id is None: if self.part.fields.image_id is None:
return RebrickableImage.nil_name() return RebrickableImage.nil_name()
else: else:
return self.part.fields.part_img_url_id return self.part.fields.image_id
if self.minifigure is not None: if self.minifigure is not None:
if self.minifigure.fields.set_img_url is None: if self.minifigure.fields.image is None:
return RebrickableImage.nil_minifigure_name() return RebrickableImage.nil_minifigure_name()
else: else:
return self.minifigure.fields.fig_num return self.minifigure.fields.figure
return self.set.fields.set return self.set.fields.set
@ -105,16 +105,16 @@ class RebrickableImage(object):
# Return the url depending on the objects provided # Return the url depending on the objects provided
def url(self, /) -> str: def url(self, /) -> str:
if self.part is not None: if self.part is not None:
if self.part.fields.part_img_url is None: if self.part.fields.image is None:
return current_app.config['REBRICKABLE_IMAGE_NIL'] return current_app.config['REBRICKABLE_IMAGE_NIL']
else: else:
return self.part.fields.part_img_url return self.part.fields.image
if self.minifigure is not None: if self.minifigure is not None:
if self.minifigure.fields.set_img_url is None: if self.minifigure.fields.image is None:
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'] return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
else: else:
return self.minifigure.fields.set_img_url return self.minifigure.fields.image
return self.set.fields.image return self.set.fields.image

View File

@ -0,0 +1,111 @@
from sqlite3 import Row
from typing import Any, TYPE_CHECKING
from flask import current_app, url_for
from .exceptions import ErrorException
from .rebrickable_image import RebrickableImage
from .record import BrickRecord
if TYPE_CHECKING:
from .set import BrickSet
# A minifigure from Rebrickable
class RebrickableMinifigure(BrickRecord):
brickset: 'BrickSet | None'
# Queries
select_query: str = 'rebrickable/minifigure/select'
insert_query: str = 'rebrickable/minifigure/insert'
def __init__(
self,
/,
*,
brickset: 'BrickSet | None' = None,
record: Row | dict[str, Any] | None = None
):
super().__init__()
# Save the brickset
self.brickset = brickset
# Ingest the record if it has one
if record is not None:
self.ingest(record)
# Insert the minifigure from Rebrickable
def insert_rebrickable(self, /) -> None:
if self.brickset is None:
raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
# Insert the Rebrickable minifigure to the database
self.insert(
commit=False,
no_defer=True,
override_query=RebrickableMinifigure.insert_query
)
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(
self.brickset,
minifigure=self,
).download()
# Return a dict with common SQL parameters for a minifigure
def sql_parameters(self, /) -> dict[str, Any]:
parameters = super().sql_parameters()
# Supplement from the brickset
if self.brickset is not None and 'id' not in parameters:
parameters['id'] = self.brickset.fields.id
return parameters
# Self url
def url(self, /) -> str:
return url_for(
'minifigure.details',
figure=self.fields.figure,
)
# Compute the url for minifigure image
def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.image is None:
file = RebrickableImage.nil_minifigure_name()
else:
file = self.fields.figure
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
else:
if self.fields.image is None:
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
else:
return self.fields.image
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
try:
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
number=self.fields.figure,
)
except Exception:
pass
return ''
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
# Extracting number
number = int(str(data['set_num'])[5:])
return {
'figure': str(data['set_num']),
'number': int(number),
'name': str(data['set_name']),
'quantity': int(data['quantity']),
'image': data['set_img_url'],
}

View File

@ -1,85 +0,0 @@
import logging
from typing import TYPE_CHECKING
from flask import current_app
from .minifigure import BrickMinifigure
from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
from .rebrickable_parts import RebrickableParts
if TYPE_CHECKING:
from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# Minifigures from Rebrickable
class RebrickableMinifigures(object):
socket: 'BrickSocket'
brickset: 'BrickSet'
def __init__(self, socket: 'BrickSocket', brickset: 'BrickSet', /):
# Save the socket
self.socket = socket
# Save the objects
self.brickset = brickset
# Import the minifigures from Rebrickable
def download(self, /) -> None:
self.socket.auto_progress(
message='Set {number}: loading minifigures from Rebrickable'.format( # noqa: E501
number=self.brickset.fields.set,
),
increment_total=True,
)
logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format(
set=self.brickset.fields.set,
))
minifigures = Rebrickable[BrickMinifigure](
'get_set_minifigs',
self.brickset.fields.set,
BrickMinifigure,
socket=self.socket,
brickset=self.brickset,
).list()
# Process each minifigure
total = len(minifigures)
for index, minifigure in enumerate(minifigures):
# Insert into the database
self.socket.auto_progress(
message='Set {number}: inserting minifigure {current}/{total} into database'.format( # noqa: E501
number=self.brickset.fields.set,
current=index+1,
total=total,
)
)
# Insert into database
minifigure.insert(commit=False)
# Grab the image
self.socket.progress(
message='Set {number}: downloading minifigure {current}/{total} image'.format( # noqa: E501
number=self.brickset.fields.set,
current=index+1,
total=total,
)
)
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(
self.brickset,
minifigure=minifigure
).download()
# Load the inventory
RebrickableParts(
self.socket,
self.brickset,
minifigure=minifigure,
).download()

View File

@ -0,0 +1,196 @@
import os
from sqlite3 import Row
from typing import Any, TYPE_CHECKING
from urllib.parse import urlparse
from flask import current_app, url_for
from .exceptions import ErrorException
from .rebrickable_image import RebrickableImage
from .record import BrickRecord
if TYPE_CHECKING:
from .minifigure import BrickMinifigure
from .set import BrickSet
from .socket import BrickSocket
# A part from Rebrickable
class RebrickablePart(BrickRecord):
socket: 'BrickSocket'
brickset: 'BrickSet | None'
minifigure: 'BrickMinifigure | None'
# Queries
select_query: str = 'rebrickable/part/select'
insert_query: str = 'rebrickable/part/insert'
def __init__(
self,
/,
*,
brickset: 'BrickSet | None' = None,
minifigure: 'BrickMinifigure | None' = None,
record: Row | dict[str, Any] | None = None
):
super().__init__()
# Save the brickset
self.brickset = brickset
# Save the minifigure
self.minifigure = minifigure
# Ingest the record if it has one
if record is not None:
self.ingest(record)
# Insert the part from Rebrickable
def insert_rebrickable(self, /) -> None:
if self.brickset is None:
raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501
# Insert the Rebrickable part to the database
self.insert(
commit=False,
no_defer=True,
override_query=RebrickablePart.insert_query
)
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(
self.brickset,
minifigure=self.minifigure,
part=self,
).download()
# Return a dict with common SQL parameters for a part
def sql_parameters(self, /) -> dict[str, Any]:
parameters = super().sql_parameters()
# Set id
if self.brickset is not None:
parameters['id'] = self.brickset.fields.id
# Use the minifigure number if present,
if self.minifigure is not None:
parameters['figure'] = self.minifigure.fields.figure
else:
parameters['figure'] = None
return parameters
# Self url
def url(self, /) -> str:
return url_for(
'part.details',
part=self.fields.part,
color=self.fields.color,
)
# Compute the url for the bricklink page
def url_for_bricklink(self, /) -> str:
if current_app.config['BRICKLINK_LINKS']:
try:
return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501
part=self.fields.part,
)
except Exception:
pass
return ''
# Compute the url for the part image
def url_for_image(self, /) -> str:
if not current_app.config['USE_REMOTE_IMAGES']:
if self.fields.image is None:
file = RebrickableImage.nil_name()
else:
file = self.fields.image_id
return RebrickableImage.static_url(file, 'PARTS_FOLDER')
else:
if self.fields.image is None:
return current_app.config['REBRICKABLE_IMAGE_NIL']
else:
return self.fields.image
# Compute the url for the original of the printed part
def url_for_print(self, /) -> str:
if self.fields.print is not None:
return url_for(
'part.details',
part=self.fields.print,
color=self.fields.color,
)
else:
return ''
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
try:
if self.fields.url is not None:
# The URL does not contain color info...
return '{url}{color}'.format(
url=self.fields.url,
color=self.fields.color
)
else:
return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501
part=self.fields.part,
color=self.fields.color,
)
except Exception:
pass
return ''
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(
data: dict[str, Any],
/,
*,
brickset: 'BrickSet | None' = None,
minifigure: 'BrickMinifigure | None' = None,
**_,
) -> dict[str, Any]:
record = {
'id': None,
'figure': None,
'part': data['part']['part_num'],
'color': data['color']['id'],
'spare': data['is_spare'],
'quantity': data['quantity'],
'rebrickable_inventory': data['id'],
'element': data['element_id'],
'color_id': data['color']['id'],
'color_name': data['color']['name'],
'color_rgb': data['color']['rgb'],
'color_transparent': data['color']['is_trans'],
'name': data['part']['name'],
'category': data['part']['part_cat_id'],
'image': data['part']['part_img_url'],
'image_id': None,
'url': data['part']['part_url'],
'print': data['part']['print_of']
}
if brickset is not None:
record['id'] = brickset.fields.id
if minifigure is not None:
record['figure'] = minifigure.fields.figure
# Extract the file name
if record['image'] is not None:
image_id, _ = os.path.splitext(
os.path.basename(
urlparse(record['image']).path
)
)
if image_id is not None or image_id != '':
record['image_id'] = image_id
return record

View File

@ -1,113 +0,0 @@
import logging
from typing import TYPE_CHECKING
from flask import current_app
from .part import BrickPart
from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
if TYPE_CHECKING:
from .minifigure import BrickMinifigure
from .set import BrickSet
from .socket import BrickSocket
logger = logging.getLogger(__name__)
# A list of parts from Rebrickable
class RebrickableParts(object):
socket: 'BrickSocket'
brickset: 'BrickSet'
minifigure: 'BrickMinifigure | None'
number: str
kind: str
method: str
def __init__(
self,
socket: 'BrickSocket',
brickset: 'BrickSet',
/,
*,
minifigure: 'BrickMinifigure | None' = None,
):
# Save the socket
self.socket = socket
# Save the objects
self.brickset = brickset
self.minifigure = minifigure
if self.minifigure is not None:
self.number = self.minifigure.fields.fig_num
self.kind = 'Minifigure'
self.method = 'get_minifig_elements'
else:
self.number = self.brickset.fields.set
self.kind = 'Set'
self.method = 'get_set_elements'
# Import the parts from Rebrickable
def download(self, /) -> None:
self.socket.auto_progress(
message='{kind} {number}: loading parts inventory from Rebrickable'.format( # noqa: E501
kind=self.kind,
number=self.number,
),
increment_total=True,
)
logger.debug('rebrick.lego.{method}("{number}")'.format(
method=self.method,
number=self.number,
))
inventory = Rebrickable[BrickPart](
self.method,
self.number,
BrickPart,
socket=self.socket,
brickset=self.brickset,
minifigure=self.minifigure,
).list()
# Process each part
total = len(inventory)
for index, part in enumerate(inventory):
# Skip spare parts
if (
current_app.config['SKIP_SPARE_PARTS'] and
part.fields.is_spare
):
continue
# Insert into the database
self.socket.auto_progress(
message='{kind} {number}: inserting part {current}/{total} into database'.format( # noqa: E501
kind=self.kind,
number=self.number,
current=index+1,
total=total,
)
)
# Insert into database
part.insert(commit=False)
# Grab the image
self.socket.progress(
message='{kind} {number}: downloading part {current}/{total} image'.format( # noqa: E501
kind=self.kind,
number=self.number,
current=index+1,
total=total,
)
)
if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(
self.brickset,
minifigure=self.minifigure,
part=part,
).download()

View File

@ -1,9 +1,9 @@
import logging import logging
from sqlite3 import Row from sqlite3 import Row
import traceback import traceback
from typing import Any, TYPE_CHECKING from typing import Any, Self, TYPE_CHECKING
from flask import current_app from flask import current_app, url_for
from .exceptions import ErrorException, NotFoundException from .exceptions import ErrorException, NotFoundException
from .instructions import BrickInstructions from .instructions import BrickInstructions
@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
# A set from Rebrickable # A set from Rebrickable
class RebrickableSet(BrickRecord): class RebrickableSet(BrickRecord):
socket: 'BrickSocket'
theme: 'BrickTheme' theme: 'BrickTheme'
instructions: list[BrickInstructions] instructions: list[BrickInstructions]
@ -36,7 +35,6 @@ class RebrickableSet(BrickRecord):
self, self,
/, /,
*, *,
socket: 'BrickSocket | None' = None,
record: Row | dict[str, Any] | None = None record: Row | dict[str, Any] | None = None
): ):
super().__init__() super().__init__()
@ -44,24 +42,19 @@ class RebrickableSet(BrickRecord):
# Placeholders # Placeholders
self.instructions = [] self.instructions = []
# Save the socket
if socket is not None:
self.socket = socket
# Ingest the record if it has one # Ingest the record if it has one
if record is not None: if record is not None:
self.ingest(record) self.ingest(record)
# Import the set from Rebrickable # Insert the set from Rebrickable
def download_rebrickable(self, /) -> None: def insert_rebrickable(self, /) -> None:
# Insert the Rebrickable set to the database # Insert the Rebrickable set to the database
rows, _ = self.insert( self.insert(
commit=False, commit=False,
no_defer=True, no_defer=True,
override_query=RebrickableSet.insert_query override_query=RebrickableSet.insert_query
) )
if rows > 0:
if not current_app.config['USE_REMOTE_IMAGES']: if not current_app.config['USE_REMOTE_IMAGES']:
RebrickableImage(self).download() RebrickableImage(self).download()
@ -88,20 +81,21 @@ class RebrickableSet(BrickRecord):
# Load the set from Rebrickable # Load the set from Rebrickable
def load( def load(
self, self,
socket: 'BrickSocket',
data: dict[str, Any], data: dict[str, Any],
/, /,
*, *,
from_download=False, from_download=False,
) -> bool: ) -> bool:
# Reset the progress # Reset the progress
self.socket.progress_count = 0 socket.progress_count = 0
self.socket.progress_total = 2 socket.progress_total = 2
try: try:
self.socket.auto_progress(message='Parsing set number') socket.auto_progress(message='Parsing set number')
set = parse_set(str(data['set'])) set = parse_set(str(data['set']))
self.socket.auto_progress( socket.auto_progress(
message='Set {set}: loading from Rebrickable'.format( message='Set {set}: loading from Rebrickable'.format(
set=set, set=set,
), ),
@ -118,12 +112,12 @@ class RebrickableSet(BrickRecord):
instance=self, instance=self,
).get() ).get()
self.socket.emit('SET_LOADED', self.short( socket.emit('SET_LOADED', self.short(
from_download=from_download from_download=from_download
)) ))
if not from_download: if not from_download:
self.socket.complete( socket.complete(
message='Set {set}: loaded from Rebrickable'.format( message='Set {set}: loaded from Rebrickable'.format(
set=self.fields.set set=self.fields.set
) )
@ -132,7 +126,7 @@ class RebrickableSet(BrickRecord):
return True return True
except Exception as e: except Exception as e:
self.socket.fail( socket.fail(
message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501 message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501
error=str(e), error=str(e),
data=data, data=data,
@ -144,6 +138,21 @@ class RebrickableSet(BrickRecord):
return False return False
# Select a specific set (with a set)
def select_specific(self, set: str, /) -> Self:
# Save the parameters to the fields
self.fields.set = set
# Load from database
if not self.select():
raise NotFoundException(
'Set with set {set} was not found in the database'.format(
set=self.fields.set,
),
)
return self
# Return a short form of the Rebrickable set # Return a short form of the Rebrickable set
def short(self, /, *, from_download: bool = False) -> dict[str, Any]: def short(self, /, *, from_download: bool = False) -> dict[str, Any]:
return { return {
@ -170,6 +179,10 @@ class RebrickableSet(BrickRecord):
return '' return ''
# Compute the url for the refresh button
def url_for_refresh(self, /) -> str:
return url_for('set.refresh', set=self.fields.set)
# Normalize from Rebrickable # Normalize from Rebrickable
@staticmethod @staticmethod
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:

View File

@ -9,6 +9,7 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]):
# Queries # Queries
select_query: str = 'rebrickable/set/list' select_query: str = 'rebrickable/set/list'
refresh_query: str = 'rebrickable/set/need_refresh'
# All the sets # All the sets
def all(self, /) -> Self: def all(self, /) -> Self:
@ -19,3 +20,15 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]):
self.records.append(rebrickable_set) self.records.append(rebrickable_set)
return self return self
# Sets needing refresh
def need_refresh(self, /) -> Self:
# Load the sets from the database
for record in self.select(
override_query=self.refresh_query
):
rebrickable_set = RebrickableSet(record=record)
self.records.append(rebrickable_set)
return self

View File

@ -1,5 +1,5 @@
from sqlite3 import Row from sqlite3 import Row
from typing import Any, ItemsView, Tuple from typing import Any, ItemsView
from .fields import BrickRecordFields from .fields import BrickRecordFields
from .sql import BrickSQL from .sql import BrickSQL
@ -31,14 +31,14 @@ class BrickRecord(object):
commit=True, commit=True,
no_defer=False, no_defer=False,
override_query: str | None = None override_query: str | None = None
) -> Tuple[int, str]: ) -> None:
if override_query: if override_query:
query = override_query query = override_query
else: else:
query = self.insert_query query = self.insert_query
database = BrickSQL() database = BrickSQL()
rows, q = database.execute( database.execute(
query, query,
parameters=self.sql_parameters(), parameters=self.sql_parameters(),
defer=not commit and not no_defer, defer=not commit and not no_defer,
@ -47,8 +47,6 @@ class BrickRecord(object):
if commit: if commit:
database.commit() database.commit()
return rows, q
# Shorthand to field items # Shorthand to field items
def items(self, /) -> ItemsView[str, Any]: def items(self, /) -> ItemsView[str, Any]:
return self.fields.__dict__.items() return self.fields.__dict__.items()

View File

@ -8,16 +8,26 @@ if TYPE_CHECKING:
from .part import BrickPart from .part import BrickPart
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
from .set import BrickSet from .set import BrickSet
from .set_checkbox import BrickSetCheckbox from .set_owner import BrickSetOwner
from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
from .wish import BrickWish from .wish import BrickWish
from .wish_owner import BrickWishOwner
T = TypeVar( T = TypeVar(
'T', 'T',
'BrickSet',
'BrickSetCheckbox',
'BrickPart',
'BrickMinifigure', 'BrickMinifigure',
'BrickPart',
'BrickSet',
'BrickSetOwner',
'BrickSetPurchaseLocation',
'BrickSetStatus',
'BrickSetStorage',
'BrickSetTag',
'BrickWish', 'BrickWish',
'BrickWishOwner',
'RebrickableSet' 'RebrickableSet'
) )

View File

@ -1,7 +1,12 @@
from .instructions_list import BrickInstructionsList from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList from .retired_list import BrickRetiredList
from .set_checkbox_list import BrickSetCheckboxList from .set_owner_list import BrickSetOwnerList
from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
from .theme_list import BrickThemeList from .theme_list import BrickThemeList
from .wish_owner_list import BrickWishOwnerList
# Reload everything related to a database after an operation # Reload everything related to a database after an operation
@ -11,13 +16,28 @@ def reload() -> None:
# Reload the instructions # Reload the instructions
BrickInstructionsList(force=True) BrickInstructionsList(force=True)
# Reload the checkboxes # Reload the set owners
BrickSetCheckboxList(force=True) BrickSetOwnerList.new(force=True)
# Reload the set purchase locations
BrickSetPurchaseLocationList.new(force=True)
# Reload the set statuses
BrickSetStatusList.new(force=True)
# Reload the set storages
BrickSetStorageList.new(force=True)
# Reload the set tags
BrickSetTagList.new(force=True)
# Reload retired sets # Reload retired sets
BrickRetiredList(force=True) BrickRetiredList(force=True)
# Reload themes # Reload themes
BrickThemeList(force=True) BrickThemeList(force=True)
# Reload the wish owners
BrickWishOwnerList.new(force=True)
except Exception: except Exception:
pass pass

View File

@ -1,19 +1,23 @@
from datetime import datetime
import logging import logging
import traceback import traceback
from typing import Any, Self from typing import Any, Self, TYPE_CHECKING
from uuid import uuid4 from uuid import uuid4
from flask import url_for from flask import current_app, url_for
from .exceptions import DatabaseException, NotFoundException from .exceptions import NotFoundException, DatabaseException, ErrorException
from .minifigure_list import BrickMinifigureList from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList from .part_list import BrickPartList
from .rebrickable_minifigures import RebrickableMinifigures
from .rebrickable_parts import RebrickableParts
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
from .set_checkbox import BrickSetCheckbox from .set_owner_list import BrickSetOwnerList
from .set_checkbox_list import BrickSetCheckboxList from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
from .sql import BrickSQL from .sql import BrickSQL
if TYPE_CHECKING:
from .socket import BrickSocket
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,6 +28,8 @@ class BrickSet(RebrickableSet):
select_query: str = 'set/select/full' select_query: str = 'set/select/full'
light_query: str = 'set/select/light' light_query: str = 'set/select/light'
insert_query: str = 'set/insert' insert_query: str = 'set/insert'
update_purchase_date_query: str = 'set/update/purchase_date'
update_purchase_price_query: str = 'set/update/purchase_price'
# Delete a set # Delete a set
def delete(self, /) -> None: def delete(self, /) -> None:
@ -33,81 +39,153 @@ class BrickSet(RebrickableSet):
) )
# Import a set into the database # Import a set into the database
def download(self, data: dict[str, Any], /) -> None: def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> bool:
# Load the set # Load the set
if not self.load(data, from_download=True): if not self.load(socket, data, from_download=True):
return return False
try: try:
# Insert into the database # Insert into the database
self.socket.auto_progress( socket.auto_progress(
message='Set {number}: inserting into database'.format( message='Set {set}: inserting into database'.format(
number=self.fields.set set=self.fields.set
), ),
increment_total=True, increment_total=True,
) )
# Grabbing the refresh flag
refresh: bool = bool(data.get('refresh', False))
# Generate an UUID for self # Generate an UUID for self
self.fields.id = str(uuid4()) self.fields.id = str(uuid4())
if not refresh:
# Save the storage
storage = BrickSetStorageList.get(
data.get('storage', ''),
allow_none=True
)
self.fields.storage = storage.fields.id
# Save the purchase location
purchase_location = BrickSetPurchaseLocationList.get(
data.get('purchase_location', ''),
allow_none=True
)
self.fields.purchase_location = purchase_location.fields.id
# Insert into database # Insert into database
self.insert(commit=False) self.insert(commit=False)
# Execute the parent download method # Save the owners
self.download_rebrickable() owners: list[str] = list(data.get('owners', []))
for id in owners:
owner = BrickSetOwnerList.get(id)
owner.update_set_state(self, state=True)
# Save the tags
tags: list[str] = list(data.get('tags', []))
for id in tags:
tag = BrickSetTagList.get(id)
tag.update_set_state(self, state=True)
# Insert the rebrickable set into database
self.insert_rebrickable()
# Load the inventory # Load the inventory
RebrickableParts(self.socket, self).download() if not BrickPartList.download(socket, self, refresh=refresh):
return False
# Load the minifigures # Load the minifigures
RebrickableMinifigures(self.socket, self).download() if not BrickMinifigureList.download(socket, self, refresh=refresh):
return False
# Commit the transaction to the database # Commit the transaction to the database
self.socket.auto_progress( socket.auto_progress(
message='Set {number}: writing to the database'.format( message='Set {set}: writing to the database'.format(
number=self.fields.set set=self.fields.set
), ),
increment_total=True, increment_total=True,
) )
BrickSQL().commit() BrickSQL().commit()
if refresh:
# Info # Info
logger.info('Set {number}: imported (id: {id})'.format( logger.info('Set {set}: imported (id: {id})'.format(
number=self.fields.set, set=self.fields.set,
id=self.fields.id, id=self.fields.id,
)) ))
# Complete # Complete
self.socket.complete( socket.complete(
message='Set {number}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501 message='Set {set}: refreshed'.format( # noqa: E501
number=self.fields.set, set=self.fields.set,
),
download=True
)
else:
# Info
logger.info('Set {set}: refreshed'.format(
set=self.fields.set,
))
# Complete
socket.complete(
message='Set {set}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
set=self.fields.set,
url=self.url() url=self.url()
), ),
download=True download=True
) )
except Exception as e: except Exception as e:
self.socket.fail( socket.fail(
message='Error while importing set {number}: {error}'.format( message='Error while importing set {set}: {error}'.format(
number=self.fields.set, set=self.fields.set,
error=e, error=e,
) )
) )
logger.debug(traceback.format_exc()) logger.debug(traceback.format_exc())
# Insert a Rebrickable set return False
def insert_rebrickable(self, /) -> None:
self.insert() return True
# Purchase date
def purchase_date(self, /, *, standard: bool = False) -> str:
if self.fields.purchase_date is not None:
time = datetime.fromtimestamp(self.fields.purchase_date)
if standard:
return time.strftime('%Y/%m/%d')
else:
return time.strftime(
current_app.config['PURCHASE_DATE_FORMAT']
)
else:
return ''
# Purchase price with currency
def purchase_price(self, /) -> str:
if self.fields.purchase_price is not None:
return '{price}{currency}'.format(
price=self.fields.purchase_price,
currency=current_app.config['PURCHASE_CURRENCY']
)
else:
return ''
# Minifigures # Minifigures
def minifigures(self, /) -> BrickMinifigureList: def minifigures(self, /) -> BrickMinifigureList:
return BrickMinifigureList().load(self) return BrickMinifigureList().from_set(self)
# Parts # Parts
def parts(self, /) -> BrickPartList: def parts(self, /) -> BrickPartList:
return BrickPartList().load(self) return BrickPartList().list_specific(self)
# Select a light set (with an id) # Select a light set (with an id)
def select_light(self, id: str, /) -> Self: def select_light(self, id: str, /) -> Self:
@ -131,7 +209,9 @@ class BrickSet(RebrickableSet):
# Load from database # Load from database
if not self.select( if not self.select(
statuses=BrickSetCheckboxList().as_columns(solo=True) owners=BrickSetOwnerList.as_columns(),
statuses=BrickSetStatusList.as_columns(all=True),
tags=BrickSetTagList.as_columns(),
): ):
raise NotFoundException( raise NotFoundException(
'Set with ID {id} was not found in the database'.format( 'Set with ID {id} was not found in the database'.format(
@ -141,30 +221,80 @@ class BrickSet(RebrickableSet):
return self return self
# Update a status # Update the purchase date
def update_status( def update_purchase_date(self, json: Any | None, /) -> Any:
self, value = json.get('value', None) # type: ignore
checkbox: BrickSetCheckbox,
status: bool, try:
/ if value == '':
) -> None: value = None
parameters = self.sql_parameters()
parameters['status'] = status if value is not None:
value = datetime.strptime(value, '%Y/%m/%d').timestamp()
except Exception:
raise ErrorException('{value} is not a date'.format(
value=value,
))
self.fields.purchase_date = value
# Update the status
rows, _ = BrickSQL().execute_and_commit( rows, _ = BrickSQL().execute_and_commit(
'set/update/status', self.update_purchase_date_query,
parameters=parameters, parameters=self.sql_parameters()
name=checkbox.as_column(),
) )
if rows != 1: if rows != 1:
raise DatabaseException('Could not update the status "{status}" for set {number} ({id})'.format( # noqa: E501 raise DatabaseException('Could not update the purchase date for set {set} ({id})'.format( # noqa: E501
status=checkbox.fields.name, set=self.fields.set,
number=self.fields.set,
id=self.fields.id, id=self.fields.id,
)) ))
# Info
logger.info('Purchase date changed to "{value}" for set {set} ({id})'.format( # noqa: E501
value=value,
set=self.fields.set,
id=self.fields.id,
))
return value
# Update the purchase price
def update_purchase_price(self, json: Any | None, /) -> Any:
value = json.get('value', None) # type: ignore
try:
if value == '':
value = None
if value is not None:
value = float(value)
except Exception:
raise ErrorException('{value} is not a number or empty'.format(
value=value,
))
self.fields.purchase_price = value
rows, _ = BrickSQL().execute_and_commit(
self.update_purchase_price_query,
parameters=self.sql_parameters()
)
if rows != 1:
raise DatabaseException('Could not update the purchase price for set {set} ({id})'.format( # noqa: E501
set=self.fields.set,
id=self.fields.id,
))
# Info
logger.info('Purchase price changed to "{value}" for set {set} ({id})'.format( # noqa: E501
value=value,
set=self.fields.set,
id=self.fields.id,
))
return value
# Self url # Self url
def url(self, /) -> str: def url(self, /) -> str:
return url_for('set.details', id=self.fields.id) return url_for('set.details', id=self.fields.id)
@ -179,7 +309,10 @@ class BrickSet(RebrickableSet):
# Compute the url for the set instructions # Compute the url for the set instructions
def url_for_instructions(self, /) -> str: def url_for_instructions(self, /) -> str:
if len(self.instructions): if (
not current_app.config['HIDE_SET_INSTRUCTIONS'] and
len(self.instructions)
):
return url_for( return url_for(
'set.details', 'set.details',
id=self.fields.id, id=self.fields.id,
@ -187,3 +320,22 @@ class BrickSet(RebrickableSet):
) )
else: else:
return '' return ''
# Compute the url for the refresh button
def url_for_refresh(self, /) -> str:
return url_for('set.refresh', id=self.fields.id)
# Compute the url for the set storage
def url_for_storage(self, /) -> str:
if self.fields.storage is not None:
return url_for('storage.details', id=self.fields.storage)
else:
return ''
# Update purchase date url
def url_for_purchase_date(self, /) -> str:
return url_for('set.update_purchase_date', id=self.fields.id)
# Update purchase price url
def url_for_purchase_price(self, /) -> str:
return url_for('set.update_purchase_price', id=self.fields.id)

View File

@ -1,142 +0,0 @@
from sqlite3 import Row
from typing import Any, Self, Tuple
from uuid import uuid4
from flask import url_for
from .exceptions import DatabaseException, ErrorException, NotFoundException
from .record import BrickRecord
from .sql import BrickSQL
# Lego set checkbox
class BrickSetCheckbox(BrickRecord):
# Queries
select_query: str = 'checkbox/select'
def __init__(
self,
/,
*,
record: Row | dict[str, Any] | None = None,
):
super().__init__()
# Ingest the record if it has one
if record is not None:
self.ingest(record)
# SQL column name
def as_column(self) -> str:
return 'status_{id}'.format(id=self.fields.id)
# HTML dataset name
def as_dataset(self) -> str:
return '{id}'.format(
id=self.as_column().replace('_', '-')
)
# Delete from database
def delete(self) -> None:
BrickSQL().executescript(
'checkbox/delete',
id=self.fields.id,
)
# Grab data from a form
def from_form(self, form: dict[str, str]) -> Self:
name = form.get('name', None)
grid = form.get('grid', None)
if name is None or name == '':
raise ErrorException('Checkbox name cannot be empty')
# Security: eh.
# Prevent self-ownage with accidental quote escape
self.fields.name = name
self.fields.safe_name = self.fields.name.replace("'", "''")
self.fields.displayed_on_grid = grid == 'on'
return self
# Insert into database
def insert(self, **_) -> Tuple[int, str]:
# Generate an ID for the checkbox (with underscores to make it
# column name friendly)
self.fields.id = str(uuid4()).replace('-', '_')
BrickSQL().executescript(
'checkbox/add',
id=self.fields.id,
name=self.fields.safe_name,
displayed_on_grid=self.fields.displayed_on_grid
)
# To accomodate the parent().insert we have overriden
return 0, ''
# Rename the checkbox
def rename(self, /) -> None:
# Update the name
rows, _ = BrickSQL().execute_and_commit(
'checkbox/update/name',
parameters=self.sql_parameters(),
)
if rows != 1:
raise DatabaseException('Could not update the name for checkbox {name} ({id})'.format( # noqa: E501
name=self.fields.name,
id=self.fields.id,
))
# URL to change the status
def status_url(self, id: str) -> str:
return url_for(
'set.update_status',
id=id,
checkbox_id=self.fields.id
)
# Select a specific checkbox (with an id)
def select_specific(self, id: str, /) -> Self:
# Save the parameters to the fields
self.fields.id = id
# Load from database
if not self.select():
raise NotFoundException(
'Checkbox with ID {id} was not found in the database'.format(
id=self.fields.id,
),
)
return self
# Update a status
def update_status(
self,
name: str,
status: bool,
/
) -> None:
if not hasattr(self.fields, name) or name in ['id', 'name']:
raise NotFoundException('{name} is not a field of a checkbox'.format( # noqa: E501
name=name
))
parameters = self.sql_parameters()
parameters['status'] = status
# Update the status
rows, _ = BrickSQL().execute_and_commit(
'checkbox/update/status',
parameters=parameters,
name=name,
)
if rows != 1:
raise DatabaseException('Could not update the status "{status}" for checkbox {name} ({id})'.format( # noqa: E501
status=name,
name=self.fields.name,
id=self.fields.id,
))

View File

@ -1,74 +0,0 @@
import logging
from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
from .set_checkbox import BrickSetCheckbox
logger = logging.getLogger(__name__)
# Lego sets checkbox list
class BrickSetCheckboxList(BrickRecordList[BrickSetCheckbox]):
checkboxes: dict[str, BrickSetCheckbox]
# Queries
select_query = 'checkbox/list'
def __init__(self, /, *, force: bool = False):
# Load checkboxes only if there is none already loaded
records = getattr(self, 'records', None)
if records is None or force:
# Don't use super()__init__ as it would mask class variables
self.fields = BrickRecordFields()
logger.info('Loading set checkboxes list')
BrickSetCheckboxList.records = []
BrickSetCheckboxList.checkboxes = {}
# Load the checkboxes from the database
for record in self.select():
checkbox = BrickSetCheckbox(record=record)
BrickSetCheckboxList.records.append(checkbox)
BrickSetCheckboxList.checkboxes[checkbox.fields.id] = checkbox
# Return the checkboxes as columns for a select
def as_columns(
self,
/,
*,
solo: bool = False,
table: str = 'bricktracker_set_statuses'
) -> str:
return ', '.join([
'"{table}"."{column}"'.format(
table=table,
column=record.as_column(),
)
for record
in self.records
if solo or record.fields.displayed_on_grid
])
# Grab a specific checkbox
def get(self, id: str, /) -> BrickSetCheckbox:
if id not in self.checkboxes:
raise NotFoundException(
'Checkbox with ID {id} was not found in the database'.format(
id=self.fields.id,
),
)
return self.checkboxes[id]
# Get the list of checkboxes depending on the context
def list(self, /, *, all: bool = False) -> list[BrickSetCheckbox]:
return [
record
for record
in self.records
if all or record.fields.displayed_on_grid
]

View File

@ -1,9 +1,17 @@
from typing import Self from typing import Any, Self, Union
from flask import current_app from flask import current_app
from .record_list import BrickRecordList from .record_list import BrickRecordList
from .set_checkbox_list import BrickSetCheckboxList from .set_owner import BrickSetOwner
from .set_owner_list import BrickSetOwnerList
from .set_purchase_location import BrickSetPurchaseLocation
from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage import BrickSetStorage
from .set_storage_list import BrickSetStorageList
from .set_tag import BrickSetTag
from .set_tag_list import BrickSetTagList
from .set import BrickSet from .set import BrickSet
@ -13,6 +21,8 @@ class BrickSetList(BrickRecordList[BrickSet]):
order: str order: str
# Queries # Queries
damaged_minifigure_query: str = 'set/list/damaged_minifigure'
damaged_part_query: str = 'set/list/damaged_part'
generic_query: str = 'set/list/generic' generic_query: str = 'set/list/generic'
light_query: str = 'set/list/light' light_query: str = 'set/list/light'
missing_minifigure_query: str = 'set/list/missing_minifigure' missing_minifigure_query: str = 'set/list/missing_minifigure'
@ -20,6 +30,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
select_query: str = 'set/list/all' select_query: str = 'set/list/all'
using_minifigure_query: str = 'set/list/using_minifigure' using_minifigure_query: str = 'set/list/using_minifigure'
using_part_query: str = 'set/list/using_part' using_part_query: str = 'set/list/using_part'
using_storage_query: str = 'set/list/using_storage'
def __init__(self, /): def __init__(self, /):
super().__init__() super().__init__()
@ -32,33 +43,29 @@ class BrickSetList(BrickRecordList[BrickSet]):
# All the sets # All the sets
def all(self, /) -> Self: def all(self, /) -> Self:
themes = set()
# Load the sets from the database # Load the sets from the database
for record in self.select( self.list(do_theme=True)
order=self.order,
statuses=BrickSetCheckboxList().as_columns()
):
brickset = BrickSet(record=record)
self.records.append(brickset)
themes.add(brickset.theme.name)
# Convert the set into a list and sort it
self.themes = list(themes)
self.themes.sort()
return self return self
# A generic list of the different sets # Sets with a minifigure part damaged
def generic(self, /) -> Self: def damaged_minifigure(self, figure: str, /) -> Self:
for record in self.select( # Save the parameters to the fields
override_query=self.generic_query, self.fields.figure = figure
order=self.order
):
brickset = BrickSet(record=record)
self.records.append(brickset) # Load the sets from the database
self.list(override_query=self.damaged_minifigure_query)
return self
# Sets with a part damaged
def damaged_part(self, part: str, color: int, /) -> Self:
# Save the parameters to the fields
self.fields.part = part
self.fields.color = color
# Load the sets from the database
self.list(override_query=self.damaged_part_query)
return self return self
@ -70,103 +77,116 @@ class BrickSetList(BrickRecordList[BrickSet]):
else: else:
order = '"bricktracker_sets"."rowid" DESC' order = '"bricktracker_sets"."rowid" DESC'
for record in self.select( self.list(order=order, limit=limit)
order=order,
limit=limit,
statuses=BrickSetCheckboxList().as_columns()
):
brickset = BrickSet(record=record)
self.records.append(brickset)
return self return self
# Sets missing a minifigure # Base set list
def missing_minifigure( def list(
self, self,
fig_num: str, /,
/ *,
) -> Self: override_query: str | None = None,
# Save the parameters to the fields order: str | None = None,
self.fields.fig_num = fig_num limit: int | None = None,
do_theme: bool = False,
**context: Any,
) -> None:
themes = set()
if order is None:
order = self.order
# Load the sets from the database # Load the sets from the database
for record in self.select( for record in super().select(
override_query=self.missing_minifigure_query, override_query=override_query,
order=self.order order=order,
limit=limit,
owners=BrickSetOwnerList.as_columns(),
statuses=BrickSetStatusList.as_columns(),
tags=BrickSetTagList.as_columns(),
): ):
brickset = BrickSet(record=record) brickset = BrickSet(record=record)
self.records.append(brickset) self.records.append(brickset)
if do_theme:
themes.add(brickset.theme.name)
# Convert the set into a list and sort it
if do_theme:
self.themes = list(themes)
self.themes.sort()
# Sets missing a minifigure part
def missing_minifigure(self, figure: str, /) -> Self:
# Save the parameters to the fields
self.fields.figure = figure
# Load the sets from the database
self.list(override_query=self.missing_minifigure_query)
return self return self
# Sets missing a part # Sets missing a part
def missing_part( def missing_part(self, part: str, color: int, /) -> Self:
self,
part_num: str,
color_id: int,
/,
*,
element_id: int | None = None,
) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.fields.part_num = part_num self.fields.part = part
self.fields.color_id = color_id self.fields.color = color
self.fields.element_id = element_id
# Load the sets from the database # Load the sets from the database
for record in self.select( self.list(override_query=self.missing_part_query)
override_query=self.missing_part_query,
order=self.order
):
brickset = BrickSet(record=record)
self.records.append(brickset)
return self return self
# Sets using a minifigure # Sets using a minifigure
def using_minifigure( def using_minifigure(self, figure: str, /) -> Self:
self,
fig_num: str,
/
) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.fields.fig_num = fig_num self.fields.figure = figure
# Load the sets from the database # Load the sets from the database
for record in self.select( self.list(override_query=self.using_minifigure_query)
override_query=self.using_minifigure_query,
order=self.order
):
brickset = BrickSet(record=record)
self.records.append(brickset)
return self return self
# Sets using a part # Sets using a part
def using_part( def using_part(self, part: str, color: int, /) -> Self:
self,
part_num: str,
color_id: int,
/,
*,
element_id: int | None = None,
) -> Self:
# Save the parameters to the fields # Save the parameters to the fields
self.fields.part_num = part_num self.fields.part = part
self.fields.color_id = color_id self.fields.color = color
self.fields.element_id = element_id
# Load the sets from the database # Load the sets from the database
for record in self.select( self.list(override_query=self.using_part_query)
override_query=self.using_part_query,
order=self.order
):
brickset = BrickSet(record=record)
self.records.append(brickset)
return self return self
# Sets using a storage
def using_storage(self, storage: BrickSetStorage, /) -> Self:
# Save the parameters to the fields
self.fields.storage = storage.fields.id
# Load the sets from the database
self.list(override_query=self.using_storage_query)
return self
# Helper to build the metadata lists
def set_metadata_lists(
as_class: bool = False
) -> dict[
str,
Union[
list[BrickSetOwner],
list[BrickSetPurchaseLocation],
BrickSetPurchaseLocation,
list[BrickSetStorage],
BrickSetStorageList,
list[BrickSetTag]
]
]:
return {
'brickset_owners': BrickSetOwnerList.list(),
'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501
'brickset_storages': BrickSetStorageList.list(as_class=as_class),
'brickset_tags': BrickSetTagList.list(),
}

16
bricktracker/set_owner.py Normal file
View File

@ -0,0 +1,16 @@
from .metadata import BrickMetadata
# Lego set owner metadata
class BrickSetOwner(BrickMetadata):
kind: str = 'owner'
# Set state endpoint
set_state_endpoint: str = 'set.update_owner'
# Queries
delete_query: str = 'set/metadata/owner/delete'
insert_query: str = 'set/metadata/owner/insert'
select_query: str = 'set/metadata/owner/select'
update_field_query: str = 'set/metadata/owner/update/field'
update_set_state_query: str = 'set/metadata/owner/update/state'

View File

@ -0,0 +1,21 @@
from typing import Self
from .metadata_list import BrickMetadataList
from .set_owner import BrickSetOwner
# Lego sets owner list
class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]):
kind: str = 'set owners'
# Database
table: str = 'bricktracker_set_owners'
order: str = '"bricktracker_metadata_owners"."name"'
# Queries
select_query = 'set/metadata/owner/list'
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetOwner, force=force)

View File

@ -0,0 +1,13 @@
from .metadata import BrickMetadata
# Lego set purchase location metadata
class BrickSetPurchaseLocation(BrickMetadata):
kind: str = 'purchase location'
# Queries
delete_query: str = 'set/metadata/purchase_location/delete'
insert_query: str = 'set/metadata/purchase_location/insert'
select_query: str = 'set/metadata/purchase_location/select'
update_field_query: str = 'set/metadata/purchase_location/update/field'
update_set_value_query: str = 'set/metadata/purchase_location/update/value'

View File

@ -0,0 +1,42 @@
from typing import Self
from flask import current_app
from .metadata_list import BrickMetadataList
from .set_purchase_location import BrickSetPurchaseLocation
# Lego sets purchase location list
class BrickSetPurchaseLocationList(
BrickMetadataList[BrickSetPurchaseLocation]
):
kind: str = 'set purchase locations'
# Order
order: str = '"bricktracker_metadata_purchase_locations"."name"'
# Queries
select_query: str = 'set/metadata/purchase_location/list'
all_query: str = 'set/metadata/purchase_location/all'
# Set value endpoint
set_value_endpoint: str = 'set.update_purchase_location'
# Load all purchase locations
@classmethod
def all(cls, /) -> Self:
new = cls.new()
new.override()
for record in new.select(
override_query=cls.all_query,
order=current_app.config['PURCHASE_LOCATION_DEFAULT_ORDER']
):
new.records.append(new.model(record=record))
return new
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetPurchaseLocation, force=force)

View File

@ -0,0 +1,34 @@
from typing import Self
from .metadata import BrickMetadata
# Lego set status metadata
class BrickSetStatus(BrickMetadata):
kind: str = 'status'
# Set state endpoint
set_state_endpoint: str = 'set.update_status'
# Queries
delete_query: str = 'set/metadata/status/delete'
insert_query: str = 'set/metadata/status/insert'
select_query: str = 'set/metadata/status/select'
update_field_query: str = 'set/metadata/status/update/field'
update_set_state_query: str = 'set/metadata/status/update/state'
# Grab data from a form
def from_form(self, form: dict[str, str], /) -> Self:
super().from_form(form)
grid = form.get('grid', None)
self.fields.displayed_on_grid = grid == 'on'
return self
# Insert into database
def insert(self, /, **_) -> None:
super().insert(
displayed_on_grid=self.fields.displayed_on_grid
)

View File

@ -0,0 +1,30 @@
from typing import Self
from .metadata_list import BrickMetadataList
from .set_status import BrickSetStatus
# Lego sets status list
class BrickSetStatusList(BrickMetadataList[BrickSetStatus]):
kind: str = 'set statuses'
# Database
table: str = 'bricktracker_set_statuses'
order: str = '"bricktracker_metadata_statuses"."name"'
# Queries
select_query = 'set/metadata/status/list'
# Filter the list of set status
def filter(self, all: bool = False) -> list[BrickSetStatus]:
return [
record
for record
in self.records
if all or record.fields.displayed_on_grid
]
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetStatus, force=force)

View File

@ -0,0 +1,22 @@
from .metadata import BrickMetadata
from flask import url_for
# Lego set storage metadata
class BrickSetStorage(BrickMetadata):
kind: str = 'storage'
# Queries
delete_query: str = 'set/metadata/storage/delete'
insert_query: str = 'set/metadata/storage/insert'
select_query: str = 'set/metadata/storage/select'
update_field_query: str = 'set/metadata/storage/update/field'
update_set_value_query: str = 'set/metadata/storage/update/value'
# Self url
def url(self, /) -> str:
return url_for(
'storage.details',
id=self.fields.id,
)

View File

@ -0,0 +1,40 @@
from typing import Self
from flask import current_app
from .metadata_list import BrickMetadataList
from .set_storage import BrickSetStorage
# Lego sets storage list
class BrickSetStorageList(BrickMetadataList[BrickSetStorage]):
kind: str = 'set storages'
# Order
order: str = '"bricktracker_metadata_storages"."name"'
# Queries
select_query: str = 'set/metadata/storage/list'
all_query: str = 'set/metadata/storage/all'
# Set value endpoint
set_value_endpoint: str = 'set.update_storage'
# Load all storages
@classmethod
def all(cls, /) -> Self:
new = cls.new()
new.override()
for record in new.select(
override_query=cls.all_query,
order=current_app.config['STORAGE_DEFAULT_ORDER']
):
new.records.append(new.model(record=record))
return new
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetStorage, force=force)

16
bricktracker/set_tag.py Normal file
View File

@ -0,0 +1,16 @@
from .metadata import BrickMetadata
# Lego set tag metadata
class BrickSetTag(BrickMetadata):
kind: str = 'tag'
# Set state endpoint
set_state_endpoint: str = 'set.update_tag'
# Queries
delete_query: str = 'set/metadata/tag/delete'
insert_query: str = 'set/metadata/tag/insert'
select_query: str = 'set/metadata/tag/select'
update_field_query: str = 'set/metadata/tag/update/field'
update_set_state_query: str = 'set/metadata/tag/update/state'

View File

@ -0,0 +1,21 @@
from typing import Self
from .metadata_list import BrickMetadataList
from .set_tag import BrickSetTag
# Lego sets tag list
class BrickSetTagList(BrickMetadataList[BrickSetTag]):
kind: str = 'set tags'
# Database
table: str = 'bricktracker_set_tags'
order: str = '"bricktracker_metadata_tags"."name"'
# Queries
select_query: str = 'set/metadata/tag/list'
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetTag, force=force)

View File

@ -1,14 +1,13 @@
import logging import logging
from typing import Any, Final, Tuple from typing import Any, Final, Tuple
from flask import copy_current_request_context, Flask, request from flask import Flask, request
from flask_socketio import SocketIO from flask_socketio import SocketIO
from .configuration_list import BrickConfigurationList
from .instructions import BrickInstructions from .instructions import BrickInstructions
from .instructions_list import BrickInstructionsList from .instructions_list import BrickInstructionsList
from .login import LoginManager
from .set import BrickSet from .set import BrickSet
from .socket_decorator import authenticated_socket, rebrickable_socket
from .sql import close as sql_close from .sql import close as sql_close
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -87,12 +86,8 @@ class BrickSocket(object):
self.disconnected() self.disconnected()
@self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501 @self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501
@authenticated_socket(self)
def download_instructions(data: dict[str, Any], /) -> None: def download_instructions(data: dict[str, Any], /) -> None:
# Needs to be authenticated
if LoginManager.is_not_authenticated():
self.fail(message='You need to be authenticated')
return
instructions = BrickInstructions( instructions = BrickInstructions(
'{name}.pdf'.format(name=data.get('alt', '')), '{name}.pdf'.format(name=data.get('alt', '')),
socket=self socket=self
@ -107,71 +102,28 @@ class BrickSocket(object):
except Exception: except Exception:
pass pass
# Start it in a thread if requested
if self.threaded:
@copy_current_request_context
def do_download() -> None:
instructions.download(path)
BrickInstructionsList(force=True)
self.socket.start_background_task(do_download)
else:
instructions.download(path) instructions.download(path)
BrickInstructionsList(force=True) BrickInstructionsList(force=True)
@self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace) @self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
@rebrickable_socket(self)
def import_set(data: dict[str, Any], /) -> None: def import_set(data: dict[str, Any], /) -> None:
# Needs to be authenticated logger.debug('Socket: IMPORT_SET={data} (from: {fr})'.format(
if LoginManager.is_not_authenticated(): data=data,
self.fail(message='You need to be authenticated') fr=request.sid, # type: ignore
return ))
# Needs the Rebrickable API key BrickSet().download(self, data)
try:
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
except Exception as e:
self.fail(message=str(e))
return
brickset = BrickSet(socket=self)
# Start it in a thread if requested
if self.threaded:
@copy_current_request_context
def do_download() -> None:
brickset.download(data)
self.socket.start_background_task(do_download)
else:
brickset.download(data)
@self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace) @self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace)
def load_set(data: dict[str, Any], /) -> None: def load_set(data: dict[str, Any], /) -> None:
# Needs to be authenticated logger.debug('Socket: LOAD_SET={data} (from: {fr})'.format(
if LoginManager.is_not_authenticated(): data=data,
self.fail(message='You need to be authenticated') fr=request.sid, # type: ignore
return ))
# Needs the Rebrickable API key BrickSet().load(self, data)
try:
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
except Exception as e:
self.fail(message=str(e))
return
brickset = BrickSet(socket=self)
# Start it in a thread if requested
if self.threaded:
@copy_current_request_context
def do_load() -> None:
brickset.load(data)
self.socket.start_background_task(do_load)
else:
brickset.load(data)
# Update the progress auto-incrementing # Update the progress auto-incrementing
def auto_progress( def auto_progress(

View File

@ -0,0 +1,93 @@
from functools import wraps
from threading import Thread
from typing import Callable, ParamSpec, TYPE_CHECKING, Union
from flask import copy_current_request_context
from .configuration_list import BrickConfigurationList
from .login import LoginManager
if TYPE_CHECKING:
from .socket import BrickSocket
# What a threaded function can return (None or Thread)
SocketReturn = Union[None, Thread]
# Threaded signature (*arg, **kwargs -> (None or Thread)
P = ParamSpec('P')
SocketCallable = Callable[P, SocketReturn]
# Fail if not authenticated
def authenticated_socket(
self: 'BrickSocket',
/,
*,
threaded: bool = True,
) -> Callable[[SocketCallable], SocketCallable]:
def outer(function: SocketCallable, /) -> SocketCallable:
@wraps(function)
def wrapper(*args, **kwargs) -> SocketReturn:
# Needs to be authenticated
if LoginManager.is_not_authenticated():
self.fail(message='You need to be authenticated')
return
# Apply threading
if threaded:
return threaded_socket(self)(function)(*args, **kwargs)
else:
return function(*args, **kwargs)
return wrapper
return outer
# Fail if not ready for Rebrickable (authenticated, API key)
# Automatically makes it threaded
def rebrickable_socket(
self: 'BrickSocket',
/,
*,
threaded: bool = True,
) -> Callable[[SocketCallable], SocketCallable]:
def outer(function: SocketCallable, /) -> SocketCallable:
@wraps(function)
# Automatically authenticated
@authenticated_socket(self, threaded=False)
def wrapper(*args, **kwargs) -> SocketReturn:
# Needs the Rebrickable API key
try:
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
except Exception as e:
self.fail(message=str(e))
return
# Apply threading
if threaded:
return threaded_socket(self)(function)(*args, **kwargs)
else:
return function(*args, **kwargs)
return wrapper
return outer
# Start the function in a thread if the socket is threaded
def threaded_socket(
self: 'BrickSocket',
/
) -> Callable[[SocketCallable], SocketCallable]:
def outer(function: SocketCallable, /) -> SocketCallable:
@wraps(function)
def wrapper(*args, **kwargs) -> SocketReturn:
# Start it in a thread if requested
if self.threaded:
@copy_current_request_context
def do_function() -> None:
function(*args, **kwargs)
return self.socket.start_background_task(do_function)
else:
return function(*args, **kwargs)
return wrapper
return outer

View File

@ -1,3 +1,4 @@
from importlib import import_module
import logging import logging
import os import os
import sqlite3 import sqlite3
@ -301,7 +302,37 @@ class BrickSQL(object):
version=pending.version) version=pending.version)
) )
self.executescript(pending.get_query()) # Load context from the migrations if it exists
# It looks for a file in migrations/ named after the SQL file
# and containing one function named migration_xxxx, also named
# after the SQL file, returning a context dict.
#
# For instance:
# - sql/migrations/0007.sql
# - migrations/0007.py
# - def migration_0007(BrickSQL) -> dict[str, Any]
try:
module = import_module(
'.migrations.{name}'.format(
name=pending.name
),
package='bricktracker'
)
except Exception:
module = None
# If a module has been loaded, we need to fail if an error
# occured while executing the migration function
if module is not None:
function = getattr(module, 'migration_{name}'.format(
name=pending.name
))
context: dict[str, Any] = function(self)
else:
context: dict[str, Any] = {}
self.executescript(pending.get_query(), **context)
self.execute('schema/set_version', version=pending.version) self.execute('schema/set_version', version=pending.version)
# Tells whether the database needs upgrade # Tells whether the database needs upgrade

View File

@ -1,7 +0,0 @@
SELECT
"bricktracker_set_checkboxes"."id",
"bricktracker_set_checkboxes"."name",
"bricktracker_set_checkboxes"."displayed_on_grid"
FROM "bricktracker_set_checkboxes"
{% block where %}{% endblock %}

View File

@ -1,9 +0,0 @@
BEGIN TRANSACTION;
ALTER TABLE "bricktracker_set_statuses"
DROP COLUMN "status_{{ id }}";
DELETE FROM "bricktracker_set_checkboxes"
WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM '{{ id }}';
COMMIT;

View File

@ -1 +0,0 @@
{% extends 'checkbox/base.sql' %}

View File

@ -1,5 +0,0 @@
{% extends 'checkbox/base.sql' %}
{% block where %}
WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id
{% endblock %}

View File

@ -1,3 +0,0 @@
UPDATE "bricktracker_set_checkboxes"
SET "name" = :safe_name
WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id

View File

@ -1,3 +0,0 @@
UPDATE "bricktracker_set_checkboxes"
SET "{{name}}" = :status
WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id

View File

@ -4,7 +4,7 @@ PRAGMA foreign_keys = ON;
BEGIN TRANSACTION; BEGIN TRANSACTION;
-- Create a Bricktable set table: with their unique IDs, and a reference to the Rebrickable set -- Create a Bricktracker set table: with their unique IDs, and a reference to the Rebrickable set
CREATE TABLE "bricktracker_sets" ( CREATE TABLE "bricktracker_sets" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"rebrickable_set" TEXT NOT NULL, "rebrickable_set" TEXT NOT NULL,

View File

@ -0,0 +1,74 @@
-- description: Renaming various complicated field names to something simpler, and add a bunch of extra fields for later
PRAGMA foreign_keys = ON;
BEGIN TRANSACTION;
-- Rename sets table
ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old";
-- Create a Bricktracker metadata storage table for later
CREATE TABLE "bricktracker_metadata_storages" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
-- Create a Bricktracker metadata purchase location table for later
CREATE TABLE "bricktracker_metadata_purchase_locations" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
-- Re-Create a Bricktracker set table with the simplified name
CREATE TABLE "bricktracker_sets" (
"id" TEXT NOT NULL,
"set" TEXT NOT NULL,
"description" TEXT,
"storage" TEXT, -- Storage bin location
"purchase_date" REAL, -- Purchase data
"purchase_location" TEXT, -- Purchase location
"purchase_price" REAL, -- Purchase price
PRIMARY KEY("id"),
FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set"),
FOREIGN KEY("storage") REFERENCES "bricktracker_metadata_storages"("id"),
FOREIGN KEY("purchase_location") REFERENCES "bricktracker_metadata_purchase_locations"("id")
);
-- Insert existing sets into the new table
INSERT INTO "bricktracker_sets" (
"id",
"set"
)
SELECT
"bricktracker_sets_old"."id",
"bricktracker_sets_old"."rebrickable_set"
FROM "bricktracker_sets_old";
-- Rename status table
ALTER TABLE "bricktracker_set_statuses" RENAME TO "bricktracker_set_statuses_old";
-- Re-create a table for the status of each checkbox
CREATE TABLE "bricktracker_set_statuses" (
"id" TEXT NOT NULL,
{% if structure %}{{ structure }},{% endif %}
PRIMARY KEY("id"),
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
);
-- Insert existing status into the new table
INSERT INTO "bricktracker_set_statuses" (
{% if targets %}{{ targets }},{% endif %}
"id"
)
SELECT
{% if sources %}{{ sources }},{% endif %}
"bricktracker_set_statuses_old"."bricktracker_set_id"
FROM "bricktracker_set_statuses_old";
-- Delete the original tables
DROP TABLE "bricktracker_set_statuses_old";
DROP TABLE "bricktracker_sets_old";
COMMIT;

View File

@ -0,0 +1,30 @@
-- description: Creation of the deduplicated table of Rebrickable minifigures
BEGIN TRANSACTION;
-- Create a Rebrickable minifigures table: each unique minifigure imported from Rebrickable
CREATE TABLE "rebrickable_minifigures" (
"figure" TEXT NOT NULL,
"number" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"image" TEXT,
PRIMARY KEY("figure")
);
-- Insert existing sets into the new table
INSERT INTO "rebrickable_minifigures" (
"figure",
"number",
"name",
"image"
)
SELECT
"minifigures"."fig_num",
CAST(SUBSTR("minifigures"."fig_num", 5) AS INTEGER),
"minifigures"."name",
"minifigures"."set_img_url"
FROM "minifigures"
GROUP BY
"minifigures"."fig_num";
COMMIT;

View File

@ -0,0 +1,32 @@
-- description: Migrate the Bricktracker minifigures
PRAGMA foreign_keys = ON;
BEGIN TRANSACTION;
-- Create a Bricktracker minifigures table: an amount of minifigures linked to a Bricktracker set
CREATE TABLE "bricktracker_minifigures" (
"id" TEXT NOT NULL,
"figure" TEXT NOT NULL,
"quantity" INTEGER NOT NULL,
PRIMARY KEY("id", "figure"),
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"),
FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure")
);
-- Insert existing sets into the new table
INSERT INTO "bricktracker_minifigures" (
"id",
"figure",
"quantity"
)
SELECT
"minifigures"."u_id",
"minifigures"."fig_num",
"minifigures"."quantity"
FROM "minifigures";
-- Rename the original table (don't delete it yet?)
ALTER TABLE "minifigures" RENAME TO "minifigures_old";
COMMIT;

View File

@ -0,0 +1,42 @@
-- description: Creation of the deduplicated table of Rebrickable parts, and add a bunch of extra fields for later
BEGIN TRANSACTION;
-- Create a Rebrickable parts table: each unique part imported from Rebrickable
CREATE TABLE "rebrickable_parts" (
"part" TEXT NOT NULL,
"color_id" INTEGER NOT NULL,
"color_name" TEXT NOT NULL,
"color_rgb" TEXT, -- can be NULL because it was not saved before
"color_transparent" BOOLEAN, -- can be NULL because it was not saved before
"name" TEXT NOT NULL,
"category" INTEGER, -- can be NULL because it was not saved before
"image" TEXT,
"image_id" TEXT,
"url" TEXT, -- can be NULL because it was not saved before
"print" INTEGER, -- can be NULL, was not saved before
PRIMARY KEY("part", "color_id")
);
-- Insert existing parts into the new table
INSERT INTO "rebrickable_parts" (
"part",
"color_id",
"color_name",
"name",
"image",
"image_id"
)
SELECT
"inventory"."part_num",
"inventory"."color_id",
"inventory"."color_name",
"inventory"."name",
"inventory"."part_img_url",
"inventory"."part_img_url_id"
FROM "inventory"
GROUP BY
"inventory"."part_num",
"inventory"."color_id";
COMMIT;

View File

@ -0,0 +1,60 @@
-- description: Migrate the Bricktracker parts (and missing parts), and add a bunch of extra fields for later
PRAGMA foreign_keys = ON;
BEGIN TRANSACTION;
-- Create a Bricktracker parts table: an amount of parts linked to a Bricktracker set
CREATE TABLE "bricktracker_parts" (
"id" TEXT NOT NULL,
"figure" TEXT,
"part" TEXT NOT NULL,
"color" INTEGER NOT NULL,
"spare" BOOLEAN NOT NULL,
"quantity" INTEGER NOT NULL,
"element" INTEGER,
"rebrickable_inventory" INTEGER NOT NULL,
"missing" INTEGER NOT NULL DEFAULT 0,
"damaged" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id", "figure", "part", "color", "spare"),
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"),
FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure"),
FOREIGN KEY("part", "color") REFERENCES "rebrickable_parts"("part", "color_id")
);
-- Insert existing parts into the new table
INSERT INTO "bricktracker_parts" (
"id",
"figure",
"part",
"color",
"spare",
"quantity",
"element",
"rebrickable_inventory",
"missing"
)
SELECT
"inventory"."u_id",
CASE WHEN SUBSTR("inventory"."set_num", 0, 5) = 'fig-' THEN "inventory"."set_num" ELSE NULL END,
"inventory"."part_num",
"inventory"."color_id",
"inventory"."is_spare",
"inventory"."quantity",
"inventory"."element_id",
"inventory"."id",
IFNULL("missing"."quantity", 0)
FROM "inventory"
LEFT JOIN "missing"
ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num"
AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id"
AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num"
AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id";
-- Rename the original table (don't delete it yet?)
ALTER TABLE "inventory" RENAME TO "inventory_old";
ALTER TABLE "missing" RENAME TO "missing_old";
COMMIT;

View File

@ -0,0 +1,7 @@
-- description: Rename checkboxes to status metadata
BEGIN TRANSACTION;
ALTER TABLE "bricktracker_set_checkboxes" RENAME TO "bricktracker_metadata_statuses";
COMMIT;

View File

@ -0,0 +1,26 @@
-- description: Add set owners
BEGIN TRANSACTION;
-- Create a table to define each set owners: an id and a name
CREATE TABLE "bricktracker_metadata_owners" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
-- Create a table for the set owners
CREATE TABLE "bricktracker_set_owners" (
"id" TEXT NOT NULL,
PRIMARY KEY("id"),
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
);
-- Create a table for the wish owners
CREATE TABLE "bricktracker_wish_owners" (
"set" TEXT NOT NULL,
PRIMARY KEY("set"),
FOREIGN KEY("set") REFERENCES "bricktracker_wishes"("set")
);
COMMIT;

View File

@ -0,0 +1,19 @@
-- description: Add set tags
BEGIN TRANSACTION;
-- Create a table to define each set tags: an id and a name
CREATE TABLE "bricktracker_metadata_tags" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
-- Create a table for the set tags
CREATE TABLE "bricktracker_set_tags" (
"id" TEXT NOT NULL,
PRIMARY KEY("id"),
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
);
COMMIT;

View File

@ -0,0 +1,32 @@
-- description: Add number of parts for minifigures
BEGIN TRANSACTION;
-- Add the number_of_parts column to the minifigures
ALTER TABLE "rebrickable_minifigures"
ADD COLUMN "number_of_parts" INTEGER NOT NULL DEFAULT 0;
-- Update the number of parts for each minifigure
UPDATE "rebrickable_minifigures"
SET "number_of_parts" = "parts_sum"."number_of_parts"
FROM (
SELECT
"parts"."figure",
SUM("parts"."quantity") as "number_of_parts"
FROM (
SELECT
"bricktracker_parts"."figure",
"bricktracker_parts"."quantity"
FROM "bricktracker_parts"
WHERE "bricktracker_parts"."figure" IS NOT NULL
GROUP BY
"bricktracker_parts"."figure",
"bricktracker_parts"."part",
"bricktracker_parts"."color",
"bricktracker_parts"."spare"
) "parts"
GROUP BY "parts"."figure"
) "parts_sum"
WHERE "rebrickable_minifigures"."figure" = "parts_sum"."figure";
COMMIT;

View File

@ -1,20 +1,26 @@
SELECT SELECT
"minifigures"."fig_num", "bricktracker_minifigures"."quantity",
"minifigures"."set_num", "rebrickable_minifigures"."figure",
"minifigures"."name", "rebrickable_minifigures"."number",
"minifigures"."quantity", "rebrickable_minifigures"."number_of_parts",
"minifigures"."set_img_url", "rebrickable_minifigures"."name",
"minifigures"."u_id", "rebrickable_minifigures"."image",
{% block total_missing %} {% block total_missing %}
NULL AS "total_missing", -- dummy for order: total_missing NULL AS "total_missing", -- dummy for order: total_missing
{% endblock %} {% endblock %}
{% block total_damaged %}
NULL AS "total_damaged", -- dummy for order: total_damaged
{% endblock %}
{% block total_quantity %} {% block total_quantity %}
NULL AS "total_quantity", -- dummy for order: total_quantity NULL AS "total_quantity", -- dummy for order: total_quantity
{% endblock %} {% endblock %}
{% block total_sets %} {% block total_sets %}
NULL AS "total_sets" -- dummy for order: total_sets NULL AS "total_sets" -- dummy for order: total_sets
{% endblock %} {% endblock %}
FROM "minifigures" FROM "bricktracker_minifigures"
INNER JOIN "rebrickable_minifigures"
ON "bricktracker_minifigures"."figure" IS NOT DISTINCT FROM "rebrickable_minifigures"."figure"
{% block join %}{% endblock %} {% block join %}{% endblock %}

View File

@ -1,15 +1,9 @@
INSERT INTO "minifigures" ( INSERT INTO "bricktracker_minifigures" (
"fig_num", "id",
"set_num", "figure",
"name", "quantity"
"quantity",
"set_img_url",
"u_id"
) VALUES ( ) VALUES (
:fig_num, :id,
:set_num, :figure,
:name, :quantity
:quantity,
:set_img_url,
:u_id
) )

View File

@ -1,34 +1,40 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing_join"."total", 0)) AS "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 %} {% endblock %}
{% block total_quantity %} {% block total_quantity %}
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity", SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
{% endblock %} {% endblock %}
{% block total_sets %} {% block total_sets %}
COUNT("minifigures"."set_num") AS "total_sets" IFNULL(COUNT("bricktracker_minifigures"."id"), 0) AS "total_sets"
{% endblock %} {% endblock %}
{% block join %} {% block join %}
-- LEFT JOIN + SELECT to avoid messing the total -- LEFT JOIN + SELECT to avoid messing the total
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
"missing"."set_num", "bricktracker_parts"."id",
"missing"."u_id", "bricktracker_parts"."figure",
SUM("missing"."quantity") AS total SUM("bricktracker_parts"."missing") AS "total_missing",
FROM "missing" SUM("bricktracker_parts"."damaged") AS "total_damaged"
FROM "bricktracker_parts"
WHERE "bricktracker_parts"."figure" IS NOT NULL
GROUP BY GROUP BY
"missing"."set_num", "bricktracker_parts"."id",
"missing"."u_id" "bricktracker_parts"."figure"
) missing_join ) "problem_join"
ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id" ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "problem_join"."id"
AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num" AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure"
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"minifigures"."fig_num" "rebrickable_minifigures"."figure"
{% endblock %} {% endblock %}

View File

@ -0,0 +1,28 @@
{% extends 'minifigure/base/base.sql' %}
{% block total_damaged %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endblock %}
{% block join %}
LEFT JOIN "bricktracker_parts"
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id"
AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure"
{% endblock %}
{% block where %}
WHERE "rebrickable_minifigures"."figure" IN (
SELECT "bricktracker_parts"."figure"
FROM "bricktracker_parts"
WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "bricktracker_parts"."figure" IS NOT NULL
AND "bricktracker_parts"."damaged" > 0
GROUP BY "bricktracker_parts"."figure"
)
{% endblock %}
{% block group %}
GROUP BY
"rebrickable_minifigures"."figure"
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block where %} {% block where %}
WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :u_id WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id
AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num
{% endblock %} {% endblock %}

View File

@ -1,17 +1,21 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %}
{% block total_damaged %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endblock %} {% endblock %}
{% block join %} {% block join %}
LEFT JOIN "missing" LEFT JOIN "bricktracker_parts"
ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num" ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id"
AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id" AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure"
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"minifigures"."fig_num", "rebrickable_minifigures"."figure",
"minifigures"."u_id" "bricktracker_minifigures"."id"
{% endblock %} {% endblock %}

View File

@ -1,30 +1,28 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %} {% endblock %}
{% block join %} {% block join %}
LEFT JOIN "missing" LEFT JOIN "bricktracker_parts"
ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num" ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id"
AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id" AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure"
{% endblock %} {% endblock %}
{% block where %} {% block where %}
WHERE "minifigures"."fig_num" IN ( WHERE "rebrickable_minifigures"."figure" IN (
SELECT SELECT "bricktracker_parts"."figure"
"missing"."set_num" FROM "bricktracker_parts"
FROM "missing" WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
WHERE "missing"."color_id" IS NOT DISTINCT FROM :color_id AND "bricktracker_parts"."figure" IS NOT NULL
AND "missing"."element_id" IS NOT DISTINCT FROM :element_id AND "bricktracker_parts"."missing" > 0
AND "missing"."part_num" IS NOT DISTINCT FROM :part_num GROUP BY "bricktracker_parts"."figure"
GROUP BY "missing"."set_num"
) )
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"minifigures"."fig_num" "rebrickable_minifigures"."figure"
{% endblock %} {% endblock %}

View File

@ -1,24 +1,21 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block total_quantity %} {% block total_quantity %}
SUM("minifigures"."quantity") AS "total_quantity", SUM("bricktracker_minifigures"."quantity") AS "total_quantity",
{% endblock %} {% endblock %}
{% block where %} {% block where %}
WHERE "minifigures"."fig_num" IN ( WHERE "rebrickable_minifigures"."figure" IN (
SELECT SELECT "bricktracker_parts"."figure"
"inventory"."set_num" FROM "bricktracker_parts"
FROM "inventory" WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
WHERE "inventory"."color_id" IS NOT DISTINCT FROM :color_id AND "bricktracker_parts"."figure" IS NOT NULL
AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id GROUP BY "bricktracker_parts"."figure"
AND "inventory"."part_num" IS NOT DISTINCT FROM :part_num
GROUP BY "inventory"."set_num"
) )
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"minifigures"."fig_num" "rebrickable_minifigures"."figure"
{% endblock %} {% endblock %}

View File

@ -1,38 +1,40 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing_join"."total", 0)) AS "total_missing", IFNULL("problem_join"."total_missing", 0) AS "total_missing",
{% endblock %}
{% block total_damaged %}
IFNULL("problem_join"."total_damaged", 0) AS "total_damaged",
{% endblock %} {% endblock %}
{% block total_quantity %} {% block total_quantity %}
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity", SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
{% endblock %} {% endblock %}
{% block total_sets %} {% block total_sets %}
COUNT("minifigures"."set_num") AS "total_sets" IFNULL(COUNT(DISTINCT "bricktracker_minifigures"."id"), 0) AS "total_sets"
{% endblock %} {% endblock %}
{% block join %} {% block join %}
-- LEFT JOIN + SELECT to avoid messing the total -- LEFT JOIN + SELECT to avoid messing the total
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
"missing"."set_num", "bricktracker_parts"."figure",
"missing"."u_id", SUM("bricktracker_parts"."missing") AS "total_missing",
SUM("missing"."quantity") AS "total" SUM("bricktracker_parts"."damaged") AS "total_damaged"
FROM "missing" FROM "bricktracker_parts"
GROUP BY WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
"missing"."set_num", GROUP BY "bricktracker_parts"."figure"
"missing"."u_id" ) "problem_join"
) "missing_join" ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure"
ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id"
AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num"
{% endblock %} {% endblock %}
{% block where %} {% block where %}
WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"minifigures"."fig_num" "rebrickable_minifigures"."figure"
{% endblock %} {% endblock %}

View File

@ -1,7 +1,6 @@
{% extends 'minifigure/base/select.sql' %} {% extends 'minifigure/base/base.sql' %}
{% block where %} {% block where %}
WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id
AND "minifigures"."u_id" IS NOT DISTINCT FROM :u_id AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num
{% endblock %} {% endblock %}

View File

@ -1,4 +0,0 @@
DELETE FROM "missing"
WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num
AND "missing"."id" IS NOT DISTINCT FROM :id
AND "missing"."u_id" IS NOT DISTINCT FROM :u_id

View File

@ -1,20 +0,0 @@
INSERT INTO "missing" (
"set_num",
"id",
"part_num",
"part_img_url_id",
"color_id",
"quantity",
"element_id",
"u_id"
)
VALUES(
:set_num,
:id,
:part_num,
:part_img_url_id,
:color_id,
:quantity,
:element_id,
:u_id
)

View File

@ -1,5 +0,0 @@
UPDATE "missing"
SET "quantity" = :quantity
WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num
AND "missing"."id" IS NOT DISTINCT FROM :id
AND "missing"."u_id" IS NOT DISTINCT FROM :u_id

View File

@ -0,0 +1,59 @@
SELECT
"bricktracker_parts"."id",
"bricktracker_parts"."figure",
"bricktracker_parts"."part",
"bricktracker_parts"."color",
"bricktracker_parts"."spare",
"bricktracker_parts"."quantity",
"bricktracker_parts"."element",
--"bricktracker_parts"."rebrickable_inventory",
"bricktracker_parts"."missing",
"bricktracker_parts"."damaged",
--"rebrickable_parts"."part",
--"rebrickable_parts"."color_id",
"rebrickable_parts"."color_name",
"rebrickable_parts"."color_rgb",
"rebrickable_parts"."color_transparent",
"rebrickable_parts"."name",
--"rebrickable_parts"."category",
"rebrickable_parts"."image",
"rebrickable_parts"."image_id",
"rebrickable_parts"."url",
"rebrickable_parts"."print",
{% block total_missing %}
NULL AS "total_missing", -- dummy for order: total_missing
{% endblock %}
{% block total_damaged %}
NULL AS "total_damaged", -- dummy for order: total_damaged
{% endblock %}
{% block total_quantity %}
NULL AS "total_quantity", -- dummy for order: total_quantity
{% endblock %}
{% block total_spare %}
NULL AS "total_spare", -- dummy for order: total_spare
{% endblock %}
{% block total_sets %}
NULL AS "total_sets", -- dummy for order: total_sets
{% endblock %}
{% block total_minifigures %}
NULL AS "total_minifigures" -- dummy for order: total_minifigures
{% endblock %}
FROM "bricktracker_parts"
INNER JOIN "rebrickable_parts"
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
{% block join %}{% endblock %}
{% block where %}{% endblock %}
{% block group %}{% endblock %}
{% if order %}
ORDER BY {{ order }}
{% endif %}
{% if limit %}
LIMIT {{ limit }}
{% endif %}

View File

@ -1,43 +0,0 @@
SELECT
"inventory"."set_num",
"inventory"."id",
"inventory"."part_num",
"inventory"."name",
"inventory"."part_img_url",
"inventory"."part_img_url_id",
"inventory"."color_id",
"inventory"."color_name",
"inventory"."quantity",
"inventory"."is_spare",
"inventory"."element_id",
"inventory"."u_id",
{% block total_missing %}
NULL AS "total_missing", -- dummy for order: total_missing
{% endblock %}
{% block total_quantity %}
NULL AS "total_quantity", -- dummy for order: total_quantity
{% endblock %}
{% block total_spare %}
NULL AS "total_spare", -- dummy for order: total_spare
{% endblock %}
{% block total_sets %}
NULL AS "total_sets", -- dummy for order: total_sets
{% endblock %}
{% block total_minifigures %}
NULL AS "total_minifigures" -- dummy for order: total_minifigures
{% endblock %}
FROM "inventory"
{% block join %}{% endblock %}
{% block where %}{% endblock %}
{% block group %}{% endblock %}
{% if order %}
ORDER BY {{ order }}
{% endif %}
{% if limit %}
LIMIT {{ limit }}
{% endif %}

View File

@ -1,27 +1,19 @@
INSERT INTO inventory ( INSERT INTO "bricktracker_parts" (
"set_num",
"id", "id",
"part_num", "figure",
"name", "part",
"part_img_url", "color",
"part_img_url_id", "spare",
"color_id",
"color_name",
"quantity", "quantity",
"is_spare", "element",
"element_id", "rebrickable_inventory"
"u_id"
) VALUES ( ) VALUES (
:set_num,
:id, :id,
:part_num, :figure,
:name, :part,
:part_img_url, :color,
:part_img_url_id, :spare,
:color_id,
:color_name,
:quantity, :quantity,
:is_spare, :element,
:element_id, :rebrickable_inventory
:u_id
) )

View File

@ -1,43 +1,34 @@
{% extends 'part/base/select.sql' %} {% extends 'part/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %}
{% block total_damaged %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endblock %} {% endblock %}
{% block total_quantity %} {% block total_quantity %}
SUM("inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity", SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
{% endblock %} {% endblock %}
{% block total_sets %} {% block total_sets %}
COUNT(DISTINCT "bricktracker_sets"."id") AS "total_sets", IFNULL(COUNT(DISTINCT "bricktracker_parts"."id"), 0) AS "total_sets",
{% endblock %} {% endblock %}
{% block total_minifigures %} {% block total_minifigures %}
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures" SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
{% endblock %} {% endblock %}
{% block join %} {% block join %}
LEFT JOIN "missing" LEFT JOIN "bricktracker_minifigures"
ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num"
AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
LEFT JOIN "minifigures"
ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
LEFT JOIN "bricktracker_sets"
ON "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"inventory"."part_num", "bricktracker_parts"."part",
"inventory"."name", "bricktracker_parts"."color",
"inventory"."color_id", "bricktracker_parts"."spare"
"inventory"."is_spare",
"inventory"."element_id"
{% endblock %} {% endblock %}

View File

@ -1,28 +1,21 @@
{% extends 'part/base/select.sql' %} {% extends 'part/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %} {% endblock %}
{% block join %} {% block total_damaged %}
LEFT JOIN "missing" SUM("bricktracker_parts"."damaged") AS "total_damaged",
ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num"
AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id"
AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num"
AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id"
AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id"
{% endblock %} {% endblock %}
{% block where %} {% block where %}
WHERE "inventory"."set_num" IS NOT DISTINCT FROM :set_num WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"inventory"."part_num", "bricktracker_parts"."part",
"inventory"."name", "bricktracker_parts"."color",
"inventory"."color_id", "bricktracker_parts"."spare"
"inventory"."is_spare",
"inventory"."element_id"
{% endblock %} {% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'part/base/base.sql' %}
{% block total_missing %}{% endblock %}
{% block total_damaged %}{% endblock %}
{% block where %}
WHERE "rebrickable_parts"."print" IS NOT DISTINCT FROM :print
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "bricktracker_parts"."part" IS DISTINCT FROM :part
{% endblock %}
{% block group %}
GROUP BY
"bricktracker_parts"."part",
"bricktracker_parts"."color"
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends 'part/base/select.sql' %}
{% block total_missing %}
IFNULL("missing"."quantity", 0) AS "total_missing",
{% endblock %}
{% block join %}
LEFT JOIN "missing"
ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num"
AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id"
AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num"
AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
{% endblock %}
{% block where %}
WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id
AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends 'part/base/select.sql' %}
{% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
{% endblock %}
{% block total_sets %}
COUNT("inventory"."u_id") - COUNT("minifigures"."u_id") AS "total_sets",
{% endblock %}
{% block total_minifigures %}
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures"
{% endblock %}
{% block join %}
INNER JOIN "missing"
ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num"
AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id"
AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num"
AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id"
AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id"
AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id"
LEFT JOIN "minifigures"
ON "missing"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
AND "missing"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
{% endblock %}
{% block group %}
GROUP BY
"inventory"."part_num",
"inventory"."name",
"inventory"."color_id",
"inventory"."is_spare",
"inventory"."element_id"
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends 'part/base/base.sql' %}
{% block total_missing %}
SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %}
{% block total_damaged %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endblock %}
{% block total_sets %}
IFNULL(COUNT("bricktracker_parts"."id"), 0) - IFNULL(COUNT("bricktracker_parts"."figure"), 0) AS "total_sets",
{% endblock %}
{% block total_minifigures %}
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
{% endblock %}
{% block join %}
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 %}
WHERE "bricktracker_parts"."missing" > 0
OR "bricktracker_parts"."damaged" > 0
{% endblock %}
{% block group %}
GROUP BY
"bricktracker_parts"."part",
"bricktracker_parts"."color",
"bricktracker_parts"."spare"
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends 'part/base/base.sql' %}
{% block total_missing %}
IFNULL("bricktracker_parts"."missing", 0) AS "total_missing",
{% endblock %}
{% block total_damaged %}
IFNULL("bricktracker_parts"."damaged", 0) AS "total_damaged",
{% endblock %}
{% block where %}
WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'part/base/base.sql' %}
{% block total_missing %}{% endblock %}
{% block total_damaged %}{% endblock %}
{% block where %}
WHERE "bricktracker_parts"."color" IS DISTINCT FROM :color
AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
{% endblock %}
{% block group %}
GROUP BY
"bricktracker_parts"."part",
"bricktracker_parts"."color"
{% endblock %}

View File

@ -1,40 +1,34 @@
{% extends 'part/base/select.sql' %} {% extends 'part/base/base.sql' %}
{% block total_missing %} {% block total_missing %}
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", SUM("bricktracker_parts"."missing") AS "total_missing",
{% endblock %}
{% block total_damaged %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endblock %} {% endblock %}
{% block total_quantity %} {% block total_quantity %}
SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity", SUM((NOT "bricktracker_parts"."spare") * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
{% endblock %} {% endblock %}
{% block total_spare %} {% block total_spare %}
SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_spare", SUM("bricktracker_parts"."spare" * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare",
{% endblock %} {% endblock %}
{% block join %} {% block join %}
LEFT JOIN "missing" LEFT JOIN "bricktracker_minifigures"
ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num"
AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
LEFT JOIN "minifigures"
ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
{% endblock %} {% endblock %}
{% block where %} {% block where %}
WHERE "inventory"."part_num" IS NOT DISTINCT FROM :part_num WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "inventory"."color_id" IS NOT DISTINCT FROM :color_id AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"inventory"."part_num", "bricktracker_parts"."part",
"inventory"."color_id", "bricktracker_parts"."color"
"inventory"."element_id"
{% endblock %} {% endblock %}

View File

@ -1,24 +1,18 @@
{% extends 'part/base/select.sql' %} {% extends 'part/base/base.sql' %}
{% block join %}
LEFT JOIN "missing"
ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num"
AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id"
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
{% endblock %}
{% block where %} {% block where %}
WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id
AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
AND "inventory"."id" IS NOT DISTINCT FROM :id AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare
{% endblock %} {% endblock %}
{% block group %} {% block group %}
GROUP BY GROUP BY
"inventory"."set_num", "bricktracker_parts"."id",
"inventory"."id", "bricktracker_parts"."figure",
"inventory"."part_num", "bricktracker_parts"."part",
"inventory"."color_id", "bricktracker_parts"."color",
"inventory"."element_id", "bricktracker_parts"."spare"
"inventory"."u_id"
{% endblock %} {% endblock %}

View File

@ -0,0 +1,7 @@
UPDATE "bricktracker_parts"
SET "damaged" = :damaged
WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare

View File

@ -0,0 +1,7 @@
UPDATE "bricktracker_parts"
SET "missing" = :missing
WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color
AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare

View File

@ -0,0 +1,20 @@
INSERT OR IGNORE INTO "rebrickable_minifigures" (
"figure",
"number",
"name",
"image",
"number_of_parts"
) VALUES (
:figure,
:number,
:name,
:image,
:number_of_parts
)
ON CONFLICT("figure")
DO UPDATE SET
"number" = :number,
"name" = :name,
"image" = :image,
"number_of_parts" = :number_of_parts
WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure

View File

@ -0,0 +1,6 @@
SELECT
"rebrickable_minifigures"."figure",
"rebrickable_minifigures"."number",
"rebrickable_minifigures"."name",
"rebrickable_minifigures"."image"
FROM "rebrickable_minifigures"

View File

@ -0,0 +1,8 @@
SELECT
"rebrickable_minifigures"."figure",
"rebrickable_minifigures"."number",
"rebrickable_minifigures"."name",
"rebrickable_minifigures"."image"
FROM "rebrickable_minifigures"
WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure

View File

@ -0,0 +1,38 @@
INSERT OR IGNORE INTO "rebrickable_parts" (
"part",
"color_id",
"color_name",
"color_rgb",
"color_transparent",
"name",
"category",
"image",
"image_id",
"url",
"print"
) VALUES (
:part,
:color_id,
:color_name,
:color_rgb,
:color_transparent,
:name,
:category,
:image,
:image_id,
:url,
:print
)
ON CONFLICT("part", "color_id")
DO UPDATE SET
"color_name" = :color_name,
"color_rgb" = :color_rgb,
"color_transparent" = :color_transparent,
"name" = :name,
"category" = :category,
"image" = :image,
"image_id" = :image_id,
"url" = :url,
"print" = :print
WHERE "rebrickable_parts"."part" IS NOT DISTINCT FROM :part
AND "rebrickable_parts"."color_id" IS NOT DISTINCT FROM :color_id

View File

@ -0,0 +1,13 @@
SELECT
"rebrickable_parts"."part",
"rebrickable_parts"."color_id",
"rebrickable_parts"."color_name",
"rebrickable_parts"."color_rgb",
"rebrickable_parts"."color_transparent",
"rebrickable_parts"."name",
"rebrickable_parts"."category",
"rebrickable_parts"."image",
"rebrickable_parts"."image_id",
"rebrickable_parts"."url",
"rebrickable_parts"."print"
FROM "rebrickable_parts"

View File

@ -0,0 +1,16 @@
SELECT
"rebrickable_parts"."part",
"rebrickable_parts"."color_id",
"rebrickable_parts"."color_name",
"rebrickable_parts"."color_rgb",
"rebrickable_parts"."color_transparent",
"rebrickable_parts"."name",
"rebrickable_parts"."category",
"rebrickable_parts"."image",
"rebrickable_parts"."image_id",
"rebrickable_parts"."url",
"rebrickable_parts"."print"
FROM "rebrickable_parts"
WHERE "rebrickable_minifigures"."part" IS NOT DISTINCT FROM :figure
AND "rebrickable_minifigures"."color_id" IS NOT DISTINCT FROM :color

View File

@ -21,3 +21,15 @@ INSERT OR IGNORE INTO "rebrickable_sets" (
:url, :url,
:last_modified :last_modified
) )
ON CONFLICT("set")
DO UPDATE SET
"number" = :number,
"version" = :version,
"name" = :name,
"year" = :year,
"theme_id" = :theme_id,
"number_of_parts" = :number_of_parts,
"image" = :image,
"url" = :url,
"last_modified" = :last_modified
WHERE "rebrickable_sets"."set" IS NOT DISTINCT FROM :set

View File

@ -0,0 +1,53 @@
SELECT
"rebrickable_sets"."set",
"rebrickable_sets"."name",
"rebrickable_sets"."number_of_parts",
"rebrickable_sets"."image",
"rebrickable_sets"."url",
"null_join"."null_rgb",
"null_join"."null_transparent",
"null_join"."null_url"
FROM "rebrickable_sets"
INNER JOIN (
SELECT
"null_sums"."set",
"null_sums"."null_rgb",
"null_sums"."null_transparent",
"null_sums"."null_url"
FROM (
SELECT
"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_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"
FROM (
SELECT
"bricktracker_sets"."set",
"rebrickable_parts"."color_rgb",
"rebrickable_parts"."color_transparent",
"rebrickable_parts"."url"
FROM "bricktracker_sets"
INNER JOIN "bricktracker_parts"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id"
LEFT JOIN "rebrickable_parts"
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
GROUP BY
"bricktracker_sets"."set",
"bricktracker_parts"."part",
"bricktracker_parts"."color"
) "unique_set_parts"
GROUP BY "unique_set_parts"."set"
) "null_sums"
WHERE "null_rgb" > 0
OR "null_transparent" > 0
OR "null_url" > 0
) "null_join"
ON "rebrickable_sets"."set" IS NOT DISTINCT FROM "null_join"."set"

View File

@ -1,12 +1,25 @@
BEGIN transaction; BEGIN transaction;
DROP TABLE IF EXISTS "bricktracker_metadata_owners";
DROP TABLE IF EXISTS "bricktracker_metadata_statuses";
DROP TABLE IF EXISTS "bricktracker_metadata_tags";
DROP TABLE IF EXISTS "bricktracker_minifigures";
DROP TABLE IF EXISTS "bricktracker_parts";
DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_sets";
DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes";
DROP TABLE IF EXISTS "bricktracker_set_owners";
DROP TABLE IF EXISTS "bricktracker_set_statuses"; DROP TABLE IF EXISTS "bricktracker_set_statuses";
DROP TABLE IF EXISTS "bricktracker_set_storages";
DROP TABLE IF EXISTS "bricktracker_set_tags";
DROP TABLE IF EXISTS "bricktracker_wishes"; DROP TABLE IF EXISTS "bricktracker_wishes";
DROP TABLE IF EXISTS "inventory"; DROP TABLE IF EXISTS "inventory";
DROP TABLE IF EXISTS "inventory_old";
DROP TABLE IF EXISTS "minifigures"; DROP TABLE IF EXISTS "minifigures";
DROP TABLE IF EXISTS "minifigures_old";
DROP TABLE IF EXISTS "missing"; DROP TABLE IF EXISTS "missing";
DROP TABLE IF EXISTS "missing_old";
DROP TABLE IF EXISTS "rebrickable_minifigures";
DROP TABLE IF EXISTS "rebrickable_parts";
DROP TABLE IF EXISTS "rebrickable_sets"; DROP TABLE IF EXISTS "rebrickable_sets";
DROP TABLE IF EXISTS "sets"; DROP TABLE IF EXISTS "sets";
DROP TABLE IF EXISTS "sets_old"; DROP TABLE IF EXISTS "sets_old";

View File

@ -1,5 +1,9 @@
SELECT SELECT
{% block id %}{% endblock %} {% block id %}{% endblock %}
"bricktracker_sets"."storage",
"bricktracker_sets"."purchase_date",
"bricktracker_sets"."purchase_location",
"bricktracker_sets"."purchase_price",
"rebrickable_sets"."set", "rebrickable_sets"."set",
"rebrickable_sets"."number", "rebrickable_sets"."number",
"rebrickable_sets"."version", "rebrickable_sets"."version",
@ -9,19 +13,28 @@ SELECT
"rebrickable_sets"."number_of_parts", "rebrickable_sets"."number_of_parts",
"rebrickable_sets"."image", "rebrickable_sets"."image",
"rebrickable_sets"."url", "rebrickable_sets"."url",
{% block owners %}
{% if owners %}{{ owners }},{% endif %}
{% endblock %}
{% block tags %}
{% if tags %}{{ tags }},{% endif %}
{% endblock %}
{% block statuses %} {% block statuses %}
{% if statuses %}{{ statuses }},{% endif %} {% if statuses %}{{ statuses }},{% endif %}
{% endblock %} {% endblock %}
{% block total_missing %} {% block total_missing %}
NULL AS "total_missing", -- dummy for order: total_missing NULL AS "total_missing", -- dummy for order: total_missing
{% endblock %} {% endblock %}
{% block total_damaged %}
NULL AS "total_damaged", -- dummy for order: total_damaged
{% endblock %}
{% block total_quantity %} {% block total_quantity %}
NULL AS "total_quantity", -- dummy for order: total_quantity NULL AS "total_quantity" -- dummy for order: total_quantity
{% endblock %} {% endblock %}
FROM "bricktracker_sets" FROM "bricktracker_sets"
INNER JOIN "rebrickable_sets" INNER JOIN "rebrickable_sets"
ON "bricktracker_sets"."rebrickable_set" IS NOT DISTINCT FROM "rebrickable_sets"."set" ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
{% block join %}{% endblock %} {% block join %}{% endblock %}

View File

@ -5,7 +5,11 @@
{% endblock %} {% endblock %}
{% block total_missing %} {% block total_missing %}
IFNULL("missing_join"."total", 0) AS "total_missing", IFNULL("problem_join"."total_missing", 0) AS "total_missing",
{% endblock %}
{% block total_damaged %}
IFNULL("problem_join"."total_damaged", 0) AS "total_damaged",
{% endblock %} {% endblock %}
{% block total_quantity %} {% block total_quantity %}
@ -13,30 +17,41 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures"
{% endblock %} {% endblock %}
{% block join %} {% block join %}
{% if owners %}
LEFT JOIN "bricktracker_set_owners"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
{% endif %}
{% if statuses %} {% if statuses %}
LEFT JOIN "bricktracker_set_statuses" LEFT JOIN "bricktracker_set_statuses"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."bricktracker_set_id" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id"
{% endif %}
{% if tags %}
LEFT JOIN "bricktracker_set_tags"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_tags"."id"
{% endif %} {% endif %}
-- LEFT JOIN + SELECT to avoid messing the total -- LEFT JOIN + SELECT to avoid messing the total
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
"missing"."u_id", "bricktracker_parts"."id",
SUM("missing"."quantity") AS "total" SUM("bricktracker_parts"."missing") AS "total_missing",
FROM "missing" SUM("bricktracker_parts"."damaged") AS "total_damaged"
FROM "bricktracker_parts"
{% block where_missing %}{% endblock %} {% block where_missing %}{% endblock %}
GROUP BY "u_id" GROUP BY "bricktracker_parts"."id"
) "missing_join" ) "problem_join"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "problem_join"."id"
-- LEFT JOIN + SELECT to avoid messing the total -- LEFT JOIN + SELECT to avoid messing the total
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
"minifigures"."u_id", "bricktracker_minifigures"."id",
SUM("minifigures"."quantity") AS "total" SUM("bricktracker_minifigures"."quantity") AS "total"
FROM "minifigures" FROM "bricktracker_minifigures"
{% block where_minifigures %}{% endblock %} {% block where_minifigures %}{% endblock %}
GROUP BY "u_id" GROUP BY "bricktracker_minifigures"."id"
) "minifigures_join" ) "minifigures_join"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."u_id" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."id"
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
SELECT SELECT
"bricktracker_sets"."id", "bricktracker_sets"."id",
"bricktracker_sets"."rebrickable_set" AS "set" "bricktracker_sets"."set"
FROM "bricktracker_sets" FROM "bricktracker_sets"
{% block join %}{% endblock %} {% block join %}{% endblock %}

View File

@ -6,16 +6,19 @@ BEGIN TRANSACTION;
DELETE FROM "bricktracker_sets" DELETE FROM "bricktracker_sets"
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}';
DELETE FROM "bricktracker_set_owners"
WHERE "bricktracker_set_owners"."id" IS NOT DISTINCT FROM '{{ id }}';
DELETE FROM "bricktracker_set_statuses" DELETE FROM "bricktracker_set_statuses"
WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
DELETE FROM "minifigures" DELETE FROM "bricktracker_set_tags"
WHERE "minifigures"."u_id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_set_tags"."id" IS NOT DISTINCT FROM '{{ id }}';
DELETE FROM "missing" DELETE FROM "bricktracker_minifigures"
WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}';
DELETE FROM "inventory" DELETE FROM "bricktracker_parts"
WHERE "inventory"."u_id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM '{{ id }}';
COMMIT; COMMIT;

View File

@ -1,7 +1,11 @@
INSERT OR IGNORE INTO "bricktracker_sets" ( INSERT OR IGNORE INTO "bricktracker_sets" (
"id", "id",
"rebrickable_set" "set",
"storage",
"purchase_location"
) VALUES ( ) VALUES (
:id, :id,
:set :set,
:storage,
:purchase_location
) )

View File

@ -0,0 +1,11 @@
{% extends 'set/base/full.sql' %}
{% block where %}
WHERE "bricktracker_sets"."id" IN (
SELECT "bricktracker_parts"."id"
FROM "bricktracker_parts"
WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure
AND "bricktracker_parts"."missing" > 0
GROUP BY "bricktracker_parts"."id"
)
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More