-year-month-day-hours-minutes-secondes.db`.
+
+### Restore a backup
+
+In case of a problem, you can always use the **Import a database file** button in the **Database danger zone**.
+
+![](images/upgrade-database-02.png)
+
+## Perform the upgrade
+
+The upgrade procedure is automated, you simply need to start it. Press the **Upgrade the database** button to see the planned list of changes.
+
+![](images/upgrade-database-03.png)
+
+Confirm by pressing the **Upgrade the database** button once more.
+Once the upgrade is done you wil be back to the **Admin page**. You can see that your database file is at the required version.
+
+![](images/upgrade-database-04.png)
diff --git a/entrypoint.sh b/entrypoint.sh
index 7add85e..148c366 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -13,4 +13,4 @@ then
fi
# Execute the WSGI server
-gunicorn --bind "${BK_SERVER}:${BK_PORT}" "app:app" --worker-class "eventlet" "$@"
+gunicorn --bind "${BK_SERVER}:${BK_PORT}" "app:create_app()" --worker-class "eventlet" "$@"
diff --git a/static/scripts/changer.js b/static/scripts/changer.js
new file mode 100644
index 0000000..224e24b
--- /dev/null
+++ b/static/scripts/changer.js
@@ -0,0 +1,125 @@
+// Generic state changer with visual feedback
+class BrickChanger {
+ constructor(prefix, id, url, parent = undefined) {
+ this.prefix = prefix
+ this.html_element = document.getElementById(`${prefix}-${id}`);
+ this.html_status = document.getElementById(`status-${prefix}-${id}`);
+ this.html_type = this.html_element.getAttribute("type");
+ this.url = url;
+
+ if (parent) {
+ this.html_parent = document.getElementById(`${parent}-${id}`);
+ this.parent_dataset = `data-${prefix}`
+ }
+
+ // Register an event depending on the type
+ if (this.html_type == "checkbox") {
+ var listener = "change";
+ } else {
+ var listener = "click";
+ }
+
+ this.html_element.addEventListener(listener, ((changer) => (e) => {
+ changer.change();
+ })(this));
+ }
+
+ // Clean the status
+ status_clean() {
+ if (this.html_status) {
+ const to_remove = Array.from(
+ this.html_status.classList.values()
+ ).filter(
+ (name) => name.startsWith('ri-') || name.startsWith('text-') || name.startsWith('bg-')
+ );
+
+ if (to_remove.length) {
+ this.html_status.classList.remove(...to_remove);
+ }
+ }
+ }
+
+ // Set the status to Error
+ status_error() {
+ if (this.html_status) {
+ this.status_clean();
+ this.html_status.classList.add("ri-alert-line", "text-danger");
+ }
+ }
+
+ // Set the status to OK
+ status_ok() {
+ if (this.html_status) {
+ this.status_clean();
+ this.html_status.classList.add("ri-checkbox-circle-line", "text-success");
+ }
+ }
+
+ // Set the status to Unknown
+ status_unknown() {
+ if (this.html_status) {
+ this.status_clean();
+ this.html_status.classList.add("ri-question-line", "text-warning");
+ }
+ }
+
+ async change() {
+ try {
+ this.status_unknown();
+
+ // Grab the value depending on the type
+ if (this.html_type == "checkbox") {
+ var value = this.html_element.checked;
+ } else {
+ var value = this.html_element.value;
+ }
+
+ const response = await fetch(this.url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: this.prefix,
+ value: value,
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`Response status: ${response.status}`);
+ }
+
+ const json = await response.json();
+
+ if ("error" in json) {
+ throw new Error(`Error received: ${json.error}`)
+ }
+
+ this.status_ok();
+
+ // Update the parent
+ if (this.html_parent) {
+ if (this.html_type == "checkbox") {
+ value = Number(value)
+ }
+
+ // Not going through dataset to avoid converting
+ this.html_parent.setAttribute(this.parent_dataset, value);
+ }
+ } catch (error) {
+ console.log(error.message);
+
+ this.status_error();
+ }
+ }
+}
+
+// Helper to setup the changer
+const setup_changers = () => document.querySelectorAll("*[data-changer-id]").forEach(
+ el => new BrickChanger(
+ el.dataset.changerPrefix,
+ el.dataset.changerId,
+ el.dataset.changerUrl,
+ el.dataset.changerParent
+ )
+);
\ No newline at end of file
diff --git a/static/scripts/grid.js b/static/scripts/grid.js
index 6246817..42b8ac3 100644
--- a/static/scripts/grid.js
+++ b/static/scripts/grid.js
@@ -50,15 +50,15 @@ class BrickGridSortButton {
// Grid class
class BrickGrid {
- constructor(id) {
- this.id = id;
+ constructor(grid) {
+ this.id = grid.id;
// Grid elements (built based on the initial id)
- this.html_grid = document.getElementById(id);
- this.html_sort = document.getElementById(`${id}-sort`);
- this.html_search = document.getElementById(`${id}-search`);
- this.html_filter = document.getElementById(`${id}-filter`);
- this.html_theme = document.getElementById(`${id}-theme`);
+ this.html_grid = document.getElementById(this.id);
+ this.html_sort = document.getElementById(`${this.id}-sort`);
+ this.html_search = document.getElementById(`${this.id}-search`);
+ this.html_filter = document.getElementById(`${this.id}-filter`);
+ this.html_theme = document.getElementById(`${this.id}-theme`);
// Sort buttons
this.html_sort_buttons = {};
@@ -251,3 +251,8 @@ class BrickGrid {
}
}
}
+
+// Helper to setup the grids
+const setup_grids = () => document.querySelectorAll('*[data-grid="true"]').forEach(
+ el => new BrickGrid(el)
+);
diff --git a/static/scripts/set.js b/static/scripts/set.js
index 88966d2..25ba15d 100644
--- a/static/scripts/set.js
+++ b/static/scripts/set.js
@@ -7,60 +7,6 @@ const clean_status = (status) => {
}
}
-// Change the status of a set checkbox
-const change_set_checkbox_status = async (el, kind, id, url) => {
- const status = document.getElementById(`status-${kind}-${id}`);
-
- try {
- // Set the status to unknown
- if (status) {
- clean_status(status)
- status.classList.add("ri-question-line", "text-warning");
- }
-
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- state: el.checked
- })
- });
-
- if (!response.ok) {
- throw new Error(`Response status: ${response.status}`);
- }
-
- const json = await response.json();
-
- if ("error" in json) {
- throw new Error(`Error received: ${json.error}`)
- }
-
- // Set the status to OK
- if (status) {
- clean_status(status)
- status.classList.add("ri-checkbox-circle-line", "text-success");
- }
-
- // Update the card
- const card = document.getElementById(`set-${id}`);
- if (card) {
- // Not going through dataset to avoid converting
- card.setAttribute(`data-${kind}`, Number(el.checked));
- }
- } catch (error) {
- console.log(error.message);
-
- // Set the status to not OK
- if (status) {
- clean_status(status)
- status.classList.add("ri-alert-line", "text-danger");
- }
- }
-}
-
// Change the amount of missing parts
const change_part_missing_amount = async (el, set_id, part_id, url) => {
const status = document.getElementById(`status-part-${set_id}-${part_id}`);
diff --git a/static/scripts/socket.js b/static/scripts/socket.js
index 581b09d..5a24d06 100644
--- a/static/scripts/socket.js
+++ b/static/scripts/socket.js
@@ -30,7 +30,7 @@ class BrickSocket {
// Card elements
this.html_card = document.getElementById(`${id}-card`);
- this.html_card_number = document.getElementById(`${id}-card-number`);
+ this.html_card_set = document.getElementById(`${id}-card-set`);
this.html_card_name = document.getElementById(`${id}-card-name`);
this.html_card_image_container = document.getElementById(`${id}-card-image-container`);
this.html_card_image = document.getElementById(`${id}-card-image`);
@@ -190,9 +190,9 @@ class BrickSocket {
}
if (this.bulk && this.html_input) {
- if (this.set_list_last_number !== undefined) {
- this.set_list.unshift(this.set_list_last_number);
- this.set_list_last_number = undefined;
+ if (this.set_list_last_set !== undefined) {
+ this.set_list.unshift(this.set_list_last_set);
+ this.set_list_last_set = undefined;
}
this.html_input.value = this.set_list.join(', ');
@@ -200,7 +200,7 @@ class BrickSocket {
}
// Import a set
- import_set(no_confirm, number, from_complete=false) {
+ import_set(no_confirm, set, from_complete=false) {
if (this.html_input) {
if (!this.bulk || !from_complete) {
// Reset the progress
@@ -213,10 +213,10 @@ class BrickSocket {
// Grab from the list if bulk
if (this.bulk) {
- number = this.set_list.shift()
+ set = this.set_list.shift()
// Abort if nothing left to process
- if (number === undefined) {
+ if (set === undefined) {
// Clear the input
this.html_input.value = "";
@@ -227,14 +227,14 @@ class BrickSocket {
return;
}
- // Save the pulled number
- this.set_list_last_number = number;
+ // Save the pulled set
+ this.set_list_last_set = set;
}
this.spinner(true);
this.socket.emit(this.messages.IMPORT_SET, {
- set_num: (number !== undefined) ? number : this.html_input.value,
+ set: (set !== undefined) ? set : this.html_input.value,
});
} else {
this.fail("Could not find the input field for the set number");
@@ -249,7 +249,7 @@ class BrickSocket {
this.spinner(true);
this.socket.emit(this.messages.LOAD_SET, {
- set_num: this.html_input.value
+ set: this.html_input.value
});
} else {
this.fail("Could not find the input field for the set number");
@@ -319,8 +319,8 @@ class BrickSocket {
if (this.html_card) {
this.html_card.classList.remove("d-none");
- if (this.html_card_number) {
- this.html_card_number.textContent = data["set_num"];
+ if (this.html_card_set) {
+ this.html_card_set.textContent = data["set"];
}
if (this.html_card_name) {
@@ -328,12 +328,12 @@ class BrickSocket {
}
if (this.html_card_image_container) {
- this.html_card_image_container.setAttribute("style", `background-image: url(${data["set_img_url"]})`);
+ this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`);
}
if (this.html_card_image) {
- this.html_card_image.setAttribute("src", data["set_img_url"]);
- this.html_card_image.setAttribute("alt", data["set_num"]);
+ this.html_card_image.setAttribute("src", data["image"]);
+ this.html_card_image.setAttribute("alt", data["set"]);
}
if (this.html_card_footer) {
@@ -347,12 +347,12 @@ class BrickSocket {
this.html_card_confirm.removeEventListener("click", this.confirm_listener);
}
- this.confirm_listener = ((bricksocket, number) => (e) => {
+ this.confirm_listener = ((bricksocket, set) => (e) => {
if (!bricksocket.disabled) {
bricksocket.toggle(false);
- bricksocket.import_set(false, number);
+ bricksocket.import_set(false, set);
}
- })(this, data["set_num"]);
+ })(this, data["set"]);
this.html_card_confirm.addEventListener("click", this.confirm_listener);
}
diff --git a/static/scripts/table.js b/static/scripts/table.js
new file mode 100644
index 0000000..669afc5
--- /dev/null
+++ b/static/scripts/table.js
@@ -0,0 +1,87 @@
+class BrickTable {
+ constructor(table, per_page) {
+ const columns = []
+ const no_sort = [];
+ const number = [];
+
+ // Read the table header for parameters
+ table.querySelectorAll('th').forEach((th, index) => {
+ if (th.dataset.tableNoSort) {
+ no_sort.push(index);
+ }
+
+ if (th.dataset.tableNumber) {
+ number.push(index);
+ }
+ });
+
+ if (no_sort.length) {
+ columns.push({ select: no_sort, sortable: false, searchable: false });
+ }
+
+ if (number.length) {
+ columns.push({ select: number, type: "number", searchable: false });
+ }
+
+ this.table = new simpleDatatables.DataTable(`#${table.id}`, {
+ columns: columns,
+ pagerDelta: 1,
+ perPage: per_page,
+ perPageSelect: [10, 25, 50, 100, 500, 1000],
+ searchable: true,
+ searchMethod: (table => (terms, cell, row, column, source) => table.search(terms, cell, row, column, source))(this),
+ searchQuerySeparator: "",
+ tableRender: () => {
+ baguetteBox.run("[data-lightbox]");
+ },
+ pagerRender: () => {
+ baguetteBox.run("[data-lightbox]");
+ }
+ });
+ }
+
+ // Custom search method
+ // Very simplistic but will exclude pill links
+ search(terms, cell, row, column, source) {
+ // Create a searchable string from the data stack ignoring data-search="exclude"
+ const search = this.buildSearch(cell.data).filter(data => data != "").join(" ");
+
+ // Search it
+ for (const term of terms) {
+ if (search.includes(term)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Build the search string
+ buildSearch(dataList) {
+ let search = [];
+
+ for (const data of dataList) {
+ // Exclude
+ if (data.attributes && data.attributes['data-search'] && data.attributes['data-search'] == 'exclude') {
+ continue;
+ }
+
+ // Childnodes
+ if (data.childNodes) {
+ search = search.concat(this.buildSearch(data.childNodes));
+ }
+
+ // Data
+ if(data.data) {
+ search.push(data.data.trim().toLowerCase());
+ }
+ }
+
+ return search;
+ }
+}
+
+// Helper to setup the tables
+const setup_tables = (per_page) => document.querySelectorAll('table[data-table="true"]').forEach(
+ el => new BrickTable(el, per_page)
+);
diff --git a/templates/add.html b/templates/add.html
index a4a4917..140eec6 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -4,7 +4,7 @@
{% block main %}
- {% if not config['HIDE_ADD_BULK_SET'].value %}
+ {% if not config['HIDE_ADD_BULK_SET'] %}
Too many to add?
You can import multiple sets at once with Bulk add.
@@ -47,7 +47,7 @@
@@ -70,7 +70,3 @@
{% include 'set/socket.html' %}
{% endblock %}
-
-{% block scripts %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/admin.html b/templates/admin.html
index 9e172bc..dc87256 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -12,20 +12,25 @@
Administration
- {% if delete_database %}
+ {% if delete_checkbox %}
+ {% include 'admin/checkbox/delete.html' %}
+ {% elif delete_database %}
{% include 'admin/database/delete.html' %}
{% elif drop_database %}
{% include 'admin/database/drop.html' %}
{% elif import_database %}
{% include 'admin/database/import.html' %}
+ {% elif upgrade_database %}
+ {% include 'admin/database/upgrade.html' %}
{% else %}
{% include 'admin/logout.html' %}
{% include 'admin/instructions.html' %}
- {% if not config['USE_REMOTE_IMAGES'].value %}
+ {% if not config['USE_REMOTE_IMAGES'] %}
{% include 'admin/image.html' %}
{% endif %}
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
+ {% include 'admin/checkbox.html' %}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
{% endif %}
@@ -36,4 +41,4 @@
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html
new file mode 100644
index 0000000..b71cc2a
--- /dev/null
+++ b/templates/admin/checkbox.html
@@ -0,0 +1,62 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Checkboxes', 'checkbox', 'admin', expanded=open_checkbox, icon='checkbox-line', class='p-0') }}
+{% if error %}Error: {{ error }}.
{% endif %}
+{% if database_error %}Error: {{ database_error }}.
{% endif %}
+
+ {% if brickset_checkboxes | length %}
+ {% for checkbox in brickset_checkboxes %}
+ -
+
+
+ {% endfor %}
+ {% else %}
+ - No checkbox found.
+ {% endif %}
+ -
+
+
+
+{{ accordion.footer() }}
diff --git a/templates/admin/checkbox/delete.html b/templates/admin/checkbox/delete.html
new file mode 100644
index 0000000..49d507a
--- /dev/null
+++ b/templates/admin/checkbox/delete.html
@@ -0,0 +1,25 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Checkbox danger zone', 'checkbox-danger', 'admin', expanded=true, danger=true, class='text-end') }}
+
+{{ accordion.footer() }}
diff --git a/templates/admin/configuration.html b/templates/admin/configuration.html
index 44cabdf..9ef4496 100644
--- a/templates/admin/configuration.html
+++ b/templates/admin/configuration.html
@@ -18,12 +18,10 @@
{{ entry.value }}
{% endif %}
{% endif %}
- {% if not entry.not_from_env %}
- Env: {{ entry.env_name }}
- {% if entry.extra_name %}Env: {{ entry.extra_name }}{% endif %}
- {% if entry.is_changed() %}
- Changed
- {% endif %}
+ Env: {{ entry.env_name }}
+ {% if entry.extra_name %}Env: {{ entry.extra_name }}{% endif %}
+ {% if entry.is_changed() %}
+ Changed
{% endif %}
{% endfor %}
diff --git a/templates/admin/database.html b/templates/admin/database.html
index bbe02d9..36d2b0d 100644
--- a/templates/admin/database.html
+++ b/templates/admin/database.html
@@ -2,48 +2,34 @@
{{ accordion.header('Database', 'database', 'admin', expanded=open_database, icon='database-2-line') }}
Status
-{% if exception %}An exception occured while loading this page: {{ exception }}
{% endif %}
-{% if error %}Error: {{ error }}.
{% endif %}
-{% if not is_init %}
+{% if database_exception %}An exception occured while loading this page: {{ database_exception }}
{% endif %}
+{% if database_error %}Error: {{ database_error }}.
{% endif %}
+{% if database_upgrade_needed %}
-
The database file is: {{ config['DATABASE_PATH'].value }}
. The database is not initialized.
+
Your database needs to be upgraded.
-
-
-{% else %}
- {% if count_none %}
-
-
- Your missing
table contains "None"
entries (instead of NULL
).
- This can lead to "phantom" missing parts appearing in your sets if you are coming from the original version of BrickTracker.
-
-
-
+
- {% endif %}
-
The database file is: {{ config['DATABASE_PATH'].value }}
. The database is initialized.
-
- Download the database file
-
+
+{% endif %}
+The database file is: {{ config['DATABASE_PATH'] }}
at version {{ database_version }}
+
+ Download the database file
+
+{% if database_counters %}
Records
-
- -
- Sets {{ counters['sets'] }}
-
- -
- Minifigures {{ counters['minifigures'] }}
-
- -
- Parts {{ counters['inventory'] }}
-
- -
- Missing {{ counters['missing'] }}
-
+
+ {% for counter in database_counters %}
+ -
+ {{ counter.name }} {{ counter.count }}
+
+ {% if not (loop.index % 5) %}
+
+
+ {% endif %}
+ {% endfor %}
{% endif %}
@@ -51,10 +37,7 @@
{{ accordion.header('Database danger zone', 'database-danger', 'admin', danger=true, class='text-end') }}
{% if error %}Error: {{ error }}.
{% endif %}
- Import a database file
-
- Drop the database
- Delete the database file
+ Import a database file
+ Drop the database
+ Delete the database file
{{ accordion.footer() }}
diff --git a/templates/admin/database/delete.html b/templates/admin/database/delete.html
index 675e39d..9a2d286 100644
--- a/templates/admin/database/delete.html
+++ b/templates/admin/database/delete.html
@@ -1,7 +1,7 @@
{% import 'macro/accordion.html' as accordion %}
{{ accordion.header('Database danger zone', 'database-danger', 'admin', expanded=true, danger=true, class='text-end') }}
-