Django добавление кастомного контента и действий в админку(Django add custom actions and content in admin view)

172

Передо мной стояла задача гибко расширить функционал стандартной админки джанги.

Вьюшка таблицы

Рассмотрим добавление дополнительного действия в лист, на примере пользователей. Стандартный пример — выгрузка пользователей в Exel. Вы видим обычный лист с дефолтными действиями.

Добавим небольшой HTML в шапку. Для этого в admin.py нашего модуля добавим дополнительный класс, который будет переопределять отображение модели в админке.

from django.contrib import admin
from django.contrib.auth.models import User
#Переопределяем вывод модели Юзеров
class CustomAdminUsers(admin.ModelAdmin):
    readonly_fields = ['address_report']
    list_display = ('first_name', 'email', 'is_active', 'date_joined', 'last_login',)
    # определяем дополнительный темплейт
    change_list_template = "admin/user_profile_templ.html"  

admin.site.unregister(User)
admin.site.register(User, CustomAdminUsers)

change_list_template — собственно директива определяющая дополнительный шаблон.
Он находится по пути yourmodule/static/templates/admin

Сам шаблон. В нем форма и указанный экшн.

{% extends 'admin/change_list.html' %}
{% block object-tools %}
<form action="to-exel/" method="POST">
    {% csrf_token %}
    <input class="exel-btn" type="submit" value="Выгрузить  в Exel"/>
</form>
{{ block.super }}
{% endblock %}

Отлично у нас отобразился шаблон.

Теперь нужно обработать экшн а admin.py в нашем классе.

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib import messages # сообщения джанги


#Переопределяем вывод модели Юзеров
class CustomAdminUsers(admin.ModelAdmin):
    readonly_fields = ['address_report']
    list_display = ('first_name', 'email', 'is_active', 'date_joined', 'last_login',)
    change_list_template = "admin/user_profile_templ.html" # определяем дополнительный темплейт
    
    # Определяем дополнительные урлы (код сам взял из stackoverflow)
    def get_urls(self):
        urls = super(CustomAdminUsers, self).get_urls()
        custom_urls = [url('^to-exel/$', self.to_exel, name='to_exel'), ]
        return custom_urls + urls
    # На обработчик
    def to_exel(self, request):
        # логика экшена
        messages.success(request, f"Пользователи успешно выгружены")
        return HttpResponseRedirect("../")

admin.site.unregister(User)
admin.site.register(User, CustomAdminUsers)

Собственно все для вьюшки листа.

Вьюшка редактирования модели

Теперь выведем дополнительную информацию в самой странице редактирования. Тут еще быстрее и проще. Ставим поле в readonly. И определяем функцию вывода. Либо текст, либо любой html. Соответсвенно можно брать любую информацию из других моделей и преобразовывать в нужный контент.

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib import messages # сообщения джанги
from django.utils.html import format_html


#Переопределяем вывод модели Юзеров
class CustomAdminUsers(admin.ModelAdmin):
    readonly_fields = ['some_content']
    list_display = ('first_name', 'email', 'is_active', 'date_joined', 'last_login',)
    change_list_template = "admin/user_profile_templ.html" # определяем дополнительный темплейт
    
    # Дополнительный контент
    def some_content(self, instance):
        return format_html("<a href='%s'>%s</a>" % ('somelink.ru', 'Пример ссылки'))

    # Определяем дополнительные урлы (код сам взял из stackoverflow)
    def get_urls(self):
        urls = super(CustomAdminUsers, self).get_urls()
        custom_urls = [url('^to-exel/$', self.to_exel, name='to_exel'), ]
        return custom_urls + urls
    # На обработчик
    def to_exel(self, request):
        # логика экшена
        messages.success(request, f"Пользователи успешно выгружены")
        return HttpResponseRedirect("../")

admin.site.unregister(User)
admin.site.register(User, CustomAdminUsers)

Получаем результат в конце после редактируемых полей.

0
 

Django как добавить кастом стили и скрипты js css. (Django add custom styles and js)

195

https://gist.github.com/rg3915/26076942ef4b0564cfa4b398a92c9b51

Как то мне потребовалось немного изменить стиль и js админки. В частности мульти селекта.
Для этого потребовалось подключение кастомных скриптов и стилей.

Собственно для этого используется класс Media в admin.py при регистрации модели. Пути стилей идут от статики из папки admin.

from .models import Post


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    class Media:
        css = {
            'all': ('/static/admin/css/custom.css',)
        }
        js = (
            '/static/admin/js/vendor/jquery/jquery.js',
            '/static/admin/js/vendor/select2/select2.full.js',
            '/static/admin/js/custom.js',
            )
0
 

October CMS BackendAuth получение данных бэкенд юзера

138

Иногда требуется информация от админа. То есть не просто юзера, а юзера именно из админки.
Для это используется класс BackendAuth.

use Backend\Facades\BackendAuth;

BackendAuth::check() // true false  проверка на юзера
BackendAuth::user() // вся информация о юзере
BackendAuth::user()->id // конкретное поле

Соответсвенно можете вытянуть любую нужную информацию

0
 

Woocommerce Почта РФ плагин фиксированная стоимость.

186

Есть плагин https://wordpress.org/support/plugin/russian-post-and-ems-for-woocommerce/.
У автора нет функционала фиксированной цены. По крайней мере на данный момент. Хотя в аналогичном сдек плагине это реализовано.

Как указано в топике самим автором https://wordpress.org/support/topic/фиксированная-стоимость-доставки/ можно сделать через фильтр.

add_filter( 'woocommerce_package_rates', 'override_ups_rates' );
function override_ups_rates( $rates ) {
    foreach( $rates as $rate_key => $rate ){
        // Check if the shipping method ID is UPS
        if( ($rate->method_id == 'rpaefw_post_calc') ) {
            // Set cost to zero
            $rates[$rate_key]->cost = 350;

        }
    }
    return $rates;
}

0
 

Woocommerce EKOM ошибка.

177

Почта России. Ошибка запроса для»price». CODE: 400 {«version_api»:2,»version»:»2.11.4.664″,»caption»:»Расчет тарифов»,»id»:53030,»name»:»ЕКОМ обыкновенный»,»mailtype»:53,»mailctg»:3,»directctg»:1,»from»:197022,»to»:919561,»weight»:100,»date»:20220701,»time»:230000,»postoffice»:[{«index»:197022,»tp»:1,»type»:3,»typei»:1,»name»:»САНКТ-ПЕТЕРБУРГ 22″,»regionid»:78,»regiono»:40000000,»region-main»:1,»area-main»:1,»placeid»:11102,»placeo»:40000000,»parent»:197000,»root»:190700,»courier»:190880,»pvz»:1,»item-check-view»:1,»move»:1,»weight-max»:20000,»pack-max»:99,»box»:190900},{«index»:919561,»tp»:2,»type»:16,»typei»:4,»name»:»РЯЗАНЬ-ПОЧТОМАТ (АПС)»,»regionid»:62,»regiono»:61701000001,»region-main»:1,»area-main»:1,»placeid»:17796,»placeo»:61701000001,»parent»:390700,»root»:390700,»partner»:1,»move»:1,»weight-max»:30000,»pay-card»:1,»rent»:1}],»errors»:[{«msg»:»ЕКОМ обыкновенный. Плата за доставку ЕКОМ (2635). ЕКОМ нельзя принять в 197022 \»САНКТ-ПЕТЕРБУРГ 22\». (1.11).»,»type»:1,»code»:2004}],»place»:»C5-r00-1″}

Сам задал сам отвечу)

Так понял, что указал ОПС который не поддерживает EKOM.
Соотв нужно указать тот, в котором есть.

0
 

Woocommerce javascript события (events)

170

Еще одна шпаргалка вновь :). Теперь по js событиям, которые можно использовать в определенных модулях woocommerce. Дело в том, что как бы ни хотелось или как бы ни не хотелось влезать скрипты checkout.js, cart.js, порой необходимо дописать некоторую логику, которая возникает после обновления корзины или обновления страницы checkout и тд.

//Woocommerce Checkout JS events
$( document.body ).trigger( 'init_checkout' );
$( document.body ).trigger( 'payment_method_selected' );
$( document.body ).trigger( 'update_checkout' );
$( document.body ).trigger( 'updated_checkout' );
$( document.body ).trigger( 'checkout_error' );
$( document.body ).trigger( 'applied_coupon_in_checkout' );
$( document.body ).trigger( 'removed_coupon_in_checkout' );

//Woocommerce cart page JS events
$( document.body ).trigger( 'wc_cart_emptied' );
$( document.body ).trigger( 'update_checkout' );
$( document.body ).trigger( 'updated_wc_div' );
$( document.body ).trigger( 'updated_cart_totals' );
$( document.body ).trigger( 'country_to_state_changed' );
$( document.body ).trigger( 'updated_shipping_method' );
$( document.body ).trigger( 'applied_coupon', [ coupon_code ] );
$( document.body ).trigger( 'removed_coupon', [ coupon ] );

//Woocommerce Add to cart JS events
$( document.body ).trigger( 'adding_to_cart', [ $thisbutton, data ] );
$( document.body ).trigger( 'added_to_cart', [ response.fragments, response.cart_hash, $thisbutton ] );
$( document.body ).trigger( 'removed_from_cart', [ response.fragments, response.cart_hash, $thisbutton ] );
$( document.body ).trigger( 'wc_cart_button_updated', [ $button ] );
$( document.body ).trigger( 'cart_page_refreshed' );
$( document.body ).trigger( 'cart_totals_refreshed' );
$( document.body ).trigger( 'wc_fragments_loaded' );

$( document.body ).trigger( 'init_add_payment_method' );

//Использование
jQuery('<event_target>').on('<event_name>', function(){
    console.log('<event_name> triggered');
});

//Пример
jQuery('body').on('init_checkout', function(){
    console.log('init_checkout triggered');
    // now.do.whatever();
});



0
 

Woocommerce шпараглка по корзине. Get cart data

187

Еще одна шпаргалочка, только уже по корзине

// $cart conditionals (if)
 WC()->cart->is_empty()
 WC()->cart->needs_payment()
 WC()->cart->show_shipping()
 WC()->cart->needs_shipping()
 WC()->cart->needs_shipping_address()
 WC()->cart->display_prices_including_tax()
 // Get $cart totals
 WC()->cart->get_cart_contents_count();
 WC()->cart->get_cart_subtotal();
 WC()->cart->subtotal_ex_tax;
 WC()->cart->subtotal;
 WC()->cart->get_displayed_subtotal();
 WC()->cart->get_taxes_total();
 WC()->cart->get_shipping_total();
 WC()->cart->get_coupons();
 WC()->cart->get_coupon_discount_amount( 'coupon_code' );
 WC()->cart->get_fees();
 WC()->cart->get_discount_total();
 WC()->cart->get_total();
 WC()->cart->total;
 WC()->cart->get_tax_totals();
 WC()->cart->get_cart_contents_tax();
 WC()->cart->get_fee_tax();
 WC()->cart->get_discount_tax();
 WC()->cart->get_shipping_total();
 WC()->cart->get_shipping_taxes();
 // Loop over $cart items
 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
    $product = $cart_item['data'];
    $product_id = $cart_item['product_id'];
    $quantity = $cart_item['quantity'];
    $price = WC()->cart->get_product_price( $product );
    $subtotal = WC()->cart->get_product_subtotal( $product, $cart_item['quantity'] );
    $link = $product->get_permalink( $cart_item );
    // Anything related to $product, check $product tutorial
    $attributes = $product->get_attributes();
    $whatever_attribute = $product->get_attribute( 'whatever' );
    $whatever_attribute_tax = $product->get_attribute( 'pa_whatever' );
    $any_attribute = $cart_item['variation']['attribute_whatever'];
    $meta = wc_get_formatted_cart_item_data( $cart_item );
 }
 // Get $cart customer billing / shipping
 WC()->cart->get_customer()->get_billing_first_name();
 WC()->cart->get_customer()->get_billing_last_name();
 WC()->cart->get_customer()->get_billing_company();
 WC()->cart->get_customer()->get_billing_email();
 WC()->cart->get_customer()->get_billing_phone();
 WC()->cart->get_customer()->get_billing_country();
 WC()->cart->get_customer()->get_billing_state();
 WC()->cart->get_customer()->get_billing_postcode();
 WC()->cart->get_customer()->get_billing_city();
 WC()->cart->get_customer()->get_billing_address();
 WC()->cart->get_customer()->get_billing_address_2();
 WC()->cart->get_customer()->get_shipping_first_name();
 WC()->cart->get_customer()->get_shipping_last_name();
 WC()->cart->get_customer()->get_shipping_company();
 WC()->cart->get_customer()->get_shipping_country();
 WC()->cart->get_customer()->get_shipping_state();
 WC()->cart->get_customer()->get_shipping_postcode();
 WC()->cart->get_customer()->get_shipping_city();
 WC()->cart->get_customer()->get_shipping_address();
 WC()->cart->get_customer()->get_shipping_address_2();
 // Other stuff
 WC()->cart->get_cross_sells();
 WC()->cart->get_cart_item_tax_classes_for_shipping();
 WC()->cart->get_cart_hash();
 WC()->cart->get_customer();
0
 

Woocommerce шпаргалка по Orders. Get orders data.

172

В некоторых задачах требуется получить объект заказа.
Вот некоторая шпаргалка.

// Get Order ID and Key
 $order->get_id();
 $order->get_order_key();
 // Get Order Totals $0.00
 $order->get_formatted_order_total();
 $order->get_cart_tax();
 $order->get_currency();
 $order->get_discount_tax();
 $order->get_discount_to_display();
 $order->get_discount_total();
 $order->get_fees();
 $order->get_formatted_line_subtotal();
 $order->get_shipping_tax();
 $order->get_shipping_total();
 $order->get_subtotal();
 $order->get_subtotal_to_display();
 $order->get_tax_location();
 $order->get_tax_totals();
 $order->get_taxes();
 $order->get_total();
 $order->get_total_discount();
 $order->get_total_tax();
 $order->get_total_refunded();
 $order->get_total_tax_refunded();
 $order->get_total_shipping_refunded();
 $order->get_item_count_refunded();
 $order->get_total_qty_refunded();
 $order->get_qty_refunded_for_item();
 $order->get_total_refunded_for_item();
 $order->get_tax_refunded_for_item();
 $order->get_total_tax_refunded_by_rate_id();
 $order->get_remaining_refund_amount();
 // Get and Loop Over Order Items
 foreach ( $order->get_items() as $item_id => $item ) {
    $product_id = $item->get_product_id();
    $variation_id = $item->get_variation_id();
    $product = $item->get_product();
    $product_name = $item->get_name();
    $quantity = $item->get_quantity();
    $subtotal = $item->get_subtotal();
    $total = $item->get_total();
    $tax = $item->get_subtotal_tax();
    $taxclass = $item->get_tax_class();
    $taxstat = $item->get_tax_status();
    $allmeta = $item->get_meta_data();
    $somemeta = $item->get_meta( '_whatever', true );
    $product_type = $item->get_type();
 }
 // Other Secondary Items Stuff
 $order->get_items_key();
 $order->get_items_tax_classes();
 $order->get_item_count();
 $order->get_item_total();
 $order->get_downloadable_items();
 $order->get_coupon_codes();
 // Get Order Lines
 $order->get_line_subtotal();
 $order->get_line_tax();
 $order->get_line_total();
 // Get Order Shipping
 $order->get_shipping_method();
 $order->get_shipping_methods();
 $order->get_shipping_to_display();
 // Get Order Dates
 $order->get_date_created();
 $order->get_date_modified();
 $order->get_date_completed();
 $order->get_date_paid();
 // Get Order User, Billing & Shipping Addresses
 $order->get_customer_id();
 $order->get_user_id();
 $order->get_user();
 $order->get_customer_ip_address();
 $order->get_customer_user_agent();
 $order->get_created_via();
 $order->get_customer_note();
 $order->get_address_prop();
 $order->get_billing_first_name();
 $order->get_billing_last_name();
 $order->get_billing_company();
 $order->get_billing_address_1();
 $order->get_billing_address_2();
 $order->get_billing_city();
 $order->get_billing_state();
 $order->get_billing_postcode();
 $order->get_billing_country();
 $order->get_billing_email();
 $order->get_billing_phone();
 $order->get_shipping_first_name();
 $order->get_shipping_last_name();
 $order->get_shipping_company();
 $order->get_shipping_address_1();
 $order->get_shipping_address_2();
 $order->get_shipping_city();
 $order->get_shipping_state();
 $order->get_shipping_postcode();
 $order->get_shipping_country();
 $order->get_address();
 $order->get_shipping_address_map_url();
 $order->get_formatted_billing_full_name();
 $order->get_formatted_shipping_full_name();
 $order->get_formatted_billing_address();
 $order->get_formatted_shipping_address();
 // Get Order Payment Details
 $order->get_payment_method();
 $order->get_payment_method_title();
 $order->get_transaction_id();
 // Get Order URLs
 $order->get_checkout_payment_url();
 $order->get_checkout_order_received_url();
 $order->get_cancel_order_url();
 $order->get_cancel_order_url_raw();
 $order->get_cancel_endpoint();
 $order->get_view_order_url();
 $order->get_edit_order_url();
 // Get Order Status
 $order->get_status();
 // Get Thank You Page URL
 $order->get_checkout_order_received_url();
0
 

October CMS простая сортировка. (simple sortable)

166

Я коснусь только простой сортировки, пока что. Довольно хороший гайд есть на форуме октобера https://octoclub.ru/d/21-sortable-simple-tree-nested-tree. Я опишу чуть детальней.

Например есть тестовый проект. В нем «магазин с товарами».

Я буду описывать кейс, когда уже есть лист продуктов. И только потом решили добавить сортировку. Это отличается тем, что у моих продуктов отсутствует ReorderController. При создании новой сущности, указываете, чтобы он был доступен.

При указании данного параметра создадутся вьюшка и некоторые настройки контроллера. У меня этого нет, поэтому буду делать вручную, заодно детальней.

Сначала указываю настройки reorder в контроллере, подключаю Behaivor и файл yaml.

<?php namespace Alex\Store\Controllers;use Backend\Classes\Controller;
use BackendMenu;class Prod extends Controller
{
    public $implement = [
        'Backend\Behaviors\ListController',
        'Backend\Behaviors\FormController',
        'Backend\Behaviors\ReorderController',   // добавил
    ];
    
    public $listConfig = 'config_list.yaml';
    public $formConfig = 'config_form.yaml';
    public $reorderConfig = 'config_reorder.yaml';  // добавил    public function __construct()
    {
        parent::__construct();
        BackendMenu::setContext('Alex.Store', 'main-menu-item');
    }
}

Далее вставляю config_reorder.yaml в папке /controllers/prod. Соотв указываю заголовок, модель. NameForm скажу чуть позже для чего.

title: 'Сортировка продуктов'                 
modelClass: Alex\Store\Models\Prod
nameFrom: title
toolbar:
    buttons: reorder_toolbar

Добавляем собственно вьюшку reorder.htm в туже папку с нашим контроллером /controllers/prod, так как сортировка происходит на отдельной странице.

<?php Block::put('breadcrumb') ?>
    <ul>
        <li><a href="<?= Backend::url('alex/store/prod/reorder') ?>">Category</a></li>
        <li><?= e($this->pageTitle) ?></li>
    </ul>
<?php Block::endPut() ?><?= $this->reorderRender() ?>

Далее в list tool bar добавляем кнопку на вьюшку.

<a href="<?= Backend::url('alex/store/prod/reorder') ?>" class="btn btn-default oc-icon-list"><?= e(trans('backend::lang.reorder.default_title')) ?></a>

Так отлично. Появилась кнопка а админке.

Собственно этой всей настройки можно было избежать и октобер бы сам сгенерировал как надо, при указании галочки ReorderBehavior, о чем говорил в самом начале.

Кнопка появилась, но при переходе с нее возникает ошибка, говорящая о том, что мы еще не добавили в модель Трейт сортировки.

Добавляем в нашу модель.

use \October\Rain\Database\Traits\Sortable; // Для Sortable

И далее, чтобы этот трейт корректно отрабатывал нашей моделе и соотв таблице нужно поле sort_order. Добавляем его. Тип число, nullable true.

Отлично. Заходим в сортировку. И лично у меня это выглядит так. Есть итемы, но они пустые. Это собственно поле nameForm, которое указывается в config_reorder.yaml. У меня было написано title, хотя исходя из моей таблицы нужно указать name.

Указал name. Результат

!Есть один нюанс. Он касается поля sort_order. В идеале это все делать на новой сущности. Но если мы редактируем существующую, то у поля sort_order будет стоять 0 или Null в таблице. Как было подмечено в статье https://octoclub.ru/d/21-sortable-simple-tree-nested-tree надо в ручном режиме проставить валидные значения для текущих итемов.

В моем случае все не сложно и выглядит так.

Отлично. Все почти готово. Для примера я хочу выделить классный Сыр и поставить его первым. При стандартном получении из модели, сортировка происходит автоматически.

// это из компонента Prod
public function init()
{
    $this->items =  ProdModel::get();
}

Результат

Если же вы получаете итемы другими способами, то может указать дополнительно orderBy по полю sort_order.

0
 

October CMS перевод сайта. (Translate site). Часть 3

440

В прошлой статье мы добились перевода контента путем локализации моделей. Продолжим дальше дорабатывать наш сайт. Как сказал, я долокализировал валюту и цену. Цену мне пришлось перевести из типа Number в строку, чтобы плагин отработал. Тк в идеале цена 60 рублей идет как 1 доллар, то есть числовые значения разные. Можно было решить разными путями, я сделал так.

Локализация ссылок

В нашем случае рассматриваем ссылки, которые находятся в шапке. RainlabTranslate предусмотрено, форматирование ссылки при назначении ее через фильтр page.

<ul class="nav navbar-nav">
    <li class="separator hidden-xs"></li>
    <li class="{% if this.page.id == 'home' %}active{% endif %}"><a href="{{ 'home'|page }}">Basic concepts</a></li>
    <li class="{% if this.page.id == 'ajax' %}active{% endif %}"><a href="{{ 'ajax'|page }}">AJAX framework</a></li>
    <li class="{% if this.page.id == 'plugins' %}active{% endif %}"><a href="{{ 'plugins'|page }}">Plugin components</a></li>
    <li class="{% if this.page.id == 'plugins' %}active{% endif %}"><a href="{{'store'|page}}">Store</a></li>
</ul>

При переключении языка, автоматически меняется локаль ссылки. В принципе это охватывает большинство кейсов связанных с роутингом внутри сайта. Те ссылки что присутствуют в контенте, мы можем менять через локализацию модели (см. статью 2).

Переменные локализации

О них хорошо описано как в документации так и в источниках из первой части цикла.
Вкратце. Есть переменные, которые объявляются фильтром.

{{'Store' |_}}

Чтобы RainlabTranslate ее обнаружил нам необходимо просканировать.

Далее заполняем ее для всех локалей.

И получаем результат. В данном случаем я привел пример на меню. Но переменные можно использовать в других функциональных компонентах сайта, таких как формы, нейминги полей, селектов, чекбоксов, ссылки футера, кнопок «скачать, купить, перейти», валидационные выражения и тд.

Локализация SEO

0