From 093a1817518916220722978c74302c92e03e1698 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Jun 2021 01:07:07 +0200 Subject: [PATCH 001/178] initial structure for single pricing view --- InvenTree/part/templates/part/navbar.html | 8 +- InvenTree/part/templates/part/prices.html | 490 ++++++++++++++++++++++ InvenTree/part/urls.py | 2 +- 3 files changed, 495 insertions(+), 5 deletions(-) create mode 100644 InvenTree/part/templates/part/prices.html diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index c0bc4c96a3..b123c458e9 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -70,13 +70,13 @@ {% endif %} - {% if part.purchaseable and roles.purchase_order.view %} -
  • - +
  • + - {% trans "Order Price" %} + {% trans "Prices" %}
  • + {% if part.purchaseable and roles.purchase_order.view %}
  • diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html new file mode 100644 index 0000000000..e71179284a --- /dev/null +++ b/InvenTree/part/templates/part/prices.html @@ -0,0 +1,490 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags %} +{% load inventree_extras %} + +{% block menubar %} +{% include 'part/navbar.html' with tab='prices' %} +{% endblock %} + +{% block heading %} +{% trans "General Price Information" %} +{% endblock %} + + +{% block details %} +{% default_currency as currency %} + +
    +
    +

    {% trans "Pricing ranges" %}

    + + {% if part.supplier_count > 0 %} + {% if min_total_buy_price %} + + + + + + + {% if quantity > 1 %} + + + + + + + {% endif %} + {% else %} + + + + {% endif %} + {% endif %} + + {% if part.bom_count > 0 %} + {% if min_total_bom_price %} + + + + + + + {% if quantity > 1 %} + + + + + + + {% endif %} + {% if part.has_complete_bom_pricing == False %} + + + + {% endif %} + {% else %} + + + + {% endif %} + {% endif %} + + {% if show_internal_price and roles.sales_order.view %} + {% if total_internal_part_price %} + + + + + + + + + + + {% endif %} + {% endif %} + + {% if total_part_price %} + + + + + + + + + + + {% endif %} +
    {% trans 'Supplier Pricing' %}{% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_buy_price %}Max: {% include "price.html" with price=max_unit_buy_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_buy_price %}Max: {% include "price.html" with price=max_total_buy_price %}
    + {% trans 'No supplier pricing available' %} +
    {% trans 'BOM Pricing' %}{% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_bom_price %}Max: {% include "price.html" with price=max_unit_bom_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_bom_price %}Max: {% include "price.html" with price=max_total_bom_price %}
    + {% trans 'Note: BOM pricing is incomplete for this part' %} +
    + {% trans 'No BOM pricing available' %} +
    {% trans 'Internal Price' %}{% trans 'Unit Cost' %}{% include "price.html" with price=unit_internal_part_price %}
    {% trans 'Total Cost' %}{% include "price.html" with price=total_internal_part_price %}
    {% trans 'Sale Price' %}{% trans 'Unit Cost' %}{% include "price.html" with price=unit_part_price %}
    {% trans 'Total Cost' %}{% include "price.html" with price=total_part_price %}
    + + {% if min_unit_buy_price or min_unit_bom_price %} + {% else %} +
    + {% trans 'No pricing information is available for this part.' %} +
    + {% endif %} +
    + +
    +
    + {% csrf_token %} + {{ form|crispy }} + +
    +
    +
    +{% endblock %} + +{% block post_content_panel %} +{% default_currency as currency %} +{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} + + +{% if part.purchaseable and roles.purchase_order.view %} +
    +
    +

    {% trans "Supplier Cost" %}

    +
    + +
    + PLACEHOLDER FOR SUPPLIER COST TABLE +
    +
    + + +
    +
    +

    {% trans "Purchase Price" %}

    +
    + + {% if price_history %} +

    {% trans 'Stock Pricing' %}

    + {% if price_history|length > 0 %} +
    + +
    + {% else %} +
    + {% trans 'No stock pricing history is available for this part.' %} +
    + {% endif %} + {% endif %} +
    +{% endif %} + + +{% if show_internal_price and roles.sales_order.view %} +
    +
    +

    {% trans "Internal Cost" %}

    +
    + +
    +
    + +
    + + +
    +
    +
    +{% endif %} + + +
    +
    +

    {% trans "BOM Cost" %}

    +
    + +
    +
    + PLACEHOLDER FOR BOM ITEM COST TABLE +
    +
    + + {% if part.bom_count > 0 %} +
    +

    {% trans 'BOM Pricing' %}

    +
    + +
    +
    + {% endif %} +
    +
    + +{% if part.salable and roles.sales_order.view %} +
    +
    +

    {% trans "Sale Cost" %}

    +
    + +
    +
    + +
    + + +
    +
    +
    + +
    +
    +

    {% trans "Sale Price" %}

    +
    + +
    + PLACEHOLDER FOR SALE HISTORY +
    +
    +{% endif %} + +{% endblock %} + + + +{% block js_ready %} + {{ block.super }} + + // history graphs + {% default_currency as currency %} + {% if price_history %} + var pricedata = { + labels: [ + {% for line in price_history %}'{{ line.date }}',{% endfor %} + ], + datasets: [{ + label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgb(255, 99, 132)', + yAxisID: 'y', + data: [ + {% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %} + ], + borderWidth: 1, + type: 'line' + }, + {% if 'price_diff' in price_history.0 %} + { + label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}', + backgroundColor: 'rgba(68, 157, 68, 0.2)', + borderColor: 'rgb(68, 157, 68)', + yAxisID: 'y2', + data: [ + {% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %} + ], + borderWidth: 1, + type: 'line', + hidden: true, + }, + { + label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}', + backgroundColor: 'rgba(70, 127, 155, 0.2)', + borderColor: 'rgb(70, 127, 155)', + yAxisID: 'y', + data: [ + {% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %} + ], + borderWidth: 1, + type: 'line', + hidden: true, + }, + {% endif %} + { + label: '{% trans "Quantity" %}', + backgroundColor: 'rgba(255, 206, 86, 0.2)', + borderColor: 'rgb(255, 206, 86)', + yAxisID: 'y1', + data: [ + {% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %} + ], + borderWidth: 1 + }] + } + var StockPriceChart = loadStockPricingChart(document.getElementById('StockPriceChart'), pricedata) + var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} }) + var bomdata = { + labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}], + datasets: [ + { + label: 'Price', + data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}], + backgroundColor: bom_colors, + }, + {% if bom_pie_max %} + { + label: 'Max Price', + data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}], + backgroundColor: bom_colors, + }, + {% endif %} + ] + }; + var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata) + + {% endif %} + + + // Internal pricebreaks + {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} + {% if show_internal_price and roles.sales_order.view %} + function reloadPriceBreaks() { + $("#internal-price-break-table").bootstrapTable("refresh"); + } + + $('#new-internal-price-break').click(function() { + launchModalForm("{% url 'internal-price-break-create' %}", + { + success: reloadPriceBreaks, + data: { + part: {{ part.id }}, + } + } + ); + }); + + $('#internal-price-break-table').inventreeTable({ + name: 'internalprice', + formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; }, + queryParams: { + part: {{ part.id }}, + }, + url: "{% url 'api-part-internal-price-list' %}", + onPostBody: function() { + var table = $('#internal-price-break-table'); + + table.find('.button-internal-price-break-delete').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/internal-price/${pk}/delete/`, + { + success: reloadPriceBreaks + } + ); + }); + + table.find('.button-internal-price-break-edit').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/internal-price/${pk}/edit/`, + { + success: reloadPriceBreaks + } + ); + }); + }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + sortable: true, + }, + { + field: 'price', + title: '{% trans "Price" %}', + sortable: true, + formatter: function(value, row, index) { + var html = value; + + html += `
    ` + + html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}'); + + html += `
    `; + + return html; + } + }, + ] + }) + {% endif %} + + + // Sales pricebreaks + {% if part.salable and roles.sales_order.view %} + function reloadPriceBreaks() { + $("#price-break-table").bootstrapTable("refresh"); + } + + $('#new-price-break').click(function() { + launchModalForm("{% url 'sale-price-break-create' %}", + { + success: reloadPriceBreaks, + data: { + part: {{ part.id }}, + } + } + ); + }); + + $('#price-break-table').inventreeTable({ + name: 'saleprice', + formatNoMatches: function() { return "{% trans 'No price break information found' %}"; }, + queryParams: { + part: {{ part.id }}, + }, + url: "{% url 'api-part-sale-price-list' %}", + onPostBody: function() { + var table = $('#price-break-table'); + + table.find('.button-price-break-delete').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/sale-price/${pk}/delete/`, + { + success: reloadPriceBreaks + } + ); + }); + + table.find('.button-price-break-edit').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/sale-price/${pk}/edit/`, + { + success: reloadPriceBreaks + } + ); + }); + }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + sortable: true, + }, + { + field: 'price', + title: '{% trans "Price" %}', + sortable: true, + formatter: function(value, row, index) { + var html = value; + + html += `
    ` + + html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}'); + + html += `
    `; + + return html; + } + }, + ] + }) + + {% endif %} + +{% endblock %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index d12612c604..7bd58f52f2 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -65,7 +65,7 @@ part_detail_urls = [ url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), - url(r'^order-prices/', views.PartPricingView.as_view(template_name='part/order_prices.html'), name='part-order-prices'), + url(r'^prices/', views.PartPricingView.as_view(template_name='part/prices.html'), name='part-prices'), url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), From c8ff6ee0e248959063c37204bef5f62adcaf9131 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Jun 2021 01:11:25 +0200 Subject: [PATCH 002/178] removing old views --- .../part/templates/part/internal_prices.html | 122 --------- InvenTree/part/templates/part/navbar.html | 14 +- .../part/templates/part/order_prices.html | 235 ------------------ .../part/templates/part/sale_prices.html | 108 -------- InvenTree/part/urls.py | 2 - 5 files changed, 1 insertion(+), 480 deletions(-) delete mode 100644 InvenTree/part/templates/part/internal_prices.html delete mode 100644 InvenTree/part/templates/part/order_prices.html delete mode 100644 InvenTree/part/templates/part/sale_prices.html diff --git a/InvenTree/part/templates/part/internal_prices.html b/InvenTree/part/templates/part/internal_prices.html deleted file mode 100644 index 2f54f3bb64..0000000000 --- a/InvenTree/part/templates/part/internal_prices.html +++ /dev/null @@ -1,122 +0,0 @@ -{% extends "part/part_base.html" %} -{% load static %} -{% load i18n %} -{% load inventree_extras %} - -{% block menubar %} -{% include 'part/navbar.html' with tab='internal-prices' %} -{% endblock %} - -{% block heading %} -{% trans "Internal Price Information" %} -{% endblock %} - -{% block details %} -{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} -{% if show_internal_price and roles.sales_order.view %} -
    - -
    - - -
    - -{% else %} -
    -

    {% trans "Permission Denied" %}

    - -
    - {% trans "You do not have permission to view this page." %} -
    -
    -{% endif %} -{% endblock %} - -{% block js_ready %} -{{ block.super }} - -{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} -{% if show_internal_price and roles.sales_order.view %} -function reloadPriceBreaks() { - $("#internal-price-break-table").bootstrapTable("refresh"); -} - -$('#new-internal-price-break').click(function() { - launchModalForm("{% url 'internal-price-break-create' %}", - { - success: reloadPriceBreaks, - data: { - part: {{ part.id }}, - } - } - ); -}); - -$('#internal-price-break-table').inventreeTable({ - name: 'internalprice', - formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; }, - queryParams: { - part: {{ part.id }}, - }, - url: "{% url 'api-part-internal-price-list' %}", - onPostBody: function() { - var table = $('#internal-price-break-table'); - - table.find('.button-internal-price-break-delete').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/internal-price/${pk}/delete/`, - { - success: reloadPriceBreaks - } - ); - }); - - table.find('.button-internal-price-break-edit').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/internal-price/${pk}/edit/`, - { - success: reloadPriceBreaks - } - ); - }); - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true, - }, - { - field: 'price', - title: '{% trans "Price" %}', - sortable: true, - formatter: function(value, row, index) { - var html = value; - - html += `
    ` - - html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}'); - - html += `
    `; - - return html; - } - }, - ] -}) - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/navbar.html b/InvenTree/part/templates/part/navbar.html index b123c458e9..6df4a5505f 100644 --- a/InvenTree/part/templates/part/navbar.html +++ b/InvenTree/part/templates/part/navbar.html @@ -96,19 +96,7 @@
  • {% endif %} - {% if show_internal_price and roles.sales_order.view %} -
  • - - - {% trans "Internal Price" %} - -
  • -
  • - - - {% trans "Sale Price" %} - -
  • + {% if roles.sales_order.view %}
  • diff --git a/InvenTree/part/templates/part/order_prices.html b/InvenTree/part/templates/part/order_prices.html deleted file mode 100644 index 5e5552c5f9..0000000000 --- a/InvenTree/part/templates/part/order_prices.html +++ /dev/null @@ -1,235 +0,0 @@ -{% extends "part/part_base.html" %} -{% load static %} -{% load i18n %} -{% load crispy_forms_tags %} -{% load inventree_extras %} - -{% block menubar %} -{% include 'part/navbar.html' with tab='order-prices' %} -{% endblock %} - -{% block heading %} -{% trans "Order Price Information" %} -{% endblock %} - -{% block details %} -{% default_currency as currency %} -{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} - -
    - {% csrf_token %} -
    -
    {{ form|crispy }}
    -
    - -
    -
    -
    -
    - -
    -

    {% trans "Pricing ranges" %}

    - -{% if part.supplier_count > 0 %} - {% if min_total_buy_price %} - - - - - - - {% if quantity > 1 %} - - - - - - - {% endif %} - {% else %} - - - - {% endif %} -{% endif %} - -{% if part.bom_count > 0 %} - {% if min_total_bom_price %} - - - - - - - {% if quantity > 1 %} - - - - - - - {% endif %} - {% if part.has_complete_bom_pricing == False %} - - - - {% endif %} - {% else %} - - - - {% endif %} -{% endif %} - -{% if show_internal_price and roles.sales_order.view %} -{% if total_internal_part_price %} - - - - - - - - - - -{% endif %} -{% endif %} - -{% if total_part_price %} - - - - - - - - - - -{% endif %} -
    {% trans 'Supplier Pricing' %}{% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_buy_price %}Max: {% include "price.html" with price=max_unit_buy_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_buy_price %}Max: {% include "price.html" with price=max_total_buy_price %}
    - {% trans 'No supplier pricing available' %} -
    {% trans 'BOM Pricing' %}{% trans 'Unit Cost' %}Min: {% include "price.html" with price=min_unit_bom_price %}Max: {% include "price.html" with price=max_unit_bom_price %}
    {% trans 'Total Cost' %}Min: {% include "price.html" with price=min_total_bom_price %}Max: {% include "price.html" with price=max_total_bom_price %}
    - {% trans 'Note: BOM pricing is incomplete for this part' %} -
    - {% trans 'No BOM pricing available' %} -
    {% trans 'Internal Price' %}{% trans 'Unit Cost' %}{% include "price.html" with price=unit_internal_part_price %}
    {% trans 'Total Cost' %}{% include "price.html" with price=total_internal_part_price %}
    {% trans 'Sale Price' %}{% trans 'Unit Cost' %}{% include "price.html" with price=unit_part_price %}
    {% trans 'Total Cost' %}{% include "price.html" with price=total_part_price %}
    - -{% if min_unit_buy_price or min_unit_bom_price %} -{% else %} -
    - {% trans 'No pricing information is available for this part.' %} -
    -{% endif %} -
    -{% if part.bom_count > 0 %} -
    -

    {% trans 'BOM Pricing' %}

    -
    - -
    -
    -{% endif %} -
    - -{% if price_history %} -
    -

    {% trans 'Stock Pricing' %}

    - {% if price_history|length > 0 %} -
    - -
    - {% else %} -
    - {% trans 'No stock pricing history is available for this part.' %} -
    - {% endif %} -{% endif %} -{% endblock %} - - - - -{% block js_ready %} - {{ block.super }} - - {% default_currency as currency %} - {% if price_history %} - var pricedata = { - labels: [ - {% for line in price_history %}'{{ line.date }}',{% endfor %} - ], - datasets: [{ - label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - borderColor: 'rgb(255, 99, 132)', - yAxisID: 'y', - data: [ - {% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %} - ], - borderWidth: 1, - type: 'line' - }, - {% if 'price_diff' in price_history.0 %} - { - label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}', - backgroundColor: 'rgba(68, 157, 68, 0.2)', - borderColor: 'rgb(68, 157, 68)', - yAxisID: 'y2', - data: [ - {% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %} - ], - borderWidth: 1, - type: 'line', - hidden: true, - }, - { - label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}', - backgroundColor: 'rgba(70, 127, 155, 0.2)', - borderColor: 'rgb(70, 127, 155)', - yAxisID: 'y', - data: [ - {% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %} - ], - borderWidth: 1, - type: 'line', - hidden: true, - }, - {% endif %} - { - label: '{% trans "Quantity" %}', - backgroundColor: 'rgba(255, 206, 86, 0.2)', - borderColor: 'rgb(255, 206, 86)', - yAxisID: 'y1', - data: [ - {% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %} - ], - borderWidth: 1 - }] - } - var StockPriceChart = loadStockPricingChart(document.getElementById('StockPriceChart'), pricedata) - var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} }) - var bomdata = { - labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}], - datasets: [ - { - label: 'Price', - data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}], - backgroundColor: bom_colors, - }, - {% if bom_pie_max %} - { - label: 'Max Price', - data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}], - backgroundColor: bom_colors, - }, - {% endif %} - ] - }; - var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata) - - {% endif %} - -{% endblock %} diff --git a/InvenTree/part/templates/part/sale_prices.html b/InvenTree/part/templates/part/sale_prices.html deleted file mode 100644 index 4ec826e2a6..0000000000 --- a/InvenTree/part/templates/part/sale_prices.html +++ /dev/null @@ -1,108 +0,0 @@ -{% extends "part/part_base.html" %} -{% load static %} -{% load i18n %} - -{% block menubar %} -{% include 'part/navbar.html' with tab='sales-prices' %} -{% endblock %} - -{% block heading %} -{% trans "Sell Price Information" %} -{% endblock %} - -{% block details %} - -
    - -
    - - -
    - -{% endblock %} - -{% block js_ready %} -{{ block.super }} - -function reloadPriceBreaks() { - $("#price-break-table").bootstrapTable("refresh"); -} - -$('#new-price-break').click(function() { - launchModalForm("{% url 'sale-price-break-create' %}", - { - success: reloadPriceBreaks, - data: { - part: {{ part.id }}, - } - } - ); -}); - -$('#price-break-table').inventreeTable({ - name: 'saleprice', - formatNoMatches: function() { return "{% trans 'No price break information found' %}"; }, - queryParams: { - part: {{ part.id }}, - }, - url: "{% url 'api-part-sale-price-list' %}", - onPostBody: function() { - var table = $('#price-break-table'); - - table.find('.button-price-break-delete').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/sale-price/${pk}/delete/`, - { - success: reloadPriceBreaks - } - ); - }); - - table.find('.button-price-break-edit').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/sale-price/${pk}/edit/`, - { - success: reloadPriceBreaks - } - ); - }); - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true, - }, - { - field: 'price', - title: '{% trans "Price" %}', - sortable: true, - formatter: function(value, row, index) { - var html = value; - - html += `
    ` - - html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}'); - - html += `
    `; - - return html; - } - }, - ] -}) - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 7bd58f52f2..1b11bd16ff 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -70,8 +70,6 @@ part_detail_urls = [ url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'), url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'), - url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'), - url(r'^internal-prices/', views.PartDetail.as_view(template_name='part/internal_prices.html'), name='part-internal-prices'), url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), From 522ca161d6e60a769ca7313203c082e173618f4d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Jun 2021 01:26:07 +0200 Subject: [PATCH 003/178] added permissions-check to bom --- InvenTree/part/templates/part/prices.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index e71179284a..1789b6ddff 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -140,7 +140,6 @@ -

    {% trans "Purchase Price" %}

    @@ -183,6 +182,7 @@ {% endif %} +{% if part.has_bom and roles.sales_order.view %}

    {% trans "BOM Cost" %}

    @@ -204,6 +204,8 @@ {% endif %}
    +{% endif %} + {% if part.salable and roles.sales_order.view %}
    From 058e53459b87b808a65f4f268c896cfd3a40dc1d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:07:56 +1000 Subject: [PATCH 004/178] Add simple function for determining OPTIONS --- InvenTree/InvenTree/urls.py | 2 + InvenTree/templates/base.html | 3 +- .../script/inventree => templates/js}/api.js | 0 InvenTree/templates/js/forms.js | 38 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) rename InvenTree/{InvenTree/static/script/inventree => templates/js}/api.js (100%) create mode 100644 InvenTree/templates/js/forms.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 0108418517..2ec7b02f23 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -103,6 +103,8 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ + url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'), + url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'), url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'), url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 65712b7394..128cbba2dd 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -144,11 +144,12 @@ - + + diff --git a/InvenTree/InvenTree/static/script/inventree/api.js b/InvenTree/templates/js/api.js similarity index 100% rename from InvenTree/InvenTree/static/script/inventree/api.js rename to InvenTree/templates/js/api.js diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js new file mode 100644 index 0000000000..fa2ca9eb2e --- /dev/null +++ b/InvenTree/templates/js/forms.js @@ -0,0 +1,38 @@ +/** + * This file contains code for rendering (and managing) HTML forms + * which are served via the django-drf API. + * + * The django DRF library provides an OPTIONS method for each API endpoint, + * which allows us to introspect the available fields at any given endpoint. + * + * The OPTIONS method provides the following information for each available field: + * + * - Field name + * - Field label (translated) + * - Field help text (translated) + * - Field type + * - Read / write status + * - Field required status + * - min_value / max_value + */ + + +/* + * Get the API endpoint options at the provided URL, + * using a HTTP options request. + */ +function getApiEndpointOptions(url, options={}) { + + $.ajax({ + url: url, + type: 'OPTIONS', + contentType: 'application/json', + dataType: 'json', + accepts: { + json: 'application/json', + }, + success: function(response) { + console.log(response); + } + }); +} From 332c0a43fd298593afb217125eb77e3ab7abffc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Jun 2021 12:16:04 +0200 Subject: [PATCH 005/178] clearer headings --- InvenTree/part/templates/part/prices.html | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 1789b6ddff..e2828470b8 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -115,6 +115,7 @@
    +

    {% trans "Calculation parameters" %}

    {% csrf_token %} {{ form|crispy }} From 761aa04aba0882b4d0d59a592869551c65c7b898 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Jun 2021 12:16:33 +0200 Subject: [PATCH 006/178] added bom-table --- InvenTree/part/templates/part/prices.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index e2828470b8..ac4565b8b1 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -191,8 +191,7 @@
    - PLACEHOLDER FOR BOM ITEM COST TABLE -
    +
    {% if part.bom_count > 0 %} @@ -406,6 +405,16 @@ }) {% endif %} + + // Load the BOM table data + loadBomTable($("#bom-table"), { + editable: {{ editing_enabled }}, + bom_url: "{% url 'api-bom-list' %}", + part_url: "{% url 'api-part-list' %}", + parent_id: {{ part.id }} , + sub_part_detail: true, + }); + // Sales pricebreaks {% if part.salable and roles.sales_order.view %} From eaa5913c8cf05d05b9fc95ca4215952ddc473a8e Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:30:26 +1000 Subject: [PATCH 007/178] Adds custom DRF metadata handler - Limit available "actions" data to only what the user is allowed to do --- InvenTree/InvenTree/metadata.py | 67 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/settings.py | 1 + InvenTree/templates/js/forms.js | 8 ++-- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 InvenTree/InvenTree/metadata.py diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py new file mode 100644 index 0000000000..b9d0732acf --- /dev/null +++ b/InvenTree/InvenTree/metadata.py @@ -0,0 +1,67 @@ + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from rest_framework.metadata import SimpleMetadata + +import users.models + + +class InvenTreeMetadata(SimpleMetadata): + """ + Custom metadata class for the DRF API. + + This custom metadata class imits the available "actions", + based on the user's role permissions. + + Thus when a client send an OPTIONS request to an API endpoint, + it will only receive a list of actions which it is allowed to perform! + + """ + + def determine_metadata(self, request, view): + + metadata = super().determine_metadata(request, view) + + user = request.user + + if user is None: + # No actions for you! + metadata['actions'] = {} + return metadata + + try: + # Extract the model name associated with the view + model = view.serializer_class.Meta.model + + # Construct the 'table name' from the model + app_label = model._meta.app_label + tbl_label = model._meta.model_name + + table = f"{app_label}_{tbl_label}" + + actions = metadata['actions'] + + check = users.models.RuleSet.check_table_permission + + # Map the request method to a permission type + rolemap = { + 'GET': 'view', + 'OPTIONS': 'view', + 'POST': 'add', + 'PUT': 'change', + 'PATCH': 'change', + 'DELETE': 'delete', + } + + # Remove any HTTP methods that the user does not have permission for + for method, permission in rolemap.items(): + if method in actions and not check(user, table, permission): + del actions[method] + + except AttributeError: + # We will assume that if the serializer class does *not* have a Meta + # then we don't need a permission + pass + + return metadata diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index fceaf9a58f..be13411fd8 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -341,6 +341,7 @@ REST_FRAMEWORK = { 'InvenTree.permissions.RolePermission', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata' } WSGI_APPLICATION = 'InvenTree.wsgi.application' diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index fa2ca9eb2e..822688d9ad 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -23,7 +23,8 @@ */ function getApiEndpointOptions(url, options={}) { - $.ajax({ + // Return the ajax request object + return $.ajax({ url: url, type: 'OPTIONS', contentType: 'application/json', @@ -31,8 +32,7 @@ function getApiEndpointOptions(url, options={}) { accepts: { json: 'application/json', }, - success: function(response) { - console.log(response); - } }); } + + From 82a6ff777261fe3c0a19eab807996a3edbbd37df Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 20:58:05 +1000 Subject: [PATCH 008/178] Adds unit testing for fancy new metadata class --- InvenTree/InvenTree/api_tester.py | 16 +++++ InvenTree/InvenTree/metadata.py | 38 ++++++----- InvenTree/InvenTree/test_api.py | 65 +++++++++++++++++++ .../migrations/0029_auto_20210601_1525.py | 9 ++- .../migrations/0026_auto_20201110_1011.py | 10 ++- InvenTree/users/models.py | 2 +- 6 files changed, 119 insertions(+), 21 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index a803e6797f..2ba4f0136c 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -73,6 +73,22 @@ class InvenTreeAPITestCase(APITestCase): ruleset.save() break + def getActions(self, url): + """ + Return a dict of the 'actions' available at a given endpoint. + Makes use of the HTTP 'OPTIONS' method to request this. + """ + + response = self.client.options(url) + self.assertEqual(response.status_code, 200) + + actions = response.data.get('actions', None) + + if not actions: + actions = {} + + return actions + def get(self, url, data={}, expected_code=200): """ Issue a GET request diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index b9d0732acf..b78eaa9d8c 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -40,24 +40,32 @@ class InvenTreeMetadata(SimpleMetadata): table = f"{app_label}_{tbl_label}" - actions = metadata['actions'] + actions = metadata.get('actions', None) - check = users.models.RuleSet.check_table_permission + if actions is not None: - # Map the request method to a permission type - rolemap = { - 'GET': 'view', - 'OPTIONS': 'view', - 'POST': 'add', - 'PUT': 'change', - 'PATCH': 'change', - 'DELETE': 'delete', - } + check = users.models.RuleSet.check_table_permission - # Remove any HTTP methods that the user does not have permission for - for method, permission in rolemap.items(): - if method in actions and not check(user, table, permission): - del actions[method] + # Map the request method to a permission type + rolemap = { + 'POST': 'add', + 'PUT': 'change', + 'PATCH': 'change', + 'DELETE': 'delete', + } + + # Remove any HTTP methods that the user does not have permission for + for method, permission in rolemap.items(): + if method in actions and not check(user, table, permission): + del actions[method] + + # Add a 'DELETE' action if we are allowed to delete + if check(user, table, 'delete'): + actions['DELETE'] = True + + # Add a 'VIEW' action if we are allowed to view + if check(user, table, 'view'): + actions['GET'] = True except AttributeError: # We will assume that if the serializer class does *not* have a Meta diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 8435d756fb..a877300a27 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -157,3 +157,68 @@ class APITests(InvenTreeAPITestCase): # New role permissions should have been added now self.assertIn('delete', roles['part']) self.assertIn('change', roles['build']) + + def test_list_endpoint_actions(self): + """ + Tests for the OPTIONS method for API endpoints. + """ + + self.basicAuth() + + # Without any 'part' permissions, we should not see any available actions + url = reverse('api-part-list') + + actions = self.getActions(url) + + # No actions, as there are no permissions! + self.assertEqual(len(actions), 0) + + # Assign a new role + self.assignRole('part.view') + actions = self.getActions(url) + + # As we don't have "add" permission, there should be no available API actions + self.assertEqual(len(actions), 0) + + # But let's make things interesting... + # Why don't we treat ourselves to some "add" permissions + self.assignRole('part.add') + + actions = self.getActions(url) + + self.assertIn('POST', actions) + self.assertEqual(len(actions), 1) + + def test_detail_endpoint_actions(self): + """ + Tests for detail API endpoint actions + """ + + self.basicAuth() + + url = reverse('api-part-detail', kwargs={'pk': 1}) + + actions = self.getActions(url) + + # No actions, as we do not have any permissions! + self.assertEqual(len(actions), 0) + + # Add a 'add' permission + # Note: 'add' permission automatically implies 'change' also + self.assignRole('part.add') + + actions = self.getActions(url) + + # 'add' permission does not apply here! + self.assertEqual(len(actions), 1) + self.assertIn('PUT', actions.keys()) + + # Add some other permissions + self.assignRole('part.change') + self.assignRole('part.delete') + + actions = self.getActions(url) + + self.assertEqual(len(actions), 2) + self.assertIn('PUT', actions.keys()) + self.assertIn('DELETE', actions.keys()) diff --git a/InvenTree/build/migrations/0029_auto_20210601_1525.py b/InvenTree/build/migrations/0029_auto_20210601_1525.py index e8b2d58947..fa6bab6b26 100644 --- a/InvenTree/build/migrations/0029_auto_20210601_1525.py +++ b/InvenTree/build/migrations/0029_auto_20210601_1525.py @@ -1,8 +1,13 @@ # Generated by Django 3.2 on 2021-06-01 05:25 +import logging + from django.db import migrations +logger = logging.getLogger('inventree') + + def assign_bom_items(apps, schema_editor): """ Run through existing BuildItem objects, @@ -13,7 +18,7 @@ def assign_bom_items(apps, schema_editor): BomItem = apps.get_model('part', 'bomitem') Part = apps.get_model('part', 'part') - print("Assigning BomItems to existing BuildItem objects") + logger.info("Assigning BomItems to existing BuildItem objects") count_valid = 0 count_total = 0 @@ -41,7 +46,7 @@ def assign_bom_items(apps, schema_editor): pass if count_total > 0: - print(f"Assigned BomItem for {count_valid}/{count_total} entries") + logger.info(f"Assigned BomItem for {count_valid}/{count_total} entries") def unassign_bom_items(apps, schema_editor): diff --git a/InvenTree/company/migrations/0026_auto_20201110_1011.py b/InvenTree/company/migrations/0026_auto_20201110_1011.py index 20ec7d2f6f..29a5099c3a 100644 --- a/InvenTree/company/migrations/0026_auto_20201110_1011.py +++ b/InvenTree/company/migrations/0026_auto_20201110_1011.py @@ -1,5 +1,6 @@ # Generated by Django 3.0.7 on 2020-11-10 10:11 +import logging import sys from moneyed import CURRENCIES @@ -7,6 +8,9 @@ from django.db import migrations, connection from company.models import SupplierPriceBreak +logger = logging.getLogger('inventree') + + def migrate_currencies(apps, schema_editor): """ Migrate from the 'old' method of handling currencies, @@ -19,7 +23,7 @@ def migrate_currencies(apps, schema_editor): for the SupplierPriceBreak model, to a new django-money compatible currency. """ - print("Updating currency references for SupplierPriceBreak model...") + logger.info("Updating currency references for SupplierPriceBreak model...") # A list of available currency codes currency_codes = CURRENCIES.keys() @@ -39,7 +43,7 @@ def migrate_currencies(apps, schema_editor): suffix = suffix.strip().upper() if suffix not in currency_codes: - print("Missing suffix:", suffix) + logger.warning(f"Missing suffix: '{suffix}'") while suffix not in currency_codes: # Ask the user to input a valid currency @@ -72,7 +76,7 @@ def migrate_currencies(apps, schema_editor): count += 1 if count > 0: - print(f"Updated {count} SupplierPriceBreak rows") + logger.info(f"Updated {count} SupplierPriceBreak rows") def reverse_currencies(apps, schema_editor): """ diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 2763ba0e10..23353948b1 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -208,7 +208,7 @@ class RuleSet(models.Model): return True # Print message instead of throwing an error - print("Failed permission check for", table, permission) + logger.info(f"User '{user.name}' failed permission check for {table}.{permission}") return False @staticmethod From b8a3117c83952d5fdea2f481f8366c9835b549ad Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:21:39 +1000 Subject: [PATCH 009/178] Fix unit tests --- InvenTree/InvenTree/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index a877300a27..7a69e9da6f 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -219,6 +219,7 @@ class APITests(InvenTreeAPITestCase): actions = self.getActions(url) - self.assertEqual(len(actions), 2) + self.assertEqual(len(actions), 3) + self.assertIn('GET', actions.keys()) self.assertIn('PUT', actions.keys()) self.assertIn('DELETE', actions.keys()) From 2c1db2a902e00c4118c95a20bb77cebb70facb92 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:40:09 +1000 Subject: [PATCH 010/178] Further tweaks --- InvenTree/InvenTree/metadata.py | 4 ++-- InvenTree/InvenTree/test_api.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index b78eaa9d8c..ac06f79d3e 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -60,11 +60,11 @@ class InvenTreeMetadata(SimpleMetadata): del actions[method] # Add a 'DELETE' action if we are allowed to delete - if check(user, table, 'delete'): + if 'DELETE' in view.allowed_methods and check(user, table, 'delete'): actions['DELETE'] = True # Add a 'VIEW' action if we are allowed to view - if check(user, table, 'view'): + if 'GET' in view.allowed_methods and check(user, table, 'view'): actions['GET'] = True except AttributeError: diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 7a69e9da6f..7581ec4fec 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -186,8 +186,9 @@ class APITests(InvenTreeAPITestCase): actions = self.getActions(url) + self.assertEqual(len(actions), 2) self.assertIn('POST', actions) - self.assertEqual(len(actions), 1) + self.assertIn('GET', actions) def test_detail_endpoint_actions(self): """ From 0d9808fbb8f2582484d0c2d0e84e69545a1b4e19 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 21:41:19 +1000 Subject: [PATCH 011/178] Adds 'constructForm' javascript function - Skeleton only (for now!) --- InvenTree/templates/js/forms.js | 128 +++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 822688d9ad..bcdafd9401 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -16,15 +16,70 @@ * - min_value / max_value */ +/* + * Return true if the OPTIONS specify that the user + * can perform a GET method at the endpoint. + */ +function canView(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('GET' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a POST method at the endpoint + */ +function canCreate(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('POST' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a PUT or PATCH method at the endpoint + */ +function canChange(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('PUT' in OPTIONS.actions || 'PATCH' in OPTIONS.actions); + } else { + return false; + } +} + + +/* + * Return true if the OPTIONS specify that the user + * can perform a DELETE method at the endpoint + */ +function canDelete(OPTIONS) { + + if ('actions' in OPTIONS) { + return ('DELETE' in OPTIONS.actions); + } else { + return false; + } +} + /* * Get the API endpoint options at the provided URL, * using a HTTP options request. */ -function getApiEndpointOptions(url, options={}) { +function getApiEndpointOptions(url, callback, options={}) { // Return the ajax request object - return $.ajax({ + $.ajax({ url: url, type: 'OPTIONS', contentType: 'application/json', @@ -32,7 +87,76 @@ function getApiEndpointOptions(url, options={}) { accepts: { json: 'application/json', }, + success: callback, }); } +/* + * Request API OPTIONS data from the server, + * and construct a modal form based on the response. + * + * arguments: + * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE', + * + * options: + * - method: + */ +function constructForm(url, method, options={}) { + + method = method.toUpperCase(); + + // Request OPTIONS endpoint from the API + getApiEndpointOptions(url, function(OPTIONS) { + + /* + * Determine what "type" of form we want to construct, + * based on the requested action. + * + * First we must determine if the user has the correct permissions! + */ + + switch (method) { + case 'POST': + if (canCreate(OPTIONS)) { + console.log('create'); + } else { + // User does not have permission to POST to the endpoint + console.log('cannot POST'); + // TODO + } + break; + case 'PUT': + case 'PATCH': + if (canChange(OPTIONS)) { + console.log("change"); + } else { + // User does not have permission to PUT/PATCH to the endpoint + // TODO + console.log('cannot edit'); + } + break; + case 'DELETE': + if (canDelete(OPTIONS)) { + console.log('delete'); + } else { + // User does not have permission to DELETE to the endpoint + // TODO + console.log('cannot delete'); + } + break; + case 'GET': + if (canView(OPTIONS)) { + console.log('view'); + } else { + // User does not have permission to GET to the endpoint + // TODO + console.log('cannot view'); + } + break; + default: + console.log(`constructForm() called with invalid method '${method}'`); + break; + } + }); +} \ No newline at end of file From c387e1a6fc7829521fa8f53f26c32416bb177119 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 22:11:26 +1000 Subject: [PATCH 012/178] Working on functions to construct the various form components --- InvenTree/templates/js/forms.js | 136 +++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index bcdafd9401..8d74299975 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -119,7 +119,7 @@ function constructForm(url, method, options={}) { switch (method) { case 'POST': if (canCreate(OPTIONS)) { - console.log('create'); + constructCreateForm(url, OPTIONS.actions.POST); } else { // User does not have permission to POST to the endpoint console.log('cannot POST'); @@ -159,4 +159,138 @@ function constructForm(url, method, options={}) { break; } }); +} + + +/* + * Construct a 'creation' (POST) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructCreateForm(url, fields, options={}) { + + var html = ''; + + for (const key in fields) { + //console.log('field:', key); + + html += constructField(key, fields[key], options); + } + + var modal = '#modal-form'; + + modalEnable(modal, true); + + $(modal).find('.modal-form-content').html(html); + + $(modal).modal('show'); +} + + +/* + * Construct a single form 'field' for rendering in a form. + * + * arguments: + * - name: The 'name' of the field + * - parameters: The field parameters supplied by the DRF OPTIONS method + * + * options: + * - + * + * The function constructs a fieldset which mostly replicates django "crispy" forms: + * + * - Field name + * - Field (depends on specified field type) + * - Field description (help text) + * - Field errors + */ +function constructField(name, parameters, options={}) { + + var field_name = `id_${name}`; + + var html = `
    `; + + // Add a label + html += constructLabel(name, parameters); + + html += `
    `; + + html += constructInput(name, parameters, options); + html += constructHelpText(name, parameters, options); + + // TODO: Add the "error message" + + html += `
    `; // controls + + html += `
    `; // form-group + + return html; +} + + +/* + * Construct a 'label' div + * + * arguments: + * - name: The name of the field + * - required: Is this a required field? + */ +function constructLabel(name, parameters) { + + var label_classes = 'control-label'; + + if (parameters.required) { + label_classes += ' requiredField'; + } + + var html =''; + + html += ``; + + return html; +} + + +/* + * Construct a form input based on the field parameters + * + * arguments: + * - name: The name of the field + * - parameters: Field parameters returned by the OPTIONS method + * + */ +function constructInput(name, parameters, options={}) { + + var html = ''; + + // TODO: Construct an input field based on the field type! + + return html; +} + + +/* + * Construct a 'help text' div based on the field parameters + * + * arguments: + * - name: The name of the field + * - parameters: Field parameters returned by the OPTIONS method + * + */ +function constructHelpText(name, parameters, options={}) { + + var html = `
    ${parameters.help_text}
    `; + + return html; } \ No newline at end of file From aa0237766515358c5dec4fccc588b1d5707128d5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 22:25:53 +1000 Subject: [PATCH 013/178] Updates for field rendering --- InvenTree/templates/js/forms.js | 47 +++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 8d74299975..c741530523 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -176,9 +176,14 @@ function constructCreateForm(url, fields, options={}) { var html = ''; for (const key in fields) { - //console.log('field:', key); - html += constructField(key, fields[key], options); + var field = fields[key]; + + console.log(key, field.label, field.help_text); + + var f = constructField(key, field, options); + + html += f; } var modal = '#modal-form'; @@ -218,16 +223,19 @@ function constructField(name, parameters, options={}) { html += constructLabel(name, parameters); html += `
    `; - + html += constructInput(name, parameters, options); - html += constructHelpText(name, parameters, options); - + + if (parameters.help_text) { + html += constructHelpText(name, parameters, options); + } + // TODO: Add the "error message" - + html += `
    `; // controls - + html += `
    `; // form-group - + return html; } @@ -247,15 +255,18 @@ function constructLabel(name, parameters) { label_classes += ' requiredField'; } - var html =''; + var html = `
    @@ -243,6 +250,33 @@ {% block js_ready %} {{ block.super }} + + loadSupplierPartTable( + "#supplier-table", + "{% url 'api-supplier-part-list' %}", + { + params: { + part: {{ part.id }}, + part_detail: false, + supplier_detail: true, + manufacturer_detail: true, + }, + } + ); + + loadManufacturerPartTable( + "#manufacturer-table", + "{% url 'api-manufacturer-part-list' %}", + { + params: { + part: {{ part.id }}, + part_detail: false, + manufacturer_detail: true, + }, + } + ); + + // history graphs {% default_currency as currency %} {% if price_history %} @@ -405,7 +439,7 @@ }) {% endif %} - + // Load the BOM table data loadBomTable($("#bom-table"), { editable: {{ editing_enabled }}, From 6162129e3deb43690f6c0701d051b2dd5114a3ae Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 23:36:38 +1000 Subject: [PATCH 021/178] Support choice field --- InvenTree/templates/js/forms.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 6ca4f0560c..75b65fd885 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -193,6 +193,7 @@ function constructCreateForm(url, fields, options={}) { $(modal).modal('show'); attachToggle(modal); + attachSelect(modal); } @@ -318,7 +319,7 @@ function constructInput(name, parameters, options={}) { func = constructNumberInput; break; case 'choice': - // TODO: choice field + func = constructChoiceInput; break; case 'field': // TODO: foreign key field! @@ -440,6 +441,30 @@ function constructNumberInput(name, parameters, options={}) { } +// Construct a "choice" input +function constructChoiceInput(name, parameters, options={}) { + + var html = ``; + + return html; +} + + /* * Construct a 'help text' div based on the field parameters * From 4009ec844fe24a407d265889013272e244aacbb2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 23:42:56 +1000 Subject: [PATCH 022/178] Test fixes --- InvenTree/InvenTree/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index fc9177e5c2..c56412ea68 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -38,7 +38,7 @@ class APITests(InvenTreeAPITestCase): auth = b64encode(authstring).decode("ascii") self.client.credentials(HTTP_AUTHORIZATION="Basic {auth}".format(auth=auth)) - def test_tokenAuth(self): + def tokenAuth(self): self.basicAuth() token_url = reverse('api-token') From e7bc53a54847150c6fe556318d1821811aab9af4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Jun 2021 23:51:11 +1000 Subject: [PATCH 023/178] Working on a 'update' form - Fetch existing data from the API --- InvenTree/templates/js/forms.js | 70 +++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 75b65fd885..2db0fdc8dc 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -88,6 +88,10 @@ function getApiEndpointOptions(url, callback, options={}) { json: 'application/json', }, success: callback, + error: function(request, status, error) { + // TODO: Handle error + console.log(`ERROR in getApiEndpointOptions at '${url}'`); + } }); } @@ -106,6 +110,9 @@ function constructForm(url, method, options={}) { method = method.toUpperCase(); + // Store the method in the options struct + options.method = method; + // Request OPTIONS endpoint from the API getApiEndpointOptions(url, function(OPTIONS) { @@ -119,7 +126,7 @@ function constructForm(url, method, options={}) { switch (method) { case 'POST': if (canCreate(OPTIONS)) { - constructCreateForm(url, OPTIONS.actions.POST); + constructCreateForm(url, OPTIONS.actions.POST, options); } else { // User does not have permission to POST to the endpoint console.log('cannot POST'); @@ -129,7 +136,7 @@ function constructForm(url, method, options={}) { case 'PUT': case 'PATCH': if (canChange(OPTIONS)) { - console.log("change"); + constructChangeForm(url, OPTIONS.actions.PUT, options); } else { // User does not have permission to PUT/PATCH to the endpoint // TODO @@ -177,6 +184,11 @@ function constructCreateForm(url, fields, options={}) { for (const key in fields) { + // Ignore any PK fields + if (key.toLowerCase() in ['pk', 'id']) { + continue; + } + var field = fields[key]; var f = constructField(key, field, options); @@ -197,6 +209,47 @@ function constructCreateForm(url, fields, options={}) { } +/* + * Construct a 'change' (PATCH) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructChangeForm(url, fields, options={}) { + + // Request existing data from the API endpoint + $.ajax({ + url: url, + type: 'GET', + contentType: 'application/json', + dataType: 'json', + accepts: { + json: 'application/json', + }, + success: function(data) { + + // Push existing 'value' to each field + for (const field in data) { + + if (field in fields) { + fields[field].value = data[field]; + } + } + + constructCreateForm(url, fields, options); + }, + error: function(request, status, error) { + // TODO: Handle error here + console.log(`ERROR in constructChangeForm at '${url}'`); + } + }) + +} + + /* * Construct a single form 'field' for rendering in a form. * @@ -352,6 +405,11 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`type='${type}'`); + // Existing value? + if (parameters.value) { + opts.push(`value='${parameters.value}'`); + } + // Maximum input length if (parameters.max_length) { opts.push(`maxlength='${parameters.max_length}'`); @@ -454,7 +512,13 @@ function constructChoiceInput(name, parameters, options={}) { var choice = choices[idx]; - html += ``; } From 1754af3d4392ecca28b13281b5e047ff614e51e9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 00:00:20 +1000 Subject: [PATCH 024/178] Adds ability to specify which fields are displayed --- InvenTree/templates/js/forms.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 2db0fdc8dc..1e01de0cc3 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -182,10 +182,25 @@ function constructCreateForm(url, fields, options={}) { var html = ''; + var allowed_fields = options.fields || null; + var ignored_fields = options.ignore || []; + + if (!ignored_fields.includes('pk')) { + ignored_fields.push('pk'); + } + + if (!ignored_fields.includes('id')) { + ignored_fields.push('id'); + } + for (const key in fields) { - // Ignore any PK fields - if (key.toLowerCase() in ['pk', 'id']) { + // Skip over fields + if (allowed_fields && !allowed_fields.includes(key)) { + continue; + } + + if (ignored_fields && ignored_fields.includes(key)) { continue; } @@ -405,9 +420,12 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`type='${type}'`); - // Existing value? if (parameters.value) { + // Existing value? opts.push(`value='${parameters.value}'`); + } else if (parameters.default) { + // Otherwise, a defualt value? + opts.push(`value='${parameters.default}'`); } // Maximum input length From 9f3f07aff389a25145d9b22ceac6b628cd88bb3d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 00:06:27 +1000 Subject: [PATCH 025/178] Refactor toot-toot - Now can specify the "order" of fields --- InvenTree/templates/js/forms.js | 40 +++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 1e01de0cc3..6600397ba3 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -193,23 +193,39 @@ function constructCreateForm(url, fields, options={}) { ignored_fields.push('id'); } - for (const key in fields) { - - // Skip over fields - if (allowed_fields && !allowed_fields.includes(key)) { - continue; - } + // Construct an ordered list of field names + var field_names = []; - if (ignored_fields && ignored_fields.includes(key)) { - continue; - } + if (allowed_fields) { + allowed_fields.forEach(function(name) { - var field = fields[key]; + // Only push names which are actually in the set of fields + if (name in fields) { + + if (!ignored_fields.includes(name) && !field_names.includes(name)) { + field_names.push(name); + } + } else { + console.log(`WARNING: '${name}' does not match a valid field name.`); + } + }); + } else { + for (const name in fields) { + + if (!ignored_fields.includes(name) && !field_names.includes(name)) { + field_names.push(name); + } + } + } + + field_names.forEach(function(name) { + + var field = fields[name]; - var f = constructField(key, field, options); + var f = constructField(name, field, options); html += f; - } + }); var modal = '#modal-form'; From c8085ad39da1212493f37b12069514e4e6ade58e Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 00:13:40 +1000 Subject: [PATCH 026/178] Skip nested objects --- InvenTree/templates/js/forms.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 6600397ba3..d5a5f16487 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -218,14 +218,24 @@ function constructCreateForm(url, fields, options={}) { } } - field_names.forEach(function(name) { + for (var idx = 0; idx < field_names.length; idx++) { + + var name = field_names[idx]; var field = fields[name]; - + + // Skip field types which are simply not supported + switch (field.type) { + case 'nested object': + continue; + default: + break; + } + var f = constructField(name, field, options); html += f; - }); + } var modal = '#modal-form'; From 9feef935f463be08f246592bf6dbeed1cee9a33c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 00:16:30 +1000 Subject: [PATCH 027/178] Readonly fields --- InvenTree/templates/js/forms.js | 35 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index d5a5f16487..47e3337a20 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -169,16 +169,8 @@ function constructForm(url, method, options={}) { } -/* - * Construct a 'creation' (POST) form, to create a new model in the database. - * - * arguments: - * - fields: The 'actions' object provided by the OPTIONS endpoint - * - * options: - * - - */ -function constructCreateForm(url, fields, options={}) { + +function constructFormBody(url, fields, options={}) { var html = ''; @@ -250,6 +242,22 @@ function constructCreateForm(url, fields, options={}) { } +/* + * Construct a 'creation' (POST) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructCreateForm(url, fields, options={}) { + + // We should have enough information to create the form! + constructFormBody(url, fields, options); +} + + /* * Construct a 'change' (PATCH) form, to create a new model in the database. * @@ -280,7 +288,7 @@ function constructChangeForm(url, fields, options={}) { } } - constructCreateForm(url, fields, options); + constructFormBody(url, fields, options); }, error: function(request, status, error) { // TODO: Handle error here @@ -446,6 +454,11 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`type='${type}'`); + // Read only? + if (parameters.read_only) { + opts.push(`readonly=''`); + } + if (parameters.value) { // Existing value? opts.push(`value='${parameters.value}'`); From 9f27a77689904901f1d57e55f911af1e65e8762c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 01:19:09 +0200 Subject: [PATCH 028/178] price break js refactor --- InvenTree/part/templates/part/prices.html | 171 ++-------------------- InvenTree/templates/js/part.js | 99 +++++++++++++ 2 files changed, 115 insertions(+), 155 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 21bc8d405a..749b5cb928 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -359,84 +359,15 @@ // Internal pricebreaks {% settings_value "PART_INTERNAL_PRICE" as show_internal_price %} {% if show_internal_price and roles.sales_order.view %} - function reloadPriceBreaks() { - $("#internal-price-break-table").bootstrapTable("refresh"); - } - - $('#new-internal-price-break').click(function() { - launchModalForm("{% url 'internal-price-break-create' %}", - { - success: reloadPriceBreaks, - data: { - part: {{ part.id }}, - } - } + initPriceBreakSet( + $('#internal-price-break-table'), + {{part.id}}, + 'internal price break', + 'internal-price', + "{% url 'api-part-internal-price-list' %}", + $('#new-internal-price-break'), + '{% url 'internal-price-break-create' %}' ); - }); - - $('#internal-price-break-table').inventreeTable({ - name: 'internalprice', - formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; }, - queryParams: { - part: {{ part.id }}, - }, - url: "{% url 'api-part-internal-price-list' %}", - onPostBody: function() { - var table = $('#internal-price-break-table'); - - table.find('.button-internal-price-break-delete').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/internal-price/${pk}/delete/`, - { - success: reloadPriceBreaks - } - ); - }); - - table.find('.button-internal-price-break-edit').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/internal-price/${pk}/edit/`, - { - success: reloadPriceBreaks - } - ); - }); - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true, - }, - { - field: 'price', - title: '{% trans "Price" %}', - sortable: true, - formatter: function(value, row, index) { - var html = value; - - html += `
    ` - - html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}'); - - html += `
    `; - - return html; - } - }, - ] - }) {% endif %} @@ -452,85 +383,15 @@ // Sales pricebreaks {% if part.salable and roles.sales_order.view %} - function reloadPriceBreaks() { - $("#price-break-table").bootstrapTable("refresh"); - } - - $('#new-price-break').click(function() { - launchModalForm("{% url 'sale-price-break-create' %}", - { - success: reloadPriceBreaks, - data: { - part: {{ part.id }}, - } - } + initPriceBreakSet( + $('#price-break-table'), + {{part.id}}, + 'sale price break', + 'sale-price', + "{% url 'api-part-sale-price-list' %}", + $('#new-price-break'), + '{% url 'sale-price-break-create' %}' ); - }); - - $('#price-break-table').inventreeTable({ - name: 'saleprice', - formatNoMatches: function() { return "{% trans 'No price break information found' %}"; }, - queryParams: { - part: {{ part.id }}, - }, - url: "{% url 'api-part-sale-price-list' %}", - onPostBody: function() { - var table = $('#price-break-table'); - - table.find('.button-price-break-delete').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/sale-price/${pk}/delete/`, - { - success: reloadPriceBreaks - } - ); - }); - - table.find('.button-price-break-edit').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm( - `/part/sale-price/${pk}/edit/`, - { - success: reloadPriceBreaks - } - ); - }); - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true, - }, - { - field: 'price', - title: '{% trans "Price" %}', - sortable: true, - formatter: function(value, row, index) { - var html = value; - - html += `
    ` - - html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}'); - - html += `
    `; - - return html; - } - }, - ] - }) - {% endif %} {% endblock %} diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 66174e2f15..82fd416e15 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -769,6 +769,105 @@ function loadPartTestTemplateTable(table, options) { } +function loadPriceBreakTable(table, options) { + /* + * Load PriceBreak table. + */ + + var name = options.name || 'pricebreak'; + var human_name = options.human_name || 'price break'; + + table.inventreeTable({ + name: name, + method: 'get', + formatNoMatches: function() { + return `{% trans "No ${human_name} information found" %}`; + }, + url: options.url, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + sortable: true, + }, + { + field: 'price', + title: '{% trans "Price" %}', + sortable: true, + formatter: function(value, row, index) { + var html = value; + + html += `
    ` + + html += makeIconButton('fa-edit icon-blue', `button-${name}-edit`, row.pk, `{% trans "Edit ${human_name}" %}`); + html += makeIconButton('fa-trash-alt icon-red', `button-${name}-delete`, row.pk, `{% trans "Delete ${human_name}" %}`); + + html += `
    `; + + return html; + } + }, + ] + }); +} + + +function initPriceBreakSet(table, part_id, pb_human_name, pb_url_slug, pb_url, pb_new_btn, pb_new_url) { + + loadPriceBreakTable( + table, + { + name: pb_url_slug, + human_name: pb_human_name, + url: pb_url, + } + ); + + function reloadPriceBreakTable(){ + table.bootstrapTable("refresh"); + } + + pb_new_btn.click(function() { + launchModalForm(pb_new_url, + { + success: reloadPriceBreakTable, + data: { + part: part_id, + } + } + ); + }); + + table.on('click', `.button-${pb_url_slug}-delete`, function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/${pb_url_slug}/${pk}/delete/`, + { + success: reloadPriceBreakTable + } + ); + }); + + table.on('click', `.button-${pb_url_slug}-edit`, function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/part/${pb_url_slug}/${pk}/edit/`, + { + success: reloadPriceBreakTable + } + ); + }); +} + + function loadStockPricingChart(context, data) { return new Chart(context, { type: 'bar', From b350a971a431d35726ae2c95db674b6e256d28df Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 13:12:46 +1000 Subject: [PATCH 029/178] Working on custom field info in metadata class --- InvenTree/InvenTree/metadata.py | 70 +++++++++++++++++++++++++++++++++ InvenTree/templates/js/forms.js | 11 ++++++ 2 files changed, 81 insertions(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index ac06f79d3e..412b001cdd 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -2,6 +2,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.db import models + +from collections import OrderedDict + +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.utils.encoding import force_str + +from rest_framework import exceptions, serializers, fields +from rest_framework.request import clone_request +from rest_framework.utils.field_mapping import ClassLookupDict + from rest_framework.metadata import SimpleMetadata import users.models @@ -17,6 +29,9 @@ class InvenTreeMetadata(SimpleMetadata): Thus when a client send an OPTIONS request to an API endpoint, it will only receive a list of actions which it is allowed to perform! + Additionally, we include some extra information about database models, + so we can perform lookup for ForeignKey related fields. + """ def determine_metadata(self, request, view): @@ -73,3 +88,58 @@ class InvenTreeMetadata(SimpleMetadata): pass return metadata + + def get_field_info(self, field): + """ + Given an instance of a serializer field, return a dictionary + of metadata about it. + """ + + field_info = OrderedDict() + + field_info['type'] = self.label_lookup[field] + field_info['required'] = getattr(field, 'required', False) + + if field_info['type'] == 'field': + + # If the field is a 'ForeignKey' field + if isinstance(field, serializers.ModelSerializer): # or isinstance(field, serializers.PrimaryKeyRelatedField): + model = field.Meta.model + + # Construct the 'table name' from the model + app_label = model._meta.app_label + tbl_label = model._meta.model_name + + table = f"{app_label}_{tbl_label}" + field_info['model'] = table + + print(field_info['type'], field, type(field)) + + attrs = [ + 'read_only', 'label', 'help_text', + 'min_length', 'max_length', + 'min_value', 'max_value' + ] + + for attr in attrs: + value = getattr(field, attr, None) + if value is not None and value != '': + field_info[attr] = force_str(value, strings_only=True) + + if getattr(field, 'child', None): + field_info['child'] = self.get_field_info(field.child) + elif getattr(field, 'fields', None): + field_info['children'] = self.get_serializer_info(field) + + if (not field_info.get('read_only') and + not isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField)) and + hasattr(field, 'choices')): + field_info['choices'] = [ + { + 'value': choice_value, + 'display_name': force_str(choice_name, strings_only=True) + } + for choice_value, choice_name in field.choices.items() + ] + + return field_info \ No newline at end of file diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 47e3337a20..ffc7dc8de8 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1,3 +1,6 @@ +{% load i18n %} +{% load inventree_extras %} + /** * This file contains code for rendering (and managing) HTML forms * which are served via the django-drf API. @@ -229,16 +232,24 @@ function constructFormBody(url, fields, options={}) { html += f; } + // TODO: Dynamically create the modals, + // so that we can have an infinite number of stacks! var modal = '#modal-form'; modalEnable(modal, true); + var title = options.title || '{% trans "Form Title" %}'; + + modalSetTitle(modal, title); + $(modal).find('.modal-form-content').html(html); $(modal).modal('show'); attachToggle(modal); attachSelect(modal); + + modalShowSubmitButton(modal, true); } From b273dc613b66ab070f54528b840c79d707abd437 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Jun 2021 23:15:19 +1000 Subject: [PATCH 030/178] Scratch that --- InvenTree/InvenTree/metadata.py | 55 --------------------------------- 1 file changed, 55 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 412b001cdd..b344053db0 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -88,58 +88,3 @@ class InvenTreeMetadata(SimpleMetadata): pass return metadata - - def get_field_info(self, field): - """ - Given an instance of a serializer field, return a dictionary - of metadata about it. - """ - - field_info = OrderedDict() - - field_info['type'] = self.label_lookup[field] - field_info['required'] = getattr(field, 'required', False) - - if field_info['type'] == 'field': - - # If the field is a 'ForeignKey' field - if isinstance(field, serializers.ModelSerializer): # or isinstance(field, serializers.PrimaryKeyRelatedField): - model = field.Meta.model - - # Construct the 'table name' from the model - app_label = model._meta.app_label - tbl_label = model._meta.model_name - - table = f"{app_label}_{tbl_label}" - field_info['model'] = table - - print(field_info['type'], field, type(field)) - - attrs = [ - 'read_only', 'label', 'help_text', - 'min_length', 'max_length', - 'min_value', 'max_value' - ] - - for attr in attrs: - value = getattr(field, attr, None) - if value is not None and value != '': - field_info[attr] = force_str(value, strings_only=True) - - if getattr(field, 'child', None): - field_info['child'] = self.get_field_info(field.child) - elif getattr(field, 'fields', None): - field_info['children'] = self.get_serializer_info(field) - - if (not field_info.get('read_only') and - not isinstance(field, (serializers.RelatedField, serializers.ManyRelatedField)) and - hasattr(field, 'choices')): - field_info['choices'] = [ - { - 'value': choice_value, - 'display_name': force_str(choice_name, strings_only=True) - } - for choice_value, choice_name in field.choices.items() - ] - - return field_info \ No newline at end of file From 04374c71c211046e09e381f666f35f6b56a9ac0d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 00:17:58 +1000 Subject: [PATCH 031/178] Annotate models with their API list view - It will make sense, trust me --- InvenTree/build/models.py | 8 ++++ InvenTree/company/api.py | 2 +- InvenTree/company/models.py | 20 ++++++++++ .../company/supplier_part_pricing.html | 2 +- InvenTree/label/models.py | 9 +++++ InvenTree/order/models.py | 29 +++++++++++++++ InvenTree/part/models.py | 37 +++++++++++++++++++ InvenTree/part/serializers.py | 6 ++- InvenTree/report/models.py | 21 +++++++++++ InvenTree/stock/api.py | 2 +- InvenTree/stock/models.py | 20 ++++++++++ InvenTree/stock/templates/stock/item.html | 2 +- 12 files changed, 153 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index fad5a2934d..848d774d1c 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -60,6 +60,10 @@ class Build(MPTTModel): responsible: User (or group) responsible for completing the build """ + @staticmethod + def get_api_url(): + return reverse('api-build-list') + OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) class Meta: @@ -1117,6 +1121,10 @@ class BuildItem(models.Model): quantity: Number of units allocated """ + @staticmethod + def get_api_url(): + return reverse('api-build-item-list') + def get_absolute_url(self): # TODO - Fix! return '/build/item/{pk}/'.format(pk=self.id) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 6cd1e83dfa..16887414de 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -424,7 +424,7 @@ company_api_urls = [ url(r'^part/', include(supplier_part_api_urls)), - url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price'), + url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'), url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 093d545f78..1c2306fc6a 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -84,6 +84,10 @@ class Company(models.Model): currency_code: Specifies the default currency for the company """ + @staticmethod + def get_api_url(): + return reverse('api-company-list') + class Meta: ordering = ['name', ] constraints = [ @@ -297,6 +301,10 @@ class ManufacturerPart(models.Model): description: Descriptive notes field """ + @staticmethod + def get_api_url(): + return reverse('api-manufacturer-part-list') + class Meta: unique_together = ('part', 'manufacturer', 'MPN') @@ -380,6 +388,10 @@ class ManufacturerPartParameter(models.Model): Each parameter is a simple string (text) value. """ + @staticmethod + def get_api_url(): + return reverse('api-manufacturer-part-parameter-list') + class Meta: unique_together = ('manufacturer_part', 'name') @@ -432,6 +444,10 @@ class SupplierPart(models.Model): packaging: packaging that the part is supplied in, e.g. "Reel" """ + @staticmethod + def get_api_url(): + return reverse('api-supplier-part-list') + def get_absolute_url(self): return reverse('supplier-part-detail', kwargs={'pk': self.id}) @@ -660,6 +676,10 @@ class SupplierPriceBreak(common.models.PriceBreak): currency: Reference to the currency of this pricebreak (leave empty for base currency) """ + @staticmethod + def get_api_url(): + return reverse('api-part-supplier-price-list') + part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks', verbose_name=_('Part'),) class Meta: diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 9da3f3df7e..89b33049c6 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -39,7 +39,7 @@ $('#price-break-table').inventreeTable({ queryParams: { part: {{ part.id }}, }, - url: "{% url 'api-part-supplier-price' %}", + url: "{% url 'api-part-supplier-price-list' %}", onPostBody: function() { var table = $('#price-break-table'); diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index a5d8314193..8a6684d7e3 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -12,6 +12,7 @@ import datetime from django.conf import settings from django.db import models +from django.urls import reverse from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.exceptions import ValidationError, FieldError @@ -237,6 +238,10 @@ class StockItemLabel(LabelTemplate): Template for printing StockItem labels """ + @staticmethod + def get_api_url(): + return reverse('api-stockitem-label-list') + SUBDIR = "stockitem" filters = models.CharField( @@ -290,6 +295,10 @@ class StockLocationLabel(LabelTemplate): Template for printing StockLocation labels """ + @staticmethod + def get_api_url(): + return reverse('api-stocklocation-label-list') + SUBDIR = "stocklocation" filters = models.CharField( diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index c150331f94..1aa34838b7 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -136,6 +136,10 @@ class PurchaseOrder(Order): target_date: Expected delivery target date for PurchaseOrder completion (optional) """ + @staticmethod + def get_api_url(): + return reverse('api-po-list') + OVERDUE_FILTER = Q(status__in=PurchaseOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) @staticmethod @@ -407,6 +411,10 @@ class SalesOrder(Order): target_date: Target date for SalesOrder completion (optional) """ + @staticmethod + def get_api_url(): + return reverse('api-so-list') + OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) @staticmethod @@ -585,6 +593,10 @@ class PurchaseOrderAttachment(InvenTreeAttachment): Model for storing file attachments against a PurchaseOrder object """ + @staticmethod + def get_api_url(): + return reverse('api-po-attachment-list') + def getSubdir(self): return os.path.join("po_files", str(self.order.id)) @@ -596,6 +608,10 @@ class SalesOrderAttachment(InvenTreeAttachment): Model for storing file attachments against a SalesOrder object """ + @staticmethod + def get_api_url(): + return reverse('api-so-attachment-list') + def getSubdir(self): return os.path.join("so_files", str(self.order.id)) @@ -629,6 +645,11 @@ class PurchaseOrderLineItem(OrderLineItem): """ + @staticmethod + def get_api_url(): + return reverse('api-po-line-list') + + class Meta: unique_together = ( ('order', 'part') @@ -712,6 +733,10 @@ class SalesOrderLineItem(OrderLineItem): sale_price: The unit sale price for this OrderLineItem """ + @staticmethod + def get_api_url(): + return reverse('api-so-line-list') + order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order')) part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) @@ -774,6 +799,10 @@ class SalesOrderAllocation(models.Model): """ + @staticmethod + def get_api_url(): + return reverse('api-so-allocation-list') + class Meta: unique_together = [ # Cannot allocate any given StockItem to the same line more than once diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8ddf049216..b979f7ece5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -75,6 +75,10 @@ class PartCategory(InvenTreeTree): default_keywords = models.CharField(null=True, blank=True, max_length=250, verbose_name=_('Default keywords'), help_text=_('Default keywords for parts in this category')) + @staticmethod + def get_api_url(): + return reverse('api-part-category-list') + def get_absolute_url(self): return reverse('category-detail', kwargs={'pk': self.id}) @@ -329,6 +333,11 @@ class Part(MPTTModel): # For legacy reasons the 'variant_of' field is used to indicate the MPTT parent parent_attr = 'variant_of' + @staticmethod + def get_api_url(): + + return reverse('api-part-list') + def get_context_data(self, request, **kwargs): """ Return some useful context data about this part for template rendering @@ -1966,6 +1975,10 @@ class PartAttachment(InvenTreeAttachment): Model for storing file attachments against a Part object """ + @staticmethod + def get_api_url(): + return reverse('api-part-attachment-list') + def getSubdir(self): return os.path.join("part_files", str(self.part.id)) @@ -1977,6 +1990,10 @@ class PartSellPriceBreak(common.models.PriceBreak): """ Represents a price break for selling this part """ + + @staticmethod + def get_api_url(): + return reverse('api-part-sale-price-list') part = models.ForeignKey( Part, on_delete=models.CASCADE, @@ -1994,6 +2011,10 @@ class PartInternalPriceBreak(common.models.PriceBreak): Represents a price break for internally selling this part """ + @staticmethod + def get_api_url(): + return reverse('api-part-internal-price-list') + part = models.ForeignKey( Part, on_delete=models.CASCADE, related_name='internalpricebreaks', @@ -2038,6 +2059,10 @@ class PartTestTemplate(models.Model): run on the model (refer to the validate_unique function). """ + @staticmethod + def get_api_url(): + return reverse('api-part-test-template-list') + def save(self, *args, **kwargs): self.clean() @@ -2136,6 +2161,10 @@ class PartParameterTemplate(models.Model): units: The units of the Parameter [string] """ + @staticmethod + def get_api_url(): + return reverse('api-part-param-template-list') + def __str__(self): s = str(self.name) if self.units: @@ -2173,6 +2202,10 @@ class PartParameter(models.Model): data: The data (value) of the Parameter [string] """ + @staticmethod + def get_api_url(): + return reverse('api-part-param-list') + def __str__(self): # String representation of a PartParameter (used in the admin interface) return "{part} : {param} = {data}{units}".format( @@ -2264,6 +2297,10 @@ class BomItem(models.Model): allow_variants: Stock for part variants can be substituted for this BomItem """ + @staticmethod + def get_api_url(): + return reverse('api-bom-list') + def save(self, *args, **kwargs): self.clean() diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 6c47f1310f..e73ff54c25 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -4,6 +4,7 @@ JSON serializers for Part app import imghdr from decimal import Decimal +from django.urls import reverse_lazy from django.db import models from django.db.models import Q from django.db.models.functions import Coalesce @@ -187,6 +188,9 @@ class PartSerializer(InvenTreeModelSerializer): Used when displaying all details of a single component. """ + def get_api_url(self): + return reverse_lazy('api-part-list') + def __init__(self, *args, **kwargs): """ Custom initialization method for PartSerializer, @@ -305,7 +309,7 @@ class PartSerializer(InvenTreeModelSerializer): image = InvenTreeImageSerializerField(required=False, allow_null=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - starred = serializers.SerializerMethodField() + starred = serializers.BooleanField() # PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...) category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index ecb4d91492..c33347f643 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -11,6 +11,7 @@ import logging import datetime +from django.urls import reverse from django.db import models from django.conf import settings from django.core.exceptions import ValidationError, FieldError @@ -307,6 +308,10 @@ class TestReport(ReportTemplateBase): Render a TestReport against a StockItem object. """ + @staticmethod + def get_api_url(): + return reverse('api-stockitem-testreport-list') + @classmethod def getSubdir(cls): return 'test' @@ -361,6 +366,10 @@ class BuildReport(ReportTemplateBase): Build order / work order report """ + @staticmethod + def get_api_url(): + return reverse('api-build-report-list') + @classmethod def getSubdir(cls): return 'build' @@ -400,6 +409,10 @@ class BillOfMaterialsReport(ReportTemplateBase): Render a Bill of Materials against a Part object """ + @staticmethod + def get_api_url(): + return reverse('api-bom-report-list') + @classmethod def getSubdir(cls): return 'bom' @@ -430,6 +443,10 @@ class PurchaseOrderReport(ReportTemplateBase): Render a report against a PurchaseOrder object """ + @staticmethod + def get_api_url(): + return reverse('api-po-report-list') + @classmethod def getSubdir(cls): return 'purchaseorder' @@ -464,6 +481,10 @@ class SalesOrderReport(ReportTemplateBase): Render a report against a SalesOrder object """ + @staticmethod + def get_api_url(): + return reverse('api-so-report-list') + @classmethod def getSubdir(cls): return 'salesorder' diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 3fc440cae4..d1add67d9d 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1148,7 +1148,7 @@ stock_api_urls = [ url(r'^$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'), ])), - url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'), + url(r'track/?', StockTrackingList.as_view(), name='api-stock-tracking-list'), url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'), diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 9786b360d9..a29630b4d7 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -52,6 +52,10 @@ class StockLocation(InvenTreeTree): Stock locations can be heirarchical as required """ + @staticmethod + def get_api_url(): + return reverse('api-location-list') + owner = models.ForeignKey(Owner, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Owner'), help_text=_('Select Owner'), @@ -161,6 +165,10 @@ class StockItem(MPTTModel): packaging: Description of how the StockItem is packaged (e.g. "reel", "loose", "tape" etc) """ + @staticmethod + def get_api_url(): + return reverse('api-stock-list') + # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( quantity__gt=0, @@ -1608,6 +1616,10 @@ class StockItemAttachment(InvenTreeAttachment): Model for storing file attachments against a StockItem object. """ + @staticmethod + def get_api_url(): + return reverse('api-stock-attachment-list') + def getSubdir(self): return os.path.join("stock_files", str(self.stock_item.id)) @@ -1639,6 +1651,10 @@ class StockItemTracking(models.Model): deltas: The changes associated with this history item """ + @staticmethod + def get_api_url(): + return reverse('api-stock-tracking-list') + def get_absolute_url(self): return '/stock/track/{pk}'.format(pk=self.id) @@ -1697,6 +1713,10 @@ class StockItemTestResult(models.Model): date: Date the test result was recorded """ + @staticmethod + def get_api_url(): + return reverse('api-stock-test-result-list') + def save(self, *args, **kwargs): super().clean() diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 5d551c29fb..7564e7864e 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -57,7 +57,7 @@ item: {{ item.pk }}, user_detail: true, }, - url: "{% url 'api-stock-track' %}", + url: "{% url 'api-stock-tracking-list' %}", }); {% endblock %} \ No newline at end of file From 970a5d5eedc44db40a00db55cb4a3e63bb9a5a72 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 00:36:22 +1000 Subject: [PATCH 032/178] Include API endpoints in OPTIONS metadata --- InvenTree/InvenTree/metadata.py | 46 ++++++++++++++++++++++++--------- InvenTree/templates/js/forms.js | 23 +++++++++++++++-- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index b344053db0..5ca87b9eee 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -1,24 +1,18 @@ - # -*- coding: utf-8 -*- + from __future__ import unicode_literals -from django.db import models - -from collections import OrderedDict - -from django.core.exceptions import PermissionDenied -from django.http import Http404 -from django.utils.encoding import force_str - -from rest_framework import exceptions, serializers, fields -from rest_framework.request import clone_request -from rest_framework.utils.field_mapping import ClassLookupDict +import logging +from rest_framework import serializers from rest_framework.metadata import SimpleMetadata import users.models +logger = logging.getLogger('inventree') + + class InvenTreeMetadata(SimpleMetadata): """ Custom metadata class for the DRF API. @@ -88,3 +82,31 @@ class InvenTreeMetadata(SimpleMetadata): pass return metadata + + def get_field_info(self, field): + """ + Given an instance of a serializer field, return a dictionary + of metadata about it. + + We take the regular DRF metadata and add our own unique flavor + """ + + # Run super method first + field_info = super().get_field_info(field) + + # Introspect writable related fields + if field_info['type'] == 'field' and not field_info['read_only']: + + # If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset + if isinstance(field, serializers.PrimaryKeyRelatedField): + model = field.queryset.model + else: + logger.debug("Could not extract model for:", field_info['label'], '->', field) + model = None + + if model: + # Mark this field as "related", and point to the URL where we can get the data! + field_info['type'] = 'related field' + field_info['api_url'] = model.get_api_url() + + return field_info diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index ffc7dc8de8..64a062c752 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -213,6 +213,8 @@ function constructFormBody(url, fields, options={}) { } } + // Render selected fields + for (var idx = 0; idx < field_names.length; idx++) { var name = field_names[idx]; @@ -242,6 +244,7 @@ function constructFormBody(url, fields, options={}) { modalSetTitle(modal, title); + // Insert generated form content $(modal).find('.modal-form-content').html(html); $(modal).modal('show'); @@ -434,8 +437,8 @@ function constructInput(name, parameters, options={}) { case 'choice': func = constructChoiceInput; break; - case 'field': - // TODO: foreign key field! + case 'related field': + func = constructRelatedFieldInput; break; default: // Unsupported field type! @@ -597,6 +600,22 @@ function constructChoiceInput(name, parameters, options={}) { } +/* + * Construct a "related field" input. + * This will create a "select" input which will then, (after form is loaded), + * be converted into a select2 input. + * This will then be served custom data from the API (as required)... + */ +function constructRelatedFieldInput(name, parameters, options={}) { + + var html = ``; + + // Don't load any options - they will be filled via an AJAX request + + return html; +} + + /* * Construct a 'help text' div based on the field parameters * From c5df91efce2b1fc02073c53a21a058bd34878e27 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 00:38:28 +1000 Subject: [PATCH 033/178] PEP Fix --- InvenTree/order/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1aa34838b7..e294e95030 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -649,7 +649,6 @@ class PurchaseOrderLineItem(OrderLineItem): def get_api_url(): return reverse('api-po-line-list') - class Meta: unique_together = ( ('order', 'part') From b99af16bfd2fc815121e22ed299e5cbe02a03bfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Jun 2021 22:13:56 +0200 Subject: [PATCH 034/178] preparing for price breaks diagrams --- InvenTree/part/templates/part/prices.html | 44 +++++++++++++---------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 749b5cb928..1b8b3959cf 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -176,16 +176,20 @@

    {% trans "Internal Cost" %}

    -
    -
    - +
    +
    - - -
    -
    +
    +
    + +
    + + +
    +
    +
    {% endif %} @@ -220,16 +224,20 @@

    {% trans "Sale Cost" %}

    -
    -
    - +
    +
    - - -
    -
    +
    +
    + +
    + + +
    +
    +
    From 9977b0bf59883f9de9f1ee3d2005208f22c0335d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 07:36:13 +1000 Subject: [PATCH 035/178] Include model name in metadata --- InvenTree/InvenTree/metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 5ca87b9eee..611b12101c 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -108,5 +108,6 @@ class InvenTreeMetadata(SimpleMetadata): # Mark this field as "related", and point to the URL where we can get the data! field_info['type'] = 'related field' field_info['api_url'] = model.get_api_url() + field_info['model'] = model._meta.model_name return field_info From b20af54b769846c3d11d3c46375c852d0eb1ff63 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 07:36:37 +1000 Subject: [PATCH 036/178] Create select2 instance for related field --- InvenTree/part/serializers.py | 2 +- InvenTree/templates/js/forms.js | 200 ++++++++++++++++++++++++-------- 2 files changed, 153 insertions(+), 49 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index e73ff54c25..7200309afa 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -309,7 +309,7 @@ class PartSerializer(InvenTreeModelSerializer): image = InvenTreeImageSerializerField(required=False, allow_null=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) - starred = serializers.BooleanField() + starred = serializers.SerializerMethodField() # PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...) category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 64a062c752..8f1b04241a 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -99,6 +99,63 @@ function getApiEndpointOptions(url, callback, options={}) { } +/* + * Construct a 'creation' (POST) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructCreateForm(url, fields, options={}) { + + // We should have enough information to create the form! + constructFormBody(url, fields, options); +} + + +/* + * Construct a 'change' (PATCH) form, to create a new model in the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS endpoint + * + * options: + * - + */ +function constructChangeForm(url, fields, options={}) { + + // Request existing data from the API endpoint + $.ajax({ + url: url, + type: 'GET', + contentType: 'application/json', + dataType: 'json', + accepts: { + json: 'application/json', + }, + success: function(data) { + + // Push existing 'value' to each field + for (const field in data) { + + if (field in fields) { + fields[field].value = data[field]; + } + } + + constructFormBody(url, fields, options); + }, + error: function(request, status, error) { + // TODO: Handle error here + console.log(`ERROR in constructChangeForm at '${url}'`); + } + }) + +} + + /* * Request API OPTIONS data from the server, * and construct a modal form based on the response. @@ -213,6 +270,10 @@ function constructFormBody(url, fields, options={}) { } } + // Push the ordered field names into the options, + // allowing successive functions to access them. + options.field_names = field_names; + // Render selected fields for (var idx = 0; idx < field_names.length; idx++) { @@ -221,8 +282,8 @@ function constructFormBody(url, fields, options={}) { var field = fields[name]; - // Skip field types which are simply not supported switch (field.type) { + // Skip field types which are simply not supported case 'nested object': continue; default: @@ -246,70 +307,113 @@ function constructFormBody(url, fields, options={}) { // Insert generated form content $(modal).find('.modal-form-content').html(html); - + $(modal).modal('show'); + // Setup related fields + initializeRelatedFields(modal, url, fields, options) + attachToggle(modal); - attachSelect(modal); + // attachSelect(modal); modalShowSubmitButton(modal, true); } -/* - * Construct a 'creation' (POST) form, to create a new model in the database. - * - * arguments: - * - fields: The 'actions' object provided by the OPTIONS endpoint - * - * options: - * - - */ -function constructCreateForm(url, fields, options={}) { - - // We should have enough information to create the form! - constructFormBody(url, fields, options); -} +function initializeRelatedFields(modal, url, fields, options) { + var field_names = options.field_names; -/* - * Construct a 'change' (PATCH) form, to create a new model in the database. - * - * arguments: - * - fields: The 'actions' object provided by the OPTIONS endpoint - * - * options: - * - - */ -function constructChangeForm(url, fields, options={}) { + for (var idx = 0; idx < field_names.length; idx++) { - // Request existing data from the API endpoint - $.ajax({ - url: url, - type: 'GET', - contentType: 'application/json', - dataType: 'json', - accepts: { - json: 'application/json', - }, - success: function(data) { + var name = field_names[idx]; - // Push existing 'value' to each field - for (const field in data) { + var field = fields[name] || null; - if (field in fields) { - fields[field].value = data[field]; + if (!field || field.type != 'related field') continue; + + if (!field.api_url) { + // TODO: Provide manual api_url option? + console.log(`Related field '${name}' missing 'api_url' parameter.`); + continue; + } + + // Find the select element and attach a select2 to it + var select = $(modal).find(`#id_${name}`); + + select.select2({ + ajax: { + url: field.api_url, + dataType: 'json', + dropdownParent: $(modal), + dropdownAutoWidth: false, + matcher: partialMatcher, + data: function(params) { + // Re-format search term into InvenTree API style + console.log('params;', params); + return { + search: params.term, + }; + }, + processResults: function(data) { + + var rows = []; + + // Only ever show the first x items + for (var idx = 0; idx < data.length && idx < 50; idx++) { + var row = data[idx]; + + // Reformat to match select2 requirements + row.id = row.id || row.pk; + + // TODO: Fix me? + row.text = `This is ${url}/${row.id}/`; + + rows.push(row); + } + + console.log(rows); + + // Ref: https://select2.org/data-sources/formats + var results = { + results: rows, + }; + + return results; } } + }); - constructFormBody(url, fields, options); - }, - error: function(request, status, error) { - // TODO: Handle error here - console.log(`ERROR in constructChangeForm at '${url}'`); - } - }) + + //console.log('select:', select); + + /* + $.ajax({ + url: url, + type: 'GET', + contentType: 'application/json', + dataType: 'json', + success: function(response) { + + // Create a new ' + response.forEach(function(item) { + + }) + + }, + error: function(request, status, error) { + // TODO: Handle error + console.log(`ERROR in initializeRelatedFields at URL '${url}'`); + } + }) + */ + } + + // Fix some styling issues + // TODO: Push this off to CSS! + $(modal + ' .select2-container').addClass('select-full-width'); + $(modal + ' .select2-container').css('width', '100%'); } From 9e7d1710dbd3005c9f889f9b269c1592f8c711a8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 13:23:29 +1000 Subject: [PATCH 037/178] Fixes for select2 rendering issues --- InvenTree/InvenTree/static/css/inventree.css | 12 ++++ InvenTree/templates/js/forms.js | 74 +++++++++++--------- InvenTree/templates/modals.html | 8 +-- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index eed6c6ad21..573f966a41 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -960,4 +960,16 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da .sidebar-icon { min-width: 19px; +} + +.select2-close-mask { + z-index: 99999; +} + +.select2-dropdown { + z-index: 99998; +} + +.select2-container { + width: 100%; } \ No newline at end of file diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 8f1b04241a..036928616c 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -316,6 +316,11 @@ function constructFormBody(url, fields, options={}) { attachToggle(modal); // attachSelect(modal); + //$(modal + ' .select').select2(); + + $(modal + ' .select2-container').addClass('select-full-width'); + $(modal + ' .select2-container').css('width', '100%'); + modalShowSubmitButton(modal, true); } @@ -341,22 +346,23 @@ function initializeRelatedFields(modal, url, fields, options) { // Find the select element and attach a select2 to it var select = $(modal).find(`#id_${name}`); + console.log('modal:', modal); + select.select2({ ajax: { url: field.api_url, dataType: 'json', dropdownParent: $(modal), dropdownAutoWidth: false, - matcher: partialMatcher, + // matcher: partialMatcher, data: function(params) { // Re-format search term into InvenTree API style - console.log('params;', params); return { search: params.term, }; }, processResults: function(data) { - + // Convert the returned InvenTree data into select2-friendly format var rows = []; // Only ever show the first x items @@ -367,53 +373,51 @@ function initializeRelatedFields(modal, url, fields, options) { row.id = row.id || row.pk; // TODO: Fix me? - row.text = `This is ${url}/${row.id}/`; + row.text = `This is ${field.api_url}${row.id}/`; rows.push(row); } - console.log(rows); - // Ref: https://select2.org/data-sources/formats var results = { results: rows, }; return results; + }, + templateResult: function(item, container) { + // Custom formatting for the item + console.log("templateResult:", item); + if (field.model) { + // If the 'model' is specified, hand it off to the custom model render + return renderModelData(field.model, item, field, options); + } else { + // Simply render the 'text' parameter + return item.text; + } } } }); - - - - //console.log('select:', select); - - /* - $.ajax({ - url: url, - type: 'GET', - contentType: 'application/json', - dataType: 'json', - success: function(response) { - - // Create a new ' - response.forEach(function(item) { - - }) - - }, - error: function(request, status, error) { - // TODO: Handle error - console.log(`ERROR in initializeRelatedFields at URL '${url}'`); - } - }) - */ } +} - // Fix some styling issues - // TODO: Push this off to CSS! - $(modal + ' .select2-container').addClass('select-full-width'); - $(modal + ' .select2-container').css('width', '100%'); + +/* + * Render a "foreign key" model reference in a select2 instance. + * Allows custom rendering with access to the entire serialized object. + * + * arguments: + * - model: The name of the InvenTree model e.g. 'stockitem' + * - data: The JSON data representation of the modal instance (GET request) + * - parameters: The field definition (OPTIONS) request + * - options: Other options provided at time of modal creation by the client + */ +function renderModelData(model, data, paramaters, options) { + + console.log(model, '->', data); + + // TODO: Implement? + return data.text; } diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index e0cae3e580..e2bd44554c 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -7,7 +7,7 @@
    -
  • '),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()});b.$results.find(".select2-results__option[aria-selected]").each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j",{class:"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("unselect",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):h-g<0&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");if("true"===c.attr("aria-selected"))return void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{}));d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),c<=2?this.$results.scrollTop(0):(g>this.$results.outerHeight()||g<0)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id,a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2");a(".select2.select2-container--open").each(function(){var b=a(this);this!=d[0]&&b.data("element").select2("close")})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){b.find(".selection").append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html(''),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection");return this.options.get("escapeMarkup")(c(a,b))},e.prototype.selectionContainer=function(){return a("")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('
      '),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection");return this.options.get("escapeMarkup")(c(a,b))},d.prototype.selectionContainer=function(){return a('
    • ×
    • ')},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d1||c)return a.call(this,b);this.clear();var d=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(d)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e0||0===c.length)){var d=a('×');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){if(a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented(),a.which===c.BACKSPACE&&""===e.$search.val()){var b=e.$searchContainer.prev(".select2-selection__choice");if(b.length>0){var d=b.data("data");e.searchRemoveChoice(d),a.preventDefault()}}});var f=document.documentMode,g=f&&f<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){if(g)return void e.$selection.off("input.search input.searchcheck");e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{a=.75*(this.$search.val().length+1)+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"}}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),null!=c.id?d+="-"+c.id.toString():d+="-"+a.generateChars(4),d},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength)return void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}});a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;if(d.maximumSelectionLength>0&&f>=d.maximumSelectionLength)return void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}});a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()||e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){e.showSearch(a)?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){e.$results.offset().top+e.$results.outerHeight(!1)+50>=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1)&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
    • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id;this.$container.parents().filter(b.hasScroll).off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),null==l.tokenSeparators&&null==l.tokenizer||(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){null==c(d,e.children[g])&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var h=b(e.text).toUpperCase(),i=b(d.term).toUpperCase();return h.indexOf(i)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)},new D}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return e<=0?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;h=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=a&&0!==a.length||(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("select2/compat/utils",["jquery"],function(a){function b(b,c,d){var e,f,g=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0===this.indexOf("select2-")&&g.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&null!=(f=d(this))&&g.push(f)})),b.attr("class",g.join(" "))}return{syncCssClasses:b}}),b.define("select2/compat/containerCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("containerCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptContainerCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("containerCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/dropdownCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("dropdownCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptDropdownCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("dropdownCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/initSelection",["jquery"],function(a){function b(a,b,c){c.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=c.get("initSelection"),this._isInitialized=!1,a.call(this,b,c)}return b.prototype.current=function(b,c){var d=this;if(this._isInitialized)return void b.call(this,c);this.initSelection.call(null,this.$element,function(b){d._isInitialized=!0,a.isArray(b)||(b=[b]),c(b)})},b}),b.define("select2/compat/inputData",["jquery"],function(a){function b(a,b,c){this._currentData=[],this._valueSeparator=c.get("valueSeparator")||",","hidden"===b.prop("type")&&c.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a `');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){if(a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented(),a.which===c.BACKSPACE&&""===e.$search.val()){var b=e.$searchContainer.prev(".select2-selection__choice");if(b.length>0){var d=b.data("data");e.searchRemoveChoice(d),a.preventDefault()}}});var f=document.documentMode,g=f&&f<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){if(g)return void e.$selection.off("input.search input.searchcheck");e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{a=.75*(this.$search.val().length+1)+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"}}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),null!=c.id?d+="-"+c.id.toString():d+="-"+a.generateChars(4),d},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength)return void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}});a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;if(d.maximumSelectionLength>0&&f>=d.maximumSelectionLength)return void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}});a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()||e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){e.showSearch(a)?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){e.$results.offset().top+e.$results.outerHeight(!1)+50>=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1)&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
    • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id;this.$container.parents().filter(b.hasScroll).off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),null==l.tokenSeparators&&null==l.tokenizer||(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){null==c(d,e.children[g])&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var h=b(e.text).toUpperCase(),i=b(d.term).toUpperCase();return h.indexOf(i)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)},new D}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return e<=0?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;h=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=a&&0!==a.length||(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if("object"==typeof(b=b||{}))return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=c[b].apply(c,f)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/css/select2-bootstrap.css b/InvenTree/InvenTree/static/select2/css/select2-bootstrap.css similarity index 100% rename from InvenTree/InvenTree/static/css/select2-bootstrap.css rename to InvenTree/InvenTree/static/select2/css/select2-bootstrap.css diff --git a/InvenTree/InvenTree/static/css/select2.css b/InvenTree/InvenTree/static/select2/css/select2.css similarity index 97% rename from InvenTree/InvenTree/static/css/select2.css rename to InvenTree/InvenTree/static/select2/css/select2.css index 447b2b86cc..750b3207ae 100644 --- a/InvenTree/InvenTree/static/css/select2.css +++ b/InvenTree/InvenTree/static/select2/css/select2.css @@ -118,12 +118,14 @@ .select2-hidden-accessible { border: 0 !important; clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; height: 1px !important; - margin: -1px !important; overflow: hidden !important; padding: 0 !important; position: absolute !important; - width: 1px !important; } + width: 1px !important; + white-space: nowrap !important; } .select2-container--default .select2-selection--single { background-color: #fff; @@ -186,16 +188,13 @@ width: 100%; } .select2-container--default .select2-selection--multiple .select2-selection__rendered li { list-style: none; } - .select2-container--default .select2-selection--multiple .select2-selection__placeholder { - color: #999; - margin-top: 5px; - float: left; } .select2-container--default .select2-selection--multiple .select2-selection__clear { cursor: pointer; float: right; font-weight: bold; margin-top: 5px; - margin-right: 10px; } + margin-right: 10px; + padding: 1px; } .select2-container--default .select2-selection--multiple .select2-selection__choice { background-color: #e4e4e4; border: 1px solid #aaa; @@ -214,7 +213,7 @@ .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { color: #333; } -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { float: right; } .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { @@ -420,9 +419,7 @@ color: #555; } .select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - float: right; } - -.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; margin-left: 5px; margin-right: auto; } diff --git a/InvenTree/InvenTree/static/select2/css/select2.min.css b/InvenTree/InvenTree/static/select2/css/select2.min.css new file mode 100644 index 0000000000..7c18ad59df --- /dev/null +++ b/InvenTree/InvenTree/static/select2/css/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/InvenTree/InvenTree/static/select2/js/i18n/af.js b/InvenTree/InvenTree/static/select2/js/i18n/af.js new file mode 100644 index 0000000000..32e5ac7de8 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/af.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/af",[],function(){return{errorLoading:function(){return"Die resultate kon nie gelaai word nie."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Verwyders asseblief "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Voer asseblief "+(e.minimum-e.input.length)+" of meer karakters"},loadingMore:function(){return"Meer resultate word gelaai…"},maximumSelected:function(e){var n="Kies asseblief net "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"Geen resultate gevind"},searching:function(){return"Besig…"},removeAllItems:function(){return"Verwyder alle items"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ar.js b/InvenTree/InvenTree/static/select2/js/i18n/ar.js new file mode 100644 index 0000000000..64e1caad34 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ar.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(n){return"الرجاء حذف "+(n.input.length-n.maximum)+" عناصر"},inputTooShort:function(n){return"الرجاء إضافة "+(n.minimum-n.input.length)+" عناصر"},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(n){return"تستطيع إختيار "+n.maximum+" بنود فقط"},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"},removeAllItems:function(){return"قم بإزالة كل العناصر"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/az.js b/InvenTree/InvenTree/static/select2/js/i18n/az.js new file mode 100644 index 0000000000..1d52c260f2 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/az.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/az",[],function(){return{inputTooLong:function(n){return n.input.length-n.maximum+" simvol silin"},inputTooShort:function(n){return n.minimum-n.input.length+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(n){return"Sadəcə "+n.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"},removeAllItems:function(){return"Bütün elementləri sil"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/bg.js b/InvenTree/InvenTree/static/select2/js/i18n/bg.js new file mode 100644 index 0000000000..73b730a705 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/bg.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bg",[],function(){return{inputTooLong:function(n){var e=n.input.length-n.maximum,u="Моля въведете с "+e+" по-малко символ";return e>1&&(u+="a"),u},inputTooShort:function(n){var e=n.minimum-n.input.length,u="Моля въведете още "+e+" символ";return e>1&&(u+="a"),u},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(n){var e="Можете да направите до "+n.maximum+" ";return n.maximum>1?e+="избора":e+="избор",e},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"},removeAllItems:function(){return"Премахнете всички елементи"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/bn.js b/InvenTree/InvenTree/static/select2/js/i18n/bn.js new file mode 100644 index 0000000000..2d17b9d8e0 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/bn.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bn",[],function(){return{errorLoading:function(){return"ফলাফলগুলি লোড করা যায়নি।"},inputTooLong:function(n){var e=n.input.length-n.maximum,u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।";return 1!=e&&(u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।"),u},inputTooShort:function(n){return n.minimum-n.input.length+" টি অক্ষর অথবা অধিক অক্ষর লিখুন।"},loadingMore:function(){return"আরো ফলাফল লোড হচ্ছে ..."},maximumSelected:function(n){var e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।";return 1!=n.maximum&&(e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।"),e},noResults:function(){return"কোন ফলাফল পাওয়া যায়নি।"},searching:function(){return"অনুসন্ধান করা হচ্ছে ..."}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/bs.js b/InvenTree/InvenTree/static/select2/js/i18n/bs.js new file mode 100644 index 0000000000..46b084d758 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/bs.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/bs",[],function(){function e(e,n,r,t){return e%10==1&&e%100!=11?n:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?r:t}return{errorLoading:function(){return"Preuzimanje nije uspijelo."},inputTooLong:function(n){var r=n.input.length-n.maximum,t="Obrišite "+r+" simbol";return t+=e(r,"","a","a")},inputTooShort:function(n){var r=n.minimum-n.input.length,t="Ukucajte bar još "+r+" simbol";return t+=e(r,"","a","a")},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(n){var r="Možete izabrati samo "+n.maximum+" stavk";return r+=e(n.maximum,"u","e","i")},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Uklonite sve stavke"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ca.js b/InvenTree/InvenTree/static/select2/js/i18n/ca.js new file mode 100644 index 0000000000..82dbbb7a21 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ca.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Si us plau, elimina "+n+" car";return r+=1==n?"àcter":"àcters"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Si us plau, introdueix "+n+" car";return r+=1==n?"àcter":"àcters"},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var n="Només es pot seleccionar "+e.maximum+" element";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"},removeAllItems:function(){return"Treu tots els elements"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/cs.js b/InvenTree/InvenTree/static/select2/js/i18n/cs.js new file mode 100644 index 0000000000..7116d6c1df --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/cs.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/cs",[],function(){function e(e,n){switch(e){case 2:return n?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(n){var t=n.input.length-n.maximum;return 1==t?"Prosím, zadejte o jeden znak méně.":t<=4?"Prosím, zadejte o "+e(t,!0)+" znaky méně.":"Prosím, zadejte o "+t+" znaků méně."},inputTooShort:function(n){var t=n.minimum-n.input.length;return 1==t?"Prosím, zadejte ještě jeden znak.":t<=4?"Prosím, zadejte ještě další "+e(t,!0)+" znaky.":"Prosím, zadejte ještě dalších "+t+" znaků."},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(n){var t=n.maximum;return 1==t?"Můžete zvolit jen jednu položku.":t<=4?"Můžete zvolit maximálně "+e(t,!1)+" položky.":"Můžete zvolit maximálně "+t+" položek."},noResults:function(){return"Nenalezeny žádné položky."},searching:function(){return"Vyhledávání…"},removeAllItems:function(){return"Odstraňte všechny položky"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/da.js b/InvenTree/InvenTree/static/select2/js/i18n/da.js new file mode 100644 index 0000000000..cda32c34aa --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/da.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){return"Angiv venligst "+(e.input.length-e.maximum)+" tegn mindre"},inputTooShort:function(e){return"Angiv venligst "+(e.minimum-e.input.length)+" tegn mere"},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var n="Du kan kun vælge "+e.maximum+" emne";return 1!=e.maximum&&(n+="r"),n},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"},removeAllItems:function(){return"Fjern alle elementer"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/de.js b/InvenTree/InvenTree/static/select2/js/i18n/de.js new file mode 100644 index 0000000000..c2e61e5800 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/de.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/de",[],function(){return{errorLoading:function(){return"Die Ergebnisse konnten nicht geladen werden."},inputTooLong:function(e){return"Bitte "+(e.input.length-e.maximum)+" Zeichen weniger eingeben"},inputTooShort:function(e){return"Bitte "+(e.minimum-e.input.length)+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var n="Sie können nur "+e.maximum+" Element";return 1!=e.maximum&&(n+="e"),n+=" auswählen"},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"},removeAllItems:function(){return"Entferne alle Elemente"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/dsb.js b/InvenTree/InvenTree/static/select2/js/i18n/dsb.js new file mode 100644 index 0000000000..02f283abad --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/dsb.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/dsb",[],function(){var n=["znamuško","znamušce","znamuška","znamuškow"],e=["zapisk","zapiska","zapiski","zapiskow"],u=function(n,e){return 1===n?e[0]:2===n?e[1]:n>2&&n<=4?e[2]:n>=5?e[3]:void 0};return{errorLoading:function(){return"Wuslědki njejsu se dali zacytaś."},inputTooLong:function(e){var a=e.input.length-e.maximum;return"Pšosym lašuj "+a+" "+u(a,n)},inputTooShort:function(e){var a=e.minimum-e.input.length;return"Pšosym zapódaj nanejmjenjej "+a+" "+u(a,n)},loadingMore:function(){return"Dalšne wuslědki se zacytaju…"},maximumSelected:function(n){return"Móžoš jano "+n.maximum+" "+u(n.maximum,e)+"wubraś."},noResults:function(){return"Žedne wuslědki namakane"},searching:function(){return"Pyta se…"},removeAllItems:function(){return"Remove all items"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/el.js b/InvenTree/InvenTree/static/select2/js/i18n/el.js new file mode 100644 index 0000000000..d4922a1df5 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/el.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/el",[],function(){return{errorLoading:function(){return"Τα αποτελέσματα δεν μπόρεσαν να φορτώσουν."},inputTooLong:function(n){var e=n.input.length-n.maximum,u="Παρακαλώ διαγράψτε "+e+" χαρακτήρ";return 1==e&&(u+="α"),1!=e&&(u+="ες"),u},inputTooShort:function(n){return"Παρακαλώ συμπληρώστε "+(n.minimum-n.input.length)+" ή περισσότερους χαρακτήρες"},loadingMore:function(){return"Φόρτωση περισσότερων αποτελεσμάτων…"},maximumSelected:function(n){var e="Μπορείτε να επιλέξετε μόνο "+n.maximum+" επιλογ";return 1==n.maximum&&(e+="ή"),1!=n.maximum&&(e+="ές"),e},noResults:function(){return"Δεν βρέθηκαν αποτελέσματα"},searching:function(){return"Αναζήτηση…"},removeAllItems:function(){return"Καταργήστε όλα τα στοιχεία"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/en.js b/InvenTree/InvenTree/static/select2/js/i18n/en.js new file mode 100644 index 0000000000..3b19285734 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/en.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Please delete "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var n="You can only select "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/es.js b/InvenTree/InvenTree/static/select2/js/i18n/es.js new file mode 100644 index 0000000000..68afd6d259 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/es.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"No se pudieron cargar los resultados"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Por favor, elimine "+n+" car";return r+=1==n?"ácter":"acteres"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Por favor, introduzca "+n+" car";return r+=1==n?"ácter":"acteres"},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var n="Sólo puede seleccionar "+e.maximum+" elemento";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Eliminar todos los elementos"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/et.js b/InvenTree/InvenTree/static/select2/js/i18n/et.js new file mode 100644 index 0000000000..070b61a26d --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/et.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var n=e.input.length-e.maximum,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" vähem"},inputTooShort:function(e){var n=e.minimum-e.input.length,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" rohkem"},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var n="Saad vaid "+e.maximum+" tulemus";return 1==e.maximum?n+="e":n+="t",n+=" valida"},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"},removeAllItems:function(){return"Eemalda kõik esemed"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/eu.js b/InvenTree/InvenTree/static/select2/js/i18n/eu.js new file mode 100644 index 0000000000..90d5e73f8a --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/eu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gutxiago"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gehiago"},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return 1===e.maximum?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"},removeAllItems:function(){return"Kendu elementu guztiak"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/fa.js b/InvenTree/InvenTree/static/select2/js/i18n/fa.js new file mode 100644 index 0000000000..e1ffdbed0d --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/fa.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(n){return"لطفاً "+(n.input.length-n.maximum)+" کاراکتر را حذف نمایید"},inputTooShort:function(n){return"لطفاً تعداد "+(n.minimum-n.input.length)+" کاراکتر یا بیشتر وارد نمایید"},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(n){return"شما تنها می‌توانید "+n.maximum+" آیتم را انتخاب نمایید"},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."},removeAllItems:function(){return"همه موارد را حذف کنید"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/fi.js b/InvenTree/InvenTree/static/select2/js/i18n/fi.js new file mode 100644 index 0000000000..ffed1247dd --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/fi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fi",[],function(){return{errorLoading:function(){return"Tuloksia ei saatu ladattua."},inputTooLong:function(n){return"Ole hyvä ja anna "+(n.input.length-n.maximum)+" merkkiä vähemmän"},inputTooShort:function(n){return"Ole hyvä ja anna "+(n.minimum-n.input.length)+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(n){return"Voit valita ainoastaan "+n.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){return"Haetaan…"},removeAllItems:function(){return"Poista kaikki kohteet"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/fr.js b/InvenTree/InvenTree/static/select2/js/i18n/fr.js new file mode 100644 index 0000000000..dd02f973ff --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/fr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/fr",[],function(){return{errorLoading:function(){return"Les résultats ne peuvent pas être chargés."},inputTooLong:function(e){var n=e.input.length-e.maximum;return"Supprimez "+n+" caractère"+(n>1?"s":"")},inputTooShort:function(e){var n=e.minimum-e.input.length;return"Saisissez au moins "+n+" caractère"+(n>1?"s":"")},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){return"Vous pouvez seulement sélectionner "+e.maximum+" élément"+(e.maximum>1?"s":"")},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"},removeAllItems:function(){return"Supprimer tous les éléments"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/gl.js b/InvenTree/InvenTree/static/select2/js/i18n/gl.js new file mode 100644 index 0000000000..208a005705 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/gl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/gl",[],function(){return{errorLoading:function(){return"Non foi posíbel cargar os resultados."},inputTooLong:function(e){var n=e.input.length-e.maximum;return 1===n?"Elimine un carácter":"Elimine "+n+" caracteres"},inputTooShort:function(e){var n=e.minimum-e.input.length;return 1===n?"Engada un carácter":"Engada "+n+" caracteres"},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){return 1===e.maximum?"Só pode seleccionar un elemento":"Só pode seleccionar "+e.maximum+" elementos"},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Elimina todos os elementos"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/he.js b/InvenTree/InvenTree/static/select2/js/i18n/he.js new file mode 100644 index 0000000000..25a8805aa0 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/he.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/he",[],function(){return{errorLoading:function(){return"שגיאה בטעינת התוצאות"},inputTooLong:function(n){var e=n.input.length-n.maximum,r="נא למחוק ";return r+=1===e?"תו אחד":e+" תווים"},inputTooShort:function(n){var e=n.minimum-n.input.length,r="נא להכניס ";return r+=1===e?"תו אחד":e+" תווים",r+=" או יותר"},loadingMore:function(){return"טוען תוצאות נוספות…"},maximumSelected:function(n){var e="באפשרותך לבחור עד ";return 1===n.maximum?e+="פריט אחד":e+=n.maximum+" פריטים",e},noResults:function(){return"לא נמצאו תוצאות"},searching:function(){return"מחפש…"},removeAllItems:function(){return"הסר את כל הפריטים"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/hi.js b/InvenTree/InvenTree/static/select2/js/i18n/hi.js new file mode 100644 index 0000000000..f3ed798434 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/hi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hi",[],function(){return{errorLoading:function(){return"परिणामों को लोड नहीं किया जा सका।"},inputTooLong:function(n){var e=n.input.length-n.maximum,r=e+" अक्षर को हटा दें";return e>1&&(r=e+" अक्षरों को हटा दें "),r},inputTooShort:function(n){return"कृपया "+(n.minimum-n.input.length)+" या अधिक अक्षर दर्ज करें"},loadingMore:function(){return"अधिक परिणाम लोड हो रहे है..."},maximumSelected:function(n){return"आप केवल "+n.maximum+" आइटम का चयन कर सकते हैं"},noResults:function(){return"कोई परिणाम नहीं मिला"},searching:function(){return"खोज रहा है..."},removeAllItems:function(){return"सभी वस्तुओं को हटा दें"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/hr.js b/InvenTree/InvenTree/static/select2/js/i18n/hr.js new file mode 100644 index 0000000000..cb3268db16 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/hr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hr",[],function(){function n(n){var e=" "+n+" znak";return n%10<5&&n%10>0&&(n%100<5||n%100>19)?n%10>1&&(e+="a"):e+="ova",e}return{errorLoading:function(){return"Preuzimanje nije uspjelo."},inputTooLong:function(e){return"Unesite "+n(e.input.length-e.maximum)},inputTooShort:function(e){return"Unesite još "+n(e.minimum-e.input.length)},loadingMore:function(){return"Učitavanje rezultata…"},maximumSelected:function(n){return"Maksimalan broj odabranih stavki je "+n.maximum},noResults:function(){return"Nema rezultata"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Ukloni sve stavke"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/hsb.js b/InvenTree/InvenTree/static/select2/js/i18n/hsb.js new file mode 100644 index 0000000000..3d5bf09dbd --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/hsb.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hsb",[],function(){var n=["znamješko","znamješce","znamješka","znamješkow"],e=["zapisk","zapiskaj","zapiski","zapiskow"],u=function(n,e){return 1===n?e[0]:2===n?e[1]:n>2&&n<=4?e[2]:n>=5?e[3]:void 0};return{errorLoading:function(){return"Wuslědki njedachu so začitać."},inputTooLong:function(e){var a=e.input.length-e.maximum;return"Prošu zhašej "+a+" "+u(a,n)},inputTooShort:function(e){var a=e.minimum-e.input.length;return"Prošu zapodaj znajmjeńša "+a+" "+u(a,n)},loadingMore:function(){return"Dalše wuslědki so začitaja…"},maximumSelected:function(n){return"Móžeš jenož "+n.maximum+" "+u(n.maximum,e)+"wubrać"},noResults:function(){return"Žane wuslědki namakane"},searching:function(){return"Pyta so…"},removeAllItems:function(){return"Remove all items"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/hu.js b/InvenTree/InvenTree/static/select2/js/i18n/hu.js new file mode 100644 index 0000000000..4893aa2f70 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/hu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/hu",[],function(){return{errorLoading:function(){return"Az eredmények betöltése nem sikerült."},inputTooLong:function(e){return"Túl hosszú. "+(e.input.length-e.maximum)+" karakterrel több, mint kellene."},inputTooShort:function(e){return"Túl rövid. Még "+(e.minimum-e.input.length)+" karakter hiányzik."},loadingMore:function(){return"Töltés…"},maximumSelected:function(e){return"Csak "+e.maximum+" elemet lehet kiválasztani."},noResults:function(){return"Nincs találat."},searching:function(){return"Keresés…"},removeAllItems:function(){return"Távolítson el minden elemet"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/hy.js b/InvenTree/InvenTree/static/select2/js/i18n/hy.js new file mode 100644 index 0000000000..8230007141 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/hy.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hy",[],function(){return{errorLoading:function(){return"Արդյունքները հնարավոր չէ բեռնել։"},inputTooLong:function(n){return"Խնդրում ենք հեռացնել "+(n.input.length-n.maximum)+" նշան"},inputTooShort:function(n){return"Խնդրում ենք մուտքագրել "+(n.minimum-n.input.length)+" կամ ավել նշաններ"},loadingMore:function(){return"Բեռնվում են նոր արդյունքներ․․․"},maximumSelected:function(n){return"Դուք կարող եք ընտրել առավելագույնը "+n.maximum+" կետ"},noResults:function(){return"Արդյունքներ չեն գտնվել"},searching:function(){return"Որոնում․․․"},removeAllItems:function(){return"Հեռացնել բոլոր տարրերը"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/id.js b/InvenTree/InvenTree/static/select2/js/i18n/id.js new file mode 100644 index 0000000000..4a0b3bf009 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/id.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/id",[],function(){return{errorLoading:function(){return"Data tidak boleh diambil."},inputTooLong:function(n){return"Hapuskan "+(n.input.length-n.maximum)+" huruf"},inputTooShort:function(n){return"Masukkan "+(n.minimum-n.input.length)+" huruf lagi"},loadingMore:function(){return"Mengambil data…"},maximumSelected:function(n){return"Anda hanya dapat memilih "+n.maximum+" pilihan"},noResults:function(){return"Tidak ada data yang sesuai"},searching:function(){return"Mencari…"},removeAllItems:function(){return"Hapus semua item"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/is.js b/InvenTree/InvenTree/static/select2/js/i18n/is.js new file mode 100644 index 0000000000..cca5bbecf0 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/is.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/is",[],function(){return{inputTooLong:function(n){var t=n.input.length-n.maximum,e="Vinsamlegast styttið texta um "+t+" staf";return t<=1?e:e+"i"},inputTooShort:function(n){var t=n.minimum-n.input.length,e="Vinsamlegast skrifið "+t+" staf";return t>1&&(e+="i"),e+=" í viðbót"},loadingMore:function(){return"Sæki fleiri niðurstöður…"},maximumSelected:function(n){return"Þú getur aðeins valið "+n.maximum+" atriði"},noResults:function(){return"Ekkert fannst"},searching:function(){return"Leita…"},removeAllItems:function(){return"Fjarlægðu öll atriði"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/it.js b/InvenTree/InvenTree/static/select2/js/i18n/it.js new file mode 100644 index 0000000000..507c7d9f29 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/it.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/it",[],function(){return{errorLoading:function(){return"I risultati non possono essere caricati."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Per favore cancella "+n+" caratter";return t+=1!==n?"i":"e"},inputTooShort:function(e){return"Per favore inserisci "+(e.minimum-e.input.length)+" o più caratteri"},loadingMore:function(){return"Caricando più risultati…"},maximumSelected:function(e){var n="Puoi selezionare solo "+e.maximum+" element";return 1!==e.maximum?n+="i":n+="o",n},noResults:function(){return"Nessun risultato trovato"},searching:function(){return"Sto cercando…"},removeAllItems:function(){return"Rimuovi tutti gli oggetti"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ja.js b/InvenTree/InvenTree/static/select2/js/i18n/ja.js new file mode 100644 index 0000000000..451025e2c7 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ja.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ja",[],function(){return{errorLoading:function(){return"結果が読み込まれませんでした"},inputTooLong:function(n){return n.input.length-n.maximum+" 文字を削除してください"},inputTooShort:function(n){return"少なくとも "+(n.minimum-n.input.length)+" 文字を入力してください"},loadingMore:function(){return"読み込み中…"},maximumSelected:function(n){return n.maximum+" 件しか選択できません"},noResults:function(){return"対象が見つかりません"},searching:function(){return"検索しています…"},removeAllItems:function(){return"すべてのアイテムを削除"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ka.js b/InvenTree/InvenTree/static/select2/js/i18n/ka.js new file mode 100644 index 0000000000..60c593b705 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ka.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ka",[],function(){return{errorLoading:function(){return"მონაცემების ჩატვირთვა შეუძლებელია."},inputTooLong:function(n){return"გთხოვთ აკრიფეთ "+(n.input.length-n.maximum)+" სიმბოლოთი ნაკლები"},inputTooShort:function(n){return"გთხოვთ აკრიფეთ "+(n.minimum-n.input.length)+" სიმბოლო ან მეტი"},loadingMore:function(){return"მონაცემების ჩატვირთვა…"},maximumSelected:function(n){return"თქვენ შეგიძლიათ აირჩიოთ არაუმეტეს "+n.maximum+" ელემენტი"},noResults:function(){return"რეზულტატი არ მოიძებნა"},searching:function(){return"ძიება…"},removeAllItems:function(){return"ამოიღე ყველა ელემენტი"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/km.js b/InvenTree/InvenTree/static/select2/js/i18n/km.js new file mode 100644 index 0000000000..4dca94f414 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/km.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/km",[],function(){return{errorLoading:function(){return"មិនអាចទាញយកទិន្នន័យ"},inputTooLong:function(n){return"សូមលុបចេញ "+(n.input.length-n.maximum)+" អក្សរ"},inputTooShort:function(n){return"សូមបញ្ចូល"+(n.minimum-n.input.length)+" អក្សរ រឺ ច្រើនជាងនេះ"},loadingMore:function(){return"កំពុងទាញយកទិន្នន័យបន្ថែម..."},maximumSelected:function(n){return"អ្នកអាចជ្រើសរើសបានតែ "+n.maximum+" ជម្រើសប៉ុណ្ណោះ"},noResults:function(){return"មិនមានលទ្ធផល"},searching:function(){return"កំពុងស្វែងរក..."},removeAllItems:function(){return"លុបធាតុទាំងអស់"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ko.js b/InvenTree/InvenTree/static/select2/js/i18n/ko.js new file mode 100644 index 0000000000..f2880fb004 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ko.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ko",[],function(){return{errorLoading:function(){return"결과를 불러올 수 없습니다."},inputTooLong:function(n){return"너무 깁니다. "+(n.input.length-n.maximum)+" 글자 지워주세요."},inputTooShort:function(n){return"너무 짧습니다. "+(n.minimum-n.input.length)+" 글자 더 입력해주세요."},loadingMore:function(){return"불러오는 중…"},maximumSelected:function(n){return"최대 "+n.maximum+"개까지만 선택 가능합니다."},noResults:function(){return"결과가 없습니다."},searching:function(){return"검색 중…"},removeAllItems:function(){return"모든 항목 삭제"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/lt.js b/InvenTree/InvenTree/static/select2/js/i18n/lt.js new file mode 100644 index 0000000000..f6a42155ad --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/lt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/lt",[],function(){function n(n,e,i,t){return n%10==1&&(n%100<11||n%100>19)?e:n%10>=2&&n%10<=9&&(n%100<11||n%100>19)?i:t}return{inputTooLong:function(e){var i=e.input.length-e.maximum,t="Pašalinkite "+i+" simbol";return t+=n(i,"į","ius","ių")},inputTooShort:function(e){var i=e.minimum-e.input.length,t="Įrašykite dar "+i+" simbol";return t+=n(i,"į","ius","ių")},loadingMore:function(){return"Kraunama daugiau rezultatų…"},maximumSelected:function(e){var i="Jūs galite pasirinkti tik "+e.maximum+" element";return i+=n(e.maximum,"ą","us","ų")},noResults:function(){return"Atitikmenų nerasta"},searching:function(){return"Ieškoma…"},removeAllItems:function(){return"Pašalinti visus elementus"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/lv.js b/InvenTree/InvenTree/static/select2/js/i18n/lv.js new file mode 100644 index 0000000000..806dc5c433 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/lv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/lv",[],function(){function e(e,n,u,i){return 11===e?n:e%10==1?u:i}return{inputTooLong:function(n){var u=n.input.length-n.maximum,i="Lūdzu ievadiet par "+u;return(i+=" simbol"+e(u,"iem","u","iem"))+" mazāk"},inputTooShort:function(n){var u=n.minimum-n.input.length,i="Lūdzu ievadiet vēl "+u;return i+=" simbol"+e(u,"us","u","us")},loadingMore:function(){return"Datu ielāde…"},maximumSelected:function(n){var u="Jūs varat izvēlēties ne vairāk kā "+n.maximum;return u+=" element"+e(n.maximum,"us","u","us")},noResults:function(){return"Sakritību nav"},searching:function(){return"Meklēšana…"},removeAllItems:function(){return"Noņemt visus vienumus"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/mk.js b/InvenTree/InvenTree/static/select2/js/i18n/mk.js new file mode 100644 index 0000000000..cb7b84a263 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/mk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/mk",[],function(){return{inputTooLong:function(n){var e=(n.input.length,n.maximum,"Ве молиме внесете "+n.maximum+" помалку карактер");return 1!==n.maximum&&(e+="и"),e},inputTooShort:function(n){var e=(n.minimum,n.input.length,"Ве молиме внесете уште "+n.maximum+" карактер");return 1!==n.maximum&&(e+="и"),e},loadingMore:function(){return"Вчитување резултати…"},maximumSelected:function(n){var e="Можете да изберете само "+n.maximum+" ставк";return 1===n.maximum?e+="а":e+="и",e},noResults:function(){return"Нема пронајдено совпаѓања"},searching:function(){return"Пребарување…"},removeAllItems:function(){return"Отстрани ги сите предмети"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ms.js b/InvenTree/InvenTree/static/select2/js/i18n/ms.js new file mode 100644 index 0000000000..6bd7eaa3e0 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ms.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ms",[],function(){return{errorLoading:function(){return"Keputusan tidak berjaya dimuatkan."},inputTooLong:function(n){return"Sila hapuskan "+(n.input.length-n.maximum)+" aksara"},inputTooShort:function(n){return"Sila masukkan "+(n.minimum-n.input.length)+" atau lebih aksara"},loadingMore:function(){return"Sedang memuatkan keputusan…"},maximumSelected:function(n){return"Anda hanya boleh memilih "+n.maximum+" pilihan"},noResults:function(){return"Tiada padanan yang ditemui"},searching:function(){return"Mencari…"},removeAllItems:function(){return"Keluarkan semua item"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/nb.js b/InvenTree/InvenTree/static/select2/js/i18n/nb.js new file mode 100644 index 0000000000..25d89c6870 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/nb.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/nb",[],function(){return{errorLoading:function(){return"Kunne ikke hente resultater."},inputTooLong:function(e){return"Vennligst fjern "+(e.input.length-e.maximum)+" tegn"},inputTooShort:function(e){return"Vennligst skriv inn "+(e.minimum-e.input.length)+" tegn til"},loadingMore:function(){return"Laster flere resultater…"},maximumSelected:function(e){return"Du kan velge maks "+e.maximum+" elementer"},noResults:function(){return"Ingen treff"},searching:function(){return"Søker…"},removeAllItems:function(){return"Fjern alle elementer"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ne.js b/InvenTree/InvenTree/static/select2/js/i18n/ne.js new file mode 100644 index 0000000000..1c39f67210 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ne.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ne",[],function(){return{errorLoading:function(){return"नतिजाहरु देखाउन सकिएन।"},inputTooLong:function(n){var e=n.input.length-n.maximum,u="कृपया "+e+" अक्षर मेटाउनुहोस्।";return 1!=e&&(u+="कृपया "+e+" अक्षरहरु मेटाउनुहोस्।"),u},inputTooShort:function(n){return"कृपया बाँकी रहेका "+(n.minimum-n.input.length)+" वा अरु धेरै अक्षरहरु भर्नुहोस्।"},loadingMore:function(){return"अरु नतिजाहरु भरिँदैछन् …"},maximumSelected:function(n){var e="तँपाई "+n.maximum+" वस्तु मात्र छान्न पाउँनुहुन्छ।";return 1!=n.maximum&&(e="तँपाई "+n.maximum+" वस्तुहरु मात्र छान्न पाउँनुहुन्छ।"),e},noResults:function(){return"कुनै पनि नतिजा भेटिएन।"},searching:function(){return"खोजि हुँदैछ…"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/nl.js b/InvenTree/InvenTree/static/select2/js/i18n/nl.js new file mode 100644 index 0000000000..2b74058d23 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/nl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/nl",[],function(){return{errorLoading:function(){return"De resultaten konden niet worden geladen."},inputTooLong:function(e){return"Gelieve "+(e.input.length-e.maximum)+" karakters te verwijderen"},inputTooShort:function(e){return"Gelieve "+(e.minimum-e.input.length)+" of meer karakters in te voeren"},loadingMore:function(){return"Meer resultaten laden…"},maximumSelected:function(e){var n=1==e.maximum?"kan":"kunnen",r="Er "+n+" maar "+e.maximum+" item";return 1!=e.maximum&&(r+="s"),r+=" worden geselecteerd"},noResults:function(){return"Geen resultaten gevonden…"},searching:function(){return"Zoeken…"},removeAllItems:function(){return"Verwijder alle items"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/pl.js b/InvenTree/InvenTree/static/select2/js/i18n/pl.js new file mode 100644 index 0000000000..4ca5748c38 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/pl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/pl",[],function(){var n=["znak","znaki","znaków"],e=["element","elementy","elementów"],r=function(n,e){return 1===n?e[0]:n>1&&n<=4?e[1]:n>=5?e[2]:void 0};return{errorLoading:function(){return"Nie można załadować wyników."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Usuń "+t+" "+r(t,n)},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Podaj przynajmniej "+t+" "+r(t,n)},loadingMore:function(){return"Trwa ładowanie…"},maximumSelected:function(n){return"Możesz zaznaczyć tylko "+n.maximum+" "+r(n.maximum,e)},noResults:function(){return"Brak wyników"},searching:function(){return"Trwa wyszukiwanie…"},removeAllItems:function(){return"Usuń wszystkie przedmioty"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ps.js b/InvenTree/InvenTree/static/select2/js/i18n/ps.js new file mode 100644 index 0000000000..9b008e4c14 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ps.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ps",[],function(){return{errorLoading:function(){return"پايلي نه سي ترلاسه کېدای"},inputTooLong:function(n){var e=n.input.length-n.maximum,r="د مهربانۍ لمخي "+e+" توری ړنګ کړئ";return 1!=e&&(r=r.replace("توری","توري")),r},inputTooShort:function(n){return"لږ تر لږه "+(n.minimum-n.input.length)+" يا ډېر توري وليکئ"},loadingMore:function(){return"نوري پايلي ترلاسه کيږي..."},maximumSelected:function(n){var e="تاسو يوازي "+n.maximum+" قلم په نښه کولای سی";return 1!=n.maximum&&(e=e.replace("قلم","قلمونه")),e},noResults:function(){return"پايلي و نه موندل سوې"},searching:function(){return"لټول کيږي..."},removeAllItems:function(){return"ټول توکي لرې کړئ"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/pt-BR.js b/InvenTree/InvenTree/static/select2/js/i18n/pt-BR.js new file mode 100644 index 0000000000..c991e2550a --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/pt-BR.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/pt-BR",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Apague "+n+" caracter";return 1!=n&&(r+="es"),r},inputTooShort:function(e){return"Digite "+(e.minimum-e.input.length)+" ou mais caracteres"},loadingMore:function(){return"Carregando mais resultados…"},maximumSelected:function(e){var n="Você só pode selecionar "+e.maximum+" ite";return 1==e.maximum?n+="m":n+="ns",n},noResults:function(){return"Nenhum resultado encontrado"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Remover todos os itens"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/pt.js b/InvenTree/InvenTree/static/select2/js/i18n/pt.js new file mode 100644 index 0000000000..b5da1a6b49 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/pt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/pt",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var r=e.input.length-e.maximum,n="Por favor apague "+r+" ";return n+=1!=r?"caracteres":"caractere"},inputTooShort:function(e){return"Introduza "+(e.minimum-e.input.length)+" ou mais caracteres"},loadingMore:function(){return"A carregar mais resultados…"},maximumSelected:function(e){var r="Apenas pode seleccionar "+e.maximum+" ";return r+=1!=e.maximum?"itens":"item"},noResults:function(){return"Sem resultados"},searching:function(){return"A procurar…"},removeAllItems:function(){return"Remover todos os itens"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ro.js b/InvenTree/InvenTree/static/select2/js/i18n/ro.js new file mode 100644 index 0000000000..1ba7b40bef --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ro.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/ro",[],function(){return{errorLoading:function(){return"Rezultatele nu au putut fi incărcate."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vă rugăm să ștergeți"+t+" caracter";return 1!==t&&(n+="e"),n},inputTooShort:function(e){return"Vă rugăm să introduceți "+(e.minimum-e.input.length)+" sau mai multe caractere"},loadingMore:function(){return"Se încarcă mai multe rezultate…"},maximumSelected:function(e){var t="Aveți voie să selectați cel mult "+e.maximum;return t+=" element",1!==e.maximum&&(t+="e"),t},noResults:function(){return"Nu au fost găsite rezultate"},searching:function(){return"Căutare…"},removeAllItems:function(){return"Eliminați toate elementele"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/ru.js b/InvenTree/InvenTree/static/select2/js/i18n/ru.js new file mode 100644 index 0000000000..63a7d66c3b --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/ru.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ru",[],function(){function n(n,e,r,u){return n%10<5&&n%10>0&&n%100<5||n%100>20?n%10>1?r:e:u}return{errorLoading:function(){return"Невозможно загрузить результаты"},inputTooLong:function(e){var r=e.input.length-e.maximum,u="Пожалуйста, введите на "+r+" символ";return u+=n(r,"","a","ов"),u+=" меньше"},inputTooShort:function(e){var r=e.minimum-e.input.length,u="Пожалуйста, введите ещё хотя бы "+r+" символ";return u+=n(r,"","a","ов")},loadingMore:function(){return"Загрузка данных…"},maximumSelected:function(e){var r="Вы можете выбрать не более "+e.maximum+" элемент";return r+=n(e.maximum,"","a","ов")},noResults:function(){return"Совпадений не найдено"},searching:function(){return"Поиск…"},removeAllItems:function(){return"Удалить все элементы"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sk.js b/InvenTree/InvenTree/static/select2/js/i18n/sk.js new file mode 100644 index 0000000000..5049528ad0 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sk",[],function(){var e={2:function(e){return e?"dva":"dve"},3:function(){return"tri"},4:function(){return"štyri"}};return{errorLoading:function(){return"Výsledky sa nepodarilo načítať."},inputTooLong:function(n){var t=n.input.length-n.maximum;return 1==t?"Prosím, zadajte o jeden znak menej":t>=2&&t<=4?"Prosím, zadajte o "+e[t](!0)+" znaky menej":"Prosím, zadajte o "+t+" znakov menej"},inputTooShort:function(n){var t=n.minimum-n.input.length;return 1==t?"Prosím, zadajte ešte jeden znak":t<=4?"Prosím, zadajte ešte ďalšie "+e[t](!0)+" znaky":"Prosím, zadajte ešte ďalších "+t+" znakov"},loadingMore:function(){return"Načítanie ďalších výsledkov…"},maximumSelected:function(n){return 1==n.maximum?"Môžete zvoliť len jednu položku":n.maximum>=2&&n.maximum<=4?"Môžete zvoliť najviac "+e[n.maximum](!1)+" položky":"Môžete zvoliť najviac "+n.maximum+" položiek"},noResults:function(){return"Nenašli sa žiadne položky"},searching:function(){return"Vyhľadávanie…"},removeAllItems:function(){return"Odstráňte všetky položky"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sl.js b/InvenTree/InvenTree/static/select2/js/i18n/sl.js new file mode 100644 index 0000000000..4d0b7d3e34 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sl",[],function(){return{errorLoading:function(){return"Zadetkov iskanja ni bilo mogoče naložiti."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Prosim zbrišite "+n+" znak";return 2==n?t+="a":1!=n&&(t+="e"),t},inputTooShort:function(e){var n=e.minimum-e.input.length,t="Prosim vpišite še "+n+" znak";return 2==n?t+="a":1!=n&&(t+="e"),t},loadingMore:function(){return"Nalagam več zadetkov…"},maximumSelected:function(e){var n="Označite lahko največ "+e.maximum+" predmet";return 2==e.maximum?n+="a":1!=e.maximum&&(n+="e"),n},noResults:function(){return"Ni zadetkov."},searching:function(){return"Iščem…"},removeAllItems:function(){return"Odstranite vse elemente"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sq.js b/InvenTree/InvenTree/static/select2/js/i18n/sq.js new file mode 100644 index 0000000000..59162024ed --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sq.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sq",[],function(){return{errorLoading:function(){return"Rezultatet nuk mund të ngarkoheshin."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Të lutem fshi "+n+" karakter";return 1!=n&&(t+="e"),t},inputTooShort:function(e){return"Të lutem shkruaj "+(e.minimum-e.input.length)+" ose më shumë karaktere"},loadingMore:function(){return"Duke ngarkuar më shumë rezultate…"},maximumSelected:function(e){var n="Mund të zgjedhësh vetëm "+e.maximum+" element";return 1!=e.maximum&&(n+="e"),n},noResults:function(){return"Nuk u gjet asnjë rezultat"},searching:function(){return"Duke kërkuar…"},removeAllItems:function(){return"Hiq të gjitha sendet"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sr-Cyrl.js b/InvenTree/InvenTree/static/select2/js/i18n/sr-Cyrl.js new file mode 100644 index 0000000000..ce13ce8f9a --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sr-Cyrl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sr-Cyrl",[],function(){function n(n,e,r,u){return n%10==1&&n%100!=11?e:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?r:u}return{errorLoading:function(){return"Преузимање није успело."},inputTooLong:function(e){var r=e.input.length-e.maximum,u="Обришите "+r+" симбол";return u+=n(r,"","а","а")},inputTooShort:function(e){var r=e.minimum-e.input.length,u="Укуцајте бар још "+r+" симбол";return u+=n(r,"","а","а")},loadingMore:function(){return"Преузимање још резултата…"},maximumSelected:function(e){var r="Можете изабрати само "+e.maximum+" ставк";return r+=n(e.maximum,"у","е","и")},noResults:function(){return"Ништа није пронађено"},searching:function(){return"Претрага…"},removeAllItems:function(){return"Уклоните све ставке"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sr.js b/InvenTree/InvenTree/static/select2/js/i18n/sr.js new file mode 100644 index 0000000000..dd407a06dc --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sr",[],function(){function n(n,e,r,t){return n%10==1&&n%100!=11?e:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?r:t}return{errorLoading:function(){return"Preuzimanje nije uspelo."},inputTooLong:function(e){var r=e.input.length-e.maximum,t="Obrišite "+r+" simbol";return t+=n(r,"","a","a")},inputTooShort:function(e){var r=e.minimum-e.input.length,t="Ukucajte bar još "+r+" simbol";return t+=n(r,"","a","a")},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(e){var r="Možete izabrati samo "+e.maximum+" stavk";return r+=n(e.maximum,"u","e","i")},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Уклоните све ставке"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/sv.js b/InvenTree/InvenTree/static/select2/js/i18n/sv.js new file mode 100644 index 0000000000..1bc8724a79 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/sv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sv",[],function(){return{errorLoading:function(){return"Resultat kunde inte laddas."},inputTooLong:function(n){return"Vänligen sudda ut "+(n.input.length-n.maximum)+" tecken"},inputTooShort:function(n){return"Vänligen skriv in "+(n.minimum-n.input.length)+" eller fler tecken"},loadingMore:function(){return"Laddar fler resultat…"},maximumSelected:function(n){return"Du kan max välja "+n.maximum+" element"},noResults:function(){return"Inga träffar"},searching:function(){return"Söker…"},removeAllItems:function(){return"Ta bort alla objekt"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/th.js b/InvenTree/InvenTree/static/select2/js/i18n/th.js new file mode 100644 index 0000000000..63eab7114b --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/th.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/th",[],function(){return{errorLoading:function(){return"ไม่สามารถค้นข้อมูลได้"},inputTooLong:function(n){return"โปรดลบออก "+(n.input.length-n.maximum)+" ตัวอักษร"},inputTooShort:function(n){return"โปรดพิมพ์เพิ่มอีก "+(n.minimum-n.input.length)+" ตัวอักษร"},loadingMore:function(){return"กำลังค้นข้อมูลเพิ่ม…"},maximumSelected:function(n){return"คุณสามารถเลือกได้ไม่เกิน "+n.maximum+" รายการ"},noResults:function(){return"ไม่พบข้อมูล"},searching:function(){return"กำลังค้นข้อมูล…"},removeAllItems:function(){return"ลบรายการทั้งหมด"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/tk.js b/InvenTree/InvenTree/static/select2/js/i18n/tk.js new file mode 100644 index 0000000000..30255ff377 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/tk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/tk",[],function(){return{errorLoading:function(){return"Netije ýüklenmedi."},inputTooLong:function(e){return e.input.length-e.maximum+" harp bozuň."},inputTooShort:function(e){return"Ýene-de iň az "+(e.minimum-e.input.length)+" harp ýazyň."},loadingMore:function(){return"Köpräk netije görkezilýär…"},maximumSelected:function(e){return"Diňe "+e.maximum+" sanysyny saýlaň."},noResults:function(){return"Netije tapylmady."},searching:function(){return"Gözlenýär…"},removeAllItems:function(){return"Remove all items"}}}),e.define,e.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/tr.js b/InvenTree/InvenTree/static/select2/js/i18n/tr.js new file mode 100644 index 0000000000..fc4c0bf051 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/tr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/tr",[],function(){return{errorLoading:function(){return"Sonuç yüklenemedi"},inputTooLong:function(n){return n.input.length-n.maximum+" karakter daha girmelisiniz"},inputTooShort:function(n){return"En az "+(n.minimum-n.input.length)+" karakter daha girmelisiniz"},loadingMore:function(){return"Daha fazla…"},maximumSelected:function(n){return"Sadece "+n.maximum+" seçim yapabilirsiniz"},noResults:function(){return"Sonuç bulunamadı"},searching:function(){return"Aranıyor…"},removeAllItems:function(){return"Tüm öğeleri kaldır"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/uk.js b/InvenTree/InvenTree/static/select2/js/i18n/uk.js new file mode 100644 index 0000000000..63697e3884 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/uk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/uk",[],function(){function n(n,e,u,r){return n%100>10&&n%100<15?r:n%10==1?e:n%10>1&&n%10<5?u:r}return{errorLoading:function(){return"Неможливо завантажити результати"},inputTooLong:function(e){return"Будь ласка, видаліть "+(e.input.length-e.maximum)+" "+n(e.maximum,"літеру","літери","літер")},inputTooShort:function(n){return"Будь ласка, введіть "+(n.minimum-n.input.length)+" або більше літер"},loadingMore:function(){return"Завантаження інших результатів…"},maximumSelected:function(e){return"Ви можете вибрати лише "+e.maximum+" "+n(e.maximum,"пункт","пункти","пунктів")},noResults:function(){return"Нічого не знайдено"},searching:function(){return"Пошук…"},removeAllItems:function(){return"Видалити всі елементи"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/vi.js b/InvenTree/InvenTree/static/select2/js/i18n/vi.js new file mode 100644 index 0000000000..24f3bc2d61 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/vi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/vi",[],function(){return{inputTooLong:function(n){return"Vui lòng xóa bớt "+(n.input.length-n.maximum)+" ký tự"},inputTooShort:function(n){return"Vui lòng nhập thêm từ "+(n.minimum-n.input.length)+" ký tự trở lên"},loadingMore:function(){return"Đang lấy thêm kết quả…"},maximumSelected:function(n){return"Chỉ có thể chọn được "+n.maximum+" lựa chọn"},noResults:function(){return"Không tìm thấy kết quả"},searching:function(){return"Đang tìm…"},removeAllItems:function(){return"Xóa tất cả các mục"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/zh-CN.js b/InvenTree/InvenTree/static/select2/js/i18n/zh-CN.js new file mode 100644 index 0000000000..2c5649d310 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/zh-CN.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(n){return"请删除"+(n.input.length-n.maximum)+"个字符"},inputTooShort:function(n){return"请再输入至少"+(n.minimum-n.input.length)+"个字符"},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(n){return"最多只能选择"+n.maximum+"个项目"},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"},removeAllItems:function(){return"删除所有项目"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/select2/js/i18n/zh-TW.js b/InvenTree/InvenTree/static/select2/js/i18n/zh-TW.js new file mode 100644 index 0000000000..570a566937 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/i18n/zh-TW.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/zh-TW",[],function(){return{inputTooLong:function(n){return"請刪掉"+(n.input.length-n.maximum)+"個字元"},inputTooShort:function(n){return"請再輸入"+(n.minimum-n.input.length)+"個字元"},loadingMore:function(){return"載入中…"},maximumSelected:function(n){return"你只能選擇最多"+n.maximum+"項"},noResults:function(){return"沒有找到相符的項目"},searching:function(){return"搜尋中…"},removeAllItems:function(){return"刪除所有項目"}}}),n.define,n.require}(); \ No newline at end of file diff --git a/InvenTree/InvenTree/static/script/select2/select2.full.js b/InvenTree/InvenTree/static/select2/js/select2.full.js similarity index 89% rename from InvenTree/InvenTree/static/script/select2/select2.full.js rename to InvenTree/InvenTree/static/select2/js/select2.full.js index 608642bf64..358572a657 100644 --- a/InvenTree/InvenTree/static/script/select2/select2.full.js +++ b/InvenTree/InvenTree/static/select2/js/select2.full.js @@ -1,11 +1,11 @@ /*! - * Select2 4.0.5 + * Select2 4.0.13 * https://select2.github.io * * Released under the MIT license * https://github.com/select2/select2/blob/master/LICENSE.md */ -(function (factory) { +;(function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); @@ -574,10 +574,10 @@ S2.define('select2/utils',[ DecoratedClass.prototype = new ctr(); for (var m = 0; m < superMethods.length; m++) { - var superMethod = superMethods[m]; + var superMethod = superMethods[m]; - DecoratedClass.prototype[superMethod] = - SuperClass.prototype[superMethod]; + DecoratedClass.prototype[superMethod] = + SuperClass.prototype[superMethod]; } var calledMethod = function (methodName) { @@ -772,6 +772,70 @@ S2.define('select2/utils',[ $element.append($nodes); }; + // Cache objects in Utils.__cache instead of $.data (see #4346) + Utils.__cache = {}; + + var id = 0; + Utils.GetUniqueElementId = function (element) { + // Get a unique element Id. If element has no id, + // creates a new unique number, stores it in the id + // attribute and returns the new id. + // If an id already exists, it simply returns it. + + var select2Id = element.getAttribute('data-select2-id'); + if (select2Id == null) { + // If element has id, use it. + if (element.id) { + select2Id = element.id; + element.setAttribute('data-select2-id', select2Id); + } else { + element.setAttribute('data-select2-id', ++id); + select2Id = id.toString(); + } + } + return select2Id; + }; + + Utils.StoreData = function (element, name, value) { + // Stores an item in the cache for a specified element. + // name is the cache key. + var id = Utils.GetUniqueElementId(element); + if (!Utils.__cache[id]) { + Utils.__cache[id] = {}; + } + + Utils.__cache[id][name] = value; + }; + + Utils.GetData = function (element, name) { + // Retrieves a value from the cache by its key (name) + // name is optional. If no name specified, return + // all cache items for the specified element. + // and for a specified element. + var id = Utils.GetUniqueElementId(element); + if (name) { + if (Utils.__cache[id]) { + if (Utils.__cache[id][name] != null) { + return Utils.__cache[id][name]; + } + return $(element).data(name); // Fallback to HTML5 data attribs. + } + return $(element).data(name); // Fallback to HTML5 data attribs. + } else { + return Utils.__cache[id]; + } + }; + + Utils.RemoveData = function (element) { + // Removes all cached items for a specified element. + var id = Utils.GetUniqueElementId(element); + if (Utils.__cache[id] != null) { + delete Utils.__cache[id]; + } + + element.removeAttribute('data-select2-id'); + }; + return Utils; }); @@ -791,7 +855,7 @@ S2.define('select2/results',[ Results.prototype.render = function () { var $results = $( - '
        ' + '
          ' ); if (this.options.get('multiple')) { @@ -814,7 +878,7 @@ S2.define('select2/results',[ this.hideLoading(); var $message = $( - '
        • ' ); @@ -907,7 +971,7 @@ S2.define('select2/results',[ $options.each(function () { var $option = $(this); - var item = $.data(this, 'data'); + var item = Utils.GetData(this, 'data'); // id needs to be converted to a string when comparing var id = '' + item.id; @@ -948,11 +1012,16 @@ S2.define('select2/results',[ option.className = 'select2-results__option'; var attrs = { - 'role': 'treeitem', + 'role': 'option', 'aria-selected': 'false' }; - if (data.disabled) { + var matches = window.Element.prototype.matches || + window.Element.prototype.msMatchesSelector || + window.Element.prototype.webkitMatchesSelector; + + if ((data.element != null && matches.call(data.element, ':disabled')) || + (data.element == null && data.disabled)) { delete attrs['aria-selected']; attrs['aria-disabled'] = 'true'; } @@ -1012,7 +1081,7 @@ S2.define('select2/results',[ this.template(data, option); } - $.data(option, 'data', data); + Utils.StoreData(option, 'data', data); return option; }; @@ -1053,7 +1122,10 @@ S2.define('select2/results',[ } self.setClasses(); - self.highlightFirstItem(); + + if (self.options.get('scrollAfterSelect')) { + self.highlightFirstItem(); + } }); container.on('unselect', function () { @@ -1062,7 +1134,10 @@ S2.define('select2/results',[ } self.setClasses(); - self.highlightFirstItem(); + + if (self.options.get('scrollAfterSelect')) { + self.highlightFirstItem(); + } }); container.on('open', function () { @@ -1098,7 +1173,7 @@ S2.define('select2/results',[ return; } - var data = $highlighted.data('data'); + var data = Utils.GetData($highlighted[0], 'data'); if ($highlighted.attr('aria-selected') == 'true') { self.trigger('close', {}); @@ -1116,8 +1191,9 @@ S2.define('select2/results',[ var currentIndex = $options.index($highlighted); - // If we are already at te top, don't move further - if (currentIndex === 0) { + // If we are already at the top, don't move further + // If no options, currentIndex will be -1 + if (currentIndex <= 0) { return; } @@ -1210,7 +1286,7 @@ S2.define('select2/results',[ function (evt) { var $this = $(this); - var data = $this.data('data'); + var data = Utils.GetData(this, 'data'); if ($this.attr('aria-selected') === 'true') { if (self.options.get('multiple')) { @@ -1233,7 +1309,7 @@ S2.define('select2/results',[ this.$results.on('mouseenter', '.select2-results__option[aria-selected]', function (evt) { - var data = $(this).data('data'); + var data = Utils.GetData(this, 'data'); self.getHighlightedResults() .removeClass('select2-results__option--highlighted'); @@ -1348,14 +1424,15 @@ S2.define('select2/selection/base',[ this._tabindex = 0; - if (this.$element.data('old-tabindex') != null) { - this._tabindex = this.$element.data('old-tabindex'); + if (Utils.GetData(this.$element[0], 'old-tabindex') != null) { + this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex'); } else if (this.$element.attr('tabindex') != null) { this._tabindex = this.$element.attr('tabindex'); } $selection.attr('title', this.$element.attr('title')); $selection.attr('tabindex', this._tabindex); + $selection.attr('aria-disabled', 'false'); this.$selection = $selection; @@ -1365,7 +1442,6 @@ S2.define('select2/selection/base',[ BaseSelection.prototype.bind = function (container, $container) { var self = this; - var id = container.id + '-container'; var resultsId = container.id + '-results'; this.container = container; @@ -1408,17 +1484,19 @@ S2.define('select2/selection/base',[ self.$selection.removeAttr('aria-activedescendant'); self.$selection.removeAttr('aria-owns'); - self.$selection.focus(); + self.$selection.trigger('focus'); self._detachCloseHandler(container); }); container.on('enable', function () { self.$selection.attr('tabindex', self._tabindex); + self.$selection.attr('aria-disabled', 'false'); }); container.on('disable', function () { self.$selection.attr('tabindex', '-1'); + self.$selection.attr('aria-disabled', 'true'); }); }; @@ -1441,7 +1519,6 @@ S2.define('select2/selection/base',[ }; BaseSelection.prototype._attachCloseHandler = function (container) { - var self = this; $(document.body).on('mousedown.select2.' + container.id, function (e) { var $target = $(e.target); @@ -1451,13 +1528,11 @@ S2.define('select2/selection/base',[ var $all = $('.select2.select2-container--open'); $all.each(function () { - var $this = $(this); - if (this == $select[0]) { return; } - var $element = $this.data('element'); + var $element = Utils.GetData(this, 'element'); $element.select2('close'); }); @@ -1481,6 +1556,27 @@ S2.define('select2/selection/base',[ throw new Error('The `update` method must be defined in child classes.'); }; + /** + * Helper method to abstract the "enabled" (not "disabled") state of this + * object. + * + * @return {true} if the instance is not disabled. + * @return {false} if the instance is disabled. + */ + BaseSelection.prototype.isEnabled = function () { + return !this.isDisabled(); + }; + + /** + * Helper method to abstract the "disabled" state of this object. + * + * @return {true} if the disabled option is true. + * @return {false} if the disabled option is false. + */ + BaseSelection.prototype.isDisabled = function () { + return this.options.get('disabled'); + }; + return BaseSelection; }); @@ -1518,7 +1614,10 @@ S2.define('select2/selection/single',[ var id = container.id + '-container'; - this.$selection.find('.select2-selection__rendered').attr('id', id); + this.$selection.find('.select2-selection__rendered') + .attr('id', id) + .attr('role', 'textbox') + .attr('aria-readonly', 'true'); this.$selection.attr('aria-labelledby', id); this.$selection.on('mousedown', function (evt) { @@ -1542,17 +1641,15 @@ S2.define('select2/selection/single',[ container.on('focus', function (evt) { if (!container.isOpen()) { - self.$selection.focus(); + self.$selection.trigger('focus'); } }); - - container.on('selection:update', function (params) { - self.update(params.data); - }); }; SingleSelection.prototype.clear = function () { - this.$selection.find('.select2-selection__rendered').empty(); + var $rendered = this.$selection.find('.select2-selection__rendered'); + $rendered.empty(); + $rendered.removeAttr('title'); // clear tooltip on empty }; SingleSelection.prototype.display = function (data, container) { @@ -1578,7 +1675,14 @@ S2.define('select2/selection/single',[ var formatted = this.display(selection, $rendered); $rendered.empty().append(formatted); - $rendered.prop('title', selection.title || selection.text); + + var title = selection.title || selection.text; + + if (title) { + $rendered.attr('title', title); + } else { + $rendered.removeAttr('title'); + } }; return SingleSelection; @@ -1623,14 +1727,14 @@ S2.define('select2/selection/multiple',[ '.select2-selection__choice__remove', function (evt) { // Ignore the event if it is disabled - if (self.options.get('disabled')) { + if (self.isDisabled()) { return; } var $remove = $(this); var $selection = $remove.parent(); - var data = $selection.data('data'); + var data = Utils.GetData($selection[0], 'data'); self.trigger('unselect', { originalEvent: evt, @@ -1641,7 +1745,9 @@ S2.define('select2/selection/multiple',[ }; MultipleSelection.prototype.clear = function () { - this.$selection.find('.select2-selection__rendered').empty(); + var $rendered = this.$selection.find('.select2-selection__rendered'); + $rendered.empty(); + $rendered.removeAttr('title'); }; MultipleSelection.prototype.display = function (data, container) { @@ -1679,9 +1785,14 @@ S2.define('select2/selection/multiple',[ var formatted = this.display(selection, $selection); $selection.append(formatted); - $selection.prop('title', selection.title || selection.text); - $selection.data('data', selection); + var title = selection.title || selection.text; + + if (title) { + $selection.attr('title', title); + } + + Utils.StoreData($selection[0], 'data', selection); $selections.push($selection); } @@ -1746,8 +1857,9 @@ S2.define('select2/selection/placeholder',[ S2.define('select2/selection/allowClear',[ 'jquery', - '../keys' -], function ($, KEYS) { + '../keys', + '../utils' +], function ($, KEYS, Utils) { function AllowClear () { } AllowClear.prototype.bind = function (decorated, container, $container) { @@ -1776,7 +1888,7 @@ S2.define('select2/selection/allowClear',[ AllowClear.prototype._handleClear = function (_, evt) { // Ignore the event if it is disabled - if (this.options.get('disabled')) { + if (this.isDisabled()) { return; } @@ -1789,10 +1901,22 @@ S2.define('select2/selection/allowClear',[ evt.stopPropagation(); - var data = $clear.data('data'); + var data = Utils.GetData($clear[0], 'data'); + + var previousVal = this.$element.val(); + this.$element.val(this.placeholder.id); + + var unselectData = { + data: data + }; + this.trigger('clear', unselectData); + if (unselectData.prevented) { + this.$element.val(previousVal); + return; + } for (var d = 0; d < data.length; d++) { - var unselectData = { + unselectData = { data: data[d] }; @@ -1802,11 +1926,12 @@ S2.define('select2/selection/allowClear',[ // If the event was prevented, don't clear it out. if (unselectData.prevented) { + this.$element.val(previousVal); return; } } - this.$element.val(this.placeholder.id).trigger('change'); + this.$element.trigger('input').trigger('change'); this.trigger('toggle', {}); }; @@ -1829,12 +1954,14 @@ S2.define('select2/selection/allowClear',[ return; } + var removeAll = this.options.get('translations').get('removeAllItems'); + var $remove = $( - '' + + '' + '×' + '' ); - $remove.data('data', data); + Utils.StoreData($remove[0], 'data', data); this.$selection.find('.select2-selection__rendered').prepend($remove); }; @@ -1856,7 +1983,7 @@ S2.define('select2/selection/search',[ '' ); @@ -1873,14 +2000,18 @@ S2.define('select2/selection/search',[ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); container.on('open', function () { + self.$search.attr('aria-controls', resultsId); self.$search.trigger('focus'); }); container.on('close', function () { self.$search.val(''); + self.$search.removeAttr('aria-controls'); self.$search.removeAttr('aria-activedescendant'); self.$search.trigger('focus'); }); @@ -1900,7 +2031,11 @@ S2.define('select2/selection/search',[ }); container.on('results:focus', function (params) { - self.$search.attr('aria-activedescendant', params.id); + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } }); this.$selection.on('focusin', '.select2-search--inline', function (evt) { @@ -1925,7 +2060,7 @@ S2.define('select2/selection/search',[ .prev('.select2-selection__choice'); if ($previousChoice.length > 0) { - var item = $previousChoice.data('data'); + var item = Utils.GetData($previousChoice[0], 'data'); self.searchRemoveChoice(item); @@ -1934,6 +2069,12 @@ S2.define('select2/selection/search',[ } }); + this.$selection.on('click', '.select2-search--inline', function (evt) { + if (self.$search.val()) { + evt.stopPropagation(); + } + }); + // Try to detect the IE version should the `documentMode` property that // is stored on the document. This is only implemented in IE and is // slightly cleaner than doing a user agent check. @@ -2019,7 +2160,7 @@ S2.define('select2/selection/search',[ this.resizeSearch(); if (searchHadFocus) { - this.$search.focus(); + this.$search.trigger('focus'); } }; @@ -2052,7 +2193,7 @@ S2.define('select2/selection/search',[ var width = ''; if (this.$search.attr('placeholder') !== '') { - width = this.$selection.find('.select2-selection__rendered').innerWidth(); + width = this.$selection.find('.select2-selection__rendered').width(); } else { var minimumWidth = this.$search.val().length + 1; @@ -2076,10 +2217,13 @@ S2.define('select2/selection/eventRelay',[ 'open', 'opening', 'close', 'closing', 'select', 'selecting', - 'unselect', 'unselecting' + 'unselect', 'unselecting', + 'clear', 'clearing' ]; - var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + var preventableEvents = [ + 'opening', 'closing', 'selecting', 'unselecting', 'clearing' + ]; decorated.call(this, container, $container); @@ -2412,6 +2556,7 @@ S2.define('select2/diacritics',[ '\u019F': 'O', '\uA74A': 'O', '\uA74C': 'O', + '\u0152': 'OE', '\u01A2': 'OI', '\uA74E': 'OO', '\u0222': 'OU', @@ -2821,6 +2966,7 @@ S2.define('select2/diacritics',[ '\uA74B': 'o', '\uA74D': 'o', '\u0275': 'o', + '\u0153': 'oe', '\u01A3': 'oi', '\u0223': 'ou', '\uA74F': 'oo', @@ -2989,8 +3135,9 @@ S2.define('select2/diacritics',[ '\u03CD': '\u03C5', '\u03CB': '\u03C5', '\u03B0': '\u03C5', - '\u03C9': '\u03C9', - '\u03C2': '\u03C3' + '\u03CE': '\u03C9', + '\u03C2': '\u03C3', + '\u2019': '\'' }; return diacritics; @@ -3075,7 +3222,7 @@ S2.define('select2/data/select',[ if ($(data.element).is('option')) { data.element.selected = true; - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); return; } @@ -3096,13 +3243,13 @@ S2.define('select2/data/select',[ } self.$element.val(val); - self.$element.trigger('change'); + self.$element.trigger('input').trigger('change'); }); } else { var val = data.id; this.$element.val(val); - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); } }; @@ -3118,7 +3265,7 @@ S2.define('select2/data/select',[ if ($(data.element).is('option')) { data.element.selected = false; - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); return; } @@ -3136,7 +3283,7 @@ S2.define('select2/data/select',[ self.$element.val(val); - self.$element.trigger('change'); + self.$element.trigger('input').trigger('change'); }); }; @@ -3158,7 +3305,7 @@ S2.define('select2/data/select',[ // Remove anything added to child elements this.$element.find('*').each(function () { // Remove any custom data set by Select2 - $.removeData(this, 'data'); + Utils.RemoveData(this); }); }; @@ -3231,7 +3378,7 @@ S2.define('select2/data/select',[ normalizedData.element = option; // Override the option's data with the combined data - $.data(option, 'data', normalizedData); + Utils.StoreData(option, 'data', normalizedData); return $option; }; @@ -3239,7 +3386,7 @@ S2.define('select2/data/select',[ SelectAdapter.prototype.item = function ($option) { var data = {}; - data = $.data($option[0], 'data'); + data = Utils.GetData($option[0], 'data'); if (data != null) { return data; @@ -3277,13 +3424,13 @@ S2.define('select2/data/select',[ data = this._normalizeItem(data); data.element = $option[0]; - $.data($option[0], 'data', data); + Utils.StoreData($option[0], 'data', data); return data; }; SelectAdapter.prototype._normalizeItem = function (item) { - if (!$.isPlainObject(item)) { + if (item !== Object(item)) { item = { id: item, text: item @@ -3329,15 +3476,19 @@ S2.define('select2/data/array',[ 'jquery' ], function (SelectAdapter, Utils, $) { function ArrayAdapter ($element, options) { - var data = options.get('data') || []; + this._dataToConvert = options.get('data') || []; ArrayAdapter.__super__.constructor.call(this, $element, options); - - this.addOptions(this.convertToOptions(data)); } Utils.Extend(ArrayAdapter, SelectAdapter); + ArrayAdapter.prototype.bind = function (container, $container) { + ArrayAdapter.__super__.bind.call(this, container, $container); + + this.addOptions(this.convertToOptions(this._dataToConvert)); + }; + ArrayAdapter.prototype.select = function (data) { var $option = this.$element.find('option').filter(function (i, elm) { return elm.value == data.id.toString(); @@ -3487,7 +3638,8 @@ S2.define('select2/data/ajax',[ }, function () { // Attempt to detect if a request was aborted // Only works if the transport exposes a status property - if ($request.status && $request.status === '0') { + if ('status' in $request && + ($request.status === 0 || $request.status === '0')) { return; } @@ -3626,8 +3778,6 @@ S2.define('select2/data/tags',[ }; Tags.prototype._removeOldTags = function (_) { - var tag = this._lastTag; - var $options = this.$element.find('option[data-select2-tag]'); $options.each(function () { @@ -3702,7 +3852,7 @@ S2.define('select2/data/tokenizer',[ // Replace the search term if we have the search box if (this.$search.length) { this.$search.val(tokenData.term); - this.$search.focus(); + this.$search.trigger('focus'); } params.term = tokenData.term; @@ -3831,10 +3981,30 @@ S2.define('select2/data/maximumSelectionLength',[ decorated.call(this, $e, options); } + MaximumSelectionLength.prototype.bind = + function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function () { + self._checkIfMaximumSelected(); + }); + }; + MaximumSelectionLength.prototype.query = function (decorated, params, callback) { var self = this; + this._checkIfMaximumSelected(function () { + decorated.call(self, params, callback); + }); + }; + + MaximumSelectionLength.prototype._checkIfMaximumSelected = + function (_, successCallback) { + var self = this; + this.current(function (currentData) { var count = currentData != null ? currentData.length : 0; if (self.maximumSelectionLength > 0 && @@ -3847,7 +4017,10 @@ S2.define('select2/data/maximumSelectionLength',[ }); return; } - decorated.call(self, params, callback); + + if (successCallback) { + successCallback(); + } }); }; @@ -3886,7 +4059,7 @@ S2.define('select2/dropdown',[ }; Dropdown.prototype.position = function ($dropdown, $container) { - // Should be implmented in subclasses + // Should be implemented in subclasses }; Dropdown.prototype.destroy = function () { @@ -3910,7 +4083,7 @@ S2.define('select2/dropdown/search',[ '' + '' + + ' spellcheck="false" role="searchbox" aria-autocomplete="list" />' + '' ); @@ -3925,6 +4098,8 @@ S2.define('select2/dropdown/search',[ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); this.$search.on('keydown', function (evt) { @@ -3947,23 +4122,27 @@ S2.define('select2/dropdown/search',[ container.on('open', function () { self.$search.attr('tabindex', 0); + self.$search.attr('aria-controls', resultsId); - self.$search.focus(); + self.$search.trigger('focus'); window.setTimeout(function () { - self.$search.focus(); + self.$search.trigger('focus'); }, 0); }); container.on('close', function () { self.$search.attr('tabindex', -1); + self.$search.removeAttr('aria-controls'); + self.$search.removeAttr('aria-activedescendant'); self.$search.val(''); + self.$search.trigger('blur'); }); container.on('focus', function () { if (!container.isOpen()) { - self.$search.focus(); + self.$search.trigger('focus'); } }); @@ -3978,6 +4157,14 @@ S2.define('select2/dropdown/search',[ } } }); + + container.on('results:focus', function (params) { + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } + }); }; Search.prototype.handleSearch = function (evt) { @@ -4062,6 +4249,7 @@ S2.define('select2/dropdown/infiniteScroll',[ if (this.showLoadingMore(data)) { this.$results.append(this.$loadingMore); + this.loadMoreIfNeeded(); } }; @@ -4080,25 +4268,27 @@ S2.define('select2/dropdown/infiniteScroll',[ self.loading = true; }); - this.$results.on('scroll', function () { - var isLoadMoreVisible = $.contains( - document.documentElement, - self.$loadingMore[0] - ); + this.$results.on('scroll', this.loadMoreIfNeeded.bind(this)); + }; - if (self.loading || !isLoadMoreVisible) { - return; - } + InfiniteScroll.prototype.loadMoreIfNeeded = function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + this.$loadingMore[0] + ); - var currentOffset = self.$results.offset().top + - self.$results.outerHeight(false); - var loadingMoreOffset = self.$loadingMore.offset().top + - self.$loadingMore.outerHeight(false); + if (this.loading || !isLoadMoreVisible) { + return; + } - if (currentOffset + 50 >= loadingMoreOffset) { - self.loadMore(); - } - }); + var currentOffset = this.$results.offset().top + + this.$results.outerHeight(false); + var loadingMoreOffset = this.$loadingMore.offset().top + + this.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + this.loadMore(); + } }; InfiniteScroll.prototype.loadMore = function () { @@ -4119,7 +4309,7 @@ S2.define('select2/dropdown/infiniteScroll',[ var $option = $( '
        • ' + 'role="option" aria-disabled="true">' ); var message = this.options.get('translations').get('loadingMore'); @@ -4137,7 +4327,7 @@ S2.define('select2/dropdown/attachBody',[ '../utils' ], function ($, Utils) { function AttachBody (decorated, $element, options) { - this.$dropdownParent = options.get('dropdownParent') || $(document.body); + this.$dropdownParent = $(options.get('dropdownParent') || document.body); decorated.call(this, $element, options); } @@ -4145,27 +4335,14 @@ S2.define('select2/dropdown/attachBody',[ AttachBody.prototype.bind = function (decorated, container, $container) { var self = this; - var setupResultsEvents = false; - decorated.call(this, container, $container); container.on('open', function () { self._showDropdown(); self._attachPositioningHandler(container); - if (!setupResultsEvents) { - setupResultsEvents = true; - - container.on('results:all', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:append', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - } + // Must bind after the results handlers to ensure correct sizing + self._bindContainerResultHandlers(container); }); container.on('close', function () { @@ -4214,6 +4391,44 @@ S2.define('select2/dropdown/attachBody',[ this.$dropdownContainer.detach(); }; + AttachBody.prototype._bindContainerResultHandlers = + function (decorated, container) { + + // These should only be bound once + if (this._containerResultsHandlersBound) { + return; + } + + var self = this; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:message', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('select', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('unselect', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + this._containerResultsHandlersBound = true; + }; + AttachBody.prototype._attachPositioningHandler = function (decorated, container) { var self = this; @@ -4224,14 +4439,14 @@ S2.define('select2/dropdown/attachBody',[ var $watchers = this.$container.parents().filter(Utils.hasScroll); $watchers.each(function () { - $(this).data('select2-scroll-position', { + Utils.StoreData(this, 'select2-scroll-position', { x: $(this).scrollLeft(), y: $(this).scrollTop() }); }); $watchers.on(scrollEvent, function (ev) { - var position = $(this).data('select2-scroll-position'); + var position = Utils.GetData(this, 'select2-scroll-position'); $(this).scrollTop(position.y); }); @@ -4290,16 +4505,26 @@ S2.define('select2/dropdown/attachBody',[ top: container.bottom }; - // Determine what the parent element is to use for calciulating the offset + // Determine what the parent element is to use for calculating the offset var $offsetParent = this.$dropdownParent; - // For statically positoned elements, we need to get the element + // For statically positioned elements, we need to get the element // that is determining the offset if ($offsetParent.css('position') === 'static') { $offsetParent = $offsetParent.offsetParent(); } - var parentOffset = $offsetParent.offset(); + var parentOffset = { + top: 0, + left: 0 + }; + + if ( + $.contains(document.body, $offsetParent[0]) || + $offsetParent[0].isConnected + ) { + parentOffset = $offsetParent.offset(); + } css.top -= parentOffset.top; css.left -= parentOffset.left; @@ -4396,8 +4621,8 @@ S2.define('select2/dropdown/minimumResultsForSearch',[ }); S2.define('select2/dropdown/selectOnClose',[ - -], function () { + '../utils' +], function (Utils) { function SelectOnClose () { } SelectOnClose.prototype.bind = function (decorated, container, $container) { @@ -4428,7 +4653,7 @@ S2.define('select2/dropdown/selectOnClose',[ return; } - var data = $highlightedResults.data('data'); + var data = Utils.GetData($highlightedResults[0], 'data'); // Don't re-select already selected resulte if ( @@ -4469,7 +4694,7 @@ S2.define('select2/dropdown/closeOnSelect',[ var originalEvent = evt.originalEvent; // Don't close if the control key is being held - if (originalEvent && originalEvent.ctrlKey) { + if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) { return; } @@ -4523,6 +4748,9 @@ S2.define('select2/i18n/en',[],function () { }, searching: function () { return 'Searching…'; + }, + removeAllItems: function () { + return 'Remove all items'; } }; }); @@ -4761,66 +4989,29 @@ S2.define('select2/defaults',[ ); } - if (typeof options.language === 'string') { - // Check if the language is specified with a region - if (options.language.indexOf('-') > 0) { - // Extract the region information if it is included - var languageParts = options.language.split('-'); - var baseLanguage = languageParts[0]; + // If the defaults were not previously applied from an element, it is + // possible for the language option to have not been resolved + options.language = this._resolveLanguage(options.language); - options.language = [options.language, baseLanguage]; - } else { - options.language = [options.language]; + // Always fall back to English since it will always be complete + options.language.push('en'); + + var uniqueLanguages = []; + + for (var l = 0; l < options.language.length; l++) { + var language = options.language[l]; + + if (uniqueLanguages.indexOf(language) === -1) { + uniqueLanguages.push(language); } } - if ($.isArray(options.language)) { - var languages = new Translation(); - options.language.push('en'); + options.language = uniqueLanguages; - var languageNames = options.language; - - for (var l = 0; l < languageNames.length; l++) { - var name = languageNames[l]; - var language = {}; - - try { - // Try to load it with the original name - language = Translation.loadPath(name); - } catch (e) { - try { - // If we couldn't load it, check if it wasn't the full path - name = this.defaults.amdLanguageBase + name; - language = Translation.loadPath(name); - } catch (ex) { - // The translation could not be loaded at all. Sometimes this is - // because of a configuration problem, other times this can be - // because of how Select2 helps load all possible translation files. - if (options.debug && window.console && console.warn) { - console.warn( - 'Select2: The language file for "' + name + '" could not be ' + - 'automatically loaded. A fallback will be used instead.' - ); - } - - continue; - } - } - - languages.extend(language); - } - - options.translations = languages; - } else { - var baseTranslation = Translation.loadPath( - this.defaults.amdLanguageBase + 'en' - ); - var customTranslation = new Translation(options.language); - - customTranslation.extend(baseTranslation); - - options.translations = customTranslation; - } + options.translations = this._processTranslations( + options.language, + options.debug + ); return options; }; @@ -4887,13 +5078,14 @@ S2.define('select2/defaults',[ debug: false, dropdownAutoWidth: false, escapeMarkup: Utils.escapeMarkup, - language: EnglishTranslation, + language: {}, matcher: matcher, minimumInputLength: 0, maximumInputLength: 0, maximumSelectionLength: 0, minimumResultsForSearch: 0, selectOnClose: false, + scrollAfterSelect: false, sorter: function (data) { return data; }, @@ -4908,6 +5100,103 @@ S2.define('select2/defaults',[ }; }; + Defaults.prototype.applyFromElement = function (options, $element) { + var optionLanguage = options.language; + var defaultLanguage = this.defaults.language; + var elementLanguage = $element.prop('lang'); + var parentLanguage = $element.closest('[lang]').prop('lang'); + + var languages = Array.prototype.concat.call( + this._resolveLanguage(elementLanguage), + this._resolveLanguage(optionLanguage), + this._resolveLanguage(defaultLanguage), + this._resolveLanguage(parentLanguage) + ); + + options.language = languages; + + return options; + }; + + Defaults.prototype._resolveLanguage = function (language) { + if (!language) { + return []; + } + + if ($.isEmptyObject(language)) { + return []; + } + + if ($.isPlainObject(language)) { + return [language]; + } + + var languages; + + if (!$.isArray(language)) { + languages = [language]; + } else { + languages = language; + } + + var resolvedLanguages = []; + + for (var l = 0; l < languages.length; l++) { + resolvedLanguages.push(languages[l]); + + if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = languages[l].split('-'); + var baseLanguage = languageParts[0]; + + resolvedLanguages.push(baseLanguage); + } + } + + return resolvedLanguages; + }; + + Defaults.prototype._processTranslations = function (languages, debug) { + var translations = new Translation(); + + for (var l = 0; l < languages.length; l++) { + var languageData = new Translation(); + + var language = languages[l]; + + if (typeof language === 'string') { + try { + // Try to load it with the original name + languageData = Translation.loadPath(language); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + language = this.defaults.amdLanguageBase + language; + languageData = Translation.loadPath(language); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files + if (debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + language + '" could ' + + 'not be automatically loaded. A fallback will be used instead.' + ); + } + } + } + } else if ($.isPlainObject(language)) { + languageData = new Translation(language); + } else { + languageData = language; + } + + translations.extend(languageData); + } + + return translations; + }; + Defaults.prototype.set = function (key, value) { var camelKey = $.camelCase(key); @@ -4916,7 +5205,7 @@ S2.define('select2/defaults',[ var convertedData = Utils._convertData(data); - $.extend(this.defaults, convertedData); + $.extend(true, this.defaults, convertedData); }; var defaults = new Defaults(); @@ -4937,6 +5226,10 @@ S2.define('select2/options',[ this.fromElement($element); } + if ($element != null) { + this.options = Defaults.applyFromElement(this.options, $element); + } + this.options = Defaults.apply(this.options); if ($element && $element.is('input')) { @@ -4960,14 +5253,6 @@ S2.define('select2/options',[ this.options.disabled = $e.prop('disabled'); } - if (this.options.language == null) { - if ($e.prop('lang')) { - this.options.language = $e.prop('lang').toLowerCase(); - } else if ($e.closest('[lang]').prop('lang')) { - this.options.language = $e.closest('[lang]').prop('lang'); - } - } - if (this.options.dir == null) { if ($e.prop('dir')) { this.options.dir = $e.prop('dir'); @@ -4981,7 +5266,7 @@ S2.define('select2/options',[ $e.prop('disabled', this.options.disabled); $e.prop('multiple', this.options.multiple); - if ($e.data('select2Tags')) { + if (Utils.GetData($e[0], 'select2Tags')) { if (this.options.debug && window.console && console.warn) { console.warn( 'Select2: The `data-select2-tags` attribute has been changed to ' + @@ -4990,11 +5275,11 @@ S2.define('select2/options',[ ); } - $e.data('data', $e.data('select2Tags')); - $e.data('tags', true); + Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); + Utils.StoreData($e[0], 'tags', true); } - if ($e.data('ajaxUrl')) { + if (Utils.GetData($e[0], 'ajaxUrl')) { if (this.options.debug && window.console && console.warn) { console.warn( 'Select2: The `data-ajax-url` attribute has been changed to ' + @@ -5003,21 +5288,45 @@ S2.define('select2/options',[ ); } - $e.attr('ajax--url', $e.data('ajaxUrl')); - $e.data('ajax--url', $e.data('ajaxUrl')); + $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); + Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); } var dataset = {}; + function upperCaseLetter(_, letter) { + return letter.toUpperCase(); + } + + // Pre-load all of the attributes which are prefixed with `data-` + for (var attr = 0; attr < $e[0].attributes.length; attr++) { + var attributeName = $e[0].attributes[attr].name; + var prefix = 'data-'; + + if (attributeName.substr(0, prefix.length) == prefix) { + // Get the contents of the attribute after `data-` + var dataName = attributeName.substring(prefix.length); + + // Get the data contents from the consistent source + // This is more than likely the jQuery data helper + var dataValue = Utils.GetData($e[0], dataName); + + // camelCase the attribute name to match the spec + var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); + + // Store the data attribute contents into the dataset since + dataset[camelDataName] = dataValue; + } + } + // Prefer the element's `dataset` attribute if it exists // jQuery 1.x does not correctly handle data attributes with multiple dashes if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { - dataset = $.extend(true, {}, $e[0].dataset, $e.data()); - } else { - dataset = $e.data(); + dataset = $.extend(true, {}, $e[0].dataset, dataset); } - var data = $.extend(true, {}, dataset); + // Prefer our internal data cache if it exists + var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); data = Utils._convertData(data); @@ -5054,8 +5363,8 @@ S2.define('select2/core',[ './keys' ], function ($, Options, Utils, KEYS) { var Select2 = function ($element, options) { - if ($element.data('select2') != null) { - $element.data('select2').destroy(); + if (Utils.GetData($element[0], 'select2') != null) { + Utils.GetData($element[0], 'select2').destroy(); } this.$element = $element; @@ -5071,7 +5380,7 @@ S2.define('select2/core',[ // Set up the tabindex var tabindex = $element.attr('tabindex') || 0; - $element.data('old-tabindex', tabindex); + Utils.StoreData($element[0], 'old-tabindex', tabindex); $element.attr('tabindex', '-1'); // Set up containers and adapters @@ -5132,6 +5441,9 @@ S2.define('select2/core',[ // Synchronize any monitored attributes this._syncAttributes(); + Utils.StoreData($element[0], 'select2', this); + + // Ensure backwards compatibility with $element.data('select2'). $element.data('select2', this); }; @@ -5208,6 +5520,12 @@ S2.define('select2/core',[ return null; } + if (method == 'computedstyle') { + var computedStyle = window.getComputedStyle($element[0]); + + return computedStyle.width; + } + return method; }; @@ -5248,8 +5566,8 @@ S2.define('select2/core',[ if (observer != null) { this._observer = new observer(function (mutations) { - $.each(mutations, self._syncA); - $.each(mutations, self._syncS); + self._syncA(); + self._syncS(null, mutations); }); this._observer.observe(this.$element[0], { attributes: true, @@ -5371,7 +5689,7 @@ S2.define('select2/core',[ if (self.isOpen()) { if (key === KEYS.ESC || key === KEYS.TAB || (key === KEYS.UP && evt.altKey)) { - self.close(); + self.close(evt); evt.preventDefault(); } else if (key === KEYS.ENTER) { @@ -5405,7 +5723,7 @@ S2.define('select2/core',[ Select2.prototype._syncAttributes = function () { this.options.set('disabled', this.$element.prop('disabled')); - if (this.options.get('disabled')) { + if (this.isDisabled()) { if (this.isOpen()) { this.close(); } @@ -5416,7 +5734,7 @@ S2.define('select2/core',[ } }; - Select2.prototype._syncSubtree = function (evt, mutations) { + Select2.prototype._isChangeMutation = function (evt, mutations) { var changed = false; var self = this; @@ -5444,7 +5762,22 @@ S2.define('select2/core',[ } } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { changed = true; + } else if ($.isArray(mutations)) { + $.each(mutations, function(evt, mutation) { + if (self._isChangeMutation(evt, mutation)) { + // We've found a change mutation. + // Let's escape from the loop and continue + changed = true; + return false; + } + }); } + return changed; + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = this._isChangeMutation(evt, mutations); + var self = this; // Only re-pull the data if we think there is a change if (changed) { @@ -5466,7 +5799,8 @@ S2.define('select2/core',[ 'open': 'opening', 'close': 'closing', 'select': 'selecting', - 'unselect': 'unselecting' + 'unselect': 'unselecting', + 'clear': 'clearing' }; if (args === undefined) { @@ -5494,7 +5828,7 @@ S2.define('select2/core',[ }; Select2.prototype.toggleDropdown = function () { - if (this.options.get('disabled')) { + if (this.isDisabled()) { return; } @@ -5510,15 +5844,40 @@ S2.define('select2/core',[ return; } + if (this.isDisabled()) { + return; + } + this.trigger('query', {}); }; - Select2.prototype.close = function () { + Select2.prototype.close = function (evt) { if (!this.isOpen()) { return; } - this.trigger('close', {}); + this.trigger('close', { originalEvent : evt }); + }; + + /** + * Helper method to abstract the "enabled" (not "disabled") state of this + * object. + * + * @return {true} if the instance is not disabled. + * @return {false} if the instance is disabled. + */ + Select2.prototype.isEnabled = function () { + return !this.isDisabled(); + }; + + /** + * Helper method to abstract the "disabled" state of this object. + * + * @return {true} if the disabled option is true. + * @return {false} if the disabled option is false. + */ + Select2.prototype.isDisabled = function () { + return this.options.get('disabled'); }; Select2.prototype.isOpen = function () { @@ -5595,7 +5954,7 @@ S2.define('select2/core',[ }); } - this.$element.val(newVal).trigger('change'); + this.$element.val(newVal).trigger('input').trigger('change'); }; Select2.prototype.destroy = function () { @@ -5621,10 +5980,12 @@ S2.define('select2/core',[ this._syncS = null; this.$element.off('.select2'); - this.$element.attr('tabindex', this.$element.data('old-tabindex')); + this.$element.attr('tabindex', + Utils.GetData(this.$element[0], 'old-tabindex')); this.$element.removeClass('select2-hidden-accessible'); this.$element.attr('aria-hidden', 'false'); + Utils.RemoveData(this.$element[0]); this.$element.removeData('select2'); this.dataAdapter.destroy(); @@ -5652,7 +6013,7 @@ S2.define('select2/core',[ this.$container.addClass('select2-container--' + this.options.get('theme')); - $container.data('element', this.$element); + Utils.StoreData($container[0], 'element', this.$element); return $container; }; @@ -5862,8 +6223,9 @@ S2.define('select2/compat/initSelection',[ }); S2.define('select2/compat/inputData',[ - 'jquery' -], function ($) { + 'jquery', + '../utils' +], function ($, Utils) { function InputData (decorated, $element, options) { this._currentData = []; this._valueSeparator = options.get('valueSeparator') || ','; @@ -5927,13 +6289,13 @@ S2.define('select2/compat/inputData',[ }); this.$element.val(data.id); - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); } else { var value = this.$element.val(); value += this._valueSeparator + data.id; this.$element.val(value); - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); } }; @@ -5956,7 +6318,7 @@ S2.define('select2/compat/inputData',[ } self.$element.val(values.join(self._valueSeparator)); - self.$element.trigger('change'); + self.$element.trigger('input').trigger('change'); }); }; @@ -5980,7 +6342,7 @@ S2.define('select2/compat/inputData',[ InputData.prototype.addOptions = function (_, $options) { var options = $.map($options, function ($option) { - return $.data($option[0], 'data'); + return Utils.GetData($option[0], 'data'); }); this._currentData.push.apply(this._currentData, options); @@ -6383,8 +6745,9 @@ S2.define('jquery.select2',[ 'jquery-mousewheel', './select2/core', - './select2/defaults' -], function ($, _, Select2, Defaults) { + './select2/defaults', + './select2/utils' +], function ($, _, Select2, Defaults, Utils) { if ($.fn.select2 == null) { // All methods that should return the element var thisMethods = ['open', 'close', 'destroy']; @@ -6405,7 +6768,7 @@ S2.define('jquery.select2',[ var args = Array.prototype.slice.call(arguments, 1); this.each(function () { - var instance = $(this).data('select2'); + var instance = Utils.GetData(this, 'select2'); if (instance == null && window.console && console.error) { console.error( diff --git a/InvenTree/InvenTree/static/select2/js/select2.full.min.js b/InvenTree/InvenTree/static/select2/js/select2.full.min.js new file mode 100644 index 0000000000..fa781916e8 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/select2.full.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(d){var e=function(){if(d&&d.fn&&d.fn.select2&&d.fn.select2.amd)var e=d.fn.select2.amd;var t,n,i,h,o,s,f,g,m,v,y,_,r,a,w,l;function b(e,t){return r.call(e,t)}function c(e,t){var n,i,r,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},r.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},r.__cache={};var n=0;return r.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},r.StoreData=function(e,t,n){var i=r.GetUniqueElementId(e);r.__cache[i]||(r.__cache[i]={}),r.__cache[i][t]=n},r.GetData=function(e,t){var n=r.GetUniqueElementId(e);return t?r.__cache[n]&&null!=r.__cache[n][t]?r.__cache[n][t]:o(e).data(t):r.__cache[n]},r.RemoveData=function(e){var t=r.GetUniqueElementId(e);null!=r.__cache[t]&&delete r.__cache[t],e.removeAttribute("data-select2-id")},r}),e.define("select2/results",["jquery","./utils"],function(h,f){function i(e,t,n){this.$element=e,this.data=n,this.options=t,i.__super__.constructor.call(this)}return f.Extend(i,f.Observable),i.prototype.render=function(){var e=h('
            ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},i.prototype.clear=function(){this.$results.empty()},i.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),i=this.options.get("translations").get(e.message);n.append(t(i(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},i.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},i.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},i.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var i=n-1;0===e.length&&(i=0);var r=t.eq(i);r.trigger("mouseenter");var o=l.$results.offset().top,s=r.offset().top,a=l.$results.scrollTop()+(s-o);0===i?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var i=t.eq(n);i.trigger("mouseenter");var r=l.$results.offset().top+l.$results.outerHeight(!1),o=i.offset().top+i.outerHeight(!1),s=l.$results.scrollTop()+o-r;0===n?l.$results.scrollTop(0):rthis.$results.outerHeight()||o<0)&&this.$results.scrollTop(r)}},i.prototype.template=function(e,t){var n=this.options.get("templateResult"),i=this.options.get("escapeMarkup"),r=n(e,t);null==r?t.style.display="none":"string"==typeof r?t.innerHTML=i(r):h(t).append(r)},i}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,i,r){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return i.Extend(o,i.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=i.GetData(this.$element[0],"old-tabindex")?this._tabindex=i.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,i=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===r.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",i),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&i.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,i){function r(){r.__super__.constructor.apply(this,arguments)}return n.Extend(r,t),r.prototype.render=function(){var e=r.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},r.prototype.bind=function(t,e){var n=this;r.__super__.bind.apply(this,arguments);var i=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",i).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",i),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return e("")},r.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),i=this.display(t,n);n.empty().append(i);var r=t.title||t.text;r?n.attr("title",r):n.removeAttr("title")}else this.clear()},r}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(r,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
              '),e},n.prototype.bind=function(e,t){var i=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){i.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!i.isDisabled()){var t=r(this).parent(),n=l.GetData(t[0],"data");i.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return r('
            • ×
            • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×
              ');a.StoreData(i[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(i)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(i,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=i('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),t.on("open",function(){i.$search.attr("aria-controls",r),i.$search.trigger("focus")}),t.on("close",function(){i.$search.val(""),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.trigger("focus")}),t.on("enable",function(){i.$search.prop("disabled",!1),i._transferTabIndex()}),t.on("disable",function(){i.$search.prop("disabled",!0)}),t.on("focus",function(e){i.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){i.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){i._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===i.$search.val()){var t=i.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("select",function(){i._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var i=this;this._checkIfMaximumSelected(function(){e.call(i,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var i=this;this.current(function(e){var t=null!=e?e.length:0;0=i.maximumSelectionLength?i.trigger("results:message",{message:"maximumSelected",args:{maximum:i.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var i=this,r=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){i.trigger("keypress",e),i._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){i.handleSearch(e)}),t.on("open",function(){i.$search.attr("tabindex",0),i.$search.attr("aria-controls",r),i.$search.trigger("focus"),window.setTimeout(function(){i.$search.trigger("focus")},0)}),t.on("close",function(){i.$search.attr("tabindex",-1),i.$search.removeAttr("aria-controls"),i.$search.removeAttr("aria-activedescendant"),i.$search.val(""),i.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||i.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(i.showSearch(e)?i.$searchContainer.removeClass("select2-search--hide"):i.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?i.$search.attr("aria-activedescendant",e.data._resultId):i.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,i){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,i)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),i=t.length-1;0<=i;i--){var r=t[i];this.placeholder.id===r.id&&n.splice(i,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,i){this.lastParams={},e.call(this,t,n,i),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("query",function(e){i.lastParams=e,i.loading=!0}),t.on("query:append",function(e){i.lastParams=e,i.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
            • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var i=this;e.call(this,t,n),t.on("open",function(){i._showDropdown(),i._attachPositioningHandler(t),i._bindContainerResultHandlers(t)}),t.on("close",function(){i._hideDropdown(),i._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,i="scroll.select2."+t.id,r="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(i,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(i+" "+r+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+i+" "+r)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),i=null,r=this.$container.offset();r.bottom=r.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=r.top,o.bottom=r.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ar.bottom+s,d={left:r.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(i="below"),u||!c||t?!c&&u&&t&&(i="below"):i="above",("above"==i||t&&"below"!==i)&&(d.top=o.top-h.top-s),null!=i&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+i),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+i)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,i){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,i)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,i=0;i');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("select2/compat/utils",["jquery"],function(s){return{syncCssClasses:function(e,t,n){var i,r,o=[];(i=s.trim(e.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0===this.indexOf("select2-")&&o.push(this)}),(i=s.trim(t.attr("class")))&&s((i=""+i).split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&null!=(r=n(this))&&o.push(r)}),e.attr("class",o.join(" "))}}}),e.define("select2/compat/containerCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("containerCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptContainerCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("containerCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/dropdownCss",["jquery","./utils"],function(s,a){function l(e){return null}function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("dropdownCssClass")||"";s.isFunction(n)&&(n=n(this.$element));var i=this.options.get("adaptDropdownCssClass");if(i=i||l,-1!==n.indexOf(":all:")){n=n.replace(":all:","");var r=i;i=function(e){var t=r(e);return null!=t?t+" "+e:e}}var o=this.options.get("dropdownCss")||{};return s.isFunction(o)&&(o=o(this.$element)),a.syncCssClasses(t,this.$element,i),t.css(o),t.addClass(n),t},e}),e.define("select2/compat/initSelection",["jquery"],function(i){function e(e,t,n){n.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=n.get("initSelection"),this._isInitialized=!1,e.call(this,t,n)}return e.prototype.current=function(e,t){var n=this;this._isInitialized?e.call(this,t):this.initSelection.call(null,this.$element,function(e){n._isInitialized=!0,i.isArray(e)||(e=[e]),t(e)})},e}),e.define("select2/compat/inputData",["jquery","../utils"],function(s,i){function e(e,t,n){this._currentData=[],this._valueSeparator=n.get("valueSeparator")||",","hidden"===t.prop("type")&&n.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a `' + + ' spellcheck="false" role="searchbox" aria-autocomplete="list" />' + '' ); @@ -1873,14 +2000,18 @@ S2.define('select2/selection/search',[ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); container.on('open', function () { + self.$search.attr('aria-controls', resultsId); self.$search.trigger('focus'); }); container.on('close', function () { self.$search.val(''); + self.$search.removeAttr('aria-controls'); self.$search.removeAttr('aria-activedescendant'); self.$search.trigger('focus'); }); @@ -1900,7 +2031,11 @@ S2.define('select2/selection/search',[ }); container.on('results:focus', function (params) { - self.$search.attr('aria-activedescendant', params.id); + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } }); this.$selection.on('focusin', '.select2-search--inline', function (evt) { @@ -1925,7 +2060,7 @@ S2.define('select2/selection/search',[ .prev('.select2-selection__choice'); if ($previousChoice.length > 0) { - var item = $previousChoice.data('data'); + var item = Utils.GetData($previousChoice[0], 'data'); self.searchRemoveChoice(item); @@ -1934,6 +2069,12 @@ S2.define('select2/selection/search',[ } }); + this.$selection.on('click', '.select2-search--inline', function (evt) { + if (self.$search.val()) { + evt.stopPropagation(); + } + }); + // Try to detect the IE version should the `documentMode` property that // is stored on the document. This is only implemented in IE and is // slightly cleaner than doing a user agent check. @@ -2019,7 +2160,7 @@ S2.define('select2/selection/search',[ this.resizeSearch(); if (searchHadFocus) { - this.$search.focus(); + this.$search.trigger('focus'); } }; @@ -2052,7 +2193,7 @@ S2.define('select2/selection/search',[ var width = ''; if (this.$search.attr('placeholder') !== '') { - width = this.$selection.find('.select2-selection__rendered').innerWidth(); + width = this.$selection.find('.select2-selection__rendered').width(); } else { var minimumWidth = this.$search.val().length + 1; @@ -2076,10 +2217,13 @@ S2.define('select2/selection/eventRelay',[ 'open', 'opening', 'close', 'closing', 'select', 'selecting', - 'unselect', 'unselecting' + 'unselect', 'unselecting', + 'clear', 'clearing' ]; - var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + var preventableEvents = [ + 'opening', 'closing', 'selecting', 'unselecting', 'clearing' + ]; decorated.call(this, container, $container); @@ -2412,6 +2556,7 @@ S2.define('select2/diacritics',[ '\u019F': 'O', '\uA74A': 'O', '\uA74C': 'O', + '\u0152': 'OE', '\u01A2': 'OI', '\uA74E': 'OO', '\u0222': 'OU', @@ -2821,6 +2966,7 @@ S2.define('select2/diacritics',[ '\uA74B': 'o', '\uA74D': 'o', '\u0275': 'o', + '\u0153': 'oe', '\u01A3': 'oi', '\u0223': 'ou', '\uA74F': 'oo', @@ -2989,8 +3135,9 @@ S2.define('select2/diacritics',[ '\u03CD': '\u03C5', '\u03CB': '\u03C5', '\u03B0': '\u03C5', - '\u03C9': '\u03C9', - '\u03C2': '\u03C3' + '\u03CE': '\u03C9', + '\u03C2': '\u03C3', + '\u2019': '\'' }; return diacritics; @@ -3075,7 +3222,7 @@ S2.define('select2/data/select',[ if ($(data.element).is('option')) { data.element.selected = true; - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); return; } @@ -3096,13 +3243,13 @@ S2.define('select2/data/select',[ } self.$element.val(val); - self.$element.trigger('change'); + self.$element.trigger('input').trigger('change'); }); } else { var val = data.id; this.$element.val(val); - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); } }; @@ -3118,7 +3265,7 @@ S2.define('select2/data/select',[ if ($(data.element).is('option')) { data.element.selected = false; - this.$element.trigger('change'); + this.$element.trigger('input').trigger('change'); return; } @@ -3136,7 +3283,7 @@ S2.define('select2/data/select',[ self.$element.val(val); - self.$element.trigger('change'); + self.$element.trigger('input').trigger('change'); }); }; @@ -3158,7 +3305,7 @@ S2.define('select2/data/select',[ // Remove anything added to child elements this.$element.find('*').each(function () { // Remove any custom data set by Select2 - $.removeData(this, 'data'); + Utils.RemoveData(this); }); }; @@ -3231,7 +3378,7 @@ S2.define('select2/data/select',[ normalizedData.element = option; // Override the option's data with the combined data - $.data(option, 'data', normalizedData); + Utils.StoreData(option, 'data', normalizedData); return $option; }; @@ -3239,7 +3386,7 @@ S2.define('select2/data/select',[ SelectAdapter.prototype.item = function ($option) { var data = {}; - data = $.data($option[0], 'data'); + data = Utils.GetData($option[0], 'data'); if (data != null) { return data; @@ -3277,13 +3424,13 @@ S2.define('select2/data/select',[ data = this._normalizeItem(data); data.element = $option[0]; - $.data($option[0], 'data', data); + Utils.StoreData($option[0], 'data', data); return data; }; SelectAdapter.prototype._normalizeItem = function (item) { - if (!$.isPlainObject(item)) { + if (item !== Object(item)) { item = { id: item, text: item @@ -3329,15 +3476,19 @@ S2.define('select2/data/array',[ 'jquery' ], function (SelectAdapter, Utils, $) { function ArrayAdapter ($element, options) { - var data = options.get('data') || []; + this._dataToConvert = options.get('data') || []; ArrayAdapter.__super__.constructor.call(this, $element, options); - - this.addOptions(this.convertToOptions(data)); } Utils.Extend(ArrayAdapter, SelectAdapter); + ArrayAdapter.prototype.bind = function (container, $container) { + ArrayAdapter.__super__.bind.call(this, container, $container); + + this.addOptions(this.convertToOptions(this._dataToConvert)); + }; + ArrayAdapter.prototype.select = function (data) { var $option = this.$element.find('option').filter(function (i, elm) { return elm.value == data.id.toString(); @@ -3487,7 +3638,8 @@ S2.define('select2/data/ajax',[ }, function () { // Attempt to detect if a request was aborted // Only works if the transport exposes a status property - if ($request.status && $request.status === '0') { + if ('status' in $request && + ($request.status === 0 || $request.status === '0')) { return; } @@ -3626,8 +3778,6 @@ S2.define('select2/data/tags',[ }; Tags.prototype._removeOldTags = function (_) { - var tag = this._lastTag; - var $options = this.$element.find('option[data-select2-tag]'); $options.each(function () { @@ -3702,7 +3852,7 @@ S2.define('select2/data/tokenizer',[ // Replace the search term if we have the search box if (this.$search.length) { this.$search.val(tokenData.term); - this.$search.focus(); + this.$search.trigger('focus'); } params.term = tokenData.term; @@ -3831,10 +3981,30 @@ S2.define('select2/data/maximumSelectionLength',[ decorated.call(this, $e, options); } + MaximumSelectionLength.prototype.bind = + function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function () { + self._checkIfMaximumSelected(); + }); + }; + MaximumSelectionLength.prototype.query = function (decorated, params, callback) { var self = this; + this._checkIfMaximumSelected(function () { + decorated.call(self, params, callback); + }); + }; + + MaximumSelectionLength.prototype._checkIfMaximumSelected = + function (_, successCallback) { + var self = this; + this.current(function (currentData) { var count = currentData != null ? currentData.length : 0; if (self.maximumSelectionLength > 0 && @@ -3847,7 +4017,10 @@ S2.define('select2/data/maximumSelectionLength',[ }); return; } - decorated.call(self, params, callback); + + if (successCallback) { + successCallback(); + } }); }; @@ -3886,7 +4059,7 @@ S2.define('select2/dropdown',[ }; Dropdown.prototype.position = function ($dropdown, $container) { - // Should be implmented in subclasses + // Should be implemented in subclasses }; Dropdown.prototype.destroy = function () { @@ -3910,7 +4083,7 @@ S2.define('select2/dropdown/search',[ '' + '' + + ' spellcheck="false" role="searchbox" aria-autocomplete="list" />' + '' ); @@ -3925,6 +4098,8 @@ S2.define('select2/dropdown/search',[ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); this.$search.on('keydown', function (evt) { @@ -3947,23 +4122,27 @@ S2.define('select2/dropdown/search',[ container.on('open', function () { self.$search.attr('tabindex', 0); + self.$search.attr('aria-controls', resultsId); - self.$search.focus(); + self.$search.trigger('focus'); window.setTimeout(function () { - self.$search.focus(); + self.$search.trigger('focus'); }, 0); }); container.on('close', function () { self.$search.attr('tabindex', -1); + self.$search.removeAttr('aria-controls'); + self.$search.removeAttr('aria-activedescendant'); self.$search.val(''); + self.$search.trigger('blur'); }); container.on('focus', function () { if (!container.isOpen()) { - self.$search.focus(); + self.$search.trigger('focus'); } }); @@ -3978,6 +4157,14 @@ S2.define('select2/dropdown/search',[ } } }); + + container.on('results:focus', function (params) { + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } + }); }; Search.prototype.handleSearch = function (evt) { @@ -4062,6 +4249,7 @@ S2.define('select2/dropdown/infiniteScroll',[ if (this.showLoadingMore(data)) { this.$results.append(this.$loadingMore); + this.loadMoreIfNeeded(); } }; @@ -4080,25 +4268,27 @@ S2.define('select2/dropdown/infiniteScroll',[ self.loading = true; }); - this.$results.on('scroll', function () { - var isLoadMoreVisible = $.contains( - document.documentElement, - self.$loadingMore[0] - ); + this.$results.on('scroll', this.loadMoreIfNeeded.bind(this)); + }; - if (self.loading || !isLoadMoreVisible) { - return; - } + InfiniteScroll.prototype.loadMoreIfNeeded = function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + this.$loadingMore[0] + ); - var currentOffset = self.$results.offset().top + - self.$results.outerHeight(false); - var loadingMoreOffset = self.$loadingMore.offset().top + - self.$loadingMore.outerHeight(false); + if (this.loading || !isLoadMoreVisible) { + return; + } - if (currentOffset + 50 >= loadingMoreOffset) { - self.loadMore(); - } - }); + var currentOffset = this.$results.offset().top + + this.$results.outerHeight(false); + var loadingMoreOffset = this.$loadingMore.offset().top + + this.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + this.loadMore(); + } }; InfiniteScroll.prototype.loadMore = function () { @@ -4119,7 +4309,7 @@ S2.define('select2/dropdown/infiniteScroll',[ var $option = $( '
            • ' + 'role="option" aria-disabled="true">' ); var message = this.options.get('translations').get('loadingMore'); @@ -4137,7 +4327,7 @@ S2.define('select2/dropdown/attachBody',[ '../utils' ], function ($, Utils) { function AttachBody (decorated, $element, options) { - this.$dropdownParent = options.get('dropdownParent') || $(document.body); + this.$dropdownParent = $(options.get('dropdownParent') || document.body); decorated.call(this, $element, options); } @@ -4145,27 +4335,14 @@ S2.define('select2/dropdown/attachBody',[ AttachBody.prototype.bind = function (decorated, container, $container) { var self = this; - var setupResultsEvents = false; - decorated.call(this, container, $container); container.on('open', function () { self._showDropdown(); self._attachPositioningHandler(container); - if (!setupResultsEvents) { - setupResultsEvents = true; - - container.on('results:all', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - - container.on('results:append', function () { - self._positionDropdown(); - self._resizeDropdown(); - }); - } + // Must bind after the results handlers to ensure correct sizing + self._bindContainerResultHandlers(container); }); container.on('close', function () { @@ -4214,6 +4391,44 @@ S2.define('select2/dropdown/attachBody',[ this.$dropdownContainer.detach(); }; + AttachBody.prototype._bindContainerResultHandlers = + function (decorated, container) { + + // These should only be bound once + if (this._containerResultsHandlersBound) { + return; + } + + var self = this; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:message', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('select', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('unselect', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + this._containerResultsHandlersBound = true; + }; + AttachBody.prototype._attachPositioningHandler = function (decorated, container) { var self = this; @@ -4224,14 +4439,14 @@ S2.define('select2/dropdown/attachBody',[ var $watchers = this.$container.parents().filter(Utils.hasScroll); $watchers.each(function () { - $(this).data('select2-scroll-position', { + Utils.StoreData(this, 'select2-scroll-position', { x: $(this).scrollLeft(), y: $(this).scrollTop() }); }); $watchers.on(scrollEvent, function (ev) { - var position = $(this).data('select2-scroll-position'); + var position = Utils.GetData(this, 'select2-scroll-position'); $(this).scrollTop(position.y); }); @@ -4290,16 +4505,26 @@ S2.define('select2/dropdown/attachBody',[ top: container.bottom }; - // Determine what the parent element is to use for calciulating the offset + // Determine what the parent element is to use for calculating the offset var $offsetParent = this.$dropdownParent; - // For statically positoned elements, we need to get the element + // For statically positioned elements, we need to get the element // that is determining the offset if ($offsetParent.css('position') === 'static') { $offsetParent = $offsetParent.offsetParent(); } - var parentOffset = $offsetParent.offset(); + var parentOffset = { + top: 0, + left: 0 + }; + + if ( + $.contains(document.body, $offsetParent[0]) || + $offsetParent[0].isConnected + ) { + parentOffset = $offsetParent.offset(); + } css.top -= parentOffset.top; css.left -= parentOffset.left; @@ -4396,8 +4621,8 @@ S2.define('select2/dropdown/minimumResultsForSearch',[ }); S2.define('select2/dropdown/selectOnClose',[ - -], function () { + '../utils' +], function (Utils) { function SelectOnClose () { } SelectOnClose.prototype.bind = function (decorated, container, $container) { @@ -4428,7 +4653,7 @@ S2.define('select2/dropdown/selectOnClose',[ return; } - var data = $highlightedResults.data('data'); + var data = Utils.GetData($highlightedResults[0], 'data'); // Don't re-select already selected resulte if ( @@ -4469,7 +4694,7 @@ S2.define('select2/dropdown/closeOnSelect',[ var originalEvent = evt.originalEvent; // Don't close if the control key is being held - if (originalEvent && originalEvent.ctrlKey) { + if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) { return; } @@ -4523,6 +4748,9 @@ S2.define('select2/i18n/en',[],function () { }, searching: function () { return 'Searching…'; + }, + removeAllItems: function () { + return 'Remove all items'; } }; }); @@ -4761,66 +4989,29 @@ S2.define('select2/defaults',[ ); } - if (typeof options.language === 'string') { - // Check if the language is specified with a region - if (options.language.indexOf('-') > 0) { - // Extract the region information if it is included - var languageParts = options.language.split('-'); - var baseLanguage = languageParts[0]; + // If the defaults were not previously applied from an element, it is + // possible for the language option to have not been resolved + options.language = this._resolveLanguage(options.language); - options.language = [options.language, baseLanguage]; - } else { - options.language = [options.language]; + // Always fall back to English since it will always be complete + options.language.push('en'); + + var uniqueLanguages = []; + + for (var l = 0; l < options.language.length; l++) { + var language = options.language[l]; + + if (uniqueLanguages.indexOf(language) === -1) { + uniqueLanguages.push(language); } } - if ($.isArray(options.language)) { - var languages = new Translation(); - options.language.push('en'); + options.language = uniqueLanguages; - var languageNames = options.language; - - for (var l = 0; l < languageNames.length; l++) { - var name = languageNames[l]; - var language = {}; - - try { - // Try to load it with the original name - language = Translation.loadPath(name); - } catch (e) { - try { - // If we couldn't load it, check if it wasn't the full path - name = this.defaults.amdLanguageBase + name; - language = Translation.loadPath(name); - } catch (ex) { - // The translation could not be loaded at all. Sometimes this is - // because of a configuration problem, other times this can be - // because of how Select2 helps load all possible translation files. - if (options.debug && window.console && console.warn) { - console.warn( - 'Select2: The language file for "' + name + '" could not be ' + - 'automatically loaded. A fallback will be used instead.' - ); - } - - continue; - } - } - - languages.extend(language); - } - - options.translations = languages; - } else { - var baseTranslation = Translation.loadPath( - this.defaults.amdLanguageBase + 'en' - ); - var customTranslation = new Translation(options.language); - - customTranslation.extend(baseTranslation); - - options.translations = customTranslation; - } + options.translations = this._processTranslations( + options.language, + options.debug + ); return options; }; @@ -4887,13 +5078,14 @@ S2.define('select2/defaults',[ debug: false, dropdownAutoWidth: false, escapeMarkup: Utils.escapeMarkup, - language: EnglishTranslation, + language: {}, matcher: matcher, minimumInputLength: 0, maximumInputLength: 0, maximumSelectionLength: 0, minimumResultsForSearch: 0, selectOnClose: false, + scrollAfterSelect: false, sorter: function (data) { return data; }, @@ -4908,6 +5100,103 @@ S2.define('select2/defaults',[ }; }; + Defaults.prototype.applyFromElement = function (options, $element) { + var optionLanguage = options.language; + var defaultLanguage = this.defaults.language; + var elementLanguage = $element.prop('lang'); + var parentLanguage = $element.closest('[lang]').prop('lang'); + + var languages = Array.prototype.concat.call( + this._resolveLanguage(elementLanguage), + this._resolveLanguage(optionLanguage), + this._resolveLanguage(defaultLanguage), + this._resolveLanguage(parentLanguage) + ); + + options.language = languages; + + return options; + }; + + Defaults.prototype._resolveLanguage = function (language) { + if (!language) { + return []; + } + + if ($.isEmptyObject(language)) { + return []; + } + + if ($.isPlainObject(language)) { + return [language]; + } + + var languages; + + if (!$.isArray(language)) { + languages = [language]; + } else { + languages = language; + } + + var resolvedLanguages = []; + + for (var l = 0; l < languages.length; l++) { + resolvedLanguages.push(languages[l]); + + if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = languages[l].split('-'); + var baseLanguage = languageParts[0]; + + resolvedLanguages.push(baseLanguage); + } + } + + return resolvedLanguages; + }; + + Defaults.prototype._processTranslations = function (languages, debug) { + var translations = new Translation(); + + for (var l = 0; l < languages.length; l++) { + var languageData = new Translation(); + + var language = languages[l]; + + if (typeof language === 'string') { + try { + // Try to load it with the original name + languageData = Translation.loadPath(language); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + language = this.defaults.amdLanguageBase + language; + languageData = Translation.loadPath(language); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files + if (debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + language + '" could ' + + 'not be automatically loaded. A fallback will be used instead.' + ); + } + } + } + } else if ($.isPlainObject(language)) { + languageData = new Translation(language); + } else { + languageData = language; + } + + translations.extend(languageData); + } + + return translations; + }; + Defaults.prototype.set = function (key, value) { var camelKey = $.camelCase(key); @@ -4916,7 +5205,7 @@ S2.define('select2/defaults',[ var convertedData = Utils._convertData(data); - $.extend(this.defaults, convertedData); + $.extend(true, this.defaults, convertedData); }; var defaults = new Defaults(); @@ -4937,6 +5226,10 @@ S2.define('select2/options',[ this.fromElement($element); } + if ($element != null) { + this.options = Defaults.applyFromElement(this.options, $element); + } + this.options = Defaults.apply(this.options); if ($element && $element.is('input')) { @@ -4960,14 +5253,6 @@ S2.define('select2/options',[ this.options.disabled = $e.prop('disabled'); } - if (this.options.language == null) { - if ($e.prop('lang')) { - this.options.language = $e.prop('lang').toLowerCase(); - } else if ($e.closest('[lang]').prop('lang')) { - this.options.language = $e.closest('[lang]').prop('lang'); - } - } - if (this.options.dir == null) { if ($e.prop('dir')) { this.options.dir = $e.prop('dir'); @@ -4981,7 +5266,7 @@ S2.define('select2/options',[ $e.prop('disabled', this.options.disabled); $e.prop('multiple', this.options.multiple); - if ($e.data('select2Tags')) { + if (Utils.GetData($e[0], 'select2Tags')) { if (this.options.debug && window.console && console.warn) { console.warn( 'Select2: The `data-select2-tags` attribute has been changed to ' + @@ -4990,11 +5275,11 @@ S2.define('select2/options',[ ); } - $e.data('data', $e.data('select2Tags')); - $e.data('tags', true); + Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); + Utils.StoreData($e[0], 'tags', true); } - if ($e.data('ajaxUrl')) { + if (Utils.GetData($e[0], 'ajaxUrl')) { if (this.options.debug && window.console && console.warn) { console.warn( 'Select2: The `data-ajax-url` attribute has been changed to ' + @@ -5003,21 +5288,45 @@ S2.define('select2/options',[ ); } - $e.attr('ajax--url', $e.data('ajaxUrl')); - $e.data('ajax--url', $e.data('ajaxUrl')); + $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); + Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); } var dataset = {}; + function upperCaseLetter(_, letter) { + return letter.toUpperCase(); + } + + // Pre-load all of the attributes which are prefixed with `data-` + for (var attr = 0; attr < $e[0].attributes.length; attr++) { + var attributeName = $e[0].attributes[attr].name; + var prefix = 'data-'; + + if (attributeName.substr(0, prefix.length) == prefix) { + // Get the contents of the attribute after `data-` + var dataName = attributeName.substring(prefix.length); + + // Get the data contents from the consistent source + // This is more than likely the jQuery data helper + var dataValue = Utils.GetData($e[0], dataName); + + // camelCase the attribute name to match the spec + var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); + + // Store the data attribute contents into the dataset since + dataset[camelDataName] = dataValue; + } + } + // Prefer the element's `dataset` attribute if it exists // jQuery 1.x does not correctly handle data attributes with multiple dashes if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { - dataset = $.extend(true, {}, $e[0].dataset, $e.data()); - } else { - dataset = $e.data(); + dataset = $.extend(true, {}, $e[0].dataset, dataset); } - var data = $.extend(true, {}, dataset); + // Prefer our internal data cache if it exists + var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); data = Utils._convertData(data); @@ -5054,8 +5363,8 @@ S2.define('select2/core',[ './keys' ], function ($, Options, Utils, KEYS) { var Select2 = function ($element, options) { - if ($element.data('select2') != null) { - $element.data('select2').destroy(); + if (Utils.GetData($element[0], 'select2') != null) { + Utils.GetData($element[0], 'select2').destroy(); } this.$element = $element; @@ -5071,7 +5380,7 @@ S2.define('select2/core',[ // Set up the tabindex var tabindex = $element.attr('tabindex') || 0; - $element.data('old-tabindex', tabindex); + Utils.StoreData($element[0], 'old-tabindex', tabindex); $element.attr('tabindex', '-1'); // Set up containers and adapters @@ -5132,6 +5441,9 @@ S2.define('select2/core',[ // Synchronize any monitored attributes this._syncAttributes(); + Utils.StoreData($element[0], 'select2', this); + + // Ensure backwards compatibility with $element.data('select2'). $element.data('select2', this); }; @@ -5208,6 +5520,12 @@ S2.define('select2/core',[ return null; } + if (method == 'computedstyle') { + var computedStyle = window.getComputedStyle($element[0]); + + return computedStyle.width; + } + return method; }; @@ -5248,8 +5566,8 @@ S2.define('select2/core',[ if (observer != null) { this._observer = new observer(function (mutations) { - $.each(mutations, self._syncA); - $.each(mutations, self._syncS); + self._syncA(); + self._syncS(null, mutations); }); this._observer.observe(this.$element[0], { attributes: true, @@ -5371,7 +5689,7 @@ S2.define('select2/core',[ if (self.isOpen()) { if (key === KEYS.ESC || key === KEYS.TAB || (key === KEYS.UP && evt.altKey)) { - self.close(); + self.close(evt); evt.preventDefault(); } else if (key === KEYS.ENTER) { @@ -5405,7 +5723,7 @@ S2.define('select2/core',[ Select2.prototype._syncAttributes = function () { this.options.set('disabled', this.$element.prop('disabled')); - if (this.options.get('disabled')) { + if (this.isDisabled()) { if (this.isOpen()) { this.close(); } @@ -5416,7 +5734,7 @@ S2.define('select2/core',[ } }; - Select2.prototype._syncSubtree = function (evt, mutations) { + Select2.prototype._isChangeMutation = function (evt, mutations) { var changed = false; var self = this; @@ -5444,7 +5762,22 @@ S2.define('select2/core',[ } } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { changed = true; + } else if ($.isArray(mutations)) { + $.each(mutations, function(evt, mutation) { + if (self._isChangeMutation(evt, mutation)) { + // We've found a change mutation. + // Let's escape from the loop and continue + changed = true; + return false; + } + }); } + return changed; + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = this._isChangeMutation(evt, mutations); + var self = this; // Only re-pull the data if we think there is a change if (changed) { @@ -5466,7 +5799,8 @@ S2.define('select2/core',[ 'open': 'opening', 'close': 'closing', 'select': 'selecting', - 'unselect': 'unselecting' + 'unselect': 'unselecting', + 'clear': 'clearing' }; if (args === undefined) { @@ -5494,7 +5828,7 @@ S2.define('select2/core',[ }; Select2.prototype.toggleDropdown = function () { - if (this.options.get('disabled')) { + if (this.isDisabled()) { return; } @@ -5510,15 +5844,40 @@ S2.define('select2/core',[ return; } + if (this.isDisabled()) { + return; + } + this.trigger('query', {}); }; - Select2.prototype.close = function () { + Select2.prototype.close = function (evt) { if (!this.isOpen()) { return; } - this.trigger('close', {}); + this.trigger('close', { originalEvent : evt }); + }; + + /** + * Helper method to abstract the "enabled" (not "disabled") state of this + * object. + * + * @return {true} if the instance is not disabled. + * @return {false} if the instance is disabled. + */ + Select2.prototype.isEnabled = function () { + return !this.isDisabled(); + }; + + /** + * Helper method to abstract the "disabled" state of this object. + * + * @return {true} if the disabled option is true. + * @return {false} if the disabled option is false. + */ + Select2.prototype.isDisabled = function () { + return this.options.get('disabled'); }; Select2.prototype.isOpen = function () { @@ -5595,7 +5954,7 @@ S2.define('select2/core',[ }); } - this.$element.val(newVal).trigger('change'); + this.$element.val(newVal).trigger('input').trigger('change'); }; Select2.prototype.destroy = function () { @@ -5621,10 +5980,12 @@ S2.define('select2/core',[ this._syncS = null; this.$element.off('.select2'); - this.$element.attr('tabindex', this.$element.data('old-tabindex')); + this.$element.attr('tabindex', + Utils.GetData(this.$element[0], 'old-tabindex')); this.$element.removeClass('select2-hidden-accessible'); this.$element.attr('aria-hidden', 'false'); + Utils.RemoveData(this.$element[0]); this.$element.removeData('select2'); this.dataAdapter.destroy(); @@ -5652,7 +6013,7 @@ S2.define('select2/core',[ this.$container.addClass('select2-container--' + this.options.get('theme')); - $container.data('element', this.$element); + Utils.StoreData($container[0], 'element', this.$element); return $container; }; @@ -5672,8 +6033,9 @@ S2.define('jquery.select2',[ 'jquery-mousewheel', './select2/core', - './select2/defaults' -], function ($, _, Select2, Defaults) { + './select2/defaults', + './select2/utils' +], function ($, _, Select2, Defaults, Utils) { if ($.fn.select2 == null) { // All methods that should return the element var thisMethods = ['open', 'close', 'destroy']; @@ -5694,7 +6056,7 @@ S2.define('jquery.select2',[ var args = Array.prototype.slice.call(arguments, 1); this.each(function () { - var instance = $(this).data('select2'); + var instance = Utils.GetData(this, 'select2'); if (instance == null && window.console && console.error) { console.error( diff --git a/InvenTree/InvenTree/static/select2/js/select2.min.js b/InvenTree/InvenTree/static/select2/js/select2.min.js new file mode 100644 index 0000000000..e421426434 --- /dev/null +++ b/InvenTree/InvenTree/static/select2/js/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,b;function w(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&b.test(e[s])&&(e[s]=e[s].replace(b,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('
                ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):ithis.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
                  '),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.isDisabled()){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('
                • ×
                • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
                • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1 Date: Fri, 25 Jun 2021 13:47:09 +1000 Subject: [PATCH 039/178] Fixes for base template --- InvenTree/templates/base.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 128cbba2dd..f35dd09fc8 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -40,8 +40,8 @@ - - + + @@ -136,7 +136,7 @@ - + From 4cf69a5a4cee2e96e7e6af3691da60305f1db467 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 13:47:33 +1000 Subject: [PATCH 040/178] Custom rendering functions --- InvenTree/templates/js/forms.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 036928616c..cf6cbd5855 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -346,14 +346,19 @@ function initializeRelatedFields(modal, url, fields, options) { // Find the select element and attach a select2 to it var select = $(modal).find(`#id_${name}`); - console.log('modal:', modal); + // TODO: Add 'placeholder' support for entry select2 fields + + // TODO: Add 'pagination' support for the query select.select2({ ajax: { url: field.api_url, dataType: 'json', + allowClear: !field.required, // Allow non required fields to be cleared dropdownParent: $(modal), dropdownAutoWidth: false, + delay: 250, + cache: true, // matcher: partialMatcher, data: function(params) { // Re-format search term into InvenTree API style @@ -385,16 +390,20 @@ function initializeRelatedFields(modal, url, fields, options) { return results; }, - templateResult: function(item, container) { - // Custom formatting for the item - console.log("templateResult:", item); - if (field.model) { - // If the 'model' is specified, hand it off to the custom model render - return renderModelData(field.model, item, field, options); - } else { - // Simply render the 'text' parameter - return item.text; - } + }, + templateResult: function(item, container) { + console.log('templateResult:', item); + return item.text; + }, + templateSelection: function(item, container) { + // Custom formatting for the item + console.log("templateSelection:", item); + if (field.model) { + // If the 'model' is specified, hand it off to the custom model render + return renderModelData(field.model, item, field, options); + } else { + // Simply render the 'text' parameter + return item.text; } } }); From d411728be61e75a216bec4ec60264cab2420f08a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 13:58:36 +1000 Subject: [PATCH 041/178] Start of custom rendering support based on model --- InvenTree/templates/js/forms.js | 172 +++++++++++++++++++------------- 1 file changed, 104 insertions(+), 68 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index cf6cbd5855..d0c0cb5424 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -343,90 +343,126 @@ function initializeRelatedFields(modal, url, fields, options) { continue; } - // Find the select element and attach a select2 to it - var select = $(modal).find(`#id_${name}`); - - // TODO: Add 'placeholder' support for entry select2 fields - - // TODO: Add 'pagination' support for the query - - select.select2({ - ajax: { - url: field.api_url, - dataType: 'json', - allowClear: !field.required, // Allow non required fields to be cleared - dropdownParent: $(modal), - dropdownAutoWidth: false, - delay: 250, - cache: true, - // matcher: partialMatcher, - data: function(params) { - // Re-format search term into InvenTree API style - return { - search: params.term, - }; - }, - processResults: function(data) { - // Convert the returned InvenTree data into select2-friendly format - var rows = []; - - // Only ever show the first x items - for (var idx = 0; idx < data.length && idx < 50; idx++) { - var row = data[idx]; - - // Reformat to match select2 requirements - row.id = row.id || row.pk; - - // TODO: Fix me? - row.text = `This is ${field.api_url}${row.id}/`; - - rows.push(row); - } - - // Ref: https://select2.org/data-sources/formats - var results = { - results: rows, - }; - - return results; - }, - }, - templateResult: function(item, container) { - console.log('templateResult:', item); - return item.text; - }, - templateSelection: function(item, container) { - // Custom formatting for the item - console.log("templateSelection:", item); - if (field.model) { - // If the 'model' is specified, hand it off to the custom model render - return renderModelData(field.model, item, field, options); - } else { - // Simply render the 'text' parameter - return item.text; - } - } - }); + initializeRelatedField(modal, name, field, options); } } +/* + * Initializea single related-field + * + * argument: + * - modal: DOM identifier for the modal window + * - name: name of the field e.g. 'location' + * - field: Field definition from the OPTIONS request + * - options: Original options object provided by the client + */ +function initializeRelatedField(modal, name, field, options) { + + // Find the select element and attach a select2 to it + var select = $(modal).find(`#id_${name}`); + + // TODO: Add 'placeholder' support for entry select2 fields + + // TODO: Add 'pagination' support for the query + + select.select2({ + ajax: { + url: field.api_url, + dataType: 'json', + allowClear: !field.required, // Allow non required fields to be cleared + dropdownParent: $(modal), + dropdownAutoWidth: false, + delay: 250, + cache: true, + // matcher: partialMatcher, + data: function(params) { + // Re-format search term into InvenTree API style + return { + search: params.term, + }; + }, + processResults: function(data) { + // Convert the returned InvenTree data into select2-friendly format + var rows = []; + + // Only ever show the first x items + for (var idx = 0; idx < data.length && idx < 50; idx++) { + var row = data[idx]; + + // Reformat to match select2 requirements + row.id = row.id || row.pk; + + // TODO: Fix me? + row.text = `This is ${field.api_url}${row.id}/`; + + rows.push(row); + } + + // Ref: https://select2.org/data-sources/formats + var results = { + results: rows, + }; + + return results; + }, + }, + templateResult: function(item, container) { + // Custom formatting for the search results + if (field.model) { + // If the 'model' is specified, hand it off to the custom model render + return renderModelData(name, field.model, item, field, options); + } else { + // Simply render the 'text' parameter + return item.text; + } + }, + templateSelection: function(item, container) { + // Custom formatting for selected item + if (field.model) { + // If the 'model' is specified, hand it off to the custom model render + return renderModelData(name, field.model, item, field, options); + } else { + // Simply render the 'text' parameter + return item.text; + } + } + }); +} + + /* * Render a "foreign key" model reference in a select2 instance. * Allows custom rendering with access to the entire serialized object. * * arguments: + * - name: The name of the field e.g. 'location' * - model: The name of the InvenTree model e.g. 'stockitem' * - data: The JSON data representation of the modal instance (GET request) * - parameters: The field definition (OPTIONS) request * - options: Other options provided at time of modal creation by the client */ -function renderModelData(model, data, paramaters, options) { +function renderModelData(name, model, data, paramaters, options) { - console.log(model, '->', data); + // TODO: Implement this function for various models - // TODO: Implement? - return data.text; + var html = null; + + switch (model) { + case 'company': + html = `${data.name} - ${data.description}`; + default: + break; + } + + if (html != null) { + // Render HTML to an object + var $state = $(html); + return $state; + } else { + // Simple text rendering + return data.text; + } } From b29db6f258e29bbda44824f62b2e75394f52e237 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Jun 2021 15:22:40 +1000 Subject: [PATCH 042/178] Remove old debug message --- InvenTree/templates/js/forms.js | 4 ++++ InvenTree/templates/js/modals.js | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index d0c0cb5424..a1937a5ef8 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -444,6 +444,10 @@ function initializeRelatedField(modal, name, field, options) { */ function renderModelData(name, model, data, paramaters, options) { + if (!data) { + return '{% trans "Searching" %}...'; + } + // TODO: Implement this function for various models var html = null; diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index 03893a47b8..75e0f3672a 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -991,8 +991,6 @@ function hideModalImage() { function showModalImage(image_url) { // Display full-screen modal image - console.log('showing modal image: ' + image_url); - var modal = $('#modal-image-dialog'); // Set image content From 4921cd47f9f8caa7fab488200dbe6476a70868e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Jun 2021 07:40:01 +0200 Subject: [PATCH 043/178] refactor for better readabilty --- InvenTree/part/templates/part/prices.html | 30 +++++++++++++---------- InvenTree/templates/js/part.js | 8 ++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 1b8b3959cf..cf719711dc 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -340,7 +340,7 @@ borderWidth: 1 }] } - var StockPriceChart = loadStockPricingChart(document.getElementById('StockPriceChart'), pricedata) + var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), pricedata) var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} }) var bomdata = { labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}], @@ -369,12 +369,14 @@ {% if show_internal_price and roles.sales_order.view %} initPriceBreakSet( $('#internal-price-break-table'), - {{part.id}}, - 'internal price break', - 'internal-price', - "{% url 'api-part-internal-price-list' %}", - $('#new-internal-price-break'), - '{% url 'internal-price-break-create' %}' + { + part_id: {{part.id}}, + pb_human_name: 'internal price break', + pb_url_slug: 'internal-price', + pb_url: '{% url 'api-part-internal-price-list' %}', + pb_new_btn: $('#new-internal-price-break'), + pb_new_url: '{% url 'internal-price-break-create' %}', + }, ); {% endif %} @@ -393,12 +395,14 @@ {% if part.salable and roles.sales_order.view %} initPriceBreakSet( $('#price-break-table'), - {{part.id}}, - 'sale price break', - 'sale-price', - "{% url 'api-part-sale-price-list' %}", - $('#new-price-break'), - '{% url 'sale-price-break-create' %}' + { + part_id: {{part.id}}, + pb_human_name: 'sale price break', + pb_url_slug: 'sale-price', + pb_url: "{% url 'api-part-sale-price-list' %}", + pb_new_btn: $('#new-price-break'), + pb_new_url: '{% url 'sale-price-break-create' %}', + }, ); {% endif %} diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 82fd416e15..37fd36486b 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -817,6 +817,14 @@ function loadPriceBreakTable(table, options) { }); } +function initPriceBreakSet(table, options) { + + var part_id = options.part_id; + var pb_human_name = options.pb_human_name; + var pb_url_slug = options.pb_url_slug; + var pb_url = options.pb_url; + var pb_new_btn = options.pb_new_btn; + var pb_new_url = options.pb_new_url; function initPriceBreakSet(table, part_id, pb_human_name, pb_url_slug, pb_url, pb_new_btn, pb_new_url) { From d28d66795d87327536d97bc479ec4179fcb5a6d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Jun 2021 07:41:00 +0200 Subject: [PATCH 044/178] linked price break graphs --- InvenTree/InvenTree/static/css/inventree.css | 12 ++++- InvenTree/part/templates/part/prices.html | 12 ++++- InvenTree/templates/js/part.js | 46 +++++++++++++++++++- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index eed6c6ad21..7bdc6e8a9b 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -960,4 +960,14 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da .sidebar-icon { min-width: 19px; -} \ No newline at end of file +} + +.row.full-height { + display: flex; + flex-wrap: wrap; + } + +.row.full-height > [class*='col-'] { + display: flex; + flex-direction: column; + } diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index cf719711dc..1214239fe4 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -176,8 +176,11 @@

                  {% trans "Internal Cost" %}

                  -
                  +
                  +
                  + +
                  @@ -224,8 +227,11 @@

                  {% trans "Sale Cost" %}

                  -
                  +
                  +
                  + +
                  @@ -376,6 +382,7 @@ pb_url: '{% url 'api-part-internal-price-list' %}', pb_new_btn: $('#new-internal-price-break'), pb_new_url: '{% url 'internal-price-break-create' %}', + linkedGraph: $('#InternalPriceBreakChart'), }, ); {% endif %} @@ -402,6 +409,7 @@ pb_url: "{% url 'api-part-sale-price-list' %}", pb_new_btn: $('#new-price-break'), pb_new_url: '{% url 'sale-price-break-create' %}', + linkedGraph: $('#SalePriceBreakChart'), }, ); {% endif %} diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 37fd36486b..75e925266a 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -776,6 +776,8 @@ function loadPriceBreakTable(table, options) { var name = options.name || 'pricebreak'; var human_name = options.human_name || 'price break'; + var linkedGraph = options.linkedGraph || null; + var chart = null; table.inventreeTable({ name: name, @@ -784,6 +786,31 @@ function loadPriceBreakTable(table, options) { return `{% trans "No ${human_name} information found" %}`; }, url: options.url, + onLoadSuccess: function(tableData) { + if (linkedGraph) { + var labels = Array.from(tableData, x => x.quantity); + var data = Array.from(tableData, x => parseFloat(x.price)); + + // destroy chart if exists + if (chart){ + chart.destroy(); + } + chart = loadLineChart(linkedGraph, + { + labels: labels, + datasets: [ + { + label: '{% trans "Unit Price" %}', + data: data, + backgroundColor: 'rgba(255, 206, 86, 0.2)', + borderColor: 'rgb(255, 206, 86)', + stepped: true, + fill: true, + },] + } + ); + } + }, columns: [ { field: 'pk', @@ -817,6 +844,20 @@ function loadPriceBreakTable(table, options) { }); } +function loadLineChart(context, data) { + return new Chart(context, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: {position: 'bottom'}, + } + } + }); +} + function initPriceBreakSet(table, options) { var part_id = options.part_id; @@ -826,14 +867,15 @@ function initPriceBreakSet(table, options) { var pb_new_btn = options.pb_new_btn; var pb_new_url = options.pb_new_url; -function initPriceBreakSet(table, part_id, pb_human_name, pb_url_slug, pb_url, pb_new_btn, pb_new_url) { - + var linkedGraph = options.linkedGraph || null; + loadPriceBreakTable( table, { name: pb_url_slug, human_name: pb_human_name, url: pb_url, + linkedGraph: linkedGraph, } ); From 565631ef87696dff672024b847f6f91aecd020b7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 14:09:35 +1000 Subject: [PATCH 045/178] More features - Custom renderers depending on specified model name - Paginate API results --- InvenTree/InvenTree/static/css/inventree.css | 7 ++ InvenTree/InvenTree/urls.py | 1 + InvenTree/templates/base.html | 1 + InvenTree/templates/js/forms.js | 73 +++++++++++++---- InvenTree/templates/js/model_renderers.js | 83 ++++++++++++++++++++ 5 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 InvenTree/templates/js/model_renderers.js diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 573f966a41..706bc32327 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -972,4 +972,11 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da .select2-container { width: 100%; +} + +.select2-thumbnail { + max-width: 24px; + max-height: 24px; + border-radius: 4px; + margin-right: 10px; } \ No newline at end of file diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 2ec7b02f23..6a7ae7bdfd 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -105,6 +105,7 @@ settings_urls = [ dynamic_javascript_urls = [ url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'), url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'), + url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'), url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'), url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index f35dd09fc8..ec7e31a7f0 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -150,6 +150,7 @@ + diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index a1937a5ef8..a68def10b0 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -364,7 +364,8 @@ function initializeRelatedField(modal, name, field, options) { // TODO: Add 'placeholder' support for entry select2 fields - // TODO: Add 'pagination' support for the query + // limit size for AJAX requests + var pageSize = options.pageSize || 25; select.select2({ ajax: { @@ -377,31 +378,52 @@ function initializeRelatedField(modal, name, field, options) { cache: true, // matcher: partialMatcher, data: function(params) { + + if (!params.page) { + offset = 0; + } else { + offset = (params.page - 1) * pageSize; + } + // Re-format search term into InvenTree API style return { search: params.term, + offset: offset, + limit: pageSize, }; }, - processResults: function(data) { + processResults: function(response) { // Convert the returned InvenTree data into select2-friendly format - var rows = []; - // Only ever show the first x items - for (var idx = 0; idx < data.length && idx < 50; idx++) { - var row = data[idx]; + var data = []; - // Reformat to match select2 requirements - row.id = row.id || row.pk; + var more = false; - // TODO: Fix me? - row.text = `This is ${field.api_url}${row.id}/`; + if ('count' in response && 'results' in response) { + // Response is paginated + data = response.results; - rows.push(row); + // Any more data available? + if (response.next) { + more = true; + } + + } else { + // Non-paginated response + data = response; + } + + // Each 'row' must have the 'id' attribute + for (var idx = 0; idx < data.length; idx++) { + data[idx].id = data[idx].pk; } // Ref: https://select2.org/data-sources/formats var results = { - results: rows, + results: data, + pagination: { + more: more, + } }; return results; @@ -442,7 +464,7 @@ function initializeRelatedField(modal, name, field, options) { * - parameters: The field definition (OPTIONS) request * - options: Other options provided at time of modal creation by the client */ -function renderModelData(name, model, data, paramaters, options) { +function renderModelData(name, model, data, parameters, options) { if (!data) { return '{% trans "Searching" %}...'; @@ -452,20 +474,41 @@ function renderModelData(name, model, data, paramaters, options) { var html = null; + var renderer = null; + + // Find a custom renderer switch (model) { case 'company': - html = `${data.name} - ${data.description}`; + renderer = renderCompany; + break; + case 'stockitem': + renderer = renderStockItem; + break; + case 'stocklocation': + renderer = renderStockLocation; + break; + case 'part': + renderer = renderPart; + break; + case 'partcategory': + renderer = renderPartCategory; + break; default: break; } + + if (renderer != null) { + html = renderer(name, data, parameters, options); + } if (html != null) { // Render HTML to an object var $state = $(html); return $state; } else { + console.log(`ERROR: Rendering not implemented for model '${model}'`); // Simple text rendering - return data.text; + return data.id; } } diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js new file mode 100644 index 0000000000..924d85d35d --- /dev/null +++ b/InvenTree/templates/js/model_renderers.js @@ -0,0 +1,83 @@ +/* + * This file contains functions for rendering various InvenTree database models, + * in particular for displaying them in modal forms in a 'select2' context. + * + * Each renderer is provided with three arguments: + * + * - name: The 'name' of the model instance in the referring model + * - data: JSON data which represents the model instance. Returned via a GET request. + * - parameters: The field parameters provided via an OPTIONS request to the endpoint. + * - options: User options provided by the client + */ + + +// Renderer for "Company" model +function renderCompany(name, data, parameters, options) { + + var html = `${data.name} - ${data.description}`; + + return html; +} + + +// Renderer for "StockItem" model +function renderStockItem(name, data, parameters, options) { + + // TODO - Include part detail, location, quantity + // TODO - Include part image +} + + +// Renderer for "StockLocation" model +function renderStockLocation(name, data, parameters, options) { + + var html = `${data.name}`; + + if (data.description) { + html += ` - ${data.description}`; + } + + if (data.pathstring) { + html += `

                  ${data.pathstring}

                  `; + } + + return html; +} + + +// Renderer for "Part" model +function renderPart(name, data, parameters, options) { + + var image = data.image; + + if (!image) { + image = `/static/img/blank_image.png`; + } + + var html = ``; + + html += ` ${data.full_name ?? data.name}`; + + if (data.description) { + html += ` - ${data.description}`; + } + + return html; +} + + +// Renderer for "PartCategory" model +function renderPartCategory(name, data, parameters, options) { + + var html = `${data.name}`; + + if (data.description) { + html += ` - ${data.description}`; + } + + if (data.pathstring) { + html += `

                  ${data.pathstring}

                  `; + } + + return html; +} \ No newline at end of file From 949c7dd81bc9c691e1925cd3ea0dc025d54f8e9b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 14:30:14 +1000 Subject: [PATCH 046/178] Set modal form title --- InvenTree/part/api.py | 3 ++- InvenTree/templates/js/forms.js | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 60cea121a7..8ef6902600 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -337,8 +337,9 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): def get_serializer(self, *args, **kwargs): + # By default, include 'category_detail' information in the detail view try: - kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False)) + kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', True)) except AttributeError: pass diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index a68def10b0..5996e1a1cd 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -161,17 +161,18 @@ function constructChangeForm(url, fields, options={}) { * and construct a modal form based on the response. * * arguments: - * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE', + * - url: API URL * * options: - * - method: + * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE', + * - title: The form title + * - fields: list of fields to display + * - exclude: List of fields to exclude */ -function constructForm(url, method, options={}) { +function constructForm(url, options={}) { - method = method.toUpperCase(); - - // Store the method in the options struct - options.method = method; + // Default HTTP method + options.method = options.method || 'PATCH'; // Request OPTIONS endpoint from the API getApiEndpointOptions(url, function(OPTIONS) { @@ -183,7 +184,7 @@ function constructForm(url, method, options={}) { * First we must determine if the user has the correct permissions! */ - switch (method) { + switch (options.method) { case 'POST': if (canCreate(OPTIONS)) { constructCreateForm(url, OPTIONS.actions.POST, options); @@ -322,6 +323,10 @@ function constructFormBody(url, fields, options={}) { $(modal + ' .select2-container').css('width', '100%'); modalShowSubmitButton(modal, true); + + var title = options.title || '{% trans "Form Title" %}'; + + modalSetTitle(modal, title); } From e9db72017d4ba17d0d2e65a74d0c9a24d704c611 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 17:54:14 +1000 Subject: [PATCH 047/178] Extract field data on submit --- InvenTree/templates/js/forms.js | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 5996e1a1cd..93ab9e696e 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -223,7 +223,7 @@ function constructForm(url, options={}) { } break; default: - console.log(`constructForm() called with invalid method '${method}'`); + console.log(`constructForm() called with invalid method '${options.method}'`); break; } }); @@ -302,9 +302,10 @@ function constructFormBody(url, fields, options={}) { modalEnable(modal, true); - var title = options.title || '{% trans "Form Title" %}'; - - modalSetTitle(modal, title); + // Set the form title and button labels + modalSetTitle(modal, options.title || '{% trans "Form Title" %}'); + modalSetSubmitText(options.submitText || '{% trans "Submit" %}'); + modalSetCloseText(options.cancelText || '{% trans "Cancel" %}'); // Insert generated form content $(modal).find('.modal-form-content').html(html); @@ -324,9 +325,28 @@ function constructFormBody(url, fields, options={}) { modalShowSubmitButton(modal, true); - var title = options.title || '{% trans "Form Title" %}'; + $(modal).off('click', '#modal-form-submit'); + $(modal).on('click', '#modal-form-submit', function() { - modalSetTitle(modal, title); + var patch_data = {}; + + // Construct submit data + field_names.forEach(function(name) { + var field = fields[name] || null; + + if (field) { + var field_value = getFieldValue(name); + + patch_data[name] = field_value; + } else { + console.log(`Could not find field matching '${name}'`); + } + }) + + + console.log(patch_data); + + }); } From 9dd2765bd222058b70b7cc1bd686a0934915c9d9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 19:11:20 +1000 Subject: [PATCH 048/178] Handle returned error messages --- InvenTree/templates/js/api.js | 5 +- InvenTree/templates/js/forms.js | 188 +++++++++++++++++++++++--------- 2 files changed, 142 insertions(+), 51 deletions(-) diff --git a/InvenTree/templates/js/api.js b/InvenTree/templates/js/api.js index b43bcc8419..aa446028a5 100644 --- a/InvenTree/templates/js/api.js +++ b/InvenTree/templates/js/api.js @@ -103,10 +103,11 @@ function inventreePut(url, data={}, options={}) { } }, error: function(xhr, ajaxOptions, thrownError) { - console.error('Error on UPDATE to ' + url); - console.error(thrownError); if (options.error) { options.error(xhr, ajaxOptions, thrownError); + } else { + console.error(`Error on ${method} to '${url}' - STATUS ${xhr.status}`); + console.error(thrownError); } }, complete: function(xhr, status) { diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 93ab9e696e..8a302bc2b1 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -79,7 +79,7 @@ function canDelete(OPTIONS) { * Get the API endpoint options at the provided URL, * using a HTTP options request. */ -function getApiEndpointOptions(url, callback, options={}) { +function getApiEndpointOptions(url, callback, options) { // Return the ajax request object $.ajax({ @@ -108,10 +108,10 @@ function getApiEndpointOptions(url, callback, options={}) { * options: * - */ -function constructCreateForm(url, fields, options={}) { +function constructCreateForm(fields, options) { // We should have enough information to create the form! - constructFormBody(url, fields, options); + constructFormBody(fields, options); } @@ -124,11 +124,11 @@ function constructCreateForm(url, fields, options={}) { * options: * - */ -function constructChangeForm(url, fields, options={}) { +function constructChangeForm(fields, options) { // Request existing data from the API endpoint $.ajax({ - url: url, + url: options.url, type: 'GET', contentType: 'application/json', dataType: 'json', @@ -145,7 +145,7 @@ function constructChangeForm(url, fields, options={}) { } } - constructFormBody(url, fields, options); + constructFormBody(fields, options); }, error: function(request, status, error) { // TODO: Handle error here @@ -160,16 +160,16 @@ function constructChangeForm(url, fields, options={}) { * Request API OPTIONS data from the server, * and construct a modal form based on the response. * - * arguments: - * - url: API URL - * * options: * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE', * - title: The form title * - fields: list of fields to display * - exclude: List of fields to exclude */ -function constructForm(url, options={}) { +function constructForm(url, options) { + + // Save the URL + options.url = url; // Default HTTP method options.method = options.method || 'PATCH'; @@ -187,7 +187,7 @@ function constructForm(url, options={}) { switch (options.method) { case 'POST': if (canCreate(OPTIONS)) { - constructCreateForm(url, OPTIONS.actions.POST, options); + constructCreateForm(OPTIONS.actions.POST, options); } else { // User does not have permission to POST to the endpoint console.log('cannot POST'); @@ -197,7 +197,7 @@ function constructForm(url, options={}) { case 'PUT': case 'PATCH': if (canChange(OPTIONS)) { - constructChangeForm(url, OPTIONS.actions.PUT, options); + constructChangeForm(OPTIONS.actions.PUT, options); } else { // User does not have permission to PUT/PATCH to the endpoint // TODO @@ -231,7 +231,7 @@ function constructForm(url, options={}) { -function constructFormBody(url, fields, options={}) { +function constructFormBody(fields, options) { var html = ''; @@ -298,7 +298,10 @@ function constructFormBody(url, fields, options={}) { // TODO: Dynamically create the modals, // so that we can have an infinite number of stacks! - var modal = '#modal-form'; + + options.modal = options.modal || '#modal-form'; + + var modal = options.modal; modalEnable(modal, true); @@ -313,12 +316,9 @@ function constructFormBody(url, fields, options={}) { $(modal).modal('show'); // Setup related fields - initializeRelatedFields(modal, url, fields, options) + initializeRelatedFields(fields, options) attachToggle(modal); - // attachSelect(modal); - - //$(modal + ' .select').select2(); $(modal + ' .select2-container').addClass('select-full-width'); $(modal + ' .select2-container').css('width', '100%'); @@ -328,29 +328,119 @@ function constructFormBody(url, fields, options={}) { $(modal).off('click', '#modal-form-submit'); $(modal).on('click', '#modal-form-submit', function() { - var patch_data = {}; - - // Construct submit data - field_names.forEach(function(name) { - var field = fields[name] || null; - - if (field) { - var field_value = getFieldValue(name); - - patch_data[name] = field_value; - } else { - console.log(`Could not find field matching '${name}'`); - } - }) - - - console.log(patch_data); - + submitFormData(fields, options); }); } -function initializeRelatedFields(modal, url, fields, options) { +/* + * Submit form data to the server. + * + */ +function submitFormData(fields, options) { + + // Data to be sent to the server + var data = {}; + + // Extract values for each field + options.field_names.forEach(function(name) { + + var field = fields[name] || null; + + if (field) { + var value = getFieldValue(name); + + // TODO - Custom parsing depending on type? + + data[name] = value; + } else { + console.log(`WARNING: Could not find field matching '${name}'`); + } + }); + + // Submit data + inventreePut( + options.url, + data, + { + method: options.method, + success: function(response, status) { + console.log('success', '->', status); + }, + error: function(xhr, status, thrownError) { + + switch (xhr.status) { + case 400: // Bad request + handleFormErrors(xhr.responseJSON, fields, options); + break; + default: + console.log(`WARNING: Unhandled response code - ${xhr.status}`); + break; + } + } + } + ); +} + + +/* + * Remove all error text items from the form + */ +function clearFormErrors(options) { + + // Remove the individual error messages + $(options.modal).find('.form-error-message').remove(); + + // Remove the "has error" class + $(options.modal).find('.has-error').removeClass('has-error'); +} + + +/* + * Display form error messages as returned from the server. + * + * arguments: + * - errors: The JSON error response from the server + * - fields: The form data object + * - options: Form options provided by the client + */ +function handleFormErrors(errors, fields, options) { + + // Remove any existing error messages from the form + clearFormErrors(options); + + for (field_name in errors) { + if (field_name in fields) { + + // Add the 'has-error' class + $(options.modal).find(`#div_id_${field_name}`).addClass('has-error'); + + var field_dom = $(options.modal).find(`#id_${field_name}`); + + var field_errors = errors[field_name]; + + // Add an entry for each returned error message + for (var idx = field_errors.length-1; idx >= 0; idx--) { + + var error_text = field_errors[idx]; + + var html = ` + + ${error_text} + `; + + $(html).insertAfter(field_dom); + } + + } else { + console.log(`WARNING: handleFormErrors found no match for field '${field_name}'`); + } + } + +} + + +function initializeRelatedFields(fields, options) { var field_names = options.field_names; @@ -368,7 +458,7 @@ function initializeRelatedFields(modal, url, fields, options) { continue; } - initializeRelatedField(modal, name, field, options); + initializeRelatedField(name, field, options); } } @@ -385,7 +475,7 @@ function initializeRelatedFields(modal, url, fields, options) { function initializeRelatedField(modal, name, field, options) { // Find the select element and attach a select2 to it - var select = $(modal).find(`#id_${name}`); + var select = $(options.modal).find(`#id_${name}`); // TODO: Add 'placeholder' support for entry select2 fields @@ -397,7 +487,7 @@ function initializeRelatedField(modal, name, field, options) { url: field.api_url, dataType: 'json', allowClear: !field.required, // Allow non required fields to be cleared - dropdownParent: $(modal), + dropdownParent: $(options.modal), dropdownAutoWidth: false, delay: 250, cache: true, @@ -555,7 +645,7 @@ function renderModelData(name, model, data, parameters, options) { * - Field description (help text) * - Field errors */ -function constructField(name, parameters, options={}) { +function constructField(name, parameters, options) { var field_name = `id_${name}`; @@ -631,7 +721,7 @@ function constructLabel(name, parameters) { * - parameters: Field parameters returned by the OPTIONS method * */ -function constructInput(name, parameters, options={}) { +function constructInput(name, parameters, options) { var html = ''; @@ -741,7 +831,7 @@ function constructInputOptions(name, classes, type, parameters) { // Construct a "checkbox" input -function constructCheckboxInput(name, parameters, options={}) { +function constructCheckboxInput(name, parameters, options) { return constructInputOptions( name, @@ -754,7 +844,7 @@ function constructCheckboxInput(name, parameters, options={}) { // Construct a "text" input -function constructTextInput(name, parameters, options={}) { +function constructTextInput(name, parameters, options) { var classes = ''; var type = ''; @@ -784,7 +874,7 @@ function constructTextInput(name, parameters, options={}) { // Construct a "number" field -function constructNumberInput(name, parameters, options={}) { +function constructNumberInput(name, parameters, options) { return constructInputOptions( name, @@ -796,7 +886,7 @@ function constructNumberInput(name, parameters, options={}) { // Construct a "choice" input -function constructChoiceInput(name, parameters, options={}) { +function constructChoiceInput(name, parameters, options) { var html = ``; @@ -849,7 +939,7 @@ function constructRelatedFieldInput(name, parameters, options={}) { * - parameters: Field parameters returned by the OPTIONS method * */ -function constructHelpText(name, parameters, options={}) { +function constructHelpText(name, parameters, options) { var html = `
                  ${parameters.help_text}
                  `; @@ -864,7 +954,7 @@ function constructHelpText(name, parameters, options={}) { * - name: The name of the field * - parameters: Field parameters returned by the OPTIONS method */ -function constructErrorMessage(name, parameters, options={}) { +function constructErrorMessage(name, parameters, options) { var errors_html = ''; From 2eb756568376187943ec6ee98b528660f9621d0c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 19:30:57 +1000 Subject: [PATCH 049/178] Callback handler for form success --- InvenTree/templates/js/forms.js | 62 ++++++++++++++------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 8a302bc2b1..c6d515a620 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -365,7 +365,7 @@ function submitFormData(fields, options) { { method: options.method, success: function(response, status) { - console.log('success', '->', status); + handleFormSuccess(response, options); }, error: function(xhr, status, thrownError) { @@ -383,6 +383,31 @@ function submitFormData(fields, options) { } +/* + * Handle successful form posting + * + * arguments: + * - response: The JSON response object from the server + * - options: The original options object provided by the client + */ +function handleFormSuccess(response, options) { + + // Close the modal + if (!options.preventClose) { + // TODO: Actually just *delete* the modal, + // rather than hiding it!! + $(options.modal).modal('hide'); + } + + if (response.url) { + // GOTO + window.location.href = response.url; + } + +} + + + /* * Remove all error text items from the form */ @@ -664,10 +689,6 @@ function constructField(name, parameters, options) { html += constructInput(name, parameters, options); - if (parameters.errors) { - html += constructErrorMessage(name, parameters, options); - } - if (parameters.help_text) { html += constructHelpText(name, parameters, options); } @@ -941,36 +962,7 @@ function constructRelatedFieldInput(name, parameters, options) { */ function constructHelpText(name, parameters, options) { - var html = `
                  ${parameters.help_text}
                  `; + var html = `
                  ${parameters.help_text}
                  `; return html; -} - - -/* - * Construct an 'error message' div for the field - * - * arguments: - * - name: The name of the field - * - parameters: Field parameters returned by the OPTIONS method - */ -function constructErrorMessage(name, parameters, options) { - - var errors_html = ''; - - for (var idx = 0; idx < parameters.errors.length; idx++) { - - var err = parameters.errors[idx]; - - var html = ''; - - html += ``; - html += `${err}`; - html += ``; - - errors_html += html; - - } - - return errors_html; } \ No newline at end of file From f696bb2e2a53d8828060e511276ed1deaa89d719 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 19:49:56 +1000 Subject: [PATCH 050/178] Correctly read out boolean fields --- InvenTree/templates/js/forms.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index c6d515a620..b33f5048b3 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -246,6 +246,11 @@ function constructFormBody(fields, options) { ignored_fields.push('id'); } + // Provide each field object with its own name + for(field in fields) { + fields[field].name = field; + } + // Construct an ordered list of field names var field_names = []; @@ -348,9 +353,8 @@ function submitFormData(fields, options) { var field = fields[name] || null; if (field) { - var value = getFieldValue(name); - // TODO - Custom parsing depending on type? + var value = getFormFieldValue(name, field, options); data[name] = value; } else { @@ -383,6 +387,29 @@ function submitFormData(fields, options) { } +/* + * Extract and field value before sending back to the server + * + * arguments: + * - name: The name of the field + * - field: The field specification provided from the OPTIONS request + * - options: The original options object provided by the client + */ +function getFormFieldValue(name, field, options) { + + // Find the HTML element + var el = $(options.modal).find(`#id_${name}`); + + switch (field.type) { + case 'boolean': + return el.is(":checked"); + default: + return el.val(); + } +} + + + /* * Handle successful form posting * From d80948369b65ff7594ae5cf8d74aaad1f39cbb68 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 27 Jun 2021 21:44:21 +1000 Subject: [PATCH 051/178] Include 'default' value in OPTIONS request for any fields with specified default values --- InvenTree/InvenTree/metadata.py | 38 +++++++++++++++++-- .../company/templates/company/index.html | 25 ++++++++++-- RELEASE.md | 4 ++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 611b12101c..85815d2558 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -6,6 +6,7 @@ import logging from rest_framework import serializers from rest_framework.metadata import SimpleMetadata +from rest_framework.utils import model_meta import users.models @@ -41,11 +42,11 @@ class InvenTreeMetadata(SimpleMetadata): try: # Extract the model name associated with the view - model = view.serializer_class.Meta.model + self.model = view.serializer_class.Meta.model # Construct the 'table name' from the model - app_label = model._meta.app_label - tbl_label = model._meta.model_name + app_label = self.model._meta.app_label + tbl_label = self.model._meta.model_name table = f"{app_label}_{tbl_label}" @@ -83,6 +84,37 @@ class InvenTreeMetadata(SimpleMetadata): return metadata + def get_serializer_info(self, serializer): + """ + Override get_serializer_info so that we can add 'default' values + to any fields whose Meta.model specifies a default value + """ + + field_info = super().get_serializer_info(serializer) + + try: + ModelClass = serializer.Meta.model + + model_fields = model_meta.get_field_info(ModelClass) + + for name, field in model_fields.fields.items(): + + if field.has_default() and name in field_info.keys(): + + default = field.default + + if callable(default): + try: + default = default() + except: + continue + + field_info[name]['default'] = default + except AttributeError: + pass + + return field_info + def get_field_info(self, field): """ Given an instance of a serializer field, return a dictionary diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 3a3168f24e..2f7319fb74 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -15,10 +15,13 @@ {% if pagetype == 'manufacturers' and roles.purchase_order.add or pagetype == 'suppliers' and roles.purchase_order.add or pagetype == 'customers' and roles.sales_order.add %}
                  + +
                  -
                  {% endif %} @@ -35,6 +38,22 @@ }); }); + $('#new-company-2').click(function() { + constructForm( + '{% url "api-company-list" %}', + { + method: 'POST', + fields: [ + 'name', + 'description', + 'is_supplier', + 'is_manufacturer', + 'is_customer', + ] + } + ); + }); + loadCompanyTable("#company-table", "{% url 'api-company-list' %}", { pagetype: '{{ pagetype }}', diff --git a/RELEASE.md b/RELEASE.md index 90fb027a62..7d6d0fba23 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -21,3 +21,7 @@ Create new release for the [inventree documentation](https://github.com/inventre ### Python Library Release Create new release for the [inventree python library](https://github.com/inventree/inventree-python) + +## App Release + +Create new versioned release for the InvenTree mobile app. From 0e9b82c475a28a83399cc41cf517bf2d0a95e00d Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 27 Jun 2021 21:58:22 +1000 Subject: [PATCH 052/178] Load default values into rendered form --- .../company/templates/company/index.html | 5 ++ InvenTree/templates/js/forms.js | 48 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 2f7319fb74..28129b1f36 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -46,6 +46,11 @@ fields: [ 'name', 'description', + 'website', + 'address', + 'phone', + 'email', + 'contact', 'is_supplier', 'is_manufacturer', 'is_customer', diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index b33f5048b3..7d6cc5cd98 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -110,6 +110,16 @@ function getApiEndpointOptions(url, callback, options) { */ function constructCreateForm(fields, options) { + // Check if default values were provided for any fields + for (const name in fields) { + + var field = fields[name]; + + if (field.default != null) { + field.value = field.default; + } + } + // We should have enough information to create the form! constructFormBody(fields, options); } @@ -320,6 +330,8 @@ function constructFormBody(fields, options) { $(modal).modal('show'); + updateFieldValues(fields, options); + // Setup related fields initializeRelatedFields(fields, options) @@ -387,6 +399,41 @@ function submitFormData(fields, options) { } +/* + * Update (set) the field values based on the specified data. + * + * Iterate through each of the displayed fields, + * and set the 'val' attribute of each one. + * + */ +function updateFieldValues(fields, options) { + + for (var idx = 0; idx < options.field_names.length; idx++) { + + var name = options.field_names[idx]; + + var field = fields[name] || null; + + if (field == null) { continue; } + + var value = field.value || field.default || null; + + if (value == null) { continue; } + + var el = $(options.modal).find(`#id_${name}`); + + switch (field.type) { + case 'boolean': + el.prop('checked', value); + break; + default: + el.val(value); + break; + } + } +} + + /* * Extract and field value before sending back to the server * @@ -409,7 +456,6 @@ function getFormFieldValue(name, field, options) { } - /* * Handle successful form posting * From 16f25f54d4797b2cfbd89d79a950fbeb91cf4783 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 17:45:31 +0200 Subject: [PATCH 053/178] sorting price-breaks on start --- InvenTree/part/templates/part/prices.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 1214239fe4..31732d3d94 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -189,7 +189,8 @@
                  - +
                  @@ -240,7 +241,8 @@
                  - +
                  From 984efd7493cad851e52ad2582cd639ec1d82eb8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 17:51:49 +0200 Subject: [PATCH 054/178] sort graph-data as well --- InvenTree/templates/js/part.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 75e925266a..bab8a914db 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -788,6 +788,10 @@ function loadPriceBreakTable(table, options) { url: options.url, onLoadSuccess: function(tableData) { if (linkedGraph) { + // sort array + tableData = tableData.sort((a,b)=>a.quantity-b.quantity); + + // split up for graph definition var labels = Array.from(tableData, x => x.quantity); var data = Array.from(tableData, x => parseFloat(x.price)); From d71aee00cd34f3bbe059edd2aae154f922b19e03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 17:54:33 +0200 Subject: [PATCH 055/178] refactor of variable names --- InvenTree/templates/js/part.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index bab8a914db..888f1d245a 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -792,8 +792,8 @@ function loadPriceBreakTable(table, options) { tableData = tableData.sort((a,b)=>a.quantity-b.quantity); // split up for graph definition - var labels = Array.from(tableData, x => x.quantity); - var data = Array.from(tableData, x => parseFloat(x.price)); + var graphLabels = Array.from(tableData, x => x.quantity); + var graphData = Array.from(tableData, x => parseFloat(x.price)); // destroy chart if exists if (chart){ @@ -801,11 +801,11 @@ function loadPriceBreakTable(table, options) { } chart = loadLineChart(linkedGraph, { - labels: labels, + labels: graphLabels, datasets: [ { label: '{% trans "Unit Price" %}', - data: data, + data: graphData, backgroundColor: 'rgba(255, 206, 86, 0.2)', borderColor: 'rgb(255, 206, 86)', stepped: true, From ef07c93634ce202df4665145e8ce77f19f39cb37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 18:31:40 +0200 Subject: [PATCH 056/178] section anchors --- InvenTree/InvenTree/static/css/inventree.css | 11 ++++- InvenTree/part/templates/part/prices.html | 45 ++++++++++++++++---- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index a0030ee7e6..41e0937a8b 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -965,9 +965,16 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da .row.full-height { display: flex; flex-wrap: wrap; - } +} .row.full-height > [class*='col-'] { display: flex; flex-direction: column; - } +} + +a.anchor { + display: block; + position: relative; + top: -60px; + visibility: hidden; +} \ No newline at end of file diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 31732d3d94..0d5b3e9d91 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -17,13 +17,17 @@ {% default_currency as currency %}
                  +

                  {% trans "Pricing ranges" %}

                  {% if part.supplier_count > 0 %} {% if min_total_buy_price %} - + @@ -48,7 +52,9 @@ {% if part.bom_count > 0 %} {% if min_total_bom_price %} - + @@ -94,7 +100,10 @@ {% if total_part_price %} - + @@ -132,8 +141,11 @@ {% if part.purchaseable and roles.purchase_order.view %}
                  +
                  -

                  {% trans "Supplier Cost" %}

                  +

                  {% trans "Supplier Cost" %} + +

                  @@ -149,8 +161,11 @@
                  +
                  -

                  {% trans "Purchase Price" %}

                  +

                  {% trans "Purchase Price" %} + +

                  {% if price_history %} @@ -172,8 +187,11 @@ {% if show_internal_price and roles.sales_order.view %}
                  +
                  -

                  {% trans "Internal Cost" %}

                  +

                  {% trans "Internal Cost" %} + +

                  @@ -200,8 +218,11 @@ {% if part.has_bom and roles.sales_order.view %}
                  +
                  -

                  {% trans "BOM Cost" %}

                  +

                  {% trans "BOM Cost" %} + +

                  @@ -224,8 +245,11 @@ {% if part.salable and roles.sales_order.view %}
                  +
                  -

                  {% trans "Sale Cost" %}

                  +

                  {% trans "Sale Cost" %} + +

                  @@ -249,8 +273,11 @@
                  +
                  -

                  {% trans "Sale Price" %}

                  +

                  {% trans "Sale Price" %} + +

                  From f479c0cd27d6589d519d27660786b85b5a1b987c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 20:46:52 +0200 Subject: [PATCH 057/178] naming refactor --- InvenTree/part/templates/part/prices.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index 0d5b3e9d91..b1403b3157 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -293,6 +293,8 @@ {% block js_ready %} {{ block.super }} + {% default_currency as currency %} + loadSupplierPartTable( "#supplier-table", @@ -321,9 +323,8 @@ // history graphs - {% default_currency as currency %} {% if price_history %} - var pricedata = { + var purchasepricedata = { labels: [ {% for line in price_history %}'{{ line.date }}',{% endfor %} ], @@ -375,7 +376,7 @@ borderWidth: 1 }] } - var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), pricedata) + var StockPriceChart = loadStockPricingChart($('#StockPriceChart'), purchasepricedata) var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} }) var bomdata = { labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}], From e06397adc1da181e3ad281958a4df8411dab503e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 21:31:10 +0200 Subject: [PATCH 058/178] refactor --- InvenTree/part/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2f129dd30b..cb89f67de3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -979,6 +979,8 @@ class PartPricingView(PartDetail): """ returns context with pricing information """ ctx = PartPricing.get_pricing(self, quantity, currency) part = self.get_part() + default_currency = inventree_settings.currency_code_default() + # Stock history if part.total_stock > 1: price_history = [] @@ -990,7 +992,7 @@ class PartPricingView(PartDetail): continue # convert purchase price to current currency - only one currency in the graph - price = convert_money(stock_item.purchase_price, inventree_settings.currency_code_default()) + price = convert_money(stock_item.purchase_price, default_currency) line = { 'price': price.amount, 'qty': stock_item.quantity From 4462b1e25064a0bca7ac4c415d57e30f8d3cb023 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 21:31:34 +0200 Subject: [PATCH 059/178] order stock histroy items --- InvenTree/part/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index cb89f67de3..7a683a29b5 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -984,8 +984,8 @@ class PartPricingView(PartDetail): # Stock history if part.total_stock > 1: price_history = [] - stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date') - stock = stock.prefetch_related('purchase_order', 'supplier_part') + stock = part.stock_entries(include_variants=False, in_stock=True).\ + order_by('purchase_order__issue_date').prefetch_related('purchase_order', 'supplier_part') for stock_item in stock: if None in [stock_item.purchase_price, stock_item.quantity]: From 5598f7fad15ef481f52de11d9bb2623a3ab8d1e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Jun 2021 21:32:27 +0200 Subject: [PATCH 060/178] added sale price history --- InvenTree/part/templates/part/prices.html | 41 ++++++++++++++++++++++- InvenTree/part/views.py | 31 +++++++++++++++++ InvenTree/templates/js/part.js | 33 ++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html index b1403b3157..9c216eb534 100644 --- a/InvenTree/part/templates/part/prices.html +++ b/InvenTree/part/templates/part/prices.html @@ -281,7 +281,15 @@
                  - PLACEHOLDER FOR SALE HISTORY + {% if sale_history|length > 0 %} +
                  + +
                  + {% else %} +
                  + {% trans 'No sale pice history available for this part.' %} +
                  + {% endif %}
                  {% endif %} @@ -444,4 +452,35 @@ ); {% endif %} + // Sale price history + {% if sale_history %} + var salepricedata = { + labels: [ + {% for line in sale_history %}'{{ line.date }}',{% endfor %} + ], + datasets: [{ + label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgb(255, 99, 132)', + yAxisID: 'y', + data: [ + {% for line in sale_history %}{{ line.price|stringformat:".2f" }},{% endfor %} + ], + borderWidth: 1, + }, + { + label: '{% trans "Quantity" %}', + backgroundColor: 'rgba(255, 206, 86, 0.2)', + borderColor: 'rgb(255, 206, 86)', + yAxisID: 'y1', + data: [ + {% for line in sale_history %}{{ line.qty|stringformat:"f" }},{% endfor %} + ], + borderWidth: 1, + type: 'bar', + }] + } + var SalePriceChart = loadSellPricingChart($('#SalePriceChart'), salepricedata) + {% endif %} + {% endblock %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7a683a29b5..30e95535d6 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -50,6 +50,7 @@ import common.settings as inventree_settings from . import forms as part_forms from .bom import MakeBomTemplate, BomUploadManager, ExportBom, IsValidBOMFormat +from order.models import PurchaseOrderLineItem from .admin import PartResource @@ -1038,6 +1039,36 @@ class PartPricingView(PartDetail): # add to global context ctx['bom_parts'] = ctx_bom_parts + # Sale price history + sale_items = PurchaseOrderLineItem.objects.filter(part__part=part).order_by('order__issue_date').\ + prefetch_related('order', ).all() + + if sale_items: + sale_history = [] + + for sale_item in sale_items: + # check for not fully defined elements + if None in [sale_item.purchase_price, sale_item.quantity]: + continue + + price = convert_money(sale_item.purchase_price, default_currency) + line = { + 'price': price.amount if price else 0, + 'qty': sale_item.quantity, + } + + # set date for graph labels + if sale_item.order.issue_date: + line['date'] = sale_item.order.issue_date.strftime('%d.%m.%Y') + elif sale_item.order.creation_date: + line['date'] = sale_item.order.creation_date.strftime('%d.%m.%Y') + else: + line['date'] = _('None') + + sale_history.append(line) + + ctx['sale_history'] = sale_history + return ctx def get_initials(self): diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 888f1d245a..7fa63098e1 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -977,3 +977,36 @@ function loadBomChart(context, data) { } }); } + +function loadSellPricingChart(context, data) { + return new Chart(context, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: {legend: {position: 'bottom'}}, + scales: { + y: { + type: 'linear', + position: 'left', + grid: {display: false}, + title: { + display: true, + text: '{% trans "Unit Price" %}' + } + }, + y1: { + type: 'linear', + position: 'right', + grid: {display: false}, + titel: { + display: true, + text: '{% trans "Quantity" %}', + position: 'right' + } + }, + }, + } + }); +} From 6335372208078d5c588938bbc9bd34500d08edc1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 09:28:38 +1000 Subject: [PATCH 061/178] Store instance data when performing an "update" --- InvenTree/templates/js/forms.js | 5 ++++- InvenTree/templates/js/model_renderers.js | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 7d6cc5cd98..063f953966 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -155,6 +155,9 @@ function constructChangeForm(fields, options) { } } + // Store the entire data object + options.instance = data; + constructFormBody(fields, options); }, error: function(request, status, error) { @@ -570,7 +573,7 @@ function initializeRelatedFields(fields, options) { * - field: Field definition from the OPTIONS request * - options: Original options object provided by the client */ -function initializeRelatedField(modal, name, field, options) { +function initializeRelatedField(name, field, options) { // Find the select element and attach a select2 to it var select = $(options.modal).find(`#id_${name}`); diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index 924d85d35d..ae32c06eff 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -1,3 +1,5 @@ +{% load i18n %} + /* * This file contains functions for rendering various InvenTree database models, * in particular for displaying them in modal forms in a 'select2' context. @@ -69,12 +71,14 @@ function renderPart(name, data, parameters, options) { // Renderer for "PartCategory" model function renderPartCategory(name, data, parameters, options) { - var html = `${data.name}`; + var html = `${data.name}`; if (data.description) { html += ` - ${data.description}`; } + html += `{% trans "Location ID" %}: ${data.pk}`; + if (data.pathstring) { html += `

                  ${data.pathstring}

                  `; } From ba2537d125061af4f4bceccd1adda30cbaa9e9fd Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 13:03:34 +1000 Subject: [PATCH 062/178] Refactor the way that field options are passed to a form --- InvenTree/templates/js/forms.js | 49 ++++++++++++++------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 063f953966..4e8791f125 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -243,21 +243,20 @@ function constructForm(url, options) { } - +/* + * Construct a modal form based on the provided options + * + * arguments: + * - fields: The endpoint description returned from the OPTIONS request + * - options: form options object provided by the client. + */ function constructFormBody(fields, options) { var html = ''; - var allowed_fields = options.fields || null; - var ignored_fields = options.ignore || []; - - if (!ignored_fields.includes('pk')) { - ignored_fields.push('pk'); - } - - if (!ignored_fields.includes('id')) { - ignored_fields.push('id'); - } + // Client must provide set of fields to be displayed, + // otherwise *all* fields will be displayed + var displayed_fields = options.fields || fields; // Provide each field object with its own name for(field in fields) { @@ -267,25 +266,13 @@ function constructFormBody(fields, options) { // Construct an ordered list of field names var field_names = []; - if (allowed_fields) { - allowed_fields.forEach(function(name) { + for (var name in displayed_fields) { - // Only push names which are actually in the set of fields - if (name in fields) { - - if (!ignored_fields.includes(name) && !field_names.includes(name)) { - field_names.push(name); - } - } else { - console.log(`WARNING: '${name}' does not match a valid field name.`); - } - }); - } else { - for (const name in fields) { - - if (!ignored_fields.includes(name) && !field_names.includes(name)) { - field_names.push(name); - } + // Only push names which are actually in the set of fields + if (name in fields) { + field_names.push(name); + } else { + console.log(`WARNING: '${name}' does not match a valid field name.`); } } @@ -429,6 +416,10 @@ function updateFieldValues(fields, options) { case 'boolean': el.prop('checked', value); break; + case 'related field': + // TODO + console.log(`related field '${name}'`); + break; default: el.val(value); break; From e2942238a9036c10c5a64b6fec371ae31fdff82c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 13:10:41 +1000 Subject: [PATCH 063/178] Bug fix - check for null rather than just ! --- InvenTree/templates/js/forms.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 4e8791f125..c90569d9c0 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -406,7 +406,11 @@ function updateFieldValues(fields, options) { if (field == null) { continue; } - var value = field.value || field.default || null; + var value = field.value; + + if (value == null) { + value = field.default; + } if (value == null) { continue; } From 41539b75db59c91e0673e49149fa9b0a0cf9808e Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 14:19:05 +1000 Subject: [PATCH 064/178] Adds custom filters for AJAX queries --- InvenTree/templates/js/forms.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index c90569d9c0..6cc665513f 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -261,6 +261,15 @@ function constructFormBody(fields, options) { // Provide each field object with its own name for(field in fields) { fields[field].name = field; + + var field_options = displayed_fields[field]; + + // Copy custom options across to the fields object + if (field_options) { + + // Query filters + fields[field].filters = field_options.filters; + } } // Construct an ordered list of field names @@ -596,12 +605,15 @@ function initializeRelatedField(name, field, options) { offset = (params.page - 1) * pageSize; } - // Re-format search term into InvenTree API style - return { - search: params.term, - offset: offset, - limit: pageSize, - }; + // Custom query filters can be specified against each field + var query = field.filters || {}; + + // Add search and pagination options + query.search = params.term; + query.offset = offset; + query.limit = pageSize; + + return query; }, processResults: function(response) { // Convert the returned InvenTree data into select2-friendly format From fbff9bfb2d25c99b7328c144674026b963fd060b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 15:10:17 +1000 Subject: [PATCH 065/178] Insert buttons for secondary modals --- InvenTree/templates/js/forms.js | 34 +++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 6cc665513f..833c4eb667 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -269,6 +269,9 @@ function constructFormBody(fields, options) { // Query filters fields[field].filters = field_options.filters; + + // Secondary modal options + fields[field].secondary = field_options.secondary; } } @@ -568,6 +571,29 @@ function initializeRelatedFields(fields, options) { } +/* + * Add a button to launch a secondary modal, to create a new modal instance. + * + * arguments: + * - name: The name of the field + * - field: The field data object + * - options: The options object provided by the client + */ +function addSecondaryModal(name, field, options) { + + var html = ` + +
                  + ${field.secondary.label} +
                  +
                  `; + + $(options.modal).find(`label[for="id_${name}"]`).append(html); + + // TODO: Launch a callback +} + + /* * Initializea single related-field * @@ -582,6 +608,11 @@ function initializeRelatedField(name, field, options) { // Find the select element and attach a select2 to it var select = $(options.modal).find(`#id_${name}`); + // Add a button to launch a 'secondary' modal + if (field.secondary != null) { + addSecondaryModal(name, field, options); + } + // TODO: Add 'placeholder' support for entry select2 fields // limit size for AJAX requests @@ -591,12 +622,11 @@ function initializeRelatedField(name, field, options) { ajax: { url: field.api_url, dataType: 'json', - allowClear: !field.required, // Allow non required fields to be cleared + allowClear: !field.required, dropdownParent: $(options.modal), dropdownAutoWidth: false, delay: 250, cache: true, - // matcher: partialMatcher, data: function(params) { if (!params.page) { From e58507977995fc4f324445ac86c53f9dcb3eacc2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 19:32:48 +1000 Subject: [PATCH 066/178] Callback function for fields after editing --- InvenTree/templates/js/forms.js | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 833c4eb667..9c24aa7780 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -272,6 +272,9 @@ function constructFormBody(fields, options) { // Secondary modal options fields[field].secondary = field_options.secondary; + + // Edit callback + fields[field].onEdit = field_options.onEdit; } } @@ -335,7 +338,10 @@ function constructFormBody(fields, options) { updateFieldValues(fields, options); // Setup related fields - initializeRelatedFields(fields, options) + initializeRelatedFields(fields, options); + + // Attach edit callbacks (if required) + addFieldCallbacks(fields, options); attachToggle(modal); @@ -548,6 +554,35 @@ function handleFormErrors(errors, fields, options) { } +/* + * Attach callbacks to specified fields, + * triggered after the field value is edited. + * + * Callback function is called with arguments (name, field, options) + */ +function addFieldCallbacks(fields, options) { + + for (var idx = 0; idx < options.field_names.length; idx++) { + + var name = options.field_names[idx]; + + var field = fields[name]; + + if (!field || !field.onEdit) continue; + + addFieldCallback(name, field, options); + } +} + + +function addFieldCallback(name, field, options) { + + $(options.modal).find(`#id_${name}`).change(function() { + field.onEdit(name, field, options); + }); +} + + function initializeRelatedFields(fields, options) { var field_names = options.field_names; From f3ed05a09e4f5ccf1b65577a0189c80b85c34be6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 20:13:18 +1000 Subject: [PATCH 067/178] Automatically associate ''filters' with relations --- InvenTree/InvenTree/metadata.py | 17 ++++++++++ .../company/templates/company/index.html | 33 ++++++++++++++++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 85815d2558..89b5b14493 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -97,6 +97,7 @@ class InvenTreeMetadata(SimpleMetadata): model_fields = model_meta.get_field_info(ModelClass) + # Iterate through simple fields for name, field in model_fields.fields.items(): if field.has_default() and name in field_info.keys(): @@ -110,9 +111,25 @@ class InvenTreeMetadata(SimpleMetadata): continue field_info[name]['default'] = default + + # Iterate through relations + for name, relation in model_fields.relations.items(): + + if relation.reverse: + print("skipping reverse relation -", name) + continue + + print('filters:', name, relation.model_field.get_limit_choices_to()) + + continue + # Extract and provide the "limit_choices_to" filters + field_info[name]['filters'] = relation.model_field.get_limit_choices_to() + except AttributeError: pass + print(field_info.keys()) + return field_info def get_field_info(self, field): diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 28129b1f36..f02ae1e6c8 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -40,12 +40,34 @@ $('#new-company-2').click(function() { constructForm( - '{% url "api-company-list" %}', + '{% url "api-build-list" %}', { method: 'POST', - fields: [ - 'name', - 'description', + title: '{% trans "Edit Part Details" %}', + fields: { + name: { + onEdit: function() { + console.log('Edited name field'); + } + }, + description: {}, + category: { + filters: { + parent: 1, + }, + secondary: { + label: '{% trans "New Category" %}', + }, + }, + active: { + onEdit: function() { + console.log('edited active field'); + } + }, + purchaseable: {}, + salable: {}, + component: {}, + /* 'website', 'address', 'phone', @@ -54,7 +76,8 @@ 'is_supplier', 'is_manufacturer', 'is_customer', - ] + */ + } } ); }); From ac7564d069bb59834d950fcf788ebd8277a66f82 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 21:29:51 +1000 Subject: [PATCH 068/178] Extract "limit_choices_to" options for relatedfields - Specify as 'filters' for 'related field' type - Extremely handy to be able to filter AJAX requests in a DRY manner! --- InvenTree/InvenTree/metadata.py | 22 +++++++++---------- .../company/templates/company/index.html | 7 +++++- InvenTree/templates/js/forms.js | 4 ++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 89b5b14493..34490d1079 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -90,7 +90,7 @@ class InvenTreeMetadata(SimpleMetadata): to any fields whose Meta.model specifies a default value """ - field_info = super().get_serializer_info(serializer) + serializer_info = super().get_serializer_info(serializer) try: ModelClass = serializer.Meta.model @@ -100,7 +100,7 @@ class InvenTreeMetadata(SimpleMetadata): # Iterate through simple fields for name, field in model_fields.fields.items(): - if field.has_default() and name in field_info.keys(): + if field.has_default() and name in serializer_info.keys(): default = field.default @@ -110,27 +110,27 @@ class InvenTreeMetadata(SimpleMetadata): except: continue - field_info[name]['default'] = default + serializer_info[name]['default'] = default # Iterate through relations for name, relation in model_fields.relations.items(): - if relation.reverse: - print("skipping reverse relation -", name) + if name not in serializer_info.keys(): + # Skip relation not defined in serializer continue - print('filters:', name, relation.model_field.get_limit_choices_to()) + if relation.reverse: + # Ignore reverse relations + continue - continue # Extract and provide the "limit_choices_to" filters - field_info[name]['filters'] = relation.model_field.get_limit_choices_to() + # This is used to automatically filter AJAX requests + serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to() except AttributeError: pass - print(field_info.keys()) - - return field_info + return serializer_info def get_field_info(self, field): """ diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index f02ae1e6c8..fef338078a 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -45,6 +45,12 @@ method: 'POST', title: '{% trans "Edit Part Details" %}', fields: { + part: { + filters: { + } + }, + quantity: {}, + /* name: { onEdit: function() { console.log('Edited name field'); @@ -67,7 +73,6 @@ purchaseable: {}, salable: {}, component: {}, - /* 'website', 'address', 'phone', diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 9c24aa7780..9343751926 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -267,8 +267,8 @@ function constructFormBody(fields, options) { // Copy custom options across to the fields object if (field_options) { - // Query filters - fields[field].filters = field_options.filters; + // Override existing query filters (if provided!) + fields[field].filters = Object.assign(fields[field].filters || {}, field_options.filters); // Secondary modal options fields[field].secondary = field_options.secondary; From ed2f21f583358c7d6af1d4360ce14ae9d4c48c10 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Jun 2021 21:38:42 +1000 Subject: [PATCH 069/178] Display field prefix element in form --- InvenTree/company/templates/company/index.html | 4 ++++ InvenTree/templates/js/forms.js | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index fef338078a..796796528f 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -45,6 +45,10 @@ method: 'POST', title: '{% trans "Edit Part Details" %}', fields: { + title: { + prefix: `` + }, + reference: {}, part: { filters: { } diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 9343751926..318dfab1b4 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -275,6 +275,9 @@ function constructFormBody(fields, options) { // Edit callback fields[field].onEdit = field_options.onEdit; + + // Field prefix + fields[field].prefix = field_options.prefix; } } @@ -835,14 +838,21 @@ function constructField(name, parameters, options) { html += `
                  `; + if (parameters.prefix) { + html += `
                  ${parameters.prefix}`; + } + html += constructInput(name, parameters, options); + if (parameters.prefix) { + html += `
                  `; // input-group + } + if (parameters.help_text) { html += constructHelpText(name, parameters, options); } html += `
                  `; // controls - html += `
                  `; // form-group return html; From c3ef8d2dfbfa197a8dafd8f5842d5278ffea5ce6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 09:14:26 +1000 Subject: [PATCH 070/178] Fixes for model renderers --- InvenTree/templates/js/api.js | 8 ++++++++ InvenTree/templates/js/model_renderers.js | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/api.js b/InvenTree/templates/js/api.js index aa446028a5..5e8905a1dd 100644 --- a/InvenTree/templates/js/api.js +++ b/InvenTree/templates/js/api.js @@ -18,7 +18,15 @@ function getCookie(name) { } function inventreeGet(url, filters={}, options={}) { + + // Middleware token required for data update + //var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + var csrftoken = getCookie('csrftoken'); + return $.ajax({ + beforeSend: function(xhr, settings) { + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, url: url, type: 'GET', data: filters, diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index ae32c06eff..24e9c1d328 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -18,6 +18,8 @@ function renderCompany(name, data, parameters, options) { var html = `${data.name} - ${data.description}`; + html += `{% trans "Company ID" %}: ${data.pk}`; + return html; } @@ -39,6 +41,8 @@ function renderStockLocation(name, data, parameters, options) { html += ` - ${data.description}`; } + html += `{% trans "Location ID" %}: ${data.pk}`; + if (data.pathstring) { html += `

                  ${data.pathstring}

                  `; } @@ -64,6 +68,8 @@ function renderPart(name, data, parameters, options) { html += ` - ${data.description}`; } + html += `{% trans "Part ID" %}: ${data.pk}`; + return html; } @@ -77,7 +83,7 @@ function renderPartCategory(name, data, parameters, options) { html += ` - ${data.description}`; } - html += `{% trans "Location ID" %}: ${data.pk}`; + html += `{% trans "Category ID" %}: ${data.pk}`; if (data.pathstring) { html += `

                  ${data.pathstring}

                  `; From 25a01be995076f66901cf367cef67f094b86f56f Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 09:25:40 +1000 Subject: [PATCH 071/178] Added warning message for missing model information --- InvenTree/templates/js/forms.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 318dfab1b4..7f66a9fd30 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -726,8 +726,9 @@ function initializeRelatedField(name, field, options) { // If the 'model' is specified, hand it off to the custom model render return renderModelData(name, field.model, item, field, options); } else { - // Simply render the 'text' parameter - return item.text; + // Return a simple renderering + console.log(`WARNING: templateResult() missing 'field.model' for '${name}'`); + return `${name} - ${item.id}`; } }, templateSelection: function(item, container) { @@ -736,8 +737,9 @@ function initializeRelatedField(name, field, options) { // If the 'model' is specified, hand it off to the custom model render return renderModelData(name, field.model, item, field, options); } else { - // Simply render the 'text' parameter - return item.text; + // Return a simple renderering + console.log(`WARNING: templateSelection() missing 'field.model' for '${name}'`); + return `${name} - ${item.id}`; } } }); From 0037056ee81058e4941765759103e547656aedb3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 09:26:40 +1000 Subject: [PATCH 072/178] Better default renderer --- InvenTree/templates/js/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 7f66a9fd30..e388acf00c 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -801,7 +801,7 @@ function renderModelData(name, model, data, parameters, options) { } else { console.log(`ERROR: Rendering not implemented for model '${model}'`); // Simple text rendering - return data.id; + return `${model} - ID ${data.id}`; } } From 374344d0e2be5064eac9f0025c08a716c9e4aaa5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 09:28:00 +1000 Subject: [PATCH 073/178] Refactor switch statement --- InvenTree/templates/js/forms.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index e388acf00c..b7dc645775 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -913,20 +913,12 @@ function constructInput(name, parameters, options) { func = constructCheckboxInput; break; case 'string': - func = constructTextInput; - break; case 'url': - func = constructTextInput; - break; case 'email': func = constructTextInput; break; case 'integer': - func = constructNumberInput; - break; case 'float': - func = constructNumberInput; - break; case 'decimal': func = constructNumberInput; break; From 9312a5d3b4e104e8dea1c6ddb2c72a00f012c379 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 12:39:39 +1000 Subject: [PATCH 074/178] Correctly render selected value of a related field Ref: https://select2.org/programmatic-control/add-select-clear-items#preselecting-options-in-an-remotely-sourced-ajax-select2 --- InvenTree/templates/js/forms.js | 60 +++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index b7dc645775..fb3963d9e2 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -442,8 +442,7 @@ function updateFieldValues(fields, options) { el.prop('checked', value); break; case 'related field': - // TODO - console.log(`related field '${name}'`); + // TODO? break; default: el.val(value); @@ -721,10 +720,20 @@ function initializeRelatedField(name, field, options) { }, }, templateResult: function(item, container) { + + // Extract 'instance' data passed through from an initial value + // Or, use the raw 'item' data as a backup + var data = item; + + if (item.element && item.element.instance) { + data = item.element.instance; + } + // Custom formatting for the search results if (field.model) { // If the 'model' is specified, hand it off to the custom model render - return renderModelData(name, field.model, item, field, options); + var html = renderModelData(name, field.model, data, field, options); + return $(html); } else { // Return a simple renderering console.log(`WARNING: templateResult() missing 'field.model' for '${name}'`); @@ -732,10 +741,20 @@ function initializeRelatedField(name, field, options) { } }, templateSelection: function(item, container) { + + // Extract 'instance' data passed through from an initial value + // Or, use the raw 'item' data as a backup + var data = item; + + if (item.element && item.element.instance) { + data = item.element.instance; + } + // Custom formatting for selected item if (field.model) { // If the 'model' is specified, hand it off to the custom model render - return renderModelData(name, field.model, item, field, options); + var html = renderModelData(name, field.model, data, field, options); + return $(html); } else { // Return a simple renderering console.log(`WARNING: templateSelection() missing 'field.model' for '${name}'`); @@ -743,6 +762,35 @@ function initializeRelatedField(name, field, options) { } } }); + + // If a 'value' is already defined, grab the model info from the server + if (field.value) { + var pk = field.value; + var url = `${field.api_url}/${pk}/`.replace('//', '/'); + + inventreeGet(url, {}, { + success: function(data) { + + // Create a new option, simply use the model name as the text (for now) + // Note: The correct rendering will be computed later by templateSelection function + var option = new Option(name, data.pk, true, true); + + // Store the returned data as 'instance' parameter of the created option, + // so that it can be retrieved later! + option.instance = data; + + select.append(option).trigger('change'); + + // manually trigger the `select2:select` event + select.trigger({ + type: 'select2:select', + params: { + data: data + } + }); + } + }); + } } @@ -795,9 +843,7 @@ function renderModelData(name, model, data, parameters, options) { } if (html != null) { - // Render HTML to an object - var $state = $(html); - return $state; + return html; } else { console.log(`ERROR: Rendering not implemented for model '${model}'`); // Simple text rendering From f18c2a7a3ddf60ef3c439dbd7657ece60be191a9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 12:48:56 +1000 Subject: [PATCH 075/178] Fix rendering during search --- InvenTree/templates/js/forms.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index fb3963d9e2..dd72031149 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -729,6 +729,10 @@ function initializeRelatedField(name, field, options) { data = item.element.instance; } + if (!data.pk) { + return $(searching()); + } + // Custom formatting for the search results if (field.model) { // If the 'model' is specified, hand it off to the custom model render @@ -750,6 +754,10 @@ function initializeRelatedField(name, field, options) { data = item.element.instance; } + if (!data.pk) { + return $(searching()); + } + // Custom formatting for selected item if (field.model) { // If the 'model' is specified, hand it off to the custom model render @@ -794,6 +802,11 @@ function initializeRelatedField(name, field, options) { } +// Render a 'no results' element +function searching() { + return `{% trans "Searching" %}...`; +} + /* * Render a "foreign key" model reference in a select2 instance. * Allows custom rendering with access to the entire serialized object. From da6d170ce21ddac372777b7c5608f51d8a7edfd4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 19:39:45 +1000 Subject: [PATCH 076/178] Add 'help_text' for related fields --- InvenTree/InvenTree/metadata.py | 3 ++ InvenTree/part/test_api.py | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 34490d1079..fa3dee4794 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -127,6 +127,9 @@ class InvenTreeMetadata(SimpleMetadata): # This is used to automatically filter AJAX requests serializer_info[name]['filters'] = relation.model_field.get_limit_choices_to() + if 'help_text' not in serializer_info[name] and hasattr(relation.model_field, 'help_text'): + serializer_info[name]['help_text'] = relation.model_field.help_text + except AttributeError: pass diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 4922ed4e04..3e184c9fe2 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -16,6 +16,65 @@ from company.models import Company from common.models import InvenTreeSetting +class PartOptionsAPITest(InvenTreeAPITestCase): + """ + Tests for the various OPTIONS endpoints in the /part/ API + + Ensure that the required field details are provided! + """ + + roles = [ + 'part.add', + ] + + def setUp(self): + + super().setUp() + + def test_part(self): + """ + Test the Part API OPTIONS + """ + + actions = self.getActions(reverse('api-part-list'))['POST'] + + # Check that a bunch o' fields are contained + for f in ['assembly', 'component', 'description', 'image', 'IPN']: + self.assertTrue(f in actions.keys()) + + # Active is a 'boolean' field + active = actions['active'] + + self.assertTrue(active['default']) + self.assertEqual(active['help_text'], 'Is this part active?') + self.assertEqual(active['type'], 'boolean') + self.assertEqual(active['read_only'], False) + + # String field + ipn = actions['IPN'] + self.assertEqual(ipn['type'], 'string') + self.assertFalse(ipn['required']) + self.assertEqual(ipn['max_length'], 100) + self.assertEqual(ipn['help_text'], 'Internal Part Number') + + # Related field + category = actions['category'] + + self.assertEqual(category['type'], 'related field') + self.assertTrue(category['required']) + self.assertFalse(category['read_only']) + self.assertEqual(category['label'], 'Category') + self.assertEqual(category['model'], 'partcategory') + self.assertEqual(category['api_url'], reverse('api-part-category-list')) + self.assertEqual(category['help_text'], 'Part category') + + def test_category(self): + pass + + def test_bom_item(self): + pass + + class PartAPITest(InvenTreeAPITestCase): """ Series of tests for the Part DRF API From 4aed6993d4041421bbef047a8db33de784949c9b Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 19:48:49 +1000 Subject: [PATCH 077/178] Add some more unit tests --- InvenTree/part/test_api.py | 45 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 3e184c9fe2..0c1f083383 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -69,10 +69,51 @@ class PartOptionsAPITest(InvenTreeAPITestCase): self.assertEqual(category['help_text'], 'Part category') def test_category(self): - pass + """ + Test the PartCategory API OPTIONS endpoint + """ + + actions = self.getActions(reverse('api-part-category-list')) + + # actions should *not* contain 'POST' as we do not have the correct role + self.assertFalse('POST' in actions) + + self.assignRole('part_category.add') + + actions = self.getActions(reverse('api-part-category-list'))['POST'] + + name = actions['name'] + + self.assertTrue(name['required']) + self.assertEqual(name['label'], 'Name') + + loc = actions['default_location'] + self.assertEqual(loc['api_url'], reverse('api-location-list')) def test_bom_item(self): - pass + """ + Test the BomItem API OPTIONS endpoint + """ + + actions = self.getActions(reverse('api-bom-list'))['POST'] + + inherited = actions['inherited'] + + self.assertEqual(inherited['type'], 'boolean') + + # 'part' reference + part = actions['part'] + + self.assertTrue(part['required']) + self.assertFalse(part['read_only']) + self.assertTrue(part['filters']['assembly']) + + # 'sub_part' reference + sub_part = actions['sub_part'] + + self.assertTrue(sub_part['required']) + self.assertEqual(sub_part['type'], 'related field') + self.assertTrue(sub_part['filters']['component']) class PartAPITest(InvenTreeAPITestCase): From 981cc2e24e44abd2c7e48512f62936924b98cc81 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 19:51:31 +1000 Subject: [PATCH 078/178] Fix select2 styling --- InvenTree/InvenTree/static/css/inventree.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 70f4032afe..375e02c8ca 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -979,4 +979,8 @@ input[type="date"].form-control, input[type="time"].form-control, input[type="da max-height: 24px; border-radius: 4px; margin-right: 10px; +} + +.select2-selection { + overflow-y: clip; } \ No newline at end of file From 5230a5a41b5b23306759d7624b4d30b7ce1cd0e9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 19:55:32 +1000 Subject: [PATCH 079/178] Add "success" functionality for form posting --- InvenTree/templates/js/forms.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index dd72031149..8c74def263 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -490,11 +490,21 @@ function handleFormSuccess(response, options) { $(options.modal).modal('hide'); } - if (response.url) { - // GOTO - window.location.href = response.url; + if (options.onSuccess) { + // Callback function + options.onSuccess(response, options); } + if (options.follow && response.url) { + // Follow the returned URL + window.location.href = response.url; + } else if (options.reload) { + // Reload the current page + location.reload(); + } else if (options.redirect) { + // Redirect to a specified URL + window.location.href = options.redirect; + } } From cf0feffe26e77d48580e283e2d2e7171c6d6471d Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 20:44:44 +1000 Subject: [PATCH 080/178] Allow override of values from calling function --- InvenTree/company/serializers.py | 1 + .../company/templates/company/index.html | 86 ++++++++----------- InvenTree/company/test_views.py | 15 ---- InvenTree/company/views.py | 4 - InvenTree/templates/js/company.js | 70 +++++++++++++++ InvenTree/templates/js/forms.js | 21 ++++- 6 files changed, 125 insertions(+), 72 deletions(-) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 1e97756987..3dd4e9334e 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -70,6 +70,7 @@ class CompanySerializer(InvenTreeModelSerializer): 'phone', 'address', 'email', + 'currency', 'contact', 'link', 'image', diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 796796528f..025251c883 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -16,10 +16,7 @@ {% if pagetype == 'manufacturers' and roles.purchase_order.add or pagetype == 'suppliers' and roles.purchase_order.add or pagetype == 'customers' and roles.sales_order.add %}
                  -
                  @@ -32,60 +29,49 @@ {% endblock %} {% block js_ready %} {{ block.super }} - $('#new-company').click(function () { - launchModalForm("{{ create_url }}", { - follow: true - }); - }); - $('#new-company-2').click(function() { + $('#new-company').click(function() { + + createCompany({ + fields: { + is_supplier: { + value: {% if pagetype == 'suppliers' %}true{% else %}false{% endif %}, + }, + is_manufacturer: { + value: {% if pagetype == 'manufacturers' %}true{% else %}false{% endif %}, + }, + is_customer: { + value: {% if pagetype == 'customers' %}true{% else %}false{% endif %}, + }, + } + }); + + return; + constructForm( - '{% url "api-build-list" %}', + '{% url "api-company-list" %}', { method: 'POST', - title: '{% trans "Edit Part Details" %}', + title: '{% trans "Create new Company" %}', + follow: true, fields: { - title: { - prefix: `` - }, - reference: {}, - part: { - filters: { - } - }, - quantity: {}, - /* - name: { - onEdit: function() { - console.log('Edited name field'); - } - }, + name: {}, description: {}, - category: { - filters: { - parent: 1, - }, - secondary: { - label: '{% trans "New Category" %}', - }, + website: {}, + address: {}, + currency: {}, + phone: {}, + email: {}, + contact: {}, + is_supplier: { + value: {% if pagetype == 'suppliers' %}true{% else %}false{% endif %}, }, - active: { - onEdit: function() { - console.log('edited active field'); - } + is_manufacturer: { + value: {% if pagetype == 'manufacturers' %}true{% else %}false{% endif %}, + }, + is_customer: { + value: {% if pagetype == 'customers' %}true{% else %}false{% endif %}, }, - purchaseable: {}, - salable: {}, - component: {}, - 'website', - 'address', - 'phone', - 'email', - 'contact', - 'is_supplier', - 'is_manufacturer', - 'is_customer', - */ } } ); diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index cdb2d32af9..a2ef75c688 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -188,21 +188,6 @@ class CompanyViewTest(CompanyViewTestBase): response = self.client.get(reverse('company-index')) self.assertEqual(response.status_code, 200) - def test_company_create(self): - """ - Test the view for creating a company - """ - - # Check that different company types return different form titles - response = self.client.get(reverse('supplier-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertContains(response, 'Create new Supplier') - - response = self.client.get(reverse('manufacturer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertContains(response, 'Create new Manufacturer') - - response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertContains(response, 'Create new Customer') - class ManufacturerPartViewTests(CompanyViewTestBase): """ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 74a583710a..a5cdcd00a0 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -63,21 +63,18 @@ class CompanyIndex(InvenTreeRoleMixin, ListView): 'title': _('Suppliers'), 'button_text': _('New Supplier'), 'filters': {'is_supplier': 'true'}, - 'create_url': reverse('supplier-create'), 'pagetype': 'suppliers', }, reverse('manufacturer-index'): { 'title': _('Manufacturers'), 'button_text': _('New Manufacturer'), 'filters': {'is_manufacturer': 'true'}, - 'create_url': reverse('manufacturer-create'), 'pagetype': 'manufacturers', }, reverse('customer-index'): { 'title': _('Customers'), 'button_text': _('New Customer'), 'filters': {'is_customer': 'true'}, - 'create_url': reverse('customer-create'), 'pagetype': 'customers', } } @@ -86,7 +83,6 @@ class CompanyIndex(InvenTreeRoleMixin, ListView): 'title': _('Companies'), 'button_text': _('New Company'), 'filters': {}, - 'create_url': reverse('company-create'), 'pagetype': 'companies' } diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 078b40f4b9..bd9c1cf458 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -1,5 +1,75 @@ {% load i18n %} +/* + * Launches a form to create a new company. + * As this can be called from many different contexts, + * we abstract it here! + */ +function createCompany(options={}) { + + // Default field set + var fields = { + name: {}, + description: {}, + website: {}, + address: {}, + currency: {}, + phone: {}, + email: {}, + contact: {}, + is_supplier: {}, + is_manufacturer: {}, + is_customer: {} + }; + + // Override / update default fields as required + fields = Object.assign(fields, options.fields || {}); + + constructForm( + '{% url "api-company-list" %}', + { + method: 'POST', + fields: fields, + follow: true, + title: '{% trans "Add new Company" %}', + } + ); +} + + +// Launch form to create a new manufacturer part +function createManufacturerPart(options={}) { + + var fields = { + 'part': { + secondary: { + label: '{% trans "New Part" %}', + } + }, + 'manufacturer': { + secondary: { + label: '{% trans "New Manufacturer" %}', + } + }, + 'MPN': {}, + 'description': {}, + 'link': {}, + }; + + fields = Object.assign(fields, options.fields || {}); + + constructForm( + '{% url "api-manufacturer-part-list" %}', + { + fields: fields, + method: 'POST', + follow: true, + title: '{% trans "Add new Manufacturer Part" %}', + } + ); +} + + function loadCompanyTable(table, url, options={}) { /* * Load company listing data into specified table. diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 8c74def263..e2ccb096b3 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -109,14 +109,24 @@ function getApiEndpointOptions(url, callback, options) { * - */ function constructCreateForm(fields, options) { - + // Check if default values were provided for any fields for (const name in fields) { var field = fields[name]; - if (field.default != null) { - field.value = field.default; + var field_options = options.fields[name] || {}; + + // If a 'value' is not provided for the field, + if (field.value == null) { + + if ('value' in field_options) { + // Client has specified the default value for the field + field.value = field_options.value; + } else if (field.default != null) { + // OPTIONS endpoint provided default value for this field + field.value = field.default; + } } } @@ -278,6 +288,11 @@ function constructFormBody(fields, options) { // Field prefix fields[field].prefix = field_options.prefix; + + // // Field value? + // if (fields[field].value == null) { + // fields[field].value = field_options.value; + // } } } From c25967eff678b1082b27883e2e3e6c5fde96eb77 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 21:17:48 +1000 Subject: [PATCH 081/178] Replace CompanyCreate and CompanyEdit forms with AJAX form - Adds the ability to specify an "icon" for each field --- .../templates/company/company_base.html | 6 +- .../company/templates/company/index.html | 49 +++----------- InvenTree/company/views.py | 20 ++---- InvenTree/templates/js/company.js | 64 ++++++++++++++----- InvenTree/templates/js/forms.js | 7 +- 5 files changed, 67 insertions(+), 79 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index a276f5df4f..197ddbf75e 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -111,11 +111,7 @@ }); $('#company-edit').click(function() { - launchModalForm( - "{% url 'company-edit' company.id %}", - { - reload: true - }); + editCompany({{ company.id }}); }); $("#company-order-2").click(function() { diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html index 025251c883..672d82fdb7 100644 --- a/InvenTree/company/templates/company/index.html +++ b/InvenTree/company/templates/company/index.html @@ -32,49 +32,16 @@ $('#new-company').click(function() { + var fields = companyFormFields(); + + // Field overrides + fields.is_supplier.value = {% if pagetype == 'suppliers' %}true{% else %}false{% endif %}; + fields.is_manufacturer.value = {% if pagetype == 'manufacturers' %}true{% else %}false{% endif %}; + fields.is_customer.value = {% if pagetype == 'customers' %}true{% else %}false{% endif %}; + createCompany({ - fields: { - is_supplier: { - value: {% if pagetype == 'suppliers' %}true{% else %}false{% endif %}, - }, - is_manufacturer: { - value: {% if pagetype == 'manufacturers' %}true{% else %}false{% endif %}, - }, - is_customer: { - value: {% if pagetype == 'customers' %}true{% else %}false{% endif %}, - }, - } + fields: fields, }); - - return; - - constructForm( - '{% url "api-company-list" %}', - { - method: 'POST', - title: '{% trans "Create new Company" %}', - follow: true, - fields: { - name: {}, - description: {}, - website: {}, - address: {}, - currency: {}, - phone: {}, - email: {}, - contact: {}, - is_supplier: { - value: {% if pagetype == 'suppliers' %}true{% else %}false{% endif %}, - }, - is_manufacturer: { - value: {% if pagetype == 'manufacturers' %}true{% else %}false{% endif %}, - }, - is_customer: { - value: {% if pagetype == 'customers' %}true{% else %}false{% endif %}, - }, - } - } - ); }); loadCompanyTable("#company-table", "{% url 'api-company-list' %}", diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index a5cdcd00a0..c42e3e2465 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -246,23 +246,11 @@ class CompanyImage(AjaxUpdateView): } -class CompanyEdit(AjaxUpdateView): - """ View for editing a Company object """ - model = Company - form_class = EditCompanyForm - context_object_name = 'company' - ajax_template_name = 'modal_form.html' - ajax_form_title = _('Edit Company') - permission_required = 'company.change_company' - - def get_data(self): - return { - 'info': _('Edited company information'), - } - - class CompanyCreate(AjaxCreateView): - """ View for creating a new Company object """ + """ + View for creating a new Company object + """ + model = Company context_object_name = 'company' form_class = EditCompanyForm diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index bd9c1cf458..b069901ce1 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -1,5 +1,52 @@ {% load i18n %} + +// Returns a default form-set for creating / editing a Company object +function companyFormFields(options={}) { + + return { + name: {}, + description: {}, + website: { + icon: 'fa-globe', + }, + address: { + icon: 'fa-envelope', + }, + currency: { + icon: 'fa-dollar-sign', + }, + phone: { + icon: 'fa-phone', + }, + email: { + icon: 'fa-at', + }, + contact: { + icon: 'fa-address-card', + }, + is_supplier: {}, + is_manufacturer: {}, + is_customer: {} + }; +} + + +function editCompany(pk, options={}) { + + var fields = options.fields || companyFormFields(); + + constructForm( + `/api/company/${pk}/`, + { + method: 'PATCH', + fields: fields, + reload: true, + title: '{% trans "Edit Company" %}', + } + ); +}; + /* * Launches a form to create a new company. * As this can be called from many different contexts, @@ -8,22 +55,7 @@ function createCompany(options={}) { // Default field set - var fields = { - name: {}, - description: {}, - website: {}, - address: {}, - currency: {}, - phone: {}, - email: {}, - contact: {}, - is_supplier: {}, - is_manufacturer: {}, - is_customer: {} - }; - - // Override / update default fields as required - fields = Object.assign(fields, options.fields || {}); + var fields = options.fields || companyFormFields(); constructForm( '{% url "api-company-list" %}', diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index e2ccb096b3..e96f9dee2a 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -287,7 +287,12 @@ function constructFormBody(fields, options) { fields[field].onEdit = field_options.onEdit; // Field prefix - fields[field].prefix = field_options.prefix; + if (field_options.prefix) { + fields[field].prefix = field_options.prefix; + } else if (field_options.icon) { + // Specify icon like 'fa-user' + fields[field].prefix = ``; + } // // Field value? // if (fields[field].value == null) { From 170ed37d033bb72b71b045f8e43334a5cd3a7ba1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 21:20:53 +1000 Subject: [PATCH 082/178] Delete CompanyCreate AJAX view --- .../company/detail_manufacturer_part.html | 2 +- .../company/detail_supplier_part.html | 2 +- .../company/manufacturer_part_suppliers.html | 2 +- InvenTree/company/urls.py | 6 -- InvenTree/company/views.py | 55 ------------------- .../templates/order/purchase_orders.html | 2 +- .../order/templates/order/sales_orders.html | 2 +- .../part/templates/part/manufacturer.html | 2 +- InvenTree/part/templates/part/supplier.html | 4 +- 9 files changed, 8 insertions(+), 69 deletions(-) diff --git a/InvenTree/company/templates/company/detail_manufacturer_part.html b/InvenTree/company/templates/company/detail_manufacturer_part.html index 902d456eaf..9916d597ed 100644 --- a/InvenTree/company/templates/company/detail_manufacturer_part.html +++ b/InvenTree/company/templates/company/detail_manufacturer_part.html @@ -71,7 +71,7 @@ field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new Manufacturer" %}', - url: "{% url 'manufacturer-create' %}", + //url: "{% url 'manufacturer-create' %}", }, ] }); diff --git a/InvenTree/company/templates/company/detail_supplier_part.html b/InvenTree/company/templates/company/detail_supplier_part.html index bf92f96843..d12e53aeca 100644 --- a/InvenTree/company/templates/company/detail_supplier_part.html +++ b/InvenTree/company/templates/company/detail_supplier_part.html @@ -71,7 +71,7 @@ field: 'supplier', label: "{% trans 'New Supplier' %}", title: "{% trans 'Create new Supplier' %}", - url: "{% url 'supplier-create' %}", + // url: "{% url 'supplier-create' %}", }, ] }); diff --git a/InvenTree/company/templates/company/manufacturer_part_suppliers.html b/InvenTree/company/templates/company/manufacturer_part_suppliers.html index 9f445ec215..440d43d646 100644 --- a/InvenTree/company/templates/company/manufacturer_part_suppliers.html +++ b/InvenTree/company/templates/company/manufacturer_part_suppliers.html @@ -81,7 +81,7 @@ $('#supplier-create').click(function () { field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new supplier" %}', - url: "{% url 'supplier-create' %}" + // url: "{% url 'supplier-create' %}" }, ] }); diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 51aa81f1c7..0aa4d39364 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -8,7 +8,6 @@ from . import views company_detail_urls = [ - url(r'edit/?', views.CompanyEdit.as_view(), name='company-edit'), url(r'delete/?', views.CompanyDelete.as_view(), name='company-delete'), # url(r'orders/?', views.CompanyDetail.as_view(template_name='company/orders.html'), name='company-detail-orders'), @@ -31,11 +30,6 @@ company_detail_urls = [ company_urls = [ - url(r'new/supplier/', views.CompanyCreate.as_view(), name='supplier-create'), - url(r'new/manufacturer/', views.CompanyCreate.as_view(), name='manufacturer-create'), - url(r'new/customer/', views.CompanyCreate.as_view(), name='customer-create'), - url(r'new/?', views.CompanyCreate.as_view(), name='company-create'), - url(r'^(?P\d+)/', include(company_detail_urls)), url(r'suppliers/', views.CompanyIndex.as_view(), name='supplier-index'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index c42e3e2465..7d8440f9de 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -246,61 +246,6 @@ class CompanyImage(AjaxUpdateView): } -class CompanyCreate(AjaxCreateView): - """ - View for creating a new Company object - """ - - model = Company - context_object_name = 'company' - form_class = EditCompanyForm - ajax_template_name = 'modal_form.html' - permission_required = 'company.add_company' - - def get_form_title(self): - - url = self.request.path - - if url == reverse('supplier-create'): - return _("Create new Supplier") - - if url == reverse('manufacturer-create'): - return _('Create new Manufacturer') - - if url == reverse('customer-create'): - return _('Create new Customer') - - return _('Create new Company') - - def get_initial(self): - """ Initial values for the form data """ - initials = super().get_initial().copy() - - url = self.request.path - - if url == reverse('supplier-create'): - initials['is_supplier'] = True - initials['is_customer'] = False - initials['is_manufacturer'] = False - - elif url == reverse('manufacturer-create'): - initials['is_manufacturer'] = True - initials['is_supplier'] = True - initials['is_customer'] = False - - elif url == reverse('customer-create'): - initials['is_customer'] = True - initials['is_manufacturer'] = False - initials['is_supplier'] = False - - return initials - - def get_data(self): - return { - 'success': _("Created new company"), - } - - class CompanyDelete(AjaxDeleteView): """ View for deleting a Company object """ diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index a2a5d5d0fa..3eec3d3a2f 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -184,7 +184,7 @@ $("#po-create").click(function() { field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new Supplier" %}', - url: '{% url "supplier-create" %}', + // url: '{% url "supplier-create" %}', } ] } diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index 448ab4b095..2cec25b3d2 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -186,7 +186,7 @@ $("#so-create").click(function() { field: 'customer', label: '{% trans "New Customer" %}', title: '{% trans "Create new Customer" %}', - url: '{% url "customer-create" %}', + // url: '{% url "customer-create" %}', } ] } diff --git a/InvenTree/part/templates/part/manufacturer.html b/InvenTree/part/templates/part/manufacturer.html index 82f02ba85f..4460ad9fe2 100644 --- a/InvenTree/part/templates/part/manufacturer.html +++ b/InvenTree/part/templates/part/manufacturer.html @@ -51,7 +51,7 @@ field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new manufacturer" %}', - url: "{% url 'manufacturer-create' %}", + // url: "{% url 'manufacturer-create' %}", } ] }); diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 45d2d1d55c..7959bb79bb 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -49,13 +49,13 @@ field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new supplier" %}', - url: "{% url 'supplier-create' %}" + // url: "{% url 'supplier-create' %}" }, { field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new manufacturer" %}', - url: "{% url 'manufacturer-create' %}", + // url: "{% url 'manufacturer-create' %}", } ] }); From 6156fffd1def322557e125f2371eebacc7eb00e0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 21:25:20 +1000 Subject: [PATCH 083/178] Remove broken URLs --- .../company/templates/company/detail_manufacturer_part.html | 1 - InvenTree/company/templates/company/detail_supplier_part.html | 1 - .../company/templates/company/manufacturer_part_suppliers.html | 1 - InvenTree/order/templates/order/purchase_orders.html | 1 - InvenTree/order/templates/order/sales_orders.html | 1 - InvenTree/part/templates/part/manufacturer.html | 1 - InvenTree/part/templates/part/supplier.html | 2 -- 7 files changed, 8 deletions(-) diff --git a/InvenTree/company/templates/company/detail_manufacturer_part.html b/InvenTree/company/templates/company/detail_manufacturer_part.html index 9916d597ed..a162334040 100644 --- a/InvenTree/company/templates/company/detail_manufacturer_part.html +++ b/InvenTree/company/templates/company/detail_manufacturer_part.html @@ -71,7 +71,6 @@ field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new Manufacturer" %}', - //url: "{% url 'manufacturer-create' %}", }, ] }); diff --git a/InvenTree/company/templates/company/detail_supplier_part.html b/InvenTree/company/templates/company/detail_supplier_part.html index d12e53aeca..1cb9a2ad38 100644 --- a/InvenTree/company/templates/company/detail_supplier_part.html +++ b/InvenTree/company/templates/company/detail_supplier_part.html @@ -71,7 +71,6 @@ field: 'supplier', label: "{% trans 'New Supplier' %}", title: "{% trans 'Create new Supplier' %}", - // url: "{% url 'supplier-create' %}", }, ] }); diff --git a/InvenTree/company/templates/company/manufacturer_part_suppliers.html b/InvenTree/company/templates/company/manufacturer_part_suppliers.html index 440d43d646..59969d9708 100644 --- a/InvenTree/company/templates/company/manufacturer_part_suppliers.html +++ b/InvenTree/company/templates/company/manufacturer_part_suppliers.html @@ -81,7 +81,6 @@ $('#supplier-create').click(function () { field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new supplier" %}', - // url: "{% url 'supplier-create' %}" }, ] }); diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index 3eec3d3a2f..15d58788bc 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -184,7 +184,6 @@ $("#po-create").click(function() { field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new Supplier" %}', - // url: '{% url "supplier-create" %}', } ] } diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index 2cec25b3d2..bfa6d85a9d 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -186,7 +186,6 @@ $("#so-create").click(function() { field: 'customer', label: '{% trans "New Customer" %}', title: '{% trans "Create new Customer" %}', - // url: '{% url "customer-create" %}', } ] } diff --git a/InvenTree/part/templates/part/manufacturer.html b/InvenTree/part/templates/part/manufacturer.html index 4460ad9fe2..dc04efc503 100644 --- a/InvenTree/part/templates/part/manufacturer.html +++ b/InvenTree/part/templates/part/manufacturer.html @@ -51,7 +51,6 @@ field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new manufacturer" %}', - // url: "{% url 'manufacturer-create' %}", } ] }); diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 7959bb79bb..c0486cc42a 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -49,13 +49,11 @@ field: 'supplier', label: '{% trans "New Supplier" %}', title: '{% trans "Create new supplier" %}', - // url: "{% url 'supplier-create' %}" }, { field: 'manufacturer', label: '{% trans "New Manufacturer" %}', title: '{% trans "Create new manufacturer" %}', - // url: "{% url 'manufacturer-create' %}", } ] }); From 8b3a49755419e25c10c31e1c03a914bfdddd26f0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 23:05:37 +1000 Subject: [PATCH 084/178] Remove unused Form --- InvenTree/company/forms.py | 36 -------------------------------- InvenTree/company/serializers.py | 2 ++ InvenTree/company/views.py | 2 +- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 80673b4fa4..9da15a63e1 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -22,42 +22,6 @@ from .models import SupplierPart from .models import SupplierPriceBreak -class EditCompanyForm(HelperForm): - """ Form for editing a Company object """ - - field_prefix = { - 'website': 'fa-globe-asia', - 'email': 'fa-at', - 'address': 'fa-envelope', - 'contact': 'fa-user-tie', - 'phone': 'fa-phone', - } - - currency = django.forms.ChoiceField( - required=False, - label=_('Currency'), - help_text=_('Default currency used for this company'), - choices=[('', '----------')] + djmoney.settings.CURRENCY_CHOICES, - initial=currency_code_default, - ) - - class Meta: - model = Company - fields = [ - 'name', - 'description', - 'website', - 'address', - 'currency', - 'phone', - 'email', - 'contact', - 'is_supplier', - 'is_manufacturer', - 'is_customer', - ] - - class CompanyImageForm(HelperForm): """ Form for uploading a Company image """ diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 3dd4e9334e..dd3175239b 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -6,6 +6,8 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount +import djmoney.settings + from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeImageSerializerField diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 7d8440f9de..5a887d9d38 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -30,7 +30,7 @@ from .models import SupplierPriceBreak from part.models import Part -from .forms import EditCompanyForm, EditManufacturerPartParameterForm +from .forms import EditManufacturerPartParameterForm from .forms import CompanyImageForm from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm From 33ec91acc7e1b4a757c1da9c19b2d448686e1a6b Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 29 Jun 2021 23:14:39 +1000 Subject: [PATCH 085/178] Add "default" from serializer field (if present) - Overrides model default - Set choices for currency serializer field - Adds some unit testing --- InvenTree/InvenTree/metadata.py | 5 +++ InvenTree/company/forms.py | 1 - InvenTree/company/models.py | 6 ++- InvenTree/company/serializers.py | 7 ++++ InvenTree/company/test_api.py | 69 +++++++++++++++++++++++++++++++- 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index fa3dee4794..24e4dab7d7 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -7,6 +7,7 @@ import logging from rest_framework import serializers from rest_framework.metadata import SimpleMetadata from rest_framework.utils import model_meta +from rest_framework.fields import empty import users.models @@ -146,6 +147,10 @@ class InvenTreeMetadata(SimpleMetadata): # Run super method first field_info = super().get_field_info(field) + # If a default value is specified for the serializer field, add it! + if 'default' not in field_info and not field.default == empty: + field_info['default'] = field.get_default() + # Introspect writable related fields if field_info['type'] == 'field' and not field_info['read_only']: diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 9da15a63e1..68e32628b4 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -11,7 +11,6 @@ from InvenTree.fields import RoundingDecimalFormField from django.utils.translation import ugettext_lazy as _ import django.forms -import djmoney.settings from djmoney.forms.fields import MoneyField from common.settings import currency_code_default diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 1c2306fc6a..58cfbe7011 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -105,7 +105,11 @@ class Company(models.Model): blank=True, ) - website = models.URLField(blank=True, verbose_name=_('Website'), help_text=_('Company website URL')) + website = models.URLField( + blank=True, + verbose_name=_('Website'), + help_text=_('Company website URL') + ) address = models.CharField(max_length=200, verbose_name=_('Address'), diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index dd3175239b..9e64fbad0a 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -17,6 +17,8 @@ from .models import Company from .models import ManufacturerPart, ManufacturerPartParameter from .models import SupplierPart, SupplierPriceBreak +from common.settings import currency_code_default + class CompanyBriefSerializer(InvenTreeModelSerializer): """ Serializer for Company object (limited detail) """ @@ -60,6 +62,11 @@ class CompanySerializer(InvenTreeModelSerializer): parts_supplied = serializers.IntegerField(read_only=True) parts_manufactured = serializers.IntegerField(read_only=True) + currency = serializers.ChoiceField( + choices=djmoney.settings.CURRENCY_CHOICES, + default=currency_code_default, + ) + class Meta: model = Company fields = [ diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index dd42b97801..345f4bf1a4 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -62,12 +62,79 @@ class CompanyTest(InvenTreeAPITestCase): self.assertEqual(response.data['name'], 'ACMOO') def test_company_search(self): - # Test search functionality in company list + """ + Test search functionality in company list + """ + url = reverse('api-company-list') data = {'search': 'cup'} response = self.get(url, data) self.assertEqual(len(response.data), 2) + def test_company_create(self): + """ + Test that we can create a company via the API! + """ + + url = reverse('api-company-list') + + # Name is required + response = self.post( + url, + { + 'description': 'A description!', + }, + expected_code=400 + ) + + # Minimal example, checking default values + response = self.post( + url, + { + 'name': 'My API Company', + 'description': 'A company created via the API', + }, + expected_code=201 + ) + + self.assertTrue(response.data['is_supplier']) + self.assertFalse(response.data['is_customer']) + self.assertFalse(response.data['is_manufacturer']) + + self.assertEqual(response.data['currency'], 'USD') + + # Maximal example, specify values + response = self.post( + url, + { + 'name': "Another Company", + 'description': "Also created via the API!", + 'currency': 'AUD', + 'is_supplier': False, + 'is_manufacturer': True, + 'is_customer': True, + }, + expected_code=201 + ) + + self.assertEqual(response.data['currency'], 'AUD') + self.assertFalse(response.data['is_supplier']) + self.assertTrue(response.data['is_customer']) + self.assertTrue(response.data['is_manufacturer']) + + # Attempt to create with invalid currency + response = self.post( + url, + { + 'name': "A name", + 'description': 'A description', + 'currency': 'POQD', + }, + expected_code=400 + ) + + self.assertTrue('currency' in response.data) + class ManufacturerTest(InvenTreeAPITestCase): """ From 293b5d4c071041dd849691fd2203e88af548584f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 00:13:53 +1000 Subject: [PATCH 086/178] Allow file and image fields - Use FormData class - Replace existing Company image upload form --- .../templates/company/company_base.html | 18 +++++- InvenTree/company/urls.py | 1 - InvenTree/company/views.py | 14 ----- InvenTree/templates/js/company.js | 33 ----------- InvenTree/templates/js/forms.js | 58 ++++++++++++++++--- 5 files changed, 65 insertions(+), 59 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 197ddbf75e..52d5abf3e6 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -157,10 +157,22 @@ {% endif %} $("#company-image-upload").click(function() { - launchModalForm( - "{% url 'company-image' company.id %}", + + constructForm( + '{% url "api-company-detail" company.pk %}', { - reload: true + method: 'PATCH', + fields: { + image: {}, + }, + title: '{% trans "Upload Image" %}', + onSuccess: function(data) { + if (data.image) { + $('#company-image').attr('src', data.image); + } else { + location.reload(); + } + } } ); }); diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 0aa4d39364..51685215b6 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -20,7 +20,6 @@ company_detail_urls = [ url(r'^sales-orders/', views.CompanyDetail.as_view(template_name='company/sales_orders.html'), name='company-detail-sales-orders'), url(r'^notes/', views.CompanyNotes.as_view(), name='company-notes'), - url(r'^thumbnail/', views.CompanyImage.as_view(), name='company-image'), url(r'^thumb-download/', views.CompanyImageDownloadFromURL.as_view(), name='company-image-download'), # Any other URL diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 5a887d9d38..8416ce2e3f 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -232,20 +232,6 @@ class CompanyImageDownloadFromURL(AjaxUpdateView): ) -class CompanyImage(AjaxUpdateView): - """ View for uploading an image for the Company """ - model = Company - ajax_template_name = 'modal_form.html' - ajax_form_title = _('Update Company Image') - form_class = CompanyImageForm - permission_required = 'company.change_company' - - def get_data(self): - return { - 'success': _('Updated company image'), - } - - class CompanyDelete(AjaxDeleteView): """ View for deleting a Company object """ diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index b069901ce1..96beb0a041 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -69,39 +69,6 @@ function createCompany(options={}) { } -// Launch form to create a new manufacturer part -function createManufacturerPart(options={}) { - - var fields = { - 'part': { - secondary: { - label: '{% trans "New Part" %}', - } - }, - 'manufacturer': { - secondary: { - label: '{% trans "New Manufacturer" %}', - } - }, - 'MPN': {}, - 'description': {}, - 'link': {}, - }; - - fields = Object.assign(fields, options.fields || {}); - - constructForm( - '{% url "api-manufacturer-part-list" %}', - { - fields: fields, - method: 'POST', - follow: true, - title: '{% trans "Add new Manufacturer Part" %}', - } - ); -} - - function loadCompanyTable(table, url, options={}) { /* * Load company listing data into specified table. diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index e96f9dee2a..535391d2e1 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -350,8 +350,8 @@ function constructFormBody(fields, options) { // Set the form title and button labels modalSetTitle(modal, options.title || '{% trans "Form Title" %}'); - modalSetSubmitText(options.submitText || '{% trans "Submit" %}'); - modalSetCloseText(options.cancelText || '{% trans "Cancel" %}'); + modalSetSubmitText(modal, options.submitText || '{% trans "Submit" %}'); + modalSetCloseText(modal, options.cancelText || '{% trans "Cancel" %}'); // Insert generated form content $(modal).find('.modal-form-content').html(html); @@ -387,8 +387,8 @@ function constructFormBody(fields, options) { */ function submitFormData(fields, options) { - // Data to be sent to the server - var data = {}; + // Form data to be uploaded to the server + var form_data = new FormData(); // Extract values for each field options.field_names.forEach(function(name) { @@ -399,16 +399,32 @@ function submitFormData(fields, options) { var value = getFormFieldValue(name, field, options); - data[name] = value; + // Handle file inputs + if (field.type == 'image upload' || field.type == 'file upload') { + + var field_el = $(options.modal).find(`#id_${name}`)[0]; + + var field_files = field_el.files; + + if (field_files.length > 0) { + // One file per field, please! + var file = field_files[0]; + + form_data.append(name, file); + } + } else { + // Normal field (not a file or image) + form_data.append(name, value); + } } else { console.log(`WARNING: Could not find field matching '${name}'`); } }); // Submit data - inventreePut( + inventreeFormDataUpload( options.url, - data, + form_data, { method: options.method, success: function(response, status) { @@ -464,6 +480,9 @@ function updateFieldValues(fields, options) { case 'related field': // TODO? break; + case 'file upload': + case 'image upload': + break; default: el.val(value); break; @@ -1017,6 +1036,10 @@ function constructInput(name, parameters, options) { case 'related field': func = constructRelatedFieldInput; break; + case 'image upload': + case 'file upload': + func = constructFileUploadInput; + break; default: // Unsupported field type! break; @@ -1101,7 +1124,6 @@ function constructCheckboxInput(name, parameters, options) { 'checkbox', parameters ); - } @@ -1193,6 +1215,26 @@ function constructRelatedFieldInput(name, parameters, options) { } +/* + * Construct a field for file upload + */ +function constructFileUploadInput(name, parameters, options) { + + var cls = 'clearablefileinput'; + + if (parameters.required) { + cls = 'fileinput'; + } + + return constructInputOptions( + name, + cls, + 'file', + parameters + ); +} + + /* * Construct a 'help text' div based on the field parameters * From 26eafe242c18eca230cf4d2d5a3e9a54610f9fe9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 00:18:25 +1000 Subject: [PATCH 087/178] Replace PartImageUpload form --- .../templates/company/company_base.html | 21 ++++++------ InvenTree/part/templates/part/part_base.html | 34 +++++++++++++------ InvenTree/part/urls.py | 1 - InvenTree/part/views.py | 15 -------- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 52d5abf3e6..88b9f2958d 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -133,6 +133,14 @@ }); }); + function reloadImage(data) { + if (data.image) { + $('#company-image').attr('src', data.image); + } else { + location.reload(); + } + } + enableDragAndDrop( "#company-thumb", "{% url 'api-company-detail' company.id %}", @@ -140,12 +148,7 @@ label: 'image', method: 'PATCH', success: function(data, status, xhr) { - - if (data.image) { - $('#company-image').attr('src', data.image); - } else { - location.reload(); - } + reloadImage(data); } } ); @@ -167,11 +170,7 @@ }, title: '{% trans "Upload Image" %}', onSuccess: function(data) { - if (data.image) { - $('#company-image').attr('src', data.image); - } else { - location.reload(); - } + reloadImage(data); } } ); diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index b486e2c589..922ff01912 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -237,6 +237,16 @@ }); {% endif %} + function reloadImage(data) { + // If image / thumbnail data present, live update + if (data.image) { + $('#part-image').attr('src', data.image); + } else { + // Otherwise, reload the page + location.reload(); + } + } + enableDragAndDrop( '#part-thumb', "{% url 'api-part-detail' part.id %}", @@ -244,14 +254,7 @@ label: 'image', method: 'PATCH', success: function(data, status, xhr) { - - // If image / thumbnail data present, live update - if (data.image) { - $('#part-image').attr('src', data.image); - } else { - // Otherwise, reload the page - location.reload(); - } + reloadImage(data); } } ); @@ -293,11 +296,20 @@ }); $("#part-image-upload").click(function() { - launchModalForm("{% url 'part-image-upload' part.id %}", + + constructForm( + '{% url "api-part-detail" part.pk %}', { - reload: true + method: 'PATCH', + fields: { + image: {}, + }, + title: '{% trans "Upload Image" %}', + onSuccess: function(data) { + reloadImage(data); + } } - ); + ) }); diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index c35786e5d3..fb4a1ab9eb 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -81,7 +81,6 @@ part_detail_urls = [ url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'), # Normal thumbnail with form - url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'), url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'), url(r'^thumb-download/', views.PartImageDownloadFromURL.as_view(), name='part-image-download'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2f129dd30b..9998a12783 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1186,21 +1186,6 @@ class PartImageDownloadFromURL(AjaxUpdateView): ) -class PartImageUpload(AjaxUpdateView): - """ View for uploading a new Part image """ - - model = Part - ajax_template_name = 'modal_form.html' - ajax_form_title = _('Upload Part Image') - - form_class = part_forms.PartImageForm - - def get_data(self): - return { - 'success': _('Updated part image'), - } - - class PartImageSelect(AjaxUpdateView): """ View for selecting Part image from existing images. """ From c425f36a355e336110fd3e1ea12b3675574f5ee4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 00:24:27 +1000 Subject: [PATCH 088/178] Remove dead class --- InvenTree/company/forms.py | 10 ---------- InvenTree/company/views.py | 1 - 2 files changed, 11 deletions(-) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 68e32628b4..3b54f3dc61 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -21,16 +21,6 @@ from .models import SupplierPart from .models import SupplierPriceBreak -class CompanyImageForm(HelperForm): - """ Form for uploading a Company image """ - - class Meta: - model = Company - fields = [ - 'image' - ] - - class CompanyImageDownloadForm(HelperForm): """ Form for downloading an image from a URL diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 8416ce2e3f..1b817fe66d 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -31,7 +31,6 @@ from .models import SupplierPriceBreak from part.models import Part from .forms import EditManufacturerPartParameterForm -from .forms import CompanyImageForm from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm from .forms import EditPriceBreakForm From 621f47e46c1e7ba14f44e0134881fbbd35e2a671 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 01:04:39 +1000 Subject: [PATCH 089/178] Replace "edit part category" form --- InvenTree/InvenTree/models.py | 11 +++++++++++ InvenTree/InvenTree/serializers.py | 13 +++++++++++++ InvenTree/part/api.py | 5 ++++- InvenTree/part/serializers.py | 1 + InvenTree/part/templates/part/category.html | 18 ++++++++++++++---- InvenTree/part/test_views.py | 5 ----- InvenTree/part/urls.py | 1 - InvenTree/templates/js/forms.js | 21 +++++++++++++++++++-- 8 files changed, 62 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 5822f8a19f..2831a23151 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -10,11 +10,13 @@ from django.db import models from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError from django.db.models.signals import pre_delete from django.dispatch import receiver from mptt.models import MPTTModel, TreeForeignKey +from mptt.exceptions import InvalidMove from .validators import validate_tree_name @@ -91,6 +93,15 @@ class InvenTreeTree(MPTTModel): parent: The item immediately above this one. An item with a null parent is a top-level item """ + def save(self, *args, **kwargs): + + try: + super().save(*args, **kwargs) + except InvalidMove: + raise ValidationError({ + 'parent': _("Invalid choice"), + }) + class Meta: abstract = True diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 772daa06ab..19c70fa29a 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -110,6 +110,19 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return initials + def save(self, **kwargs): + """ + Catch any django ValidationError thrown at the moment save() is called, + and re-throw as a DRF ValidationError + """ + + try: + super().save(**kwargs) + except (ValidationError, DjangoValidationError) as exc: + raise ValidationError(detail=serializers.as_serializer_error(exc)) + + return self.instance + def run_validation(self, data=empty): """ Perform serializer validation. diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 8ef6902600..2212743c2b 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -127,7 +127,10 @@ class CategoryList(generics.ListCreateAPIView): class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): - """ API endpoint for detail view of a single PartCategory object """ + """ + API endpoint for detail view of a single PartCategory object + """ + serializer_class = part_serializers.CategorySerializer queryset = PartCategory.objects.all() diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index ff178c5941..fb5480f668 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -39,6 +39,7 @@ class CategorySerializer(InvenTreeModelSerializer): 'name', 'description', 'default_location', + 'default_keywords', 'pathstring', 'url', 'parent', diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index ef250d4c89..125e089721 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -268,13 +268,23 @@ {% if category %} $("#cat-edit").click(function () { - launchModalForm( - "{% url 'category-edit' category.id %}", + + constructForm( + '{% url "api-part-category-detail" category.pk %}', { + fields: { + name: {}, + description: {}, + parent: {}, + default_location: {}, + default_keywords: { + icon: 'fa-key', + } + }, + title: '{% trans "Edit Part Category" %}', reload: true - }, + } ); - return false; }); {% if category.parent %} diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index c32753cbbb..231eed7896 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -294,11 +294,6 @@ class CategoryTest(PartViewTestCase): # Form should still return OK self.assertEqual(response.status_code, 200) - def test_edit(self): - """ Retrieve the part category editing form """ - response = self.client.get(reverse('category-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - def test_set_category(self): """ Test that the "SetCategory" view works """ diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index fb4a1ab9eb..80351b8dba 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -104,7 +104,6 @@ category_urls = [ # Category detail views url(r'(?P\d+)/', include([ - url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'), url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'), url(r'^parameters/', include(category_parameter_urls)), diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 535391d2e1..c472a83ffd 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -388,8 +388,13 @@ function constructFormBody(fields, options) { function submitFormData(fields, options) { // Form data to be uploaded to the server + // Only used if file / image upload is required var form_data = new FormData(); + var data = {}; + + var has_files = false; + // Extract values for each field options.field_names.forEach(function(name) { @@ -411,20 +416,31 @@ function submitFormData(fields, options) { var file = field_files[0]; form_data.append(name, file); + + has_files = true; } } else { // Normal field (not a file or image) form_data.append(name, value); + + data[name] = value; } } else { console.log(`WARNING: Could not find field matching '${name}'`); } }); + var upload_func = inventreePut; + + if (has_files) { + upload_func = inventreeFormDataUpload; + data = form_data; + } + // Submit data - inventreeFormDataUpload( + upload_func( options.url, - form_data, + data, { method: options.method, success: function(response, status) { @@ -708,6 +724,7 @@ function initializeRelatedField(name, field, options) { ajax: { url: field.api_url, dataType: 'json', + placeholder: '', allowClear: !field.required, dropdownParent: $(options.modal), dropdownAutoWidth: false, From 1f75530910d3ae336aaf1f18c364250553ad71fc Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 01:07:36 +1000 Subject: [PATCH 090/178] Specify custom help text for fields on the client side --- InvenTree/part/templates/part/category.html | 4 +++- InvenTree/templates/js/forms.js | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 125e089721..cf7348be76 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -275,7 +275,9 @@ fields: { name: {}, description: {}, - parent: {}, + parent: { + help_text: '{% trans "Select parent category" %}', + }, default_location: {}, default_keywords: { icon: 'fa-key', diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index c472a83ffd..fec168e923 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -286,6 +286,11 @@ function constructFormBody(fields, options) { // Edit callback fields[field].onEdit = field_options.onEdit; + // Custom help_text + if (field_options.help_text) { + fields[field].help_text = field_options.help_text; + } + // Field prefix if (field_options.prefix) { fields[field].prefix = field_options.prefix; From 43f26f2c659f30041465b102bca219d4a42d4fb9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 01:07:57 +1000 Subject: [PATCH 091/178] Allow custom labels --- InvenTree/templates/js/forms.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index fec168e923..54f0ae2f16 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -291,6 +291,11 @@ function constructFormBody(fields, options) { fields[field].help_text = field_options.help_text; } + // Custom label + if (field_options.label) { + fields[field].label = field_options.label; + } + // Field prefix if (field_options.prefix) { fields[field].prefix = field_options.prefix; From 7d53bcb27cada9cc9cd8d61fecb0c74e72b3735d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 08:52:53 +1000 Subject: [PATCH 092/178] Convert StockItemEditStatus to use API forms --- .../stock/templates/stock/item_base.html | 15 ++++++------- InvenTree/stock/urls.py | 1 - InvenTree/stock/views.py | 21 ------------------- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index bf9d10590f..b674c5c7fc 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -490,13 +490,14 @@ $("#stock-edit").click(function () { }); $('#stock-edit-status').click(function () { - launchModalForm( - "{% url 'stock-item-edit-status' item.id %}", - { - reload: true, - submit_text: '{% trans "Save" %}', - } - ); + + constructForm('{% url "api-stock-detail" item.pk %}', { + fields: { + status: {}, + }, + reload: true, + title: '{% trans "Edit Stock Status" %}', + }); }); {% endif %} diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index dbdbdda317..c0b6341744 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -24,7 +24,6 @@ location_urls = [ ] stock_item_detail_urls = [ - url(r'^edit_status/', views.StockItemEditStatus.as_view(), name='stock-item-edit-status'), url(r'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'), url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'), url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 9a47576442..bc3e8cb462 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1212,27 +1212,6 @@ class StockAdjust(AjaxView, FormMixin): return _("Deleted {n} stock items").format(n=count) -class StockItemEditStatus(AjaxUpdateView): - """ - View for editing stock item status field - """ - - model = StockItem - form_class = StockForms.EditStockItemStatusForm - ajax_form_title = _('Edit Stock Item Status') - - def save(self, object, form, **kwargs): - """ - Override the save method, to track the user who updated the model - """ - - item = form.save(commit=False) - - item.save(user=self.request.user) - - return item - - class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem From 87235b7e6f6679b611353f645fc63cc7dd5d0cd7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 09:17:28 +1000 Subject: [PATCH 093/178] Replace StockItemAttachmentCreate form - Also replace drag-and-drop - Add 'hidden' option for form fields - Adds renderer for StockItem model --- InvenTree/stock/serializers.py | 2 + .../templates/stock/item_attachments.html | 26 +++++++++-- InvenTree/stock/urls.py | 1 - InvenTree/stock/views.py | 46 ------------------- InvenTree/templates/js/forms.js | 24 ++++++++-- InvenTree/templates/js/model_renderers.js | 25 ++++++++-- 6 files changed, 66 insertions(+), 58 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index a0b7e3403a..38301bdd1f 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -288,6 +288,8 @@ class StockItemAttachmentSerializer(InvenTreeModelSerializer): attachment = InvenTreeAttachmentSerializerField(required=True) + # TODO: Record the uploading user when creating or updating an attachment! + class Meta: model = StockItemAttachment diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index a022403d02..abd89c57c3 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -21,10 +21,11 @@ enableDragAndDrop( '#attachment-dropzone', - "{% url 'stock-item-attachment-create' %}", + "{% url 'api-stock-attachment-list' %}", { data: { stock_item: {{ item.id }}, + user: {{ user.pk }}, }, label: 'attachment', success: function(data, status, xhr) { @@ -34,10 +35,27 @@ enableDragAndDrop( ); $("#new-attachment").click(function() { - launchModalForm("{% url 'stock-item-attachment-create' %}?item={{ item.id }}", + + constructForm( + '{% url "api-stock-attachment-list" %}', { - reload: true, - }); + method: 'POST', + fields: { + attachment: {}, + comment: {}, + stock_item: { + value: {{ item.pk }}, + hidden: true, + }, + user: { + value: {{ user.pk }}, + hidden: true, + } + }, + reload: true, + title: '{% trans "Add Attachment" %}', + } + ); }); $("#attachment-table").on('click', '.attachment-edit-button', function() { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index c0b6341744..01eb6f4704 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -64,7 +64,6 @@ stock_urls = [ # URLs for StockItem attachments url(r'^item/attachment/', include([ - url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'), url(r'^(?P\d+)/edit/', views.StockItemAttachmentEdit.as_view(), name='stock-item-attachment-edit'), url(r'^(?P\d+)/delete/', views.StockItemAttachmentDelete.as_view(), name='stock-item-attachment-delete'), ])), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index bc3e8cb462..3608d5d553 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -255,52 +255,6 @@ class StockLocationQRCode(QRCodeView): return None -class StockItemAttachmentCreate(AjaxCreateView): - """ - View for adding a new attachment for a StockItem - """ - - model = StockItemAttachment - form_class = StockForms.EditStockItemAttachmentForm - ajax_form_title = _("Add Stock Item Attachment") - ajax_template_name = "modal_form.html" - - def save(self, form, **kwargs): - """ Record the user that uploaded the attachment """ - - attachment = form.save(commit=False) - attachment.user = self.request.user - attachment.save() - - def get_data(self): - return { - 'success': _("Added attachment") - } - - def get_initial(self): - """ - Get initial data for the new StockItem attachment object. - - - Client must provide a valid StockItem ID - """ - - initials = super().get_initial() - - try: - initials['stock_item'] = StockItem.objects.get(id=self.request.GET.get('item', None)) - except (ValueError, StockItem.DoesNotExist): - pass - - return initials - - def get_form(self): - - form = super().get_form() - form.fields['stock_item'].widget = HiddenInput() - - return form - - class StockItemAttachmentEdit(AjaxUpdateView): """ View for editing a StockItemAttachment object. diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 54f0ae2f16..10d07d07e2 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -304,10 +304,7 @@ function constructFormBody(fields, options) { fields[field].prefix = ``; } - // // Field value? - // if (fields[field].value == null) { - // fields[field].value = field_options.value; - // } + fields[field].hidden = field_options.hidden; } } @@ -672,6 +669,8 @@ function initializeRelatedFields(fields, options) { if (!field || field.type != 'related field') continue; + if (field.hidden) continue; + if (!field.api_url) { // TODO: Provide manual api_url option? console.log(`Related field '${name}' missing 'api_url' parameter.`); @@ -962,6 +961,11 @@ function constructField(name, parameters, options) { var field_name = `id_${name}`; + // Hidden inputs are rendered without label / help text / etc + if (parameters.hidden) { + return constructHiddenInput(name, parameters, options); + } + var form_classes = 'form-group'; if (parameters.errors) { @@ -1142,6 +1146,18 @@ function constructInputOptions(name, classes, type, parameters) { } +// Construct a "hidden" input +function constructHiddenInput(name, parameters, options) { + + return constructInputOptions( + name, + 'hiddeninput', + 'hidden', + parameters + ); +} + + // Construct a "checkbox" input function constructCheckboxInput(name, parameters, options) { diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index 24e9c1d328..c9cf654d12 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -27,8 +27,27 @@ function renderCompany(name, data, parameters, options) { // Renderer for "StockItem" model function renderStockItem(name, data, parameters, options) { - // TODO - Include part detail, location, quantity - // TODO - Include part image + var image = data.part_detail.thumbnail || data.part_detail.image; + + if (!image) { + image = `/static/img/blank_image.png`; + } + + var html = ``; + + html += ` ${data.part_detail.full_name || data.part_detail.name}`; + + if (data.serial && data.quantity == 1) { + html += ` - {% trans "Serial Number" %}: ${data.serial}`; + } else { + html += ` - {% trans "Quantity" %}: ${data.quantity}`; + } + + if (data.part_detail.description) { + html += `

                  ${data.part_detail.description}

                  `; + } + + return html; } @@ -62,7 +81,7 @@ function renderPart(name, data, parameters, options) { var html = ``; - html += ` ${data.full_name ?? data.name}`; + html += ` ${data.full_name || data.name}`; if (data.description) { html += ` - ${data.description}`; From 54c9bd25a5de7e28cc635ecc9996b0a7282ec7a6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 09:40:54 +1000 Subject: [PATCH 094/178] Add detail endpoint for StockItemAttachment --- InvenTree/stock/api.py | 12 ++++++++++++ .../stock/templates/stock/item_attachments.html | 16 +++++++++++----- InvenTree/stock/urls.py | 1 - InvenTree/stock/views.py | 17 ----------------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index d1add67d9d..26dd97b848 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -2,6 +2,7 @@ JSON API for the Stock app """ +from django.db.models.query import QuerySet from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter @@ -931,6 +932,15 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] +class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): + """ + Detail endpoint for StockItemAttachment + """ + + queryset = StockItemAttachment.objects.all() + serializer_class = StockItemAttachmentSerializer + + class StockItemTestResultList(generics.ListCreateAPIView): """ API endpoint for listing (and creating) a StockItemTestResult object. @@ -1133,6 +1143,7 @@ stock_api_urls = [ url(r'location/', include(location_endpoints)), # These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019 + # TODO: Remove server-side forms for stock adjustment!!! url(r'count/?', StockCount.as_view(), name='api-stock-count'), url(r'add/?', StockAdd.as_view(), name='api-stock-add'), url(r'remove/?', StockRemove.as_view(), name='api-stock-remove'), @@ -1140,6 +1151,7 @@ stock_api_urls = [ # Base URL for StockItemAttachment API endpoints url(r'^attachment/', include([ + url(r'^(?P\d+)/', StockAttachmentDetail.as_view(), name='api-stock-attachment-detail'), url(r'^$', StockAttachmentList.as_view(), name='api-stock-attachment-list'), ])), diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index abd89c57c3..f5e9f8c4fc 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -61,12 +61,18 @@ $("#new-attachment").click(function() { $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - var url = `/stock/item/attachment/${button.attr('pk')}/edit/`; + var pk = button.attr('pk'); - launchModalForm(url, - { - reload: true, - }); + var url = `/api/stock/attachment/${pk}/`; + + constructForm(url, { + fields: { + attachment: {}, + comment: {}, + }, + title: '{% trans "Edit Attachment" %}', + reload: true + }); }); $("#attachment-table").on('click', '.attachment-delete-button', function() { diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 01eb6f4704..eb0acbbef2 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -64,7 +64,6 @@ stock_urls = [ # URLs for StockItem attachments url(r'^item/attachment/', include([ - url(r'^(?P\d+)/edit/', views.StockItemAttachmentEdit.as_view(), name='stock-item-attachment-edit'), url(r'^(?P\d+)/delete/', views.StockItemAttachmentDelete.as_view(), name='stock-item-attachment-delete'), ])), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 3608d5d553..5b41bfe1b2 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -255,23 +255,6 @@ class StockLocationQRCode(QRCodeView): return None -class StockItemAttachmentEdit(AjaxUpdateView): - """ - View for editing a StockItemAttachment object. - """ - - model = StockItemAttachment - form_class = StockForms.EditStockItemAttachmentForm - ajax_form_title = _("Edit Stock Item Attachment") - - def get_form(self): - - form = super().get_form() - form.fields['stock_item'].widget = HiddenInput() - - return form - - class StockItemAttachmentDelete(AjaxDeleteView): """ View for deleting a StockItemAttachment object. From 8c439e52fdea54f65f428ba9805c40fe34a7ffe2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 09:41:13 +1000 Subject: [PATCH 095/178] PEP fix --- InvenTree/stock/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 26dd97b848..534d27a8f4 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -2,7 +2,6 @@ JSON API for the Stock app """ -from django.db.models.query import QuerySet from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter From 238dccc071ed69aa5479d9afab806fd370413d03 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 09:45:36 +1000 Subject: [PATCH 096/178] Replace PartAttachmentCreate form --- .../part/templates/part/attachments.html | 21 ++++++-- InvenTree/part/test_views.py | 23 -------- InvenTree/part/urls.py | 1 - InvenTree/part/views.py | 54 ------------------- 4 files changed, 17 insertions(+), 82 deletions(-) diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index 93440e13ed..78bd621254 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -21,7 +21,7 @@ enableDragAndDrop( '#attachment-dropzone', - "{% url 'part-attachment-create' %}", + '{% url "api-part-attachment-list" %}', { data: { part: {{ part.id }}, @@ -34,10 +34,23 @@ ); $("#new-attachment").click(function() { - launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}", + + constructForm( + '{% url "api-part-attachment-list" %}', { - reload: true, - }); + method: 'POST', + fields: { + attachment: {}, + comment: {}, + part: { + value: {{ part.pk }}, + hidden: true, + } + }, + reload: true, + title: '{% trans "Add Attachment" %}', + } + ) }); $("#attachment-table").on('click', '.attachment-edit-button', function() { diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 231eed7896..3b6b245231 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -232,29 +232,6 @@ class PartRelatedTests(PartViewTestCase): self.assertEqual(n, 1) -class PartAttachmentTests(PartViewTestCase): - - def test_valid_create(self): - """ test creation of an attachment for a valid part """ - - response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # TODO - Create a new attachment using this view - - def test_invalid_create(self): - """ test creation of an attachment for an invalid part """ - - # TODO - pass - - def test_edit(self): - """ test editing an attachment """ - - # TODO - pass - - class PartQRTest(PartViewTestCase): """ Tests for the Part QR Code AJAX view """ diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 80351b8dba..cd18fc0f7e 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -18,7 +18,6 @@ part_related_urls = [ ] part_attachment_urls = [ - url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 9998a12783..c9d767d835 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -154,60 +154,6 @@ class PartRelatedDelete(AjaxDeleteView): role_required = 'part.change' -class PartAttachmentCreate(AjaxCreateView): - """ View for creating a new PartAttachment object - - - The view only makes sense if a Part object is passed to it - """ - model = PartAttachment - form_class = part_forms.EditPartAttachmentForm - ajax_form_title = _("Add part attachment") - ajax_template_name = "modal_form.html" - - def save(self, form, **kwargs): - """ - Record the user that uploaded this attachment - """ - - attachment = form.save(commit=False) - attachment.user = self.request.user - attachment.save() - - def get_data(self): - return { - 'success': _('Added attachment') - } - - def get_initial(self): - """ Get initial data for new PartAttachment object. - - - Client should have requested this form with a parent part in mind - - e.g. ?part= - """ - - initials = super(AjaxCreateView, self).get_initial() - - # TODO - If the proper part was not sent, return an error message - try: - initials['part'] = Part.objects.get(id=self.request.GET.get('part', None)) - except (ValueError, Part.DoesNotExist): - pass - - return initials - - def get_form(self): - """ Create a form to upload a new PartAttachment - - - Hide the 'part' field - """ - - form = super(AjaxCreateView, self).get_form() - - form.fields['part'].widget = HiddenInput() - - return form - - class PartAttachmentEdit(AjaxUpdateView): """ View for editing a PartAttachment object """ From b946aedb5c3eb76b3cc1e10bb5f81daaa45bcd54 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 09:49:30 +1000 Subject: [PATCH 097/178] Replace PartAttachmentEdit view --- InvenTree/part/api.py | 10 +++++++++ .../part/templates/part/attachments.html | 16 +++++++++----- InvenTree/part/urls.py | 1 - InvenTree/part/views.py | 21 ------------------- .../templates/stock/item_attachments.html | 5 ----- 5 files changed, 21 insertions(+), 32 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 2212743c2b..d9fa77afd4 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -232,6 +232,15 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] +class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): + """ + Detail endpoint for PartAttachment model + """ + + queryset = PartAttachment.objects.all() + serializer_class = part_serializers.PartAttachmentSerializer + + class PartTestTemplateList(generics.ListCreateAPIView): """ API endpoint for listing (and creating) a PartTestTemplate. @@ -1032,6 +1041,7 @@ part_api_urls = [ # Base URL for PartAttachment API endpoints url(r'^attachment/', include([ + url(r'^(?P\d+)/', PartAttachmentDetail.as_view(), name='api-part-attachment-detail'), url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'), ])), diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index 78bd621254..cff6f25892 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -56,12 +56,18 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - var url = `/part/attachment/${button.attr('pk')}/edit/`; + var pk = button.attr('pk'); - launchModalForm(url, - { - reload: true, - }); + var url = `/api/part/attachment/${pk}/`; + + constructForm(url, { + fields: { + attachment: {}, + comment: {}, + }, + title: '{% trans "Edit Attachment" %}', + reload: true, + }); }); $("#attachment-table").on('click', '.attachment-delete-button', function() { diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index cd18fc0f7e..eeb1a08be4 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -18,7 +18,6 @@ part_related_urls = [ ] part_attachment_urls = [ - url(r'^(?P\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index c9d767d835..b2d12a72ab 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -154,27 +154,6 @@ class PartRelatedDelete(AjaxDeleteView): role_required = 'part.change' -class PartAttachmentEdit(AjaxUpdateView): - """ View for editing a PartAttachment object """ - - model = PartAttachment - form_class = part_forms.EditPartAttachmentForm - ajax_template_name = 'modal_form.html' - ajax_form_title = _('Edit attachment') - - def get_data(self): - return { - 'success': _('Part attachment updated') - } - - def get_form(self): - form = super(AjaxUpdateView, self).get_form() - - form.fields['part'].widget = HiddenInput() - - return form - - class PartAttachmentDelete(AjaxDeleteView): """ View for deleting a PartAttachment """ diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index f5e9f8c4fc..13e886d8a5 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -25,7 +25,6 @@ enableDragAndDrop( { data: { stock_item: {{ item.id }}, - user: {{ user.pk }}, }, label: 'attachment', success: function(data, status, xhr) { @@ -47,10 +46,6 @@ $("#new-attachment").click(function() { value: {{ item.pk }}, hidden: true, }, - user: { - value: {{ user.pk }}, - hidden: true, - } }, reload: true, title: '{% trans "Add Attachment" %}', From 60d599b4762fd595192c1f9e96a4f04813fa5cf0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 10:03:54 +1000 Subject: [PATCH 098/178] Refactor PurchaseOrderAttachment views --- InvenTree/order/api.py | 15 +++- .../order/templates/order/po_attachments.html | 32 ++++++-- InvenTree/order/urls.py | 2 - InvenTree/order/views.py | 73 ------------------- 4 files changed, 36 insertions(+), 86 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index c22d76a52e..74d94b12f4 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -554,12 +554,22 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): serializer_class = POAttachmentSerializer +class POAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): + """ + Detail endpoint for a PurchaseOrderAttachment + """ + + queryset = PurchaseOrderAttachment.objects.all() + serializer_class = POAttachmentSerializer + + order_api_urls = [ # API endpoints for purchase orders - url(r'^po/(?P\d+)/$', PODetail.as_view(), name='api-po-detail'), url(r'po/attachment/', include([ + url(r'^(?P\d+)/$', POAttachmentDetail.as_view(), name='api-po-attachment-detail'), url(r'^.*$', POAttachmentList.as_view(), name='api-po-attachment-list'), ])), + url(r'^po/(?P\d+)/$', PODetail.as_view(), name='api-po-detail'), url(r'^po/.*$', POList.as_view(), name='api-po-list'), # API endpoints for purchase order line items @@ -568,12 +578,11 @@ order_api_urls = [ # API endpoints for sales ordesr url(r'^so/', include([ - url(r'^(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), url(r'attachment/', include([ url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), ])), - # List all sales orders + url(r'^(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), url(r'^.*$', SOList.as_view(), name='api-so-list'), ])), diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index 40052c1ec6..20d15ea90f 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -22,7 +22,7 @@ enableDragAndDrop( '#attachment-dropzone', - "{% url 'po-attachment-create' %}", + '{% url "api-po-attachment-list" %}', { data: { order: {{ order.id }}, @@ -35,20 +35,36 @@ enableDragAndDrop( ); $("#new-attachment").click(function() { - launchModalForm("{% url 'po-attachment-create' %}?order={{ order.id }}", - { - reload: true, - } - ); + + constructForm('{% url "api-po-attachment-list" %}', { + method: 'POST', + fields: { + attachment: {}, + comment: {}, + order: { + value: {{ order.pk }}, + hidden: true, + }, + }, + reload: true, + title: '{% trans "Add Attachment" %}', + }); }); $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - var url = `/order/purchase-order/attachment/${button.attr('pk')}/edit/`; + var pk = button.attr('pk'); - launchModalForm(url, { + var url = `/api/order/po/attachment/${pk}/`; + + constructForm(url, { + fields: { + attachment: {}, + comment: {}, + }, reload: true, + title: '{% trans "Edit Attachment" %}', }); }); diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 112a8cf297..692f12f298 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -46,8 +46,6 @@ purchase_order_urls = [ ])), url(r'^attachment/', include([ - url(r'^new/', views.PurchaseOrderAttachmentCreate.as_view(), name='po-attachment-create'), - url(r'^(?P\d+)/edit/', views.PurchaseOrderAttachmentEdit.as_view(), name='po-attachment-edit'), url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), ])), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 98f3384ca9..8df386e8b8 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -96,58 +96,6 @@ class SalesOrderDetail(InvenTreeRoleMixin, DetailView): template_name = 'order/sales_order_detail.html' -class PurchaseOrderAttachmentCreate(AjaxCreateView): - """ - View for creating a new PurchaseOrderAttachment - """ - - model = PurchaseOrderAttachment - form_class = order_forms.EditPurchaseOrderAttachmentForm - ajax_form_title = _("Add Purchase Order Attachment") - ajax_template_name = "modal_form.html" - - def save(self, form, **kwargs): - - attachment = form.save(commit=False) - attachment.user = self.request.user - attachment.save() - - def get_data(self): - return { - "success": _("Added attachment") - } - - def get_initial(self): - """ - Get initial data for creating a new PurchaseOrderAttachment object. - - - Client must request this form with a parent PurchaseOrder in midn. - - e.g. ?order= - """ - - initials = super(AjaxCreateView, self).get_initial() - - try: - initials["order"] = PurchaseOrder.objects.get(id=self.request.GET.get('order', -1)) - except (ValueError, PurchaseOrder.DoesNotExist): - pass - - return initials - - def get_form(self): - """ - Create a form to upload a new PurchaseOrderAttachment - - - Hide the 'order' field - """ - - form = super(AjaxCreateView, self).get_form() - - form.fields['order'].widget = HiddenInput() - - return form - - class SalesOrderAttachmentCreate(AjaxCreateView): """ View for creating a new SalesOrderAttachment """ @@ -188,27 +136,6 @@ class SalesOrderAttachmentCreate(AjaxCreateView): return form -class PurchaseOrderAttachmentEdit(AjaxUpdateView): - """ View for editing a PurchaseOrderAttachment object """ - - model = PurchaseOrderAttachment - form_class = order_forms.EditPurchaseOrderAttachmentForm - ajax_form_title = _("Edit Attachment") - - def get_data(self): - return { - 'success': _('Attachment updated') - } - - def get_form(self): - form = super(AjaxUpdateView, self).get_form() - - # Hide the 'order' field - form.fields['order'].widget = HiddenInput() - - return form - - class SalesOrderAttachmentEdit(AjaxUpdateView): """ View for editing a SalesOrderAttachment object """ From 712c9598d1d3586f38f38ba30d62dfc0ecd6d5a5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 10:09:05 +1000 Subject: [PATCH 099/178] Refactor SalesOrderAttachment forms --- InvenTree/order/api.py | 10 ++++ .../order/templates/order/so_attachments.html | 32 +++++++--- InvenTree/order/urls.py | 2 - InvenTree/order/views.py | 60 ------------------- 4 files changed, 34 insertions(+), 70 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 74d94b12f4..a128130f39 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -259,6 +259,15 @@ class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] +class SOAttachmentDetail(generics.RetrieveUpdateAPIView, AttachmentMixin): + """ + Detail endpoint for SalesOrderAttachment + """ + + queryset = SalesOrderAttachment.objects.all() + serializer_class = SOAttachmentSerializer + + class SOList(generics.ListCreateAPIView): """ API endpoint for accessing a list of SalesOrder objects. @@ -579,6 +588,7 @@ order_api_urls = [ # API endpoints for sales ordesr url(r'^so/', include([ url(r'attachment/', include([ + url(r'^(?P\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'), url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), ])), diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html index b868aea48e..90ba49a0ea 100644 --- a/InvenTree/order/templates/order/so_attachments.html +++ b/InvenTree/order/templates/order/so_attachments.html @@ -23,7 +23,7 @@ enableDragAndDrop( '#attachment-dropzone', - "{% url 'so-attachment-create' %}", + '{% url "api-so-attachment-list" %}', { data: { order: {{ order.id }}, @@ -36,20 +36,36 @@ enableDragAndDrop( ); $("#new-attachment").click(function() { - launchModalForm("{% url 'so-attachment-create' %}?order={{ order.id }}", - { - reload: true, - } - ); + + constructForm('{% url "api-so-attachment-list" %}', { + method: 'POST', + fields: { + attachment: {}, + comment: {}, + order: { + value: {{ order.pk }}, + hidden: true + } + }, + reload: true, + title: '{% trans "Add Attachment" %}' + }); }); $("#attachment-table").on('click', '.attachment-edit-button', function() { var button = $(this); - var url = `/order/sales-order/attachment/${button.attr('pk')}/edit/`; + var pk = button.attr('pk'); - launchModalForm(url, { + var url = `/api/order/so/attachment/${pk}/`; + + constructForm(url, { + fields: { + attachment: {}, + comment: {}, + }, reload: true, + title: '{% trans "Edit Attachment" %}', }); }); diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 692f12f298..db76306b3c 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -89,8 +89,6 @@ sales_order_urls = [ ])), url(r'^attachment/', include([ - url(r'^new/', views.SalesOrderAttachmentCreate.as_view(), name='so-attachment-create'), - url(r'^(?P\d+)/edit/', views.SalesOrderAttachmentEdit.as_view(), name='so-attachment-edit'), url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), ])), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 8df386e8b8..9cf53ec51c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -96,66 +96,6 @@ class SalesOrderDetail(InvenTreeRoleMixin, DetailView): template_name = 'order/sales_order_detail.html' -class SalesOrderAttachmentCreate(AjaxCreateView): - """ View for creating a new SalesOrderAttachment """ - - model = SalesOrderAttachment - form_class = order_forms.EditSalesOrderAttachmentForm - ajax_form_title = _('Add Sales Order Attachment') - - def save(self, form, **kwargs): - """ - Save the user that uploaded the attachment - """ - - attachment = form.save(commit=False) - attachment.user = self.request.user - attachment.save() - - def get_data(self): - return { - 'success': _('Added attachment') - } - - def get_initial(self): - initials = super().get_initial().copy() - - try: - initials['order'] = SalesOrder.objects.get(id=self.request.GET.get('order', None)) - except (ValueError, SalesOrder.DoesNotExist): - pass - - return initials - - def get_form(self): - """ Hide the 'order' field """ - - form = super().get_form() - form.fields['order'].widget = HiddenInput() - - return form - - -class SalesOrderAttachmentEdit(AjaxUpdateView): - """ View for editing a SalesOrderAttachment object """ - - model = SalesOrderAttachment - form_class = order_forms.EditSalesOrderAttachmentForm - ajax_form_title = _("Edit Attachment") - - def get_data(self): - return { - 'success': _('Attachment updated') - } - - def get_form(self): - form = super().get_form() - - form.fields['order'].widget = HiddenInput() - - return form - - class PurchaseOrderAttachmentDelete(AjaxDeleteView): """ View for deleting a PurchaseOrderAttachment """ From f67779c816d59f21b5a41fdeecf244037b608ae6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 10:37:38 +1000 Subject: [PATCH 100/178] Unit test fixes --- InvenTree/InvenTree/tests.py | 6 ++---- InvenTree/company/test_api.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index b7e5b98c1b..cb570d0176 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -15,8 +15,6 @@ from .validators import validate_overage, validate_part_name from . import helpers from . import version -from mptt.exceptions import InvalidMove - from decimal import Decimal import InvenTree.tasks @@ -203,7 +201,7 @@ class TestMPTT(TestCase): loc = StockLocation.objects.get(pk=4) loc.parent = loc - with self.assertRaises(InvalidMove): + with self.assertRaises(ValidationError): loc.save() def test_child_as_parent(self): @@ -214,7 +212,7 @@ class TestMPTT(TestCase): parent.parent = child - with self.assertRaises(InvalidMove): + with self.assertRaises(ValidationError): parent.save() def test_move(self): diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index 345f4bf1a4..6cd7ac7e3a 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -44,6 +44,10 @@ class CompanyTest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 2) def test_company_detail(self): + """ + Tests for the Company detail endpoint + """ + url = reverse('api-company-detail', kwargs={'pk': 1}) response = self.get(url) @@ -52,14 +56,18 @@ class CompanyTest(InvenTreeAPITestCase): # Change the name of the company # Note we should not have the correct permissions (yet) data = response.data - data['name'] = 'ACMOO' response = self.client.patch(url, data, format='json', expected_code=400) self.assignRole('company.change') + # Update the name and set the currency to a valid value + data['name'] = 'ACMOO' + data['currency'] = 'NZD' + response = self.client.patch(url, data, format='json', expected_code=200) self.assertEqual(response.data['name'], 'ACMOO') + self.assertEqual(response.data['currency'], 'NZD') def test_company_search(self): """ @@ -182,6 +190,9 @@ class ManufacturerTest(InvenTreeAPITestCase): self.assertEqual(len(response.data), 2) def test_manufacturer_part_detail(self): + """ + Tests for the ManufacturerPart detail endpoint + """ url = reverse('api-manufacturer-part-detail', kwargs={'pk': 1}) response = self.get(url) From a7d60cf5ad4f63202ccc08a8a0f9101b65d156bd Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 10:49:33 +1000 Subject: [PATCH 101/178] Exposes BuildOrderAttachment objects to the REST API --- InvenTree/build/api.py | 39 ++++++++++++++++++++++++++++------ InvenTree/build/serializers.py | 22 +++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 1cb973fe05..779c248fe5 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -11,11 +11,12 @@ from rest_framework import generics from django.conf.urls import url, include +from InvenTree.api import AttachmentMixin from InvenTree.helpers import str2bool, isNull from InvenTree.status_codes import BuildStatus -from .models import Build, BuildItem -from .serializers import BuildSerializer, BuildItemSerializer +from .models import Build, BuildItem, BuildOrderAttachment +from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer class BuildList(generics.ListCreateAPIView): @@ -226,14 +227,40 @@ class BuildItemList(generics.ListCreateAPIView): ] -build_item_api_urls = [ - url('^.*$', BuildItemList.as_view(), name='api-build-item-list'), -] +class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin): + """ + API endpoint for listing (and creating) BuildOrderAttachment objects + """ + + queryset = BuildOrderAttachment.objects.all() + serializer_class = BuildAttachmentSerializer + + +class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): + """ + Detail endpoint for a BuildOrderAttachment object + """ + + queryset = BuildOrderAttachment.objects.all() + serializer_class = BuildAttachmentSerializer + build_api_urls = [ - url(r'^item/', include(build_item_api_urls)), + # Attachments + url(r'^attachment/', include([ + url(r'^(?P\d+)/', BuildAttachmentDetail.as_view(), name='api-build-attachment-detail'), + url('^.*$', BuildAttachmentList.as_view(), name='api-build-attachment-list'), + ])), + + # Build Items + url(r'^item/', include([ + url('^.*$', BuildItemList.as_view(), name='api-build-item-list') + ])), + + # Build Detail url(r'^(?P\d+)/', BuildDetail.as_view(), name='api-build-detail'), + # Build List url(r'^.*$', BuildList.as_view(), name='api-build-list'), ] diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index d8573cfa70..06c4f36ebb 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -10,13 +10,13 @@ from django.db.models import BooleanField from rest_framework import serializers -from InvenTree.serializers import InvenTreeModelSerializer +from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializerField from stock.serializers import StockItemSerializerBrief from stock.serializers import LocationSerializer from part.serializers import PartSerializer, PartBriefSerializer -from .models import Build, BuildItem +from .models import Build, BuildItem, BuildOrderAttachment class BuildSerializer(InvenTreeModelSerializer): @@ -143,3 +143,21 @@ class BuildItemSerializer(InvenTreeModelSerializer): 'stock_item_detail', 'quantity' ] + + +class BuildAttachmentSerializer(InvenTreeModelSerializer): + """ + Serializer for a BuildAttachment + """ + + attachment = InvenTreeAttachmentSerializerField(required=True) + + class Meta: + model = BuildOrderAttachment + + fields = [ + 'pk', + 'build', + 'attachment', + 'comment' + ] From 9ea3e511b9ccb18a436a7fed89e9054ec25c0afe Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 11:05:35 +1000 Subject: [PATCH 102/178] Refactor BuildAttachment views --- .../build/templates/build/attachments.html | 39 ++++++----- InvenTree/build/urls.py | 2 - InvenTree/build/views.py | 70 ------------------- 3 files changed, 22 insertions(+), 89 deletions(-) diff --git a/InvenTree/build/templates/build/attachments.html b/InvenTree/build/templates/build/attachments.html index 8546ab42f5..9cd7761357 100644 --- a/InvenTree/build/templates/build/attachments.html +++ b/InvenTree/build/templates/build/attachments.html @@ -22,7 +22,7 @@ enableDragAndDrop( '#attachment-dropzone', - '{% url "build-attachment-create" %}', + '{% url "api-build-attachment-list" %}', { data: { build: {{ build.id }}, @@ -36,29 +36,34 @@ enableDragAndDrop( // Callback for creating a new attachment $('#new-attachment').click(function() { - launchModalForm( - '{% url "build-attachment-create" %}', - { - reload: true, - data: { - build: {{ build.pk }}, + + constructForm('{% url "api-build-attachment-list" %}', { + fields: { + attachment: {}, + comment: {}, + build: { + value: {{ build.pk }}, + hidden: true, } - } - ); + }, + method: 'POST', + reload: true, + title: '{% trans "Add Attachment" %}', + }); }); // Callback for editing an attachment $("#attachment-table").on('click', '.attachment-edit-button', function() { var pk = $(this).attr('pk'); - var url = `/build/attachment/${pk}/edit/`; - - launchModalForm( - url, - { - reload: true, - } - ); + constructForm(`/api/build/attachment/${pk}/`, { + fields: { + attachment: {}, + comment: {}, + }, + reload: true, + title: '{% trans "Edit Attachment" %}', + }); }); // Callback for deleting an attachment diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 99b6b72818..ae9ce1bf87 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -37,8 +37,6 @@ build_urls = [ ])), url('^attachment/', include([ - url('^new/', views.BuildAttachmentCreate.as_view(), name='build-attachment-create'), - url(r'^(?P\d+)/edit/', views.BuildAttachmentEdit.as_view(), name='build-attachment-edit'), url(r'^(?P\d+)/delete/', views.BuildAttachmentDelete.as_view(), name='build-attachment-delete'), ])), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 6e72f7f3e6..8cdaede60b 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -1060,76 +1060,6 @@ class BuildItemEdit(AjaxUpdateView): return form -class BuildAttachmentCreate(AjaxCreateView): - """ - View for creating a BuildAttachment - """ - - model = BuildOrderAttachment - form_class = forms.EditBuildAttachmentForm - ajax_form_title = _('Add Build Order Attachment') - - def save(self, form, **kwargs): - """ - Add information on the user that uploaded the attachment - """ - - attachment = form.save(commit=False) - attachment.user = self.request.user - attachment.save() - - def get_data(self): - return { - 'success': _('Added attachment') - } - - def get_initial(self): - """ - Get initial data for creating an attachment - """ - - initials = super().get_initial() - - try: - initials['build'] = Build.objects.get(pk=self.request.GET.get('build', -1)) - except (ValueError, Build.DoesNotExist): - pass - - return initials - - def get_form(self): - """ - Hide the 'build' field if specified - """ - - form = super().get_form() - - form.fields['build'].widget = HiddenInput() - - return form - - -class BuildAttachmentEdit(AjaxUpdateView): - """ - View for editing a BuildAttachment object - """ - - model = BuildOrderAttachment - form_class = forms.EditBuildAttachmentForm - ajax_form_title = _('Edit Attachment') - - def get_form(self): - form = super().get_form() - form.fields['build'].widget = HiddenInput() - - return form - - def get_data(self): - return { - 'success': _('Attachment updated') - } - - class BuildAttachmentDelete(AjaxDeleteView): """ View for deleting a BuildAttachment From 537c15081b2ac018f9ffa07fa1645f99328d6371 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 11:12:16 +1000 Subject: [PATCH 103/178] Fix for PK lookup in API test --- InvenTree/company/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index 6cd7ac7e3a..40176c7634 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -20,7 +20,7 @@ class CompanyTest(InvenTreeAPITestCase): super().setUp() - Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) + self.acme = Company.objects.create(name='ACME', description='Supplier', is_customer=False, is_supplier=True) Company.objects.create(name='Drippy Cup Co.', description='Customer', is_customer=True, is_supplier=False) Company.objects.create(name='Sippy Cup Emporium', description='Another supplier') @@ -48,7 +48,7 @@ class CompanyTest(InvenTreeAPITestCase): Tests for the Company detail endpoint """ - url = reverse('api-company-detail', kwargs={'pk': 1}) + url = reverse('api-company-detail', kwargs={'pk': self.acme.pk}) response = self.get(url) self.assertEqual(response.data['name'], 'ACME') From 653e3cd135469291b920001076788af72c5e62b9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 12:03:32 +1000 Subject: [PATCH 104/178] Starting work on a DELETE form --- InvenTree/InvenTree/static/css/inventree.css | 8 +++ InvenTree/templates/js/forms.js | 66 +++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 375e02c8ca..11c0c95561 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -640,6 +640,14 @@ z-index: 9999; } +.modal-header { + border-bottom: 1px solid #ddd; +} + +.modal-footer { + border-top: 1px solid #ddd; +} + .modal-primary { z-index: 10000; } diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 10d07d07e2..89b4608c8d 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -172,10 +172,48 @@ function constructChangeForm(fields, options) { }, error: function(request, status, error) { // TODO: Handle error here - console.log(`ERROR in constructChangeForm at '${url}'`); + console.log(`ERROR in constructChangeForm at '${options.url}'`); } - }) + }); +} + +/* + * Construct a 'delete' form, to remove a model instance from the database. + * + * arguments: + * - fields: The 'actions' object provided by the OPTIONS request + * - options: The 'options' object provided by the client + */ +function constructDeleteForm(fields, options) { + + // Force the "confirm" property if not set + if (!('confirm' in options)) { + options.confirm = true; + } + + // Request existing data from the API endpoint + // This data can be used to render some information on the form + $.ajax({ + url: options.url, + type: 'GET', + contentType: 'application/json', + dataType: 'json', + accepts: { + json: 'application/json', + }, + success: function(data) { + + // Store the instance data + options.instance = data; + + constructFormBody(fields, options); + }, + error: function(request, status, error) { + // TODO: Handle error here + console.log(`ERROR in constructDeleteForm at '${options.url}`); + } + }); } @@ -229,7 +267,7 @@ function constructForm(url, options) { break; case 'DELETE': if (canDelete(OPTIONS)) { - console.log('delete'); + constructDeleteForm(OPTIONS.actions.DELETE, options); } else { // User does not have permission to DELETE to the endpoint // TODO @@ -363,6 +401,14 @@ function constructFormBody(fields, options) { // Insert generated form content $(modal).find('.modal-form-content').html(html); + // Clear any existing buttons from the modal + $(modal).find('#modal-footer-buttons').html(''); + + // Insert "confirm" button (if required) + if (options.confirm) { + insertConfirmButton(options); + } + $(modal).modal('show'); updateFieldValues(fields, options); @@ -388,6 +434,20 @@ function constructFormBody(fields, options) { } +// Add a "confirm" checkbox to the modal +// The "submit" button will be disabled unless "confirm" is checked +function insertConfirmButton(options) { + + var confirm = ` + + Confirm + + `; + + $(options.modal).find('#modal-footer-buttons').append(confirm); +} + + /* * Submit form data to the server. * From 4d8e88c77936bb8b98a34aece48dc2a2223b9d20 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 12:48:14 +1000 Subject: [PATCH 105/178] BuildAttachmentDelete form --- InvenTree/build/templates/build/attachments.html | 9 +++++++++ InvenTree/build/urls.py | 4 ---- InvenTree/build/views.py | 15 --------------- InvenTree/templates/js/forms.js | 16 ++++++++++++++-- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/InvenTree/build/templates/build/attachments.html b/InvenTree/build/templates/build/attachments.html index 9cd7761357..839c275a9b 100644 --- a/InvenTree/build/templates/build/attachments.html +++ b/InvenTree/build/templates/build/attachments.html @@ -70,6 +70,15 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var pk = $(this).attr('pk'); + constructForm(`/api/build/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + reload: true, + }); + + return; + var url = `/build/attachment/${pk}/delete/`; launchModalForm( diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index ae9ce1bf87..549a20ee7e 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -36,10 +36,6 @@ build_urls = [ url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), ])), - url('^attachment/', include([ - url(r'^(?P\d+)/delete/', views.BuildAttachmentDelete.as_view(), name='build-attachment-delete'), - ])), - url(r'new/', views.BuildCreate.as_view(), name='build-create'), url(r'^(?P\d+)/', include(build_detail_urls)), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8cdaede60b..a3fd4fbd34 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -1058,18 +1058,3 @@ class BuildItemEdit(AjaxUpdateView): form.fields['install_into'].widget = HiddenInput() return form - - -class BuildAttachmentDelete(AjaxDeleteView): - """ - View for deleting a BuildAttachment - """ - - model = BuildOrderAttachment - ajax_form_title = _('Delete Attachment') - context_object_name = 'attachment' - - def get_data(self): - return { - 'danger': _('Deleted attachment') - } diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 89b4608c8d..d568e40fe7 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -438,13 +438,25 @@ function constructFormBody(fields, options) { // The "submit" button will be disabled unless "confirm" is checked function insertConfirmButton(options) { + var message = options.confirmMessage || '{% trans "Confirm" %}'; + var confirm = ` - Confirm - + ${message} + `; $(options.modal).find('#modal-footer-buttons').append(confirm); + + // Disable the 'submit' button + $(options.modal).find('#modal-form-submit').prop('disabled', true); + + // Trigger event + $(options.modal).find('#modal-confirm').change(function() { + var enabled = this.checked; + + $(options.modal).find('#modal-form-submit').prop('disabled', !enabled); + }); } From 4e23dbd0af20d350b9b48864901cc06ee0e53378 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 12:54:38 +1000 Subject: [PATCH 106/178] Refactor delete views for SalesOrderAttachment and PurchaseOrderAttachment --- .../build/templates/build/attachments.html | 11 ------- InvenTree/build/views.py | 2 +- InvenTree/order/api.py | 2 +- .../order/templates/order/po_attachments.html | 7 ++-- .../order/templates/order/so_attachments.html | 7 ++-- InvenTree/order/urls.py | 8 ----- InvenTree/order/views.py | 32 ++----------------- 7 files changed, 14 insertions(+), 55 deletions(-) diff --git a/InvenTree/build/templates/build/attachments.html b/InvenTree/build/templates/build/attachments.html index 839c275a9b..728c02fca7 100644 --- a/InvenTree/build/templates/build/attachments.html +++ b/InvenTree/build/templates/build/attachments.html @@ -76,17 +76,6 @@ $("#attachment-table").on('click', '.attachment-delete-button', function() { title: '{% trans "Delete Attachment" %}', reload: true, }); - - return; - - var url = `/build/attachment/${pk}/delete/`; - - launchModalForm( - url, - { - reload: true, - } - ); }); $("#attachment-table").inventreeTable({}); diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index a3fd4fbd34..16004dacc1 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,7 +12,7 @@ from django.forms import HiddenInput from django.urls import reverse from part.models import Part -from .models import Build, BuildItem, BuildOrderAttachment +from .models import Build, BuildItem from . import forms from stock.models import StockLocation, StockItem diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index a128130f39..748cd360d8 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -259,7 +259,7 @@ class SOAttachmentList(generics.ListCreateAPIView, AttachmentMixin): ] -class SOAttachmentDetail(generics.RetrieveUpdateAPIView, AttachmentMixin): +class SOAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): """ Detail endpoint for SalesOrderAttachment """ diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index 20d15ea90f..a6ea769aee 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -71,9 +71,12 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - var url = `/order/purchase-order/attachment/${button.attr('pk')}/delete/`; + var pk = button.attr('pk'); - launchModalForm(url, { + constructForm(`/api/order/po/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', reload: true, }); }); diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html index 90ba49a0ea..6df93bbe45 100644 --- a/InvenTree/order/templates/order/so_attachments.html +++ b/InvenTree/order/templates/order/so_attachments.html @@ -72,9 +72,12 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - var url = `/order/sales-order/attachment/${button.attr('pk')}/delete/`; + var pk = button.attr('pk'); - launchModalForm(url, { + constructForm(`/api/order/so/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', reload: true, }); }); diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index db76306b3c..3863d895c8 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -45,10 +45,6 @@ purchase_order_urls = [ ])), ])), - url(r'^attachment/', include([ - url(r'^(?P\d+)/delete/', views.PurchaseOrderAttachmentDelete.as_view(), name='po-attachment-delete'), - ])), - # Display complete list of purchase orders url(r'^.*$', views.PurchaseOrderIndex.as_view(), name='po-index'), ] @@ -88,10 +84,6 @@ sales_order_urls = [ ])), ])), - url(r'^attachment/', include([ - url(r'^(?P\d+)/delete/', views.SalesOrderAttachmentDelete.as_view(), name='so-attachment-delete'), - ])), - # Display detail view for a single SalesOrder url(r'^(?P\d+)/', include(sales_order_detail_urls)), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 9cf53ec51c..04bd0d3828 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -20,8 +20,8 @@ from django.forms import HiddenInput, IntegerField import logging from decimal import Decimal, InvalidOperation -from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment -from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment +from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrderAllocation from .admin import POLineItemResource from build.models import Build @@ -96,34 +96,6 @@ class SalesOrderDetail(InvenTreeRoleMixin, DetailView): template_name = 'order/sales_order_detail.html' -class PurchaseOrderAttachmentDelete(AjaxDeleteView): - """ View for deleting a PurchaseOrderAttachment """ - - model = PurchaseOrderAttachment - ajax_form_title = _("Delete Attachment") - ajax_template_name = "order/delete_attachment.html" - context_object_name = "attachment" - - def get_data(self): - return { - "danger": _("Deleted attachment") - } - - -class SalesOrderAttachmentDelete(AjaxDeleteView): - """ View for deleting a SalesOrderAttachment """ - - model = SalesOrderAttachment - ajax_form_title = _("Delete Attachment") - ajax_template_name = "order/delete_attachment.html" - context_object_name = "attachment" - - def get_data(self): - return { - "danger": _("Deleted attachment") - } - - class PurchaseOrderNotes(InvenTreeRoleMixin, UpdateView): """ View for updating the 'notes' field of a PurchaseOrder """ From 8f47035a7b62b27cd5baf881a29b2a0acc3f3601 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 12:58:41 +1000 Subject: [PATCH 107/178] Refactor delete view for PartAttachment and StockItemAttachment --- InvenTree/part/templates/part/attachments.html | 13 +++++++------ InvenTree/part/urls.py | 7 ------- InvenTree/part/views.py | 18 +----------------- .../templates/stock/item_attachments.html | 13 ++++++++----- InvenTree/stock/urls.py | 5 ----- InvenTree/stock/views.py | 18 +----------------- 6 files changed, 17 insertions(+), 57 deletions(-) diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index cff6f25892..5fca535098 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -72,13 +72,14 @@ $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); + var pk = button.attr('pk'); + var url = `/api/part/attachment/${pk}/`; - var url = `/part/attachment/${button.attr('pk')}/delete/`; - - launchModalForm(url, { - success: function() { - location.reload(); - } + constructForm(url, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + reload: true, }); }); diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index eeb1a08be4..489df6c116 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -17,10 +17,6 @@ part_related_urls = [ url(r'^(?P\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'), ] -part_attachment_urls = [ - url(r'^(?P\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), -] - sale_price_break_urls = [ url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'), url(r'^(?P\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'), @@ -146,9 +142,6 @@ part_urls = [ # Part related url(r'^related-parts/', include(part_related_urls)), - # Part attachments - url(r'^attachment/', include(part_attachment_urls)), - # Part price breaks url(r'^sale-price/', include(sale_price_break_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b2d12a72ab..0f0ccd8ba0 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -31,7 +31,7 @@ import io from rapidfuzz import fuzz from decimal import Decimal, InvalidOperation -from .models import PartCategory, Part, PartAttachment, PartRelated +from .models import PartCategory, Part, PartRelated from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate from .models import BomItem @@ -154,22 +154,6 @@ class PartRelatedDelete(AjaxDeleteView): role_required = 'part.change' -class PartAttachmentDelete(AjaxDeleteView): - """ View for deleting a PartAttachment """ - - model = PartAttachment - ajax_form_title = _("Delete Part Attachment") - ajax_template_name = "attachment_delete.html" - context_object_name = "attachment" - - role_required = 'part.change' - - def get_data(self): - return { - 'danger': _('Deleted part attachment') - } - - class PartTestTemplateCreate(AjaxCreateView): """ View for creating a PartTestTemplate """ diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index 13e886d8a5..f58d5db063 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -73,12 +73,15 @@ $("#attachment-table").on('click', '.attachment-edit-button', function() { $("#attachment-table").on('click', '.attachment-delete-button', function() { var button = $(this); - var url = `/stock/item/attachment/${button.attr('pk')}/delete/`; + var pk = button.attr('pk'); - launchModalForm(url, { - success: function() { - location.reload(); - } + var url = `/api/stock/attachment/${pk}/`; + + constructForm(url, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + reload: true, }); }); diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index eb0acbbef2..3e38d48f0e 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -62,11 +62,6 @@ stock_urls = [ url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'), - # URLs for StockItem attachments - url(r'^item/attachment/', include([ - url(r'^(?P\d+)/delete/', views.StockItemAttachmentDelete.as_view(), name='stock-item-attachment-delete'), - ])), - # URLs for StockItem tests url(r'^item/test/', include([ url(r'^new/', views.StockItemTestResultCreate.as_view(), name='stock-item-test-create'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 5b41bfe1b2..e8a3b8614c 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -32,7 +32,7 @@ from datetime import datetime, timedelta from company.models import Company, SupplierPart from part.models import Part -from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult +from .models import StockItem, StockLocation, StockItemTracking, StockItemTestResult import common.settings from common.models import InvenTreeSetting @@ -255,22 +255,6 @@ class StockLocationQRCode(QRCodeView): return None -class StockItemAttachmentDelete(AjaxDeleteView): - """ - View for deleting a StockItemAttachment object. - """ - - model = StockItemAttachment - ajax_form_title = _("Delete Stock Item Attachment") - ajax_template_name = "attachment_delete.html" - context_object_name = "attachment" - - def get_data(self): - return { - 'danger': _("Deleted attachment"), - } - - class StockItemAssignToCustomer(AjaxUpdateView): """ View for manually assigning a StockItem to a Customer From 09fff5b64403812e69b35658e3bb0eb88856d7f2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 14:07:15 +1000 Subject: [PATCH 108/178] Refactor PriceBreakCreate form - Handle non_field_errors --- InvenTree/company/serializers.py | 9 +++ .../company/supplier_part_pricing.html | 21 +++++-- InvenTree/company/urls.py | 1 - InvenTree/company/views.py | 61 ------------------- InvenTree/templates/js/forms.js | 30 +++++++++ 5 files changed, 55 insertions(+), 67 deletions(-) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 9e64fbad0a..9f40365757 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -2,6 +2,8 @@ JSON serializers for Company app """ +from django.utils.translation import ugettext_lazy as _ + from rest_framework import serializers from sql_util.utils import SubqueryCount @@ -249,6 +251,12 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): price = serializers.CharField() + price_currency = serializers.ChoiceField( + choices=djmoney.settings.CURRENCY_CHOICES, + default=currency_code_default, + label=_('Currency'), + ) + class Meta: model = SupplierPriceBreak fields = [ @@ -256,4 +264,5 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): 'part', 'quantity', 'price', + 'price_currency', ] diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 89b33049c6..5d2d0c5e31 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -98,12 +98,23 @@ $('#price-break-table').inventreeTable({ }); $('#new-price-break').click(function() { - launchModalForm("{% url 'price-break-create' %}", + + constructForm( + '{% url "api-part-supplier-price-list" %}', { - reload: true, - data: { - part: {{ part.id }}, - } + method: 'POST', + fields: { + quantity: {}, + part: { + value: {{ part.pk }}, + hidden: true, + }, + price: {}, + price_currency: { + }, + }, + title: '{% trans "Add Price Break" %}', + reload: true } ); }); diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 51685215b6..840f95b771 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -40,7 +40,6 @@ company_urls = [ ] price_break_urls = [ - url('^new/', views.PriceBreakCreate.as_view(), name='price-break-create'), url(r'^(?P\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'), url(r'^(?P\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 1b817fe66d..cc046173f9 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -761,67 +761,6 @@ class SupplierPartDelete(AjaxDeleteView): return self.renderJsonResponse(self.request, data=data, form=self.get_form()) -class PriceBreakCreate(AjaxCreateView): - """ View for creating a supplier price break """ - - model = SupplierPriceBreak - form_class = EditPriceBreakForm - ajax_form_title = _('Add Price Break') - ajax_template_name = 'modal_form.html' - - def get_data(self): - return { - 'success': _('Added new price break') - } - - def get_part(self): - """ - Attempt to extract SupplierPart object from the supplied data. - """ - - try: - supplier_part = SupplierPart.objects.get(pk=self.request.GET.get('part')) - return supplier_part - except (ValueError, SupplierPart.DoesNotExist): - pass - - try: - supplier_part = SupplierPart.objects.get(pk=self.request.POST.get('part')) - return supplier_part - except (ValueError, SupplierPart.DoesNotExist): - pass - - return None - - def get_form(self): - - form = super(AjaxCreateView, self).get_form() - form.fields['part'].widget = HiddenInput() - - return form - - def get_initial(self): - - initials = super(AjaxCreateView, self).get_initial() - - supplier_part = self.get_part() - - initials['part'] = self.get_part() - - if supplier_part is not None: - currency_code = supplier_part.supplier.currency_code - else: - currency_code = common.settings.currency_code_default() - - # Extract the currency object associated with the code - currency = CURRENCIES.get(currency_code, None) - - if currency: - initials['price'] = [1.0, currency] - - return initials - - class PriceBreakEdit(AjaxUpdateView): """ View for editing a supplier price break """ diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index d568e40fe7..a4baa8707f 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -302,6 +302,8 @@ function constructFormBody(fields, options) { var html = ''; + html += `
                  `; + // Client must provide set of fields to be displayed, // otherwise *all* fields will be displayed var displayed_fields = options.fields || fields; @@ -653,6 +655,9 @@ function clearFormErrors(options) { // Remove the "has error" class $(options.modal).find('.has-error').removeClass('has-error'); + + // Clear the 'non field errors' + $(options.modal).find('#non-field-errors').html(''); } @@ -669,6 +674,31 @@ function handleFormErrors(errors, fields, options) { // Remove any existing error messages from the form clearFormErrors(options); + var non_field_errors = $(options.modal).find('#non-field-errors'); + + non_field_errors.append( + `
                  + {% trans "Form errors exist" %} +
                  ` + ); + + // Non-field errors? + if ('non_field_errors' in errors) { + + var nfe = errors.non_field_errors; + + for (var idx = 0; idx < nfe.length; idx++) { + var err = nfe[idx]; + + var html = ` +
                  + ${err} +
                  `; + + non_field_errors.append(html); + } + } + for (field_name in errors) { if (field_name in fields) { From 2b394174bc65a7fa1d9ebf00ccb89e6382252fde Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 14:14:31 +1000 Subject: [PATCH 109/178] Refactor update and delete forms for SupplierPriceBreak --- InvenTree/InvenTree/urls.py | 2 -- InvenTree/company/api.py | 16 ++++++++++- .../company/supplier_part_pricing.html | 28 ++++++++++--------- InvenTree/company/urls.py | 6 ---- InvenTree/company/views.py | 24 ---------------- 5 files changed, 30 insertions(+), 46 deletions(-) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 6a7ae7bdfd..8c0cf494be 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -13,7 +13,6 @@ from django.contrib.auth import views as auth_views from company.urls import company_urls from company.urls import manufacturer_part_urls from company.urls import supplier_part_urls -from company.urls import price_break_urls from common.urls import common_urls from part.urls import part_urls @@ -126,7 +125,6 @@ urlpatterns = [ url(r'^part/', include(part_urls)), url(r'^manufacturer-part/', include(manufacturer_part_urls)), url(r'^supplier-part/', include(supplier_part_urls)), - url(r'^price-break/', include(price_break_urls)), # "Dynamic" javascript files which are rendered using InvenTree templating. url(r'^dynamic/', include(dynamic_javascript_urls)), diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 16887414de..6eac0339fa 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -394,6 +394,15 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): ] +class SupplierPriceBreakDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpoint for SupplierPriceBreak object + """ + + queryset = SupplierPriceBreak.objects.all() + serializer_class = SupplierPriceBreakSerializer + + manufacturer_part_api_urls = [ url(r'^parameter/', include([ @@ -424,7 +433,12 @@ company_api_urls = [ url(r'^part/', include(supplier_part_api_urls)), - url(r'^price-break/', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'), + # Supplier price breaks + url(r'^price-break/', include([ + + url(r'^(?P\d+)/?', SupplierPriceBreakDetail.as_view(), name='api-part-supplier-price-detail'), + url(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'), + ])), url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 5d2d0c5e31..a476b53a13 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -46,23 +46,25 @@ $('#price-break-table').inventreeTable({ table.find('.button-price-break-delete').click(function() { var pk = $(this).attr('pk'); - launchModalForm( - `/price-break/${pk}/delete/`, - { - success: reloadPriceBreaks - } - ); + constructForm(`/api/company/price-break/${pk}/`, { + method: 'DELETE', + onSuccess: reloadPriceBreaks, + title: '{% trans "Delete Price Break" %}', + }); }); table.find('.button-price-break-edit').click(function() { var pk = $(this).attr('pk'); - launchModalForm( - `/price-break/${pk}/edit/`, - { - success: reloadPriceBreaks - } - ); + constructForm(`/api/company/price-break/${pk}/`, { + fields: { + quantity: {}, + price: {}, + price_currency: {}, + }, + onSuccess: reloadPriceBreaks, + title: '{% trans "Edit Price Break" %}', + }); }); }, columns: [ @@ -114,7 +116,7 @@ $('#new-price-break').click(function() { }, }, title: '{% trans "Add Price Break" %}', - reload: true + onSuccess: reloadPriceBreaks, } ); }); diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 840f95b771..47a57d1df0 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -39,12 +39,6 @@ company_urls = [ url(r'^.*$', views.CompanyIndex.as_view(), name='company-index'), ] -price_break_urls = [ - - url(r'^(?P\d+)/edit/', views.PriceBreakEdit.as_view(), name='price-break-edit'), - url(r'^(?P\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'), -] - manufacturer_part_urls = [ url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index cc046173f9..67c4e45a2e 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -759,27 +759,3 @@ class SupplierPartDelete(AjaxDeleteView): part.delete() return self.renderJsonResponse(self.request, data=data, form=self.get_form()) - - -class PriceBreakEdit(AjaxUpdateView): - """ View for editing a supplier price break """ - - model = SupplierPriceBreak - form_class = EditPriceBreakForm - ajax_form_title = _('Edit Price Break') - ajax_template_name = 'modal_form.html' - - def get_form(self): - - form = super(AjaxUpdateView, self).get_form() - form.fields['part'].widget = HiddenInput() - - return form - - -class PriceBreakDelete(AjaxDeleteView): - """ View for deleting a supplier price break """ - - model = SupplierPriceBreak - ajax_form_title = _("Delete Price Break") - ajax_template_name = 'modal_delete_form.html' From a92fc7cf2c12ac93e8fdbf618196c4792c89c6f5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 14:15:18 +1000 Subject: [PATCH 110/178] PEP fixes --- InvenTree/company/api.py | 2 +- InvenTree/company/views.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 6eac0339fa..999e4e4039 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -438,7 +438,7 @@ company_api_urls = [ url(r'^(?P\d+)/?', SupplierPriceBreakDetail.as_view(), name='api-part-supplier-price-detail'), url(r'^.*$', SupplierPriceBreakList.as_view(), name='api-part-supplier-price-list'), - ])), + ])), url(r'^(?P\d+)/?', CompanyDetail.as_view(), name='api-company-detail'), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 67c4e45a2e..01b602d20a 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -26,14 +26,12 @@ from InvenTree.views import InvenTreeRoleMixin from .models import Company, ManufacturerPartParameter from .models import ManufacturerPart from .models import SupplierPart -from .models import SupplierPriceBreak from part.models import Part from .forms import EditManufacturerPartParameterForm from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm -from .forms import EditPriceBreakForm from .forms import CompanyImageDownloadForm import common.models From 682b2b4b2fa6bf3bd4f75ae9445e01b1c44b1304 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 17:04:21 +1000 Subject: [PATCH 111/178] Support rendering / updating of date inputs --- .../order/purchase_order_detail.html | 3 +- InvenTree/templates/js/forms.js | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 0ad73923a2..e5542f5816 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -21,7 +21,8 @@
                  {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %} + {% trans "Add Line Item" %} + {% endif %}
                  diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index a4baa8707f..de25b80146 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -501,6 +501,7 @@ function submitFormData(fields, options) { has_files = true; } } else { + // Normal field (not a file or image) form_data.append(name, value); @@ -601,12 +602,27 @@ function getFormFieldValue(name, field, options) { // Find the HTML element var el = $(options.modal).find(`#id_${name}`); + var value = null; + switch (field.type) { case 'boolean': - return el.is(":checked"); + value = el.is(":checked"); + break; + case 'date': + case 'datetime': + value = el.val(); + + // Ensure empty values are sent as nulls + if (!value || value.length == 0) { + value = null; + } + break; default: - return el.val(); + value = el.val(); + break; } + + return value; } @@ -705,7 +721,7 @@ function handleFormErrors(errors, fields, options) { // Add the 'has-error' class $(options.modal).find(`#div_id_${field_name}`).addClass('has-error'); - var field_dom = $(options.modal).find(`#id_${field_name}`); + var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`); var field_errors = errors[field_name]; @@ -719,7 +735,7 @@ function handleFormErrors(errors, fields, options) { ${error_text} `; - $(html).insertAfter(field_dom); + field_dom.append(html); } } else { @@ -1091,6 +1107,9 @@ function constructField(name, parameters, options) { html += `
                  `; // input-group } + // Div for error messages + html += `
                  `; + if (parameters.help_text) { html += constructHelpText(name, parameters, options); } @@ -1173,6 +1192,9 @@ function constructInput(name, parameters, options) { case 'file upload': func = constructFileUploadInput; break; + case 'date': + func = constructDateInput; + break; default: // Unsupported field type! break; @@ -1380,6 +1402,20 @@ function constructFileUploadInput(name, parameters, options) { } +/* + * Construct a field for a date input + */ +function constructDateInput(name, parameters, options) { + + return constructInputOptions( + name, + 'dateinput form-control', + 'date', + parameters + ); +} + + /* * Construct a 'help text' div based on the field parameters * From 9b4db43232542f26e7d46766aad50d9fa49d5bf4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 17:40:44 +1000 Subject: [PATCH 112/178] Refactoring "attachment" tables to use the API - Part attachments - StockItem attachments - PurchaseOrder attachments - SalesOrder attachments - BuildOrder attachments --- InvenTree/InvenTree/urls.py | 1 + .../build/templates/build/attachments.html | 51 ++++++------ .../order/templates/order/po_attachments.html | 62 +++++++------- .../order/templates/order/so_attachments.html | 65 +++++++-------- .../part/templates/part/attachments.html | 67 ++++++++------- .../templates/stock/item_attachments.html | 67 +++++++-------- InvenTree/templates/attachment_table.html | 31 +------ InvenTree/templates/base.html | 1 + InvenTree/templates/js/attachment.js | 82 +++++++++++++++++++ 9 files changed, 234 insertions(+), 193 deletions(-) create mode 100644 InvenTree/templates/js/attachment.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 8c0cf494be..6a0d02263d 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -103,6 +103,7 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'), + url(r'^attachment.js', DynamicJsView.as_view(template_name='js/attachment.js'), name='attachment.js'), url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'), url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'), url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'), diff --git a/InvenTree/build/templates/build/attachments.html b/InvenTree/build/templates/build/attachments.html index 728c02fca7..e969756b81 100644 --- a/InvenTree/build/templates/build/attachments.html +++ b/InvenTree/build/templates/build/attachments.html @@ -47,37 +47,38 @@ $('#new-attachment').click(function() { } }, method: 'POST', - reload: true, + onSuccess: reloadAttachmentTable, title: '{% trans "Add Attachment" %}', }); }); -// Callback for editing an attachment -$("#attachment-table").on('click', '.attachment-edit-button', function() { - var pk = $(this).attr('pk'); - - constructForm(`/api/build/attachment/${pk}/`, { - fields: { - attachment: {}, - comment: {}, +loadAttachmentTable( + '{% url "api-build-attachment-list" %}', + { + filters: { + build: {{ build.pk }}, }, - reload: true, - title: '{% trans "Edit Attachment" %}', - }); -}); + onEdit: function(pk) { + var url = `/api/build/attachment/${pk}/`; -// Callback for deleting an attachment -$("#attachment-table").on('click', '.attachment-delete-button', function() { - var pk = $(this).attr('pk'); + constructForm(url, { + fields: { + comment: {}, + }, + onSuccess: reloadAttachmentTable, + title: '{% trans "Edit Attachment" %}', + }); + }, + onDelete: function(pk) { - constructForm(`/api/build/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - reload: true, - }); -}); - -$("#attachment-table").inventreeTable({}); + constructForm(`/api/build/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + } + } +); {% endblock %} diff --git a/InvenTree/order/templates/order/po_attachments.html b/InvenTree/order/templates/order/po_attachments.html index a6ea769aee..07e425016b 100644 --- a/InvenTree/order/templates/order/po_attachments.html +++ b/InvenTree/order/templates/order/po_attachments.html @@ -34,6 +34,35 @@ enableDragAndDrop( } ); +loadAttachmentTable( + '{% url "api-po-attachment-list" %}', + { + filters: { + order: {{ order.pk }}, + }, + onEdit: function(pk) { + var url = `/api/order/po/attachment/${pk}/`; + + constructForm(url, { + fields: { + comment: {}, + }, + onSuccess: reloadAttachmentTable, + title: '{% trans "Edit Attachment" %}', + }); + }, + onDelete: function(pk) { + + constructForm(`/api/order/po/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + } + } +); + $("#new-attachment").click(function() { constructForm('{% url "api-po-attachment-list" %}', { @@ -51,37 +80,4 @@ $("#new-attachment").click(function() { }); }); -$("#attachment-table").on('click', '.attachment-edit-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - var url = `/api/order/po/attachment/${pk}/`; - - constructForm(url, { - fields: { - attachment: {}, - comment: {}, - }, - reload: true, - title: '{% trans "Edit Attachment" %}', - }); -}); - -$("#attachment-table").on('click', '.attachment-delete-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - constructForm(`/api/order/po/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - reload: true, - }); -}); - -$("#attachment-table").inventreeTable({ -}); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/so_attachments.html b/InvenTree/order/templates/order/so_attachments.html index 6df93bbe45..89f09541d1 100644 --- a/InvenTree/order/templates/order/so_attachments.html +++ b/InvenTree/order/templates/order/so_attachments.html @@ -35,6 +35,36 @@ enableDragAndDrop( } ); +loadAttachmentTable( + '{% url "api-so-attachment-list" %}', + { + filters: { + order: {{ order.pk }}, + }, + onEdit: function(pk) { + var url = `/api/order/so/attachment/${pk}/`; + + constructForm(url, { + fields: { + comment: {}, + }, + onSuccess: reloadAttachmentTable, + title: '{% trans "Edit Attachment" %}', + }); + }, + onDelete: function(pk) { + var pk = button.attr('pk'); + + constructForm(`/api/order/so/attachment/${pk}/`, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + } + } +); + $("#new-attachment").click(function() { constructForm('{% url "api-so-attachment-list" %}', { @@ -47,42 +77,9 @@ $("#new-attachment").click(function() { hidden: true } }, - reload: true, + onSuccess: reloadAttachmentTable, title: '{% trans "Add Attachment" %}' }); }); -$("#attachment-table").on('click', '.attachment-edit-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - var url = `/api/order/so/attachment/${pk}/`; - - constructForm(url, { - fields: { - attachment: {}, - comment: {}, - }, - reload: true, - title: '{% trans "Edit Attachment" %}', - }); -}); - -$("#attachment-table").on('click', '.attachment-delete-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - constructForm(`/api/order/so/attachment/${pk}/`, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - reload: true, - }); -}); - -$("#attachment-table").inventreeTable({ -}); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/attachments.html b/InvenTree/part/templates/part/attachments.html index 5fca535098..7128980472 100644 --- a/InvenTree/part/templates/part/attachments.html +++ b/InvenTree/part/templates/part/attachments.html @@ -19,6 +19,36 @@ {% block js_ready %} {{ block.super }} + loadAttachmentTable( + '{% url "api-part-attachment-list" %}', + { + filters: { + part: {{ part.pk }}, + }, + onEdit: function(pk) { + var url = `/api/part/attachment/${pk}/`; + + constructForm(url, { + fields: { + comment: {}, + }, + title: '{% trans "Edit Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + }, + onDelete: function(pk) { + var url = `/api/part/attachment/${pk}/`; + + constructForm(url, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + } + } + ); + enableDragAndDrop( '#attachment-dropzone', '{% url "api-part-attachment-list" %}', @@ -28,7 +58,7 @@ }, label: 'attachment', success: function(data, status, xhr) { - location.reload(); + reloadAttachmentTable(); } } ); @@ -47,43 +77,10 @@ hidden: true, } }, - reload: true, + onSuccess: reloadAttachmentTable, title: '{% trans "Add Attachment" %}', } ) }); - $("#attachment-table").on('click', '.attachment-edit-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - var url = `/api/part/attachment/${pk}/`; - - constructForm(url, { - fields: { - attachment: {}, - comment: {}, - }, - title: '{% trans "Edit Attachment" %}', - reload: true, - }); - }); - - $("#attachment-table").on('click', '.attachment-delete-button', function() { - var button = $(this); - var pk = button.attr('pk'); - var url = `/api/part/attachment/${pk}/`; - - constructForm(url, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - reload: true, - }); - }); - - $("#attachment-table").inventreeTable({ - }); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/item_attachments.html b/InvenTree/stock/templates/stock/item_attachments.html index f58d5db063..34ceecc550 100644 --- a/InvenTree/stock/templates/stock/item_attachments.html +++ b/InvenTree/stock/templates/stock/item_attachments.html @@ -28,11 +28,41 @@ enableDragAndDrop( }, label: 'attachment', success: function(data, status, xhr) { - location.reload(); + reloadAttachmentTable(); } } ); +loadAttachmentTable( + '{% url "api-stock-attachment-list" %}', + { + filters: { + item: {{ item.pk }}, + }, + onEdit: function(pk) { + var url = `/api/stock/attachment/${pk}/`; + + constructForm(url, { + fields: { + comment: {}, + }, + title: '{% trans "Edit Attachment" %}', + onSuccess: reloadAttachmentTable + }); + }, + onDelete: function(pk) { + var url = `/api/stock/attachment/${pk}/`; + + constructForm(url, { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Attachment" %}', + onSuccess: reloadAttachmentTable, + }); + } + } +); + $("#new-attachment").click(function() { constructForm( @@ -53,39 +83,4 @@ $("#new-attachment").click(function() { ); }); -$("#attachment-table").on('click', '.attachment-edit-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - var url = `/api/stock/attachment/${pk}/`; - - constructForm(url, { - fields: { - attachment: {}, - comment: {}, - }, - title: '{% trans "Edit Attachment" %}', - reload: true - }); -}); - -$("#attachment-table").on('click', '.attachment-delete-button', function() { - var button = $(this); - - var pk = button.attr('pk'); - - var url = `/api/stock/attachment/${pk}/`; - - constructForm(url, { - method: 'DELETE', - confirmMessage: '{% trans "Confirm Delete Operation" %}', - title: '{% trans "Delete Attachment" %}', - reload: true, - }); -}); - -$("#attachment-table").inventreeTable({ -}); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/attachment_table.html b/InvenTree/templates/attachment_table.html index 35b114cc05..18a4da9acc 100644 --- a/InvenTree/templates/attachment_table.html +++ b/InvenTree/templates/attachment_table.html @@ -10,35 +10,6 @@
                  {% trans 'Supplier Pricing' %}{% trans 'Supplier Pricing' %} + + + {% trans 'Unit Cost' %} Min: {% include "price.html" with price=min_unit_buy_price %} Max: {% include "price.html" with price=max_unit_buy_price %}
                  {% trans 'BOM Pricing' %}{% trans 'BOM Pricing' %} + + {% trans 'Unit Cost' %} Min: {% include "price.html" with price=min_unit_bom_price %} Max: {% include "price.html" with price=max_unit_bom_price %}
                  {% trans 'Sale Price' %}{% trans 'Sale Price' %} + + + {% trans 'Unit Cost' %} {% include "price.html" with price=unit_part_price %}
                  - - - - - - - - - - {% for attachment in attachments %} - - - - - - - {% endfor %} - +
                  {% trans "File" %}{% trans "Comment" %}{% trans "Uploaded" %}
                  {{ attachment.basename }}{{ attachment.comment }} - {% if attachment.upload_date %}{{ attachment.upload_date }}{% endif %} - {% if attachment.user %}{{ attachment.user.username }}{% endif %} - -
                  - - -
                  -
                  \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index ec7e31a7f0..76104d8fe2 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -149,6 +149,7 @@ + diff --git a/InvenTree/templates/js/attachment.js b/InvenTree/templates/js/attachment.js new file mode 100644 index 0000000000..4fce7e06fe --- /dev/null +++ b/InvenTree/templates/js/attachment.js @@ -0,0 +1,82 @@ +{% load i18n %} + +function reloadAttachmentTable() { + + $('#attachment-table').bootstrapTable("refresh"); +} + + +function loadAttachmentTable(url, options) { + + var table = options.table || '#attachment-table'; + + $(table).inventreeTable({ + url: url, + name: options.name || 'attachments', + formatNoMatches: function() { return '{% trans "No attachments found" %}'}, + sortable: true, + search: false, + queryParams: options.filters || {}, + onPostBody: function() { + // Add callback for 'edit' button + $(table).find('.button-attachment-edit').click(function() { + var pk = $(this).attr('pk'); + + if (options.onEdit) { + options.onEdit(pk); + } + }); + + // Add callback for 'delete' button + $(table).find('.button-attachment-delete').click(function() { + var pk = $(this).attr('pk'); + + if (options.onDelete) { + options.onDelete(pk); + } + }); + }, + columns: [ + { + field: 'attachment', + title: '{% trans "File" %}', + formatter: function(value, row) { + + var split = value.split('/'); + + return renderLink(split[split.length - 1], value); + } + }, + { + field: 'comment', + title: '{% trans "Comment" %}', + }, + { + field: 'actions', + formatter: function(value, row) { + var html = ''; + + html = `
                  `; + + html += makeIconButton( + 'fa-edit icon-blue', + 'button-attachment-edit', + row.pk, + '{% trans "Edit attachment" %}', + ); + + html += makeIconButton( + 'fa-trash-alt icon-red', + 'button-attachment-delete', + row.pk, + '{% trans "Delete attachment" %}', + ); + + html += `
                  `; + + return html; + } + } + ] + }); +} \ No newline at end of file From 30ac5dba5550bd9dbad77d8dde97b66256371877 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 17:44:23 +1000 Subject: [PATCH 113/178] Display attachment upload date --- InvenTree/build/serializers.py | 7 ++++++- InvenTree/order/serializers.py | 10 ++++++++++ InvenTree/part/serializers.py | 7 ++++++- InvenTree/stock/serializers.py | 5 +++++ InvenTree/templates/js/attachment.js | 4 ++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 06c4f36ebb..e33fcb6c7f 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -159,5 +159,10 @@ class BuildAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'build', 'attachment', - 'comment' + 'comment', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index e527b3cec9..f1eac82530 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -157,6 +157,11 @@ class POAttachmentSerializer(InvenTreeModelSerializer): 'order', 'attachment', 'comment', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', ] @@ -359,4 +364,9 @@ class SOAttachmentSerializer(InvenTreeModelSerializer): 'order', 'attachment', 'comment', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', ] diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index fb5480f668..b8b9b92d5e 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -61,7 +61,12 @@ class PartAttachmentSerializer(InvenTreeModelSerializer): 'pk', 'part', 'attachment', - 'comment' + 'comment', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', ] diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 38301bdd1f..c4ac404591 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -270,6 +270,11 @@ class LocationSerializer(InvenTreeModelSerializer): 'parent', 'pathstring', 'items', + 'upload_date', + ] + + read_only_fields = [ + 'upload_date', ] diff --git a/InvenTree/templates/js/attachment.js b/InvenTree/templates/js/attachment.js index 4fce7e06fe..4b9d522a59 100644 --- a/InvenTree/templates/js/attachment.js +++ b/InvenTree/templates/js/attachment.js @@ -51,6 +51,10 @@ function loadAttachmentTable(url, options) { field: 'comment', title: '{% trans "Comment" %}', }, + { + field: 'upload_date', + title: '{% trans "Upload Date" %}', + }, { field: 'actions', formatter: function(value, row) { From 770cd9a12d250787716d761b0f3005832df9f771 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 22:10:15 +1000 Subject: [PATCH 114/178] Fix for LocationSerializer --- InvenTree/stock/serializers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c4ac404591..38301bdd1f 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -270,11 +270,6 @@ class LocationSerializer(InvenTreeModelSerializer): 'parent', 'pathstring', 'items', - 'upload_date', - ] - - read_only_fields = [ - 'upload_date', ] From 54731746d814025ccdac214117baa36a045f9cb2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 30 Jun 2021 23:18:50 +1000 Subject: [PATCH 115/178] Render simple choice fields with select2 --- InvenTree/templates/js/forms.js | 46 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index de25b80146..76f0dba2dd 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -2,21 +2,23 @@ {% load inventree_extras %} /** + * * This file contains code for rendering (and managing) HTML forms * which are served via the django-drf API. - * + * * The django DRF library provides an OPTIONS method for each API endpoint, * which allows us to introspect the available fields at any given endpoint. - * + * * The OPTIONS method provides the following information for each available field: - * + * * - Field name * - Field label (translated) * - Field help text (translated) * - Field type * - Read / write status * - Field required status - * - min_value / max_value + * - min_value / max_value + * */ /* @@ -785,17 +787,16 @@ function initializeRelatedFields(fields, options) { var field = fields[name] || null; - if (!field || field.type != 'related field') continue; + if (!field || field.hidden) continue; - if (field.hidden) continue; - - if (!field.api_url) { - // TODO: Provide manual api_url option? - console.log(`Related field '${name}' missing 'api_url' parameter.`); - continue; + switch (field.type) { + case 'related field': + initializeRelatedField(name, field, options); + break; + case 'choice': + initializeChoiceField(name, field, options); + break; } - - initializeRelatedField(name, field, options); } } @@ -834,6 +835,12 @@ function addSecondaryModal(name, field, options) { */ function initializeRelatedField(name, field, options) { + if (!field.api_url) { + // TODO: Provide manual api_url option? + console.log(`Related field '${name}' missing 'api_url' parameter.`); + return; + } + // Find the select element and attach a select2 to it var select = $(options.modal).find(`#id_${name}`); @@ -995,6 +1002,17 @@ function initializeRelatedField(name, field, options) { } +function initializeChoiceField(name, field, options) { + + var select = $(options.modal).find(`#id_${name}`); + + select.select2({ + dropdownAutoWidth: false, + dropdownParent: $(options.modal), + }); +} + + // Render a 'no results' element function searching() { return `{% trans "Searching" %}...`; @@ -1343,8 +1361,6 @@ function constructChoiceInput(name, parameters, options) { var choices = parameters.choices || []; - // TODO: Select the selected value! - for (var idx = 0; idx < choices.length; idx++) { var choice = choices[idx]; From 59b794f0e5d9573ca65869b933b3e4ecdeafb632 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 14:25:14 +1000 Subject: [PATCH 116/178] Cleanup old forms --- InvenTree/build/forms.py | 16 +--------------- InvenTree/order/forms.py | 28 ++-------------------------- InvenTree/part/forms.py | 14 +------------- 3 files changed, 4 insertions(+), 54 deletions(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index e60df22c21..e2ca7c3f75 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -15,7 +15,7 @@ from InvenTree.fields import DatePickerFormField from InvenTree.status_codes import StockStatus -from .models import Build, BuildItem, BuildOrderAttachment +from .models import Build, BuildItem from stock.models import StockLocation, StockItem @@ -275,17 +275,3 @@ class EditBuildItemForm(HelperForm): 'quantity', 'install_into', ] - - -class EditBuildAttachmentForm(HelperForm): - """ - Form for creating / editing a BuildAttachment object - """ - - class Meta: - model = BuildOrderAttachment - fields = [ - 'build', - 'attachment', - 'comment' - ] diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 48b5245a5f..39536d457a 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -24,8 +24,8 @@ from common.forms import MatchItemForm import part.models from stock.models import StockLocation -from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderAttachment -from .models import SalesOrder, SalesOrderLineItem, SalesOrderAttachment +from .models import PurchaseOrder, PurchaseOrderLineItem +from .models import SalesOrder, SalesOrderLineItem from .models import SalesOrderAllocation @@ -170,30 +170,6 @@ class EditSalesOrderForm(HelperForm): ] -class EditPurchaseOrderAttachmentForm(HelperForm): - """ Form for editing a PurchaseOrderAttachment object """ - - class Meta: - model = PurchaseOrderAttachment - fields = [ - 'order', - 'attachment', - 'comment' - ] - - -class EditSalesOrderAttachmentForm(HelperForm): - """ Form for editing a SalesOrderAttachment object """ - - class Meta: - model = SalesOrderAttachment - fields = [ - 'order', - 'attachment', - 'comment' - ] - - class EditPurchaseOrderLineItemForm(HelperForm): """ Form for editing a PurchaseOrderLineItem object """ diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index ec799bcf8d..38d9b566aa 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _ import common.models -from .models import Part, PartCategory, PartAttachment, PartRelated +from .models import Part, PartCategory, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate @@ -185,18 +185,6 @@ class CreatePartRelatedForm(HelperForm): } -class EditPartAttachmentForm(HelperForm): - """ Form for editing a PartAttachment object """ - - class Meta: - model = PartAttachment - fields = [ - 'part', - 'attachment', - 'comment' - ] - - class SetPartCategoryForm(forms.Form): """ Form for setting the category of multiple Part objects """ From bb0a72f235da7b74e7ac3c236dc2f9e18b9c5640 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 14:31:37 +1000 Subject: [PATCH 117/178] Refactor forms for StockItemTestResult - Add DETAIL endpoint for model - Remove old views - Remove old forms --- InvenTree/stock/api.py | 10 +++ InvenTree/stock/forms.py | 33 -------- .../stock/templates/stock/item_tests.html | 81 +++++++++++++------ InvenTree/stock/urls.py | 7 -- InvenTree/stock/views.py | 68 ---------------- 5 files changed, 65 insertions(+), 134 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 534d27a8f4..b946d66aa5 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -940,6 +940,15 @@ class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMix serializer_class = StockItemAttachmentSerializer +class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpoint for StockItemTestResult + """ + + queryset = StockItemTestResult.objects.all() + serializer_class = StockItemTestResultSerializer + + class StockItemTestResultList(generics.ListCreateAPIView): """ API endpoint for listing (and creating) a StockItemTestResult object. @@ -1156,6 +1165,7 @@ stock_api_urls = [ # Base URL for StockItemTestResult API endpoints url(r'^test/', include([ + url(r'^(?P\d+)/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'), url(r'^$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'), ])), diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 92089623f9..ec3eee09d5 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -23,22 +23,6 @@ from report.models import TestReport from part.models import Part from .models import StockLocation, StockItem, StockItemTracking -from .models import StockItemAttachment -from .models import StockItemTestResult - - -class EditStockItemAttachmentForm(HelperForm): - """ - Form for creating / editing a StockItemAttachment object - """ - - class Meta: - model = StockItemAttachment - fields = [ - 'stock_item', - 'attachment', - 'comment' - ] class AssignStockItemToCustomerForm(HelperForm): @@ -65,23 +49,6 @@ class ReturnStockItemForm(HelperForm): ] -class EditStockItemTestResultForm(HelperForm): - """ - Form for creating / editing a StockItemTestResult object. - """ - - class Meta: - model = StockItemTestResult - fields = [ - 'stock_item', - 'test', - 'result', - 'value', - 'attachment', - 'notes', - ] - - class EditStockLocationForm(HelperForm): """ Form for editing a StockLocation """ diff --git a/InvenTree/stock/templates/stock/item_tests.html b/InvenTree/stock/templates/stock/item_tests.html index 4b3d9dd028..d7d26fcbba 100644 --- a/InvenTree/stock/templates/stock/item_tests.html +++ b/InvenTree/stock/templates/stock/item_tests.html @@ -48,8 +48,7 @@ loadStockTestResultsTable( ); function reloadTable() { - location.reload(); - //$("#test-result-table").bootstrapTable("refresh"); + $("#test-result-table").bootstrapTable("refresh"); } {% if item.has_test_reports %} @@ -70,15 +69,23 @@ $("#delete-test-results").click(function() { {% endif %} $("#add-test-result").click(function() { - launchModalForm( - "{% url 'stock-item-test-create' %}", { - data: { - stock_item: {{ item.id }}, - }, - success: reloadTable, - focus: 'test', - } - ); + + constructForm('{% url "api-stock-test-result-list" %}', { + method: 'POST', + fields: { + test: {}, + result: {}, + value: {}, + attachment: {}, + notes: {}, + stock_item: { + value: {{ item.pk }}, + hidden: true, + } + }, + title: '{% trans "Add Test Result" %}', + onSuccess: reloadTable, + }); }); $("#test-result-table").on('click', '.button-test-add', function() { @@ -86,35 +93,57 @@ $("#test-result-table").on('click', '.button-test-add', function() { var test_name = button.attr('pk'); - launchModalForm( - "{% url 'stock-item-test-create' %}", { - data: { - stock_item: {{ item.id }}, - test: test_name + constructForm('{% url "api-stock-test-result-list" %}', { + method: 'POST', + fields: { + test: { + value: test_name, }, - success: reloadTable, - focus: 'value', - } - ); + result: {}, + value: {}, + attachment: {}, + notes: {}, + stock_item: { + value: {{ item.pk }}, + hidden: true, + } + }, + title: '{% trans "Add Test Result" %}', + onSuccess: reloadTable, + }); }); $("#test-result-table").on('click', '.button-test-edit', function() { var button = $(this); - var url = `/stock/item/test/${button.attr('pk')}/edit/`; + var pk = button.attr('pk'); - launchModalForm(url, { - success: reloadTable, + var url = `/api/stock/test/${pk}/`; + + constructForm(url, { + fields: { + test: {}, + result: {}, + value: {}, + attachment: {}, + notes: {}, + }, + title: '{% trans "Edit Test Result" %}', + onSuccess: reloadTable, }); }); $("#test-result-table").on('click', '.button-test-delete', function() { var button = $(this); - var url = `/stock/item/test/${button.attr('pk')}/delete/`; + var pk = button.attr('pk'); - launchModalForm(url, { - success: reloadTable, + var url = `/api/stock/test/${pk}/`; + + constructForm(url, { + method: 'DELETE', + title: '{% trans "Delete Test Result" %}', + onSuccess: reloadTable, }); }); diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 3e38d48f0e..ac9474f805 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -62,13 +62,6 @@ stock_urls = [ url(r'^item/uninstall/', views.StockItemUninstall.as_view(), name='stock-item-uninstall'), - # URLs for StockItem tests - url(r'^item/test/', include([ - url(r'^new/', views.StockItemTestResultCreate.as_view(), name='stock-item-test-create'), - url(r'^(?P\d+)/edit/', views.StockItemTestResultEdit.as_view(), name='stock-item-test-edit'), - url(r'^(?P\d+)/delete/', views.StockItemTestResultDelete.as_view(), name='stock-item-test-delete'), - ])), - url(r'^track/', include(stock_tracking_urls)), url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index e8a3b8614c..eca7485c40 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -355,74 +355,6 @@ class StockItemDeleteTestData(AjaxUpdateView): return self.renderJsonResponse(request, form, data) -class StockItemTestResultCreate(AjaxCreateView): - """ - View for adding a new StockItemTestResult - """ - - model = StockItemTestResult - form_class = StockForms.EditStockItemTestResultForm - ajax_form_title = _("Add Test Result") - - def save(self, form, **kwargs): - """ - Record the user that uploaded the test result - """ - - result = form.save(commit=False) - result.user = self.request.user - result.save() - - def get_initial(self): - - initials = super().get_initial() - - try: - stock_id = self.request.GET.get('stock_item', None) - initials['stock_item'] = StockItem.objects.get(pk=stock_id) - except (ValueError, StockItem.DoesNotExist): - pass - - initials['test'] = self.request.GET.get('test', '') - - return initials - - def get_form(self): - - form = super().get_form() - form.fields['stock_item'].widget = HiddenInput() - - return form - - -class StockItemTestResultEdit(AjaxUpdateView): - """ - View for editing a StockItemTestResult - """ - - model = StockItemTestResult - form_class = StockForms.EditStockItemTestResultForm - ajax_form_title = _("Edit Test Result") - - def get_form(self): - - form = super().get_form() - - form.fields['stock_item'].widget = HiddenInput() - - return form - - -class StockItemTestResultDelete(AjaxDeleteView): - """ - View for deleting a StockItemTestResult - """ - - model = StockItemTestResult - ajax_form_title = _("Delete Test Result") - context_object_name = "result" - - class StockExportOptions(AjaxView): """ Form for selecting StockExport options """ From 9d1c1b98df4fde92bd84c1afa8ba544672ad7814 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 14:33:16 +1000 Subject: [PATCH 118/178] PEP fix --- InvenTree/stock/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index eca7485c40..280b1bb533 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -32,7 +32,7 @@ from datetime import datetime, timedelta from company.models import Company, SupplierPart from part.models import Part -from .models import StockItem, StockLocation, StockItemTracking, StockItemTestResult +from .models import StockItem, StockLocation, StockItemTracking import common.settings from common.models import InvenTreeSetting From bfc5a7dcf89d03173968a345604c7206354b9c8d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 14:44:23 +1000 Subject: [PATCH 119/178] Refactor forms for PartTestTemplate model: - Remove old forms - Remove old views - Add detail endpoint for the API --- InvenTree/part/api.py | 10 ++++ InvenTree/part/forms.py | 26 ++-------- InvenTree/part/templates/part/part_tests.html | 52 +++++++++++++------ InvenTree/part/urls.py | 7 --- InvenTree/part/views.py | 50 ------------------ 5 files changed, 50 insertions(+), 95 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index d9fa77afd4..b31283e191 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -241,6 +241,15 @@ class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixi serializer_class = part_serializers.PartAttachmentSerializer +class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Detail endpoint for PartTestTemplate model + """ + + queryset = PartTestTemplate.objects.all() + serializer_class = part_serializers.PartTestTemplateSerializer + + class PartTestTemplateList(generics.ListCreateAPIView): """ API endpoint for listing (and creating) a PartTestTemplate. @@ -1036,6 +1045,7 @@ part_api_urls = [ # Base URL for PartTestTemplate API endpoints url(r'^test-template/', include([ + url(r'^(?P\d+)/', PartTestTemplateDetail.as_view(), name='api-part-test-template-detail'), url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'), ])), diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 38d9b566aa..36a49006b0 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -5,21 +5,21 @@ Django Forms for interacting with Part objects # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from mptt.fields import TreeNodeChoiceField + from InvenTree.forms import HelperForm from InvenTree.helpers import GetExportFormats from InvenTree.fields import RoundingDecimalFormField -from mptt.fields import TreeNodeChoiceField -from django import forms -from django.utils.translation import ugettext_lazy as _ - import common.models from .models import Part, PartCategory, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate -from .models import PartTestTemplate from .models import PartSellPriceBreak, PartInternalPriceBreak @@ -65,22 +65,6 @@ class PartImageForm(HelperForm): ] -class EditPartTestTemplateForm(HelperForm): - """ Class for creating / editing a PartTestTemplate object """ - - class Meta: - model = PartTestTemplate - - fields = [ - 'part', - 'test_name', - 'description', - 'required', - 'requires_value', - 'requires_attachment', - ] - - class BomExportForm(forms.Form): """ Simple form to let user set BOM export options, before exporting a BOM (bill of materials) file. diff --git a/InvenTree/part/templates/part/part_tests.html b/InvenTree/part/templates/part/part_tests.html index dbd439afdb..3c131aa1d4 100644 --- a/InvenTree/part/templates/part/part_tests.html +++ b/InvenTree/part/templates/part/part_tests.html @@ -44,34 +44,52 @@ function reloadTable() { } $("#add-test-template").click(function() { - launchModalForm( - "{% url 'part-test-template-create' %}", - { - data: { - part: {{ part.id }}, - }, - success: reloadTable, - } - ); + + constructForm('{% url "api-part-test-template-list" %}', { + method: 'POST', + fields: { + test_name: {}, + description: {}, + required: {}, + requires_value: {}, + requires_attachment: {}, + part: { + value: {{ part.pk }}, + hidden: true, + } + }, + title: '{% trans "Add Test Result Template" %}', + onSuccess: reloadTable + }); }); $("#test-template-table").on('click', '.button-test-edit', function() { - var button = $(this); + var pk = $(this).attr('pk'); - var url = `/part/test-template/${button.attr('pk')}/edit/`; + var url = `/api/part/test-template/${pk}/`; - launchModalForm(url, { - success: reloadTable, + constructForm(url, { + fields: { + test_name: {}, + description: {}, + required: {}, + requires_value: {}, + requires_attachment: {}, + }, + title: '{% trans "Edit Test Result Template" %}', + onSuccess: reloadTable, }); }); $("#test-template-table").on('click', '.button-test-delete', function() { - var button = $(this); + var pk = $(this).attr('pk'); - var url = `/part/test-template/${button.attr('pk')}/delete/`; + var url = `/api/part/test-template/${pk}/`; - launchModalForm(url, { - success: reloadTable, + constructForm(url, { + method: 'DELETE', + title: '{% trans "Delete Test Result Template" %}', + onSuccess: reloadTable, }); }); diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 489df6c116..44336ab6a7 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -148,13 +148,6 @@ part_urls = [ # Part internal price breaks url(r'^internal-price/', include(internal_price_break_urls)), - # Part test templates - url(r'^test-template/', include([ - url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'), - url(r'^(?P\d+)/edit/', views.PartTestTemplateEdit.as_view(), name='part-test-template-edit'), - url(r'^(?P\d+)/delete/', views.PartTestTemplateDelete.as_view(), name='part-test-template-delete'), - ])), - # Part parameters url(r'^parameter/', include(part_parameter_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 0f0ccd8ba0..78fd0d7a27 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -36,7 +36,6 @@ from .models import PartParameterTemplate, PartParameter from .models import PartCategoryParameterTemplate from .models import BomItem from .models import match_part_names -from .models import PartTestTemplate from .models import PartSellPriceBreak, PartInternalPriceBreak from common.models import InvenTreeSetting @@ -154,55 +153,6 @@ class PartRelatedDelete(AjaxDeleteView): role_required = 'part.change' -class PartTestTemplateCreate(AjaxCreateView): - """ View for creating a PartTestTemplate """ - - model = PartTestTemplate - form_class = part_forms.EditPartTestTemplateForm - ajax_form_title = _("Create Test Template") - - def get_initial(self): - - initials = super().get_initial() - - try: - part_id = self.request.GET.get('part', None) - initials['part'] = Part.objects.get(pk=part_id) - except (ValueError, Part.DoesNotExist): - pass - - return initials - - def get_form(self): - - form = super().get_form() - form.fields['part'].widget = HiddenInput() - - return form - - -class PartTestTemplateEdit(AjaxUpdateView): - """ View for editing a PartTestTemplate """ - - model = PartTestTemplate - form_class = part_forms.EditPartTestTemplateForm - ajax_form_title = _("Edit Test Template") - - def get_form(self): - - form = super().get_form() - form.fields['part'].widget = HiddenInput() - - return form - - -class PartTestTemplateDelete(AjaxDeleteView): - """ View for deleting a PartTestTemplate """ - - model = PartTestTemplate - ajax_form_title = _("Delete Test Template") - - class PartSetCategory(AjaxUpdateView): """ View for settings the part category for multiple parts at once """ From 870542e4c103ba4deb674d326a461a63680a4b5d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 16:05:23 +1000 Subject: [PATCH 120/178] Refactor forms for ManufacturerPartParameter --- InvenTree/company/forms.py | 17 +---- .../company/manufacturer_part_suppliers.html | 25 +++++--- InvenTree/company/views.py | 63 +------------------ InvenTree/templates/js/company.js | 31 ++++----- 4 files changed, 36 insertions(+), 100 deletions(-) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 3b54f3dc61..63c07ff7a4 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -15,7 +15,7 @@ from djmoney.forms.fields import MoneyField from common.settings import currency_code_default -from .models import Company, ManufacturerPartParameter +from .models import Company from .models import ManufacturerPart from .models import SupplierPart from .models import SupplierPriceBreak @@ -58,21 +58,6 @@ class EditManufacturerPartForm(HelperForm): ] -class EditManufacturerPartParameterForm(HelperForm): - """ - Form for creating / editing a ManufacturerPartParameter object - """ - - class Meta: - model = ManufacturerPartParameter - fields = [ - 'manufacturer_part', - 'name', - 'value', - 'units', - ] - - class EditSupplierPartForm(HelperForm): """ Form for editing a SupplierPart object """ diff --git a/InvenTree/company/templates/company/manufacturer_part_suppliers.html b/InvenTree/company/templates/company/manufacturer_part_suppliers.html index 59969d9708..f3b87f4658 100644 --- a/InvenTree/company/templates/company/manufacturer_part_suppliers.html +++ b/InvenTree/company/templates/company/manufacturer_part_suppliers.html @@ -57,15 +57,26 @@ {% block js_ready %} {{ block.super }} +function reloadParameters() { + $("#parameter-table").bootstrapTable("refresh"); +} + $('#parameter-create').click(function() { - launchModalForm( - "{% url 'manufacturer-part-parameter-create' %}", - { - data: { - manufacturer_part: {{ part.id }}, + + constructForm('{% url "api-manufacturer-part-parameter-list" %}', { + method: 'POST', + fields: { + name: {}, + value: {}, + units: {}, + manufacturer_part: { + value: {{ part.pk }}, + hidden: true, } - } - ); + }, + title: '{% trans "Add Parameter" %}', + onSuccess: reloadParameters + }); }); $('#supplier-create').click(function () { diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 01b602d20a..c0fdcb912b 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -23,13 +23,12 @@ from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import str2bool from InvenTree.views import InvenTreeRoleMixin -from .models import Company, ManufacturerPartParameter +from .models import Company from .models import ManufacturerPart from .models import SupplierPart from part.models import Part -from .forms import EditManufacturerPartParameterForm from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm from .forms import CompanyImageDownloadForm @@ -414,66 +413,6 @@ class ManufacturerPartDelete(AjaxDeleteView): part.delete() return self.renderJsonResponse(self.request, data=data, form=self.get_form()) - - -class ManufacturerPartParameterCreate(AjaxCreateView): - """ - View for creating a new ManufacturerPartParameter object - """ - - model = ManufacturerPartParameter - form_class = EditManufacturerPartParameterForm - ajax_form_title = _('Add Manufacturer Part Parameter') - - def get_form(self): - - form = super().get_form() - - # Hide the manufacturer_part field if specified - if form.initial.get('manufacturer_part', None): - form.fields['manufacturer_part'].widget = HiddenInput() - - return form - - def get_initial(self): - - initials = super().get_initial().copy() - - manufacturer_part = self.get_param('manufacturer_part') - - if manufacturer_part: - try: - initials['manufacturer_part'] = ManufacturerPartParameter.objects.get(pk=manufacturer_part) - except (ValueError, ManufacturerPartParameter.DoesNotExist): - pass - - return initials - - -class ManufacturerPartParameterEdit(AjaxUpdateView): - """ - View for editing a ManufacturerPartParameter object - """ - - model = ManufacturerPartParameter - form_class = EditManufacturerPartParameterForm - ajax_form_title = _('Edit Manufacturer Part Parameter') - - def get_form(self): - - form = super().get_form() - - form.fields['manufacturer_part'].widget = HiddenInput() - - return form - - -class ManufacturerPartParameterDelete(AjaxDeleteView): - """ - View for deleting a ManufacturerPartParameter object - """ - - model = ManufacturerPartParameter class SupplierPartDetail(DetailView): diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 96beb0a041..dc0f0f4cc6 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -342,27 +342,28 @@ function loadManufacturerPartParameterTable(table, url, options) { $(table).find('.button-parameter-edit').click(function() { var pk = $(this).attr('pk'); - launchModalForm( - `/manufacturer-part/parameter/${pk}/edit/`, - { - success: function() { - $(table).bootstrapTable('refresh'); - } + constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, { + fields: { + name: {}, + value: {}, + units: {}, + }, + title: '{% trans "Edit Parameter" %}', + onSuccess: function() { + $(table).bootstrapTable('refresh'); } - ); - + }); }); $(table).find('.button-parameter-delete').click(function() { var pk = $(this).attr('pk'); - launchModalForm( - `/manufacturer-part/parameter/${pk}/delete/`, - { - success: function() { - $(table).bootstrapTable('refresh'); - } + constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Parameter" %}', + onSuccess: function() { + $(table).bootstrapTable('refresh'); } - ); + }); }); } }); From 96a2629fd23bfb1e8e3597f0cd380f7ce17c4502 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 16:07:42 +1000 Subject: [PATCH 121/178] Remove old URL endpoints --- InvenTree/company/urls.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 47a57d1df0..59d82e380f 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -44,15 +44,6 @@ manufacturer_part_urls = [ url(r'^delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'), - # URLs for ManufacturerPartParameter views (create / edit / delete) - url(r'^parameter/', include([ - url(r'^new/', views.ManufacturerPartParameterCreate.as_view(), name='manufacturer-part-parameter-create'), - url(r'^(?P\d)/', include([ - url(r'^edit/', views.ManufacturerPartParameterEdit.as_view(), name='manufacturer-part-parameter-edit'), - url(r'^delete/', views.ManufacturerPartParameterDelete.as_view(), name='manufacturer-part-parameter-delete'), - ])), - ])), - url(r'^(?P\d+)/', include([ url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'), From 206d7bd96a1126a2054c9b89ac21faa98a53041a Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 16:28:46 +1000 Subject: [PATCH 122/178] Refactor edit and delete forms for ManufacturerPart --- .../company/manufacturer_part_base.html | 30 ++++--- .../company/manufacturer_part_delete.html | 46 ---------- InvenTree/company/urls.py | 3 - InvenTree/company/views.py | 89 ------------------- 4 files changed, 18 insertions(+), 150 deletions(-) delete mode 100644 InvenTree/company/templates/company/manufacturer_part_delete.html diff --git a/InvenTree/company/templates/company/manufacturer_part_base.html b/InvenTree/company/templates/company/manufacturer_part_base.html index c3a64d9d76..addd9265b8 100644 --- a/InvenTree/company/templates/company/manufacturer_part_base.html +++ b/InvenTree/company/templates/company/manufacturer_part_base.html @@ -113,21 +113,27 @@ $('#order-part, #order-part2').click(function() { }); $('#edit-part').click(function () { - launchModalForm( - "{% url 'manufacturer-part-edit' part.id %}", - { - reload: true - } - ); + + constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { + fields: { + part: {}, + manufacturer: {}, + MPN: {}, + description: {}, + link: {}, + }, + title: '{% trans "Edit Manufacturer Part" %}', + reload: true, + }); }); $('#delete-part').click(function() { - launchModalForm( - "{% url 'manufacturer-part-delete' %}?part={{ part.id }}", - { - redirect: "{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}" - } - ); + + constructForm('{% url "api-manufacturer-part-detail" part.pk %}', { + method: 'DELETE', + title: '{% trans "Delete Manufacturer Part" %}', + redirect: "{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}", + }); }); {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/templates/company/manufacturer_part_delete.html b/InvenTree/company/templates/company/manufacturer_part_delete.html deleted file mode 100644 index 6c96c978d4..0000000000 --- a/InvenTree/company/templates/company/manufacturer_part_delete.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "modal_delete_form.html" %} -{% load i18n %} - -{% block pre_form_content %} -
                  - {% trans "Are you sure you want to delete the following Manufacturer Parts?" %} -
                  -{% for part in parts %} - -{% endfor %} - -{% endblock %} - -{% block form_data %} - -{% for part in parts %} - - - - - - - - -
                  - {% include "hover_image.html" with image=part.part.image %} - {{ part.part.full_name }} - - {% include "hover_image.html" with image=part.manufacturer.image %} - {{ part.manufacturer.name }} - - {{ part.MPN }} -
                  -{% if part.supplier_parts.all|length > 0 %} -
                  -

                  {% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this manufacturer part. If you delete it, the following supplier parts will also be deleted:{% endblocktrans %}

                  -
                    - {% for spart in part.supplier_parts.all %} -
                  • {{ spart.supplier.name }} - {{ spart.SKU }}
                  • - {% endfor %} -
                  -
                  -{% endif %} -{% endfor %} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 59d82e380f..105d7e89a2 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -42,10 +42,7 @@ company_urls = [ manufacturer_part_urls = [ url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'), - url(r'^delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'), - url(r'^(?P\d+)/', include([ - url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'), url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'), ])), diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index c0fdcb912b..9c28f56fa8 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -258,16 +258,6 @@ class ManufacturerPartDetail(DetailView): return ctx -class ManufacturerPartEdit(AjaxUpdateView): - """ Update view for editing ManufacturerPart """ - - model = ManufacturerPart - context_object_name = 'part' - form_class = EditManufacturerPartForm - ajax_template_name = 'modal_form.html' - ajax_form_title = _('Edit Manufacturer Part') - - class ManufacturerPartCreate(AjaxCreateView): """ Create view for making new ManufacturerPart """ @@ -336,85 +326,6 @@ class ManufacturerPartCreate(AjaxCreateView): return initials -class ManufacturerPartDelete(AjaxDeleteView): - """ Delete view for removing a ManufacturerPart. - - ManufacturerParts can be deleted using a variety of 'selectors'. - - - ?part= -> Delete a single ManufacturerPart object - - ?parts=[] -> Delete a list of ManufacturerPart objects - - """ - - success_url = '/manufacturer/' - ajax_template_name = 'company/manufacturer_part_delete.html' - ajax_form_title = _('Delete Manufacturer Part') - - role_required = 'purchase_order.delete' - - parts = [] - - def get_context_data(self): - ctx = {} - - ctx['parts'] = self.parts - - return ctx - - def get_parts(self): - """ Determine which ManufacturerPart object(s) the user wishes to delete. - """ - - self.parts = [] - - # User passes a single ManufacturerPart ID - if 'part' in self.request.GET: - try: - self.parts.append(ManufacturerPart.objects.get(pk=self.request.GET.get('part'))) - except (ValueError, ManufacturerPart.DoesNotExist): - pass - - elif 'parts[]' in self.request.GET: - - part_id_list = self.request.GET.getlist('parts[]') - - self.parts = ManufacturerPart.objects.filter(id__in=part_id_list) - - def get(self, request, *args, **kwargs): - self.request = request - self.get_parts() - - return self.renderJsonResponse(request, form=self.get_form()) - - def post(self, request, *args, **kwargs): - """ Handle the POST action for deleting ManufacturerPart object. - """ - - self.request = request - self.parts = [] - - for item in self.request.POST: - if item.startswith('manufacturer-part-'): - pk = item.replace('manufacturer-part-', '') - - try: - self.parts.append(ManufacturerPart.objects.get(pk=pk)) - except (ValueError, ManufacturerPart.DoesNotExist): - pass - - confirm = str2bool(self.request.POST.get('confirm_delete', False)) - - data = { - 'form_valid': confirm, - } - - if confirm: - for part in self.parts: - part.delete() - - return self.renderJsonResponse(self.request, data=data, form=self.get_form()) - - class SupplierPartDetail(DetailView): """ Detail view for SupplierPart """ model = SupplierPart From 9bd71c1184b7e58bceaef2f783de93b528762fd1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 17:01:30 +1000 Subject: [PATCH 123/178] Refactor deletion of multiple manufacturer part objects - issues multiple DELETE requests via the API --- .../company/detail_manufacturer_part.html | 25 ++++----- InvenTree/company/test_views.py | 32 +---------- InvenTree/part/templates/part/bom.html | 4 +- .../part/templates/part/manufacturer.html | 17 ++---- InvenTree/templates/js/company.js | 55 +++++++++++++++++++ InvenTree/templates/js/stock.js | 2 +- 6 files changed, 74 insertions(+), 61 deletions(-) diff --git a/InvenTree/company/templates/company/detail_manufacturer_part.html b/InvenTree/company/templates/company/detail_manufacturer_part.html index a162334040..992753e97b 100644 --- a/InvenTree/company/templates/company/detail_manufacturer_part.html +++ b/InvenTree/company/templates/company/detail_manufacturer_part.html @@ -25,7 +25,7 @@ {% endif %}
                  @@ -115,6 +118,46 @@ $("#supplier-part-delete").click(function() { }); }); +$("#multi-parameter-delete").click(function() { + + var selections = $("#parameter-table").bootstrapTable("getSelections"); + + var text = ` +
                  +

                  {% trans "Selected parameters will be deleted" %}:

                  +
                    `; + + selections.forEach(function(item) { + text += `
                  • ${item.name} - ${item.value}
                  • `; + }); + + text += ` +
                  +
                  `; + + showQuestionDialog( + '{% trans "Delete Parameters" %}', + text, + { + accept_text: '{% trans "Delete" %}', + accept: function() { + // Delete each parameter via the API + var requests = []; + + selections.forEach(function(item) { + var url = `/api/company/part/manufacturer/parameter/${item.pk}/`; + + requests.push(inventreeDelete(url)); + }); + + $.when.apply($, requests).then(function() { + $('#parameter-table').bootstrapTable('refresh'); + }); + } + } + ); +}); + loadSupplierPartTable( "#supplier-table", "{% url 'api-supplier-part-list' %}", @@ -140,5 +183,5 @@ loadManufacturerPartParameterTable( ); linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) - +linkButtonsToSelection($("#parameter-table"), ['#parameter-options']) {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 07da7af926..ba3e9be2b9 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -123,7 +123,7 @@ }); // Wait for *all* the requests to complete - $.when(requests).then(function() { + $.when.apply($, requests).then(function() { location.reload(); }); } diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 7c70af866b..d28bca5547 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -213,7 +213,7 @@ function deleteManufacturerParts(selections, options={}) { }); // Wait for all the requests to complete - $.when(requests).then(function() { + $.when.apply($, requests).then(function() { if (options.onSuccess) { options.onSuccess(); @@ -352,7 +352,7 @@ function loadManufacturerPartParameterTable(table, url, options) { { checkbox: true, switchable: false, - visible: false, + visible: true, }, { field: 'name', diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 97b9df0c68..b557f0c327 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -906,7 +906,7 @@ function loadStockTable(table, options) { ); }); - $.when(requests).then(function() { + $.when.apply($, requests).then(function() { $("#stock-table").bootstrapTable('refresh'); }); }) From e0f8310ca8ee17cc8a942fc6a14f4d5217ba6b9d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 21:57:07 +1000 Subject: [PATCH 126/178] Adds the ability to "clear" a non-required field with an obvious button --- InvenTree/templates/js/forms.js | 103 ++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 18 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 76f0dba2dd..a4a1d8e502 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -423,6 +423,9 @@ function constructFormBody(fields, options) { // Attach edit callbacks (if required) addFieldCallbacks(fields, options); + // Attach clear callbacks (if required) + addClearCallbacks(fields, options); + attachToggle(modal); $(modal + ' .select2-container').addClass('select-full-width'); @@ -571,22 +574,31 @@ function updateFieldValues(fields, options) { if (value == null) { continue; } - var el = $(options.modal).find(`#id_${name}`); + updateFieldValue(name, value, field, options); + } +} - switch (field.type) { - case 'boolean': - el.prop('checked', value); - break; - case 'related field': - // TODO? - break; - case 'file upload': - case 'image upload': - break; - default: - el.val(value); - break; - } + +function updateFieldValue(name, value, field, options) { + var el = $(options.modal).find(`#id_${name}`); + + switch (field.type) { + case 'boolean': + el.prop('checked', value); + break; + case 'related field': + // Clear? + if (value == null && !field.required) { + el.val(null).trigger('change'); + } + // TODO - Specify an actual value! + break; + case 'file upload': + case 'image upload': + break; + default: + el.val(value); + break; } } @@ -777,6 +789,29 @@ function addFieldCallback(name, field, options) { } +function addClearCallbacks(fields, options) { + + for (var idx = 0; idx < options.field_names.length; idx++) { + + var name = options.field_names[idx]; + + var field = fields[name]; + + if (!field || field.required) continue; + + addClearCallback(name, field, options); + } +} + + +function addClearCallback(name, field, options) { + + $(options.modal).find(`#clear_${name}`).click(function() { + updateFieldValue(name, null, field, options); + }); +} + + function initializeRelatedFields(fields, options) { var field_names = options.field_names; @@ -1114,14 +1149,46 @@ function constructField(name, parameters, options) { html += constructLabel(name, parameters); html += `
                  `; + + // Does this input deserve "extra" decorators? + var extra = parameters.prefix != null; - if (parameters.prefix) { - html += `
                  ${parameters.prefix}`; + // Some fields can have 'clear' inputs associated with them + if (!parameters.required) { + switch (parameters.type) { + case 'string': + case 'url': + case 'email': + case 'integer': + case 'float': + case 'decimal': + case 'related field': + extra = true; + break; + default: + break; + } + } + + if (extra) { + html += `
                  `; + + if (parameters.prefix) { + html += `${parameters.prefix}`; + } } html += constructInput(name, parameters, options); - if (parameters.prefix) { + if (extra) { + + if (!parameters.required) { + html += ` + + + `; + } + html += `
                  `; // input-group } From a771d7732bd696642af8199de65527e7411b3c0f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 1 Jul 2021 21:58:05 +1000 Subject: [PATCH 127/178] Icon tweak --- InvenTree/templates/js/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index a4a1d8e502..d372b10db2 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1185,7 +1185,7 @@ function constructField(name, parameters, options) { if (!parameters.required) { html += ` - + `; } From 2f1dea11239af41c838944bb0bac484e2d6d033b Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 2 Jul 2021 10:52:56 +1000 Subject: [PATCH 128/178] Modals can now be created programatically - INFINITE MODALS - API forms now create a new modal as required --- InvenTree/templates/js/forms.js | 8 ++++- InvenTree/templates/js/modals.js | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index d372b10db2..5bee4f57a6 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -237,6 +237,11 @@ function constructForm(url, options) { // Default HTTP method options.method = options.method || 'PATCH'; + // Create a new modal if one does not exists + if (!options.modal) { + options.modal = createNewModal(); + } + // Request OPTIONS endpoint from the API getApiEndpointOptions(url, function(OPTIONS) { @@ -413,6 +418,7 @@ function constructFormBody(fields, options) { insertConfirmButton(options); } + // Display the modal $(modal).modal('show'); updateFieldValues(fields, options); @@ -433,7 +439,6 @@ function constructFormBody(fields, options) { modalShowSubmitButton(modal, true); - $(modal).off('click', '#modal-form-submit'); $(modal).on('click', '#modal-form-submit', function() { submitFormData(fields, options); @@ -1163,6 +1168,7 @@ function constructField(name, parameters, options) { case 'float': case 'decimal': case 'related field': + case 'date': extra = true; break; default: diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index 75e0f3672a..ddce2fe109 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -1,5 +1,58 @@ {% load i18n %} + +/* + * Create and display a new modal dialog + */ +function createNewModal(options={}) { + + var id = options.id || 0; + + // Always increment the ID of the modal + id += 1; + + var html = ` + + `; + + $('body').append(html); + + var modal_name = `#modal-form-${id}`; + + // Automatically remove the modal when it is deleted! + $(modal_name).on('hidden.bs.modal', function(e) { + $(modal_name).remove(); + }); + + // Return the "name" of the modal + return modal_name; +} + + function makeOption(text, value, title) { /* Format an option for a select element */ From 3ff19f8c752724cdf6876e05ce3f37696aa25f1b Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 2 Jul 2021 11:06:52 +1000 Subject: [PATCH 129/178] Refactorin' --- InvenTree/templates/js/forms.js | 19 ++++++------------- InvenTree/templates/js/modals.js | 7 ++++++- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 5bee4f57a6..3f0ae6fffb 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -237,11 +237,6 @@ function constructForm(url, options) { // Default HTTP method options.method = options.method || 'PATCH'; - // Create a new modal if one does not exists - if (!options.modal) { - options.modal = createNewModal(); - } - // Request OPTIONS endpoint from the API getApiEndpointOptions(url, function(OPTIONS) { @@ -396,17 +391,15 @@ function constructFormBody(fields, options) { // TODO: Dynamically create the modals, // so that we can have an infinite number of stacks! - options.modal = options.modal || '#modal-form'; - + // Create a new modal if one does not exists + if (!options.modal) { + options.modal = createNewModal(options); + } + var modal = options.modal; modalEnable(modal, true); - - // Set the form title and button labels - modalSetTitle(modal, options.title || '{% trans "Form Title" %}'); - modalSetSubmitText(modal, options.submitText || '{% trans "Submit" %}'); - modalSetCloseText(modal, options.cancelText || '{% trans "Cancel" %}'); - + // Insert generated form content $(modal).find('.modal-form-content').html(html); diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index ddce2fe109..180f3bed95 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -31,7 +31,7 @@ function createNewModal(options={}) {
                  @@ -48,6 +48,11 @@ function createNewModal(options={}) { $(modal_name).remove(); }); + // Set labels based on supplied options + modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}'); + modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}'); + modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}'); + // Return the "name" of the modal return modal_name; } From 00e921f505f48357504c9c79bb48cefd787c2123 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 2 Jul 2021 11:13:24 +1000 Subject: [PATCH 130/178] More work on dynamic modal template --- InvenTree/templates/js/forms.js | 4 +--- InvenTree/templates/js/modals.js | 22 ++++++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 3f0ae6fffb..cb505c2a11 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -304,8 +304,6 @@ function constructFormBody(fields, options) { var html = ''; - html += `
                  `; - // Client must provide set of fields to be displayed, // otherwise *all* fields will be displayed var displayed_fields = options.fields || fields; @@ -684,7 +682,7 @@ function clearFormErrors(options) { // Remove the "has error" class $(options.modal).find('.has-error').removeClass('has-error'); - // Clear the 'non field errors' + // Hide the 'non field errors' $(options.modal).find('#non-field-errors').html(''); } diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index 180f3bed95..319158eaa0 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -19,18 +19,28 @@ function createNewModal(options={}) { - + +
                  +