State Address Registry widget

To prepare the widget of the State Address Registry, in addition to the base, we also need nginx and its plugins postgres and json… You can use ready-made

A large json function gar_select was defined in the database and returns json. This is exactly what you need to use through nginx.

So, first we add (to the http section outside the server sections) an upstream to connect to the database:

upstream gar {
  	# подключаемся к хосту postgres
  	# на порт 5432
  	# к базе gar
  	# под пользователем gar
    postgres_server host=postgres port=5432 dbname=gar user=gar;
  	# задаём лог для апстрима
    postgres_log /var/log/nginx/gar.err;
  	# будем держать по одному постоянному соединению с базой
  	# (на каждый рабочий процеесс)
    postgres_keepalive 1;
  	# будем держать по одному подготовленному оператору
  	# (на каждое постоянное соединение с базой)
    postgres_prepare 1 overflow=deallocate;
}

and in the server section we add the use

location =/gar_select.json {
  	# задаём логи
    access_log /var/log/nginx/gar_select.json.log main;
    error_log /var/log/nginx/gar_select.json.err;
  	# направляем запрос на определённый выше апстрим
    postgres_pass gar;
  	# разрешаем использование подготовленных операторов
    postgres_prepare true;
  	# задаём собственно запрос
  	# (просто вызывая ту самую функцию gar_select,
  	# передавая ей в качестве аргумента GET-параметры
  	# в виде json)
  	# ** сразу хочу предупредить: здесь НЕТ sql-инъекций! **
    postgres_query "select gar_select($json_get_vars::json)";
  	# в результате выдаём json
    postgres_output json;
}

That. we get something like a REST interface to the gar table, which we will use from JavaScript.

First, let’s load styles and scripts into index.html

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title>ГАР</title>
        <link rel="stylesheet" href="https://habr.com/static/bootstrap-5.1.1/css/bootstrap.css" />
        <link rel="stylesheet" href="https://habr.com/static/select2-4.1.0-rc.0/css/select2.css" />
        <script src="https://habr.com/static/jquery-3.4.1/jquery-3.4.1.js"></script>
        <script src="https://habr.com/static/bootstrap-5.1.1/js/bootstrap.js"></script>
        <script src="https://habr.com/static/select2-4.1.0-rc.0/js/select2.js"></script>
        <script src="https://habr.com/static/select2-4.1.0-rc.0/js/i18n/ru.js"></script>
        <script src="https://habr.com/ru/post/585476/index.js"></script>
    </head>
    <body>
        <form action="gar_select.html" method="get"><table class="table"><tr>
            <td>gar-select:</td>
            <td><select id="id_label_single" class="gar-select" name="id" data-width="100%" data-placeholder="Адрес" data-initial="9ae64229-9f7b-4149-b27a-d1f6ec74b5ce" data-id="[b5701907-1537-4e52-b93a-d566a47086f7,daa79543-d4e1-4cf7-bd3a-5936e670aea8]"/></td>
            <td><input class="btn btn-primary" type="submit" value="OK"/></td>
        </tr></table></form>
    </body>
</html>

and here is the index.js script itself

// после загрузки страницы
$(document).ready(function() {
  	// для каждого селекта с соответствующим классом
    $('select.gar-select').each(function(number, select) {
      	// определяем функцию от элемента
        function data(item) {return {
          	// возвращающую строку для select2
            id: $.isArray(item) ? item[item.length - 1].id : item.id,
            item: item,
            text: $.isArray(item) ? item.map(function(item) {return item.text}).join(', ') : item.text,
        }}
      	// а также функцию от элемента для его применения в select2
        function select2(item) {$(select).data('select2').trigger('select', {data: data(item)})}
      	// инициализируем select2
        $(select).select2({
          	// будем делать запросы
            ajax: {
              	// формируем GET-параметры для функции gar_select
                data: function(params) {return {
                    full: !$.isEmptyObject(select.value) || ($.isEmptyObject(params.term) && $.isEmptyObject(select.value) && !$.isEmptyObject(select.dataset.id)),
                    id: $.isEmptyObject(params.term) && $.isEmptyObject(select.value) && !$.isEmptyObject(select.dataset.id) ? select.dataset.id : undefined,
                    limit: select.dataset.limit || 10,
                    offset: ((params.page || 1) - 1) * (select.dataset.limit || 10),
                    parent: !$.isEmptyObject(select.value) ? select.value : undefined,
                    text: params.term,
                }},
                dataType: 'json', // задаём тип результата
                delay: 300, // и задержку
              	// задаём функцию обработки результата
                processResults: function(response, params) {return {
                    results: response.data.map(data),
                    pagination: {more: response.offset < response.count}
                }},
              	// адрес можно задать прямо в теге
                url: select.dataset.url || 'gar_select.json',
            },
            allowClear: true, // показываем кнопку очистки
            closeOnSelect: false, // не закрываем при выборе
            dropdownParent: $(select).parent(), // располагаем в родителе
            language: 'ru', // использеум русский язык
        })
      	// при нажатии на кнопку очистки
        $(select).on('select2:clear', function(event) {
          	// получаем элемент
            var item = event.params.data[0].item
            if ($.isArray(item)) {
              	// и если ещё не дошли до верха
                item = item.slice(0, -1)
              	// то показываем родителя
                if (item.length) {select2(item)}
            }
          	// открываем выбор
            setTimeout(function() {$(select).select2('open')})
        })
      	// при выборе
        $(select).on('select2:select', function(event) {
          	// запускаем поиск
            $(select).data('select2').trigger('query', {})
          	// очищаем строку поиска
            $(select).parent().find('.select2-search__field').val('').trigger('focus')
        })
      	// если задан первоначальный уид
        if (select.dataset.initial) {$.ajax({
          	// выполняем запрос
            url: $(select).data('select2').options.options.ajax.url,
            dataType: $(select).data('select2').options.options.ajax.dataType,
            data: {id: select.dataset.initial}
        }).done(function(response) {
          	// и выбираем результат
            select2(response.data)
          	// закрываем выбор
            $(select).select2('close')
        })}
    })
})

Also, using plugins evaluate and mustach you can organize an html interface:

location =/gar_select.html {
  	# задаём логи
    access_log /var/log/nginx/gar_select.html.log main;
    error_log /var/log/nginx/gar_select.html.err;
  	# вычисляем соответсвующие под-запросы
    # в соотвествующие переменные
    evaluate $json /gar_select.json;
    evaluate $template /gar_select.template;
  	# задаём данные для шаблонизатора
    mustach_json $json;
  	# задаём шаблон
    mustach_template $template;
  	# задаём тип результата
    mustach_content text/html;
}
location =/gar_select.template {
    access_log /var/log/nginx/gar_select.template.log main;
    error_log /var/log/nginx/gar_select.template.err;
    internal;
}

The actual gar_select.template template itself

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <title>ГАР</title>
        <link rel="stylesheet" href="https://habr.com/static/bootstrap-5.1.1/css/bootstrap.min.css" />
        <script src="https://habr.com/static/jquery-3.4.1/jquery-3.4.1.min.js"></script>
        <script src="https://habr.com/static/bootstrap-5.1.1/js/bootstrap.min.js"></script>
        <script>
            var limit = {{limit}};
            var offset = {{offset}};
            var count = {{count}};
        </script>
        <script src="https://habr.com/ru/post/585476/gar_select.js"></script>
    </head>
    <body>
        <form action="" method="get">
            <input type="hidden" name="child" value="true"/>
            <table class="table table-bordered table-striped table-hover table-condensed text-nowrap">
                <thead>
                    <th class="id" title="id"><input style="width:100%" class="offset" placeholder="id" name="id" value="{{query.id}}"/></th>
                    <th class="parent" title="parent"><input style="width:100%" class="offset" placeholder="parent" name="parent" value="{{query.parent}}"/></th>
                    <th class="name" title="name"><input style="width:100%" class="offset" placeholder="name" name="name" value="{{query.name}}"/></th>
                    <th class="short" title="short"><input style="width:100%" class="offset" placeholder="short" name="short" value="{{query.short}}"/></th>
                    <th class="type" title="type"><input style="width:100%" class="offset" placeholder="type" name="type" value="{{query.type}}"/></th>
                    <th class="post" title="post"><input style="width:100%" class="offset" placeholder="post" name="post" value="{{query.post}}"/></th>
                    <th class="object" title="object"><input style="width:100%" class="offset" placeholder="object" name="object" value="{{query.object}}"/></th>
                    <th class="region" title="region"><input style="width:100%" class="offset" placeholder="region" name="region" value="{{query.region}}"/></th>
                    <th class="text" title="text"><input style="width:100%" class="offset" placeholder="text" name="text" value="{{query.text}}"/></th>
                    <th class="child" title="child"><input style="width:100%" class="" type="submit" value="child"/></th>
                </thead>
                {{#data}}
                <tr>
                    <td class="id" title="id">{{id}}</td>
                    <td class="parent" title="parent"><a href="https://habr.com/ru/post/585476/gar_select.html?child=true&id={{parent}}">{{parent}}</a></td>
                    <td class="name" title="name">{{name}}</td>
                    <td class="short" title="short">{{short}}</td>
                    <td class="type" title="type">{{type}}</td>
                    <td class="post" title="post">{{post}}</td>
                    <td class="object" title="text">{{object}}</td>
                    <td class="region" title="text">{{region}}</td>
                    <td class="text" title="text">{{text}}</td>
                    <td class="child" title="child"><input name="child" type="button" value="{{child}}" parent="{{id}}"/></td>
                </tr>
                {{/data}}
            </table>
            <table class="table"><tr>
                <td id="pagination"/>
                <td>limit: <input title="limit" class="offset" placeholder="limit" name="limit" value="{{limit}}"/></td>
                <td>offset: <input title="offset" class="" placeholder="offset" name="offset" value="{{offset}}"/></td>
                <td id="record"/><td>Всего {{count}}</td>
            </tr></table>
        </form>
    </body>
</html>

and a script for pagination gar_select.js

// после загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
  	// вычисляем количество страниц
    var pages = Math.ceil(count / limit)
    // вычисляем текущую страницу
    var page = Math.ceil(offset / limit) + 1
    // если все резульаты помещяются на одну страницу
    if (count <= limit) {
      	// то скрываем пагинацию
        $('#pagination').parent().parent().hide()
    }
  	// если есть результаты
    if (count > 0) {
      	// то добавляем информацию об этом
        $('#record').append('Записи с ' + (offset + 1) + ' по ' + Math.min(offset + limit, count))
    } else {
      	// иначе - скрываем
        $('#record').hide()
    }
  	// рассчитываем кнопки пагинации
    for (var pag = 1, more = false; pag <= pages; pag++) {
        if (pages <= 15 || pag <= 3 || pag >= pages - 2 || (page - 1 <= pag && pag <= page + 1)) {
            $('#pagination').append(
                $('<input>', {
                    type: 'button',
                    value: pag,
                    disabled: pag == page,
                }).click(function(event) {
                    $('input[name=offset]').val((this.value - 1) * limit)
                    $('form').submit()
                })
            )
            more = false
        } else if (!more && (pag == 4 || pag == pages - 3)) {
            $('#pagination').append($('<span>').text('...'))
            more = true
        }
    }
  	// при изменении в верхних фильтрах
    $('input.offset').change(function(event) {
      	// сбрасываем офсет
        $('input[name=offset]').val(0)
    })
  	// по клике на дочерних
    $('input[name=child]').click(function(event) {
      	// сбрасываем офсет
        $('input[name=offset]').val(0)
      	// и всё, кроме количества
        $('input.offset').not('[name=limit]').val('')
      	// задаём родителя
        $('input[name=parent]').val($(this).attr('parent'))
      	// и отправляем форму
        $('form').submit()
    })
})

As an example that the plugin evaluate can handle nested sub-queries, and as an example of how the plugin works htmldoc you can convert html to pdf:

location =/gar_select.pdf {
  	# задаём логи
    access_log /var/log/nginx/gar_select.pdf.log main;
    error_log /var/log/nginx/gar_select.pdf.err;
  	# вычисляем под-запрос в переменную
    evaluate $html /gar_select.html;
  	# преобразуем html в pdf
    html2pdf $html;
}

Thus, you can use the widget anywhere you need to select an address. It can also be used for searching. For example, if you need to “find all services on a certain street”, then the user selects a street in the widget, the backend gets the view of this street, and using the ready-made gar_select_child function, it gets the view of all child elements (houses, apartments, rooms, .. .) with which it can already filter in its database.

-- создаём или меняем функцию от ...
CREATE OR REPLACE FUNCTION gar_select_child(parent uuid, name text, short text, type text, post text, region text) RETURNS SETOF gar_view LANGUAGE sql STABLE AS $body$
    with _ as (
        with recursive _ as ( -- рекурсивно
          	-- начиная сверху или с заданного элемента
            select gar.*, 0 as i from gar where (gar_select_child.parent is null and parent is null) or id = gar_select_child.parent
            union
          	-- и продолжая вниз
            select gar.*, _.i + 1 as i from gar inner join _ on _.id = gar.parent
        ) select * from _ where i > 0
      	-- добавляя если нужно различные фильтры
        and (gar_select_child.name is null or name ilike gar_select_child.name||'%' or name ilike '% '||gar_select_child.name||'%' or name ilike '%-'||gar_select_child.name||'%' or name ilike '%.'||gar_select_child.name||'%')
        and (gar_select_child.short is null or short ilike gar_select_child.short)
        and (gar_select_child.type is null or case when gar_select_child.type ilike '{%}' then type = any(gar_select_child.type::text[]) else type ilike gar_select_child.type||'%' end)
        and (gar_select_child.post is null or post ilike gar_select_child.post||'%')
        and (gar_select_child.region is null or case when gar_select_child.region ilike '{%}' then region = any(gar_select_child.region::smallint[]) else region = gar_select_child.region::smallint end)
        order by i
    ) select id, parent, name, short, type, post, region, gar_text(name, short, type) AS text from _ order by to_number('0'||name, '999999999'), name;
$body$;

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *