Làm cách nào để lưu trường tùy chỉnh trong bảng cơ sở dữ liệu tùy chỉnh trong khi chỉnh sửa sản phẩm từ back-end?


11

Tôi đã tạo một mô-đun tùy chỉnh để hiển thị tab tùy chỉnh trên mẫu sản phẩm ở mặt sau. Tôi đã sử dụng giải pháp này .

Bây giờ trên tab tôi đang thêm các trường tùy chỉnh để lưu trong bảng cơ sở dữ liệu tùy chỉnh. Nói<input type="text" name="my_new_field" value="123">

Cũng tạo một bộ điều khiển tùy chỉnh cho sản phẩm quản trị lưu như dưới đây.

Trong etc / di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Catalog\Controller\Adminhtml\Product\Save" type="Namespace\Module\Controller\Adminhtml\Rewrite\Product\Save" />
</config>

Và trong Trình điều khiển / adminhtml / Viết lại / Sản phẩm / Save.php

<?php

    namespace Namespace\Module\Controller\Adminhtml\Rewrite\Product;

    class Save extends \Magento\Catalog\Controller\Adminhtml\Product\save
    {

        public function execute()
        {
            echo "hello"; print_r($_POST); die;

            return parent::execute();
        }
    }

Bây giờ trong executechức năng tôi đang mong đợi giá trị POST của my_new_field. Nhưng tôi không nhận được nó. Sau khi nhận được điều đó tôi sẽ sử dụng các truy vấn tùy chỉnh để lưu dữ liệu trong bảng tùy chỉnh.

Tôi đang làm gì sai hay tôi nên sử dụng một số phương pháp khác?

Cập nhật: ngày 26 tháng 8

Tôi đã sử dụng biểu mẫu Ajax để lưu dữ liệu từ tab sản phẩm vì tôi bị hạn chế về thời gian. Tôi đã chấp nhận câu trả lời của @ william-sồiley. Bây giờ như @mageworx đã thêm vào câu trả lời của mình rằng đây không phải là một cách tiêu chuẩn để làm điều này.

Tôi muốn sử dụng tiêu chuẩn sử dụng mẫu UI trong phát triển hơn nữa. Vì vậy, câu hỏi của tôi là làm thế nào để thêm tab tùy chỉnh vào chỉnh sửa sản phẩm bằng cách sử dụng tiêu chuẩn biểu mẫu UI và lưu các trường tùy chỉnh trong bảng tùy chỉnh hoặc theo cách khác.


1
Xin chào, bạn có thể sử dụng tệp CatalogSubSaveB Before.php trong Observer cho tùy chỉnh lưu này.
Payal Patel

Câu trả lời:


14

Bạn chỉ có thể sử dụng trường nhập "trần trụi", bạn chỉ cần thêm thuộc tính sau:

data-form-part="product_form"

vì thế:

<input data-form-part="product_form" type="text" name="my_new_field" value="123">

Sau đó, bạn sẽ có thể nhận được dữ liệu POST cho đầu vào của bạn.


7

Giải pháp trên không hoàn toàn chính xác. Bạn đang thêm một trường dưới dạng phần tử html "trần trụi" và biểu mẫu sản phẩm là biểu mẫu UI có đặc thù riêng. Một lớp đặc biệt ( vendor/magento/module-ui/view/base/web/js/form/form.js) chịu trách nhiệm thu thập các trường và xác thực của chúng khi một biểu mẫu được gửi. Ngoài ra, lớp này nên bỏ lỡ các trường không liên quan đến biểu mẫu UI này hoặc không additional fieldsgiống như tất cả các trường của bạn. Bạn nên sử dụng cách đặt tên sau để đảm bảo rằng trường của bạn sẽ được gửi đến bộ điều khiển:

input type="text" name="product[my_new_field]" value="123"

Nhưng điều này không hoàn toàn chính xác vì giải pháp chính xác là không đi chệch khỏi các tiêu chuẩn sử dụng mẫu UI và sử dụng các thành phần và thành phần gốc của nó. Trong trường hợp này, bạn không nên lo lắng về điều đó vì mọi thứ sẽ được xử lý tự động.

Bạn có thể kiểm tra phương thức lưu trữ dữ liệu biểu mẫu UI để hiểu quy trình:

/**
 * Submits form
 *
 * @param {String} redirect
 */
submit: function (redirect) {
    var additional = collectData(this.additionalFields),
        source = this.source;

    _.each(additional, function (value, name) {
        source.set('data.' + name, value);
    });

    source.save({
        redirect: redirect,
        ajaxSave: this.ajaxSave,
        ajaxSaveType: this.ajaxSaveType,
        response: {
            data: this.responseData,
            status: this.responseStatus
        },
        attributes: {
            id: this.namespace
        }
    });
},

Như bạn có thể thấy từ mã này, một biểu mẫu html với tất cả các trường của nó không được gửi. Tuy nhiên, this.sourcethis.additionalFieldsđược gửi nhưng phần tử của bạn không được bao gồm trong đó vì nó được khai báo không chính xác.

CẬP NHẬT TỪ NGÀY 08/03/2016

Dưới đây là ví dụ về cách thêm một bộ trường từ blog của chúng tôi. Bạn có thể đọc toàn bộ bài viết, sử dụng liên kết dưới đây:

Nguồn: Một cách dễ dàng để thêm bộ trường với các trường vào biểu mẫu UI :

Thêm nội dung: siêu dữ liệu dạng UI và loại ảo để bổ sung.

Tạo một tập tin app/code/Vendor/Product/etc/adminhtml/di.xml. Chúng tôi sẽ đặt một sửa đổi bên trong:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool">
        <arguments>
            <argument name="modifiers" xsi:type="array">
                <item name="custom-fieldset" xsi:type="array">
                    <item name="class" xsi:type="string">Vendor\Product\Ui\DataProvider\Product\Form\Modifier\CustomFieldset</item>
                    <item name="sortOrder" xsi:type="number">10</item>
                </item>
            </argument>
        </arguments>
    </virtualType>
</config>

Bây giờ, tạo tệp sửa đổi ( app/code/Vendor/Product/Ui/DataProvider/Product/Form/Modifier/CustomFieldset.php) với một bộ trường tùy chỉnh cho trang chỉnh sửa sản phẩm và điền nó vào các trường:

<?php
namespace Vendor\Product\Ui\DataProvider\Product\Form\Modifier;

use Magento\Catalog\Model\Locator\LocatorInterface;
use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\UrlInterface;
use Magento\Ui\Component\Container;
use Magento\Ui\Component\Form\Fieldset;
use Magento\Ui\Component\Form\Element\DataType\Number;
use Magento\Ui\Component\Form\Element\DataType\Text;
use Magento\Ui\Component\Form\Element\Input;
use Magento\Ui\Component\Form\Element\Select;
use Magento\Ui\Component\Form\Element\MultiSelect;
use Magento\Ui\Component\Form\Field;

class CustomFieldset extends AbstractModifier
{

    // Components indexes
    const CUSTOM_FIELDSET_INDEX = 'custom_fieldset';
    const CUSTOM_FIELDSET_CONTENT = 'custom_fieldset_content';
    const CONTAINER_HEADER_NAME = 'custom_fieldset_content_header';

    // Fields names
    const FIELD_NAME_TEXT = 'example_text_field';
    const FIELD_NAME_SELECT = 'example_select_field';
    const FIELD_NAME_MULTISELECT = 'example_multiselect_field';

    /**
     * @var \Magento\Catalog\Model\Locator\LocatorInterface
     */
    protected $locator;

    /**
     * @var ArrayManager
     */
    protected $arrayManager;

    /**
     * @var UrlInterface
     */
    protected $urlBuilder;

    /**
     * @var array
     */
    protected $meta = [];

    /**
     * @param LocatorInterface $locator
     * @param ArrayManager $arrayManager
     * @param UrlInterface $urlBuilder
     */
    public function __construct(
        LocatorInterface $locator,
        ArrayManager $arrayManager,
        UrlInterface $urlBuilder
    ) {
        $this->locator = $locator;
        $this->arrayManager = $arrayManager;
        $this->urlBuilder = $urlBuilder;
    }

    /**
     * Data modifier, does nothing in our example.
     *
     * @param array $data
     * @return array
     */
    public function modifyData(array $data)
    {
        return $data;
    }

    /**
     * Meta-data modifier: adds ours fieldset
     *
     * @param array $meta
     * @return array
     */
    public function modifyMeta(array $meta)
    {
        $this->meta = $meta;
        $this->addCustomFieldset();

        return $this->meta;
    }

    /**
     * Merge existing meta-data with our meta-data (do not overwrite it!)
     *
     * @return void
     */
    protected function addCustomFieldset()
    {
        $this->meta = array_merge_recursive(
            $this->meta,
            [
                static::CUSTOM_FIELDSET_INDEX => $this->getFieldsetConfig(),
            ]
        );
    }

    /**
     * Declare ours fieldset config
     *
     * @return array
     */
    protected function getFieldsetConfig()
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Fieldset Title'),
                        'componentType' => Fieldset::NAME,
                        'dataScope' => static::DATA_SCOPE_PRODUCT, // save data in the product data
                        'provider' => static::DATA_SCOPE_PRODUCT . '_data_source',
                        'ns' => static::FORM_NAME,
                        'collapsible' => true,
                        'sortOrder' => 10,
                        'opened' => true,
                    ],
                ],
            ],
            'children' => [
                static::CONTAINER_HEADER_NAME => $this->getHeaderContainerConfig(10),
                static::FIELD_NAME_TEXT => $this->getTextFieldConfig(20),
                static::FIELD_NAME_SELECT => $this->getSelectFieldConfig(30),
                static::FIELD_NAME_MULTISELECT => $this->getMultiSelectFieldConfig(40),
            ],
        ];
    }

    /**
     * Get config for header container
     *
     * @param int $sortOrder
     * @return array
     */
    protected function getHeaderContainerConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => null,
                        'formElement' => Container::NAME,
                        'componentType' => Container::NAME,
                        'template' => 'ui/form/components/complex',
                        'sortOrder' => $sortOrder,
                        'content' => __('You can write any text here'),
                    ],
                ],
            ],
            'children' => [],
        ];
    }

    /**
     * Example text field config
     *
     * @param $sortOrder
     * @return array
     */
    protected function getTextFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Example Text Field'),
                        'formElement' => Field::NAME,
                        'componentType' => Input::NAME,
                        'dataScope' => static::FIELD_NAME_TEXT,
                        'dataType' => Number::NAME,
                        'sortOrder' => $sortOrder,
                    ],
                ],
            ],
        ];
    }

    /**
     * Example select field config
     *
     * @param $sortOrder
     * @return array
     */
    protected function getSelectFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Options Select'),
                        'componentType' => Field::NAME,
                        'formElement' => Select::NAME,
                        'dataScope' => static::FIELD_NAME_SELECT,
                        'dataType' => Text::NAME,
                        'sortOrder' => $sortOrder,
                        'options' => $this->_getOptions(),
                        'visible' => true,
                        'disabled' => false,
                    ],
                ],
            ],
        ];
    }

    /**
     * Example multi-select field config
     *
     * @param $sortOrder
     * @return array
     */
    protected function getMultiSelectFieldConfig($sortOrder)
    {
        return [
            'arguments' => [
                'data' => [
                    'config' => [
                        'label' => __('Options Multiselect'),
                        'componentType' => Field::NAME,
                        'formElement' => MultiSelect::NAME,
                        'dataScope' => static::FIELD_NAME_MULTISELECT,
                        'dataType' => Text::NAME,
                        'sortOrder' => $sortOrder,
                        'options' => $this->_getOptions(),
                        'visible' => true,
                        'disabled' => false,
                    ],
                ],
            ],
        ];
    }

    /**
     * Get example options as an option array:
     *      [
     *          label => string,
     *          value => option_id
     *      ]
     *
     * @return array
     */
    protected function _getOptions()
    {
        $options = [
            1 => [
                'label' => __('Option 1'),
                'value' => 1
            ],
            2 => [
                'label' => __('Option 2'),
                'value' => 2
            ],
            3 => [
                'label' => __('Option 3'),
                'value' => 3
            ],
        ];

        return $options;
    }
}

xem trước

Việc lưu dữ liệu diễn ra bên trong tệp bộ điều khiển sản phẩm vendor/magento/module-catalog/Controller/Adminhtml/Product/Save.php trong phương thức thực hiện chính. Nếu mọi thứ đã được thực hiện đúng cách, thì dữ liệu của chúng tôi sẽ được hiển thị chính xác trong dữ liệu đầu vào của phương pháp này:

xem trước

Lưu ý, nếu sản phẩm của bạn không có các thuộc tính đó ngay từ đầu, bạn nên lưu chúng theo cách thủ công. Bạn có thể làm điều này trong người quan sát.

Đầu tiên, khai báo nó trong app/code/Vendor/Product/etc/adminhtml/events.xmltệp (chúng tôi đang sử dụng phạm vi adminhtml vì biểu mẫu không tồn tại ở mặt trước):

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="catalog_product_save_after">
        <observer name="save_example_data" instance="Vendor\Product\Observer\ProductSaveAfter" />
    </event>
</config>

Sau đó, tạo lớp của người quan sát mà chúng ta đã chỉ trong thuộc tính thể hiện - app/code/Vendor/Product/Observer/ProductSaveAfter.php:

<?php
namespace Vendor\Product\Observer;

use \Magento\Framework\Event\ObserverInterface;
use \Magento\Framework\Event\Observer as EventObserver;
use Vendor\Product\Ui\DataProvider\Product\Form\Modifier\CustomFieldset;

class ProductSaveAfter implements ObserverInterface
{

    /**
     * @param EventObserver $observer
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        /** @var \Magento\Catalog\Model\Product $product */
        $product = $observer->getEvent()->getProduct();
        if (!$product) {
            return;
        }

        $exampleTextField = $product->getData(CustomFieldset::FIELD_NAME_TEXT);
        $exampleSelectField = $product->getData(CustomFieldset::FIELD_NAME_SELECT);
        $exampleMultiSelectField = $product->getData(CustomFieldset::FIELD_NAME_MULTISELECT);

        // Manipulate data here
    }
}

Dữ liệu trong trình quan sát:

xem trước

Bây giờ, bạn có thể gọi mô hình của riêng bạn từ người quan sát và lưu dữ liệu trong đó hoặc sửa đổi nó theo ý muốn.

Hãy cẩn thận! Nếu việc lưu mô hình của bạn được kết nối với việc tiết kiệm sản phẩm, thì nó có thể dẫn đến sự đệ quy.


bạn có thể đề nghị làm thế nào tôi có thể thêm các trường mẫu UI?
HungryDB

1
@HungryDB chúng tôi đã cập nhật câu trả lời ở trên và thêm liên kết đến bài viết từ blog của chúng tôi. Bạn có thể đọc làm thế nào để tạo ra một fieldset ở đó.
MageWorx

3
Cảm ơn câu trả lời @mageworx. Tôi quyết định sử dụng phương thức biểu mẫu ajax để lưu dữ liệu mặc dù tôi có giới hạn thời gian. Tôi chắc chắn sẽ thử phương pháp của bạn khi tôi có thời gian.
HungryDB

Vậy làm cách nào để lưu những dữ liệu này vào cơ sở dữ liệu?
Chi

Cảm ơn câu trả lời. Phương pháp này đang hoạt động. Tôi đã thêm một trường chọn tùy chỉnh và giá trị được lưu vào bảng của tôi bằng cách sử dụng observer. Nhưng khi chỉnh sửa cùng một sản phẩm, các giá trị của tôi không được hiển thị như đã chọn. Xin vui lòng giúp đỡ .
Vindhuja

2

Để lưu trường sản phẩm trong bảng tùy chỉnh, bạn có thể theo logic của giá theo cấp. Magento sẽ tiết kiệm giá tier với sự trợ giúp của mô hình tùy chỉnh phụ trợ của giá tier. Chúng tôi có thể theo logic tương tự cho trường / tùy chỉnh của chúng tôi. Để lưu attibute trong bảng tùy chỉnh, bạn phải tạo thuộc tính tùy chỉnh và cung cấp cho nó mô hình phụ trợ. Mô hình cuối cùng sẽ định giá và lưu và lấy lại attibute. Bạn có thể làm theo các bước dưới đây.

Bước 1. Tạo thuộc tính sản phẩm

<?php 
namespace Magentoins\TestAttribute\Setup; 
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;


class InstallData implements InstallDataInterface

{    
    private $eavSetupFactory; 
    public function __construct(EavSetupFactory $eavSetupFactory)
    {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        /** @var EavSetup $eavSetup */
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

        /**
         * Add attributes to the eav/attribute
         */

        $eavSetup->addAttribute(
            \Magento\Catalog\Model\Product::ENTITY,
            'test_attribute',
            [
                'type' => 'int',
                'backend' => 'Magentoins\TestAttribute\Model\Product\Attribute\Backend\TestAttribute',
                'frontend' => '',
                'label' => 'Test Attribute',
                'input' => '',
                'class' => '',
                'source' => '',
                'global' => \Magento\Catalog\Model\Resource\Eav\Attribute::SCOPE_GLOBAL,
                'visible' => true,
                'required' => false,
                'user_defined' => false,
                'default' => 0,
                'searchable' => false,
                'filterable' => false,
                'comparable' => false,
                'visible_on_front' => false,
                'used_in_product_listing' => true,
                'unique' => false,
                'apply_to' => ''
            ]
        );
    }
}

Bước 2. Tạo mô hình phụ trợ cho thuộc tính tùy chỉnh sản phẩm sẽ giúp xác thực và lưu và truy xuất giá trị thuộc tính

<?php
namespace Magentoins\TestAttribute\Model\Product\Attribute\Backend;

class TestAttribute extends \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice
{
  protected $_productAttributeBackendTestAttribute;
  /**
   * Website currency codes and rates
   *
   * @var array
   */
  protected $_rates;

  protected $_helper;

  protected $eavConfig;

  public function __construct(
      \Magento\Directory\Model\CurrencyFactory $currencyFactory,
      \Magento\Store\Model\StoreManagerInterface $storeManager,
      \Magento\Catalog\Helper\Data $catalogData,
      \Magento\Framework\App\Config\ScopeConfigInterface $config,
      \Magento\Framework\Locale\FormatInterface $localeFormat,
      \Magento\Catalog\Model\Product\Type $catalogProductType,
      \Magento\Customer\Api\GroupManagementInterface $groupManagement,
      \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice $productAttributeTierprice,
      \Magentoins\TestAttribute\Model\ResourceModel\Product\Attribute\Backend\TestAttribute $productAttributeBackendFixedprices,
      \Magentoins\TestAttribute\Helper\Data $helperData,
      \Magento\Eav\Model\Config $eavConfig
  ) {
    parent::__construct(
        $currencyFactory,
        $storeManager,
        $catalogData,
        $config,
        $localeFormat,
        $catalogProductType,
        $groupManagement,
        $productAttributeTierprice
    );
    $this->_productAttributeBackendTestAttribute = $productAttributeBackendTestAttribute;    

  }

  /**
   * Retrieve resource instance
   *
   */
  protected function _getResource()
  {
    return $this->_productAttributeBackendTestAttribute;
  }

  public function getAttribute()
  {
    $attribute = $this->eavConfig->getAttribute('catalog_product', 'test_attribute');
    return $attribute;
  }
  /**
   * Validate test_attribute data
   *
   */
  public function validate ($object)
  {
    $attribute = $this->getAttribute();
    $attr = $object->getData($attribute->getName());
    if (empty($attr)) {
      return true;
    }    

    return true;
  }

  /**
   * Assign test_attribute to product data   
   */
  public function afterLoad ($object)
  {
    /*$data is from your custom table*/
    $data = $this->_getResource()->loadTestAttributeData($object->getId(), $websiteId);
    $object->setData($this->getAttribute()->getName(), $data);
    $object->setOrigData($this->getAttribute()->getName(), $data);

    $valueChangedKey = $this->getAttribute()->getName() . '_changed';
    $object->setOrigData($valueChangedKey, 0);
    $object->setData($valueChangedKey, 0);

    return $this;
  }

  /**
   * After Save Attribute manipulation 
   */
  public function afterSave ($object)
  {
    $websiteId = $this->_storeManager->getStore($object->getStoreId())->getWebsiteId();
    $isGlobal = $this->getAttribute()->isScopeGlobal() || $websiteId == 0;

    $testAttribute = $object->getData($this->getAttribute()->getName());

    /*Save attribute value in custom table with the help of resource model*/

    $this->_getResource()->saveTestAttributeData($testAttribute);

    return $this;
  }

  public function beforeSave ($object)
  {
    parent::beforeSave($object);        
  }

}

Bước 2. Mô hình tài nguyên để lưu và lấy giá trị thuộc tính từ bảng tùy chỉnh

<?php
namespace Magentoins\TestAttribute\Model\ResourceModel\Product\Attribute\Backend;

use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice;

/**
 * @author
 */
class TestAttribute extends Tierprice
{
    /**
     * Initialize connection and define main table
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('magentoins_product_entity_testAttribute', 'value_id');
    }

    /**
     * Load Fixed Prices for product
     *
     * @param int $productId
     * @return Designnbuy_Fixedprices_Model_Mysql4_fixedprices
     */
    public function loadTestAttributeData($productId, $websiteId = null)
    {
        $connection = $this->getConnection();
        $columns = array (
            'test_attribute' => $this->getIdFieldName()            
        );
        $select = $connection->select()
            ->from($this->getMainTable(), $columns)
            ->where('entity_id=?', $productId)
            ->order('order');

        if (!is_null($websiteId)) {
            if ($websiteId == '0') {
                $select->where('website_id=?', $websiteId);
            } else {
                $select->where('website_id IN(?)', array ('0', $websiteId
                ));
            }
        }

        return $connection->fetchAll($select);
    }

    public function saveTestAttributeData(\Magento\Framework\DataObject $attributeObject)
    {
        $connection = $this->getConnection();
        $data = $this->_prepareDataForTable($attributeObject, $this->getMainTable());

        if (!empty($data[$this->getIdFieldName()])) {
            $where = $connection->quoteInto($this->getIdFieldName() . ' = ?', $data[$this->getIdFieldName()]);
            unset($data[$this->getIdFieldName()]);
            $connection->update($this->getMainTable(), $data, $where);
        } else {
            $connection->insert($this->getMainTable(), $data);
        }
        return $this;
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.