Пишем web на Python3, администрируем пользователей приложения

newbie

Опубликован:  2019-04-04T12:10:37.305921Z
Отредактирован:  2019-04-16T11:05:24.117154Z
Продолжаем работу над проектом selfish. На текущем этапе разработки приложения я продолжу заниматься системой администрирования пользователей и создам страницу, которая будет содержать многостраничный список всех пользователей сервиса, а для администраторов сервиса на этой странице будет предусмотрен инструмент для создания новых пользовательских аккаунтов.

1. В предыдущих сериях

Selfish - web-приложение на базе Python3 и Flask, начальная конфигурация и разработка которого детально описаны в предыдущих выпусках этого блога. Все посвященные разработке этого приложения выпуски можно отфильтровать по метке selfish.

2. Рефакторинг

Прежде чем приступить к разработке следующей функциональной страницы selfish, мне нужно привести в порядок некоторый повторяющийся код на стороне клиента. Начну с JavaScript.

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

UFc9jaej3w.png

LS2mUlC23C.png

Строчки с третьей по десятую можно вполне надёжно и безопасно упаковать в служебную функцию и заменить в конечных сценариях вызовом этой функции с соответствующими параметрами. Кроме этого, в каталоге selfish/static/js у меня находится два файла с почти одинаковыми именами: show_datetime.js и show-datetime.js.

xkUAyev85I.png

Переименовываю файл show-datetime.js.

git mv -v selfish/static/js/show-datetime.js selfish/static/js/today_field.js

3KGmkvfRIV.png

Открываю переименованный файл в редакторе и дописываю в него ещё одну функцию.

function renderTodayField(cls, moment) {
  $(cls).text(showDateTime(moment));
  setInterval(
    function() {
      $(cls).each(function() {
        $(this).text(showDateTime(moment));
      });
    }, 1000);
}

FDKIfazafy.png

Теперь необходимо поправить блоки scripts в двух целевых шаблонах: index.html и auth_base.html. Начну с первого - index.html, открываю файл в редакторе и привожу его блок scripts к следующему виду.

{% block scripts %}
  {{ super() }}
  {% assets filters='jsmin', output='generic/js/main/index.js',
            'js/custom_moment.js', 'js/today_field.js',
            'js/main/index.js' %}
    <script src="{{ ASSET_URL  }}"></script>
  {% endassets %}
{% endblock scripts %}

А блок scripts файла auth_base.html будет иметь такой вид.

{% block scripts %}
  {{ super() }}
  {% assets filters='jsmin', output='generic/js/auth/auth.js',
            'js/custom_moment.js', 'js/today_field.js', 'js/auth/auth.js' %}
    <script src="{{ ASSET_URL }}"></script>
  {% endassets %}
{% endblock scripts %}

Изменения необходимо отразить в целевых сценариях, теперь оба файла: index.js и auth.js будут выглядеть одинаково.

$(document).ready(function() {
  moment = customizeMoment(moment);
  renderTodayField('.today-field', moment);
});

В процессе дальнейшей разработки эти файлы получат дополнительный код и перестанут повторять друг друга.

Второй момент, который меня не устраивает - это таблицы стилей на странице входа в сервис. Оформление формы описано в файле auth/form-block.css, открываю этот файл в редакторе, выделяю в нём и вырезаю в буфер обмена следующий код.

.form-form {
  margin: 0;
}

.form-group {
  margin: 6px 0 0;
}

.form-label {
  width: 25%;
  float: left;
  padding-top: 7px;
}

.checkbox {
  margin: 0;
}

.checkbox label label {
  padding-left: 0;
}

.form-input {
  margin-left: 26%;
}

Создаю новый файл.

touch selfish/static/css/labeled-form.css

Открываю его в редакторе и копирую в него код из буфера обмена.

Переименовываю файл form-block.css.

git mv -v selfish/static/css/auth/form-block.css selfish/static/css/auth/auth.css

Отражаю изменения в блоке styles шаблона auth_base.html.

{% block styles %}
  {{ super() }}
  {% assets filters='cssmin', output='generic/css/auth/auth.css',
            'css/base.css', 'css/content-block.css',
            'css/labeled-form.css', 'css/auth/auth.css' %}
    <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
  {% endassets %}
{% endblock styles %}

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

0dv0V9ViNc.png

6cVrFCKgTv.png

Убеждаюсь, что часики тикают, а оформление формы не изменилось. Можно двигаться дальше. Теперь стили этой формы и вызов функции, отображающей часы, я могу беспрепятственно использовать на других разрабатываемых страницах, не нарушая при этом принципа DRY (don't repeat yourself).

3. Постраничная фильтрация и отображение данных

На текущий момент наше приложение имеет функциональную страницу с ограниченным доступом по адресу /admin/society.

9reObMYYcj.png

Пришло время разработать для этой страницы соответствующий функционал. Я планирую, что все пользователи сервиса, имеющие доступ к этой странице, увидят на ней список всех зарегистрированных в системе пользователей. Этот список будет содержать ссылки на профили пользователей, дату регистрации и название группы. Если в сервисе зарегистрируется большое количество пользователей, то их список необходимо будет разделить на соответствующее количество страниц, для которых внизу списка будут расположены соответствующие ссылки. Для реализации задуманного мне понадобится новый функциональный элемент. Создаю новый файл.

touch selfish/templates/macros/_admin.html

Открываю этот файл в редакторе и определяю в нём новый macro.

{% macro pagination_widget(pagination, endpoint) %}
  <ul class="pagination pagination-sm">
    <li {% if not pagination.has_prev %}class="disabled hidden"{% endif %}>
      <a href="{% if pagination.has_prev %}
                 {{ url_for(endpoint, page=pagination.page-1, **kwargs) }}
               {% else %}#{% endif %}">&laquo;</a>
    </li>
    {% for p in pagination.iter_pages() %}
      {% if p %}
        <li {% if p == pagination.page %}class="active"{% endif %}>
          <a href="{{ url_for(endpoint, page=p, **kwargs) }}">{{ p }}</a>
        </li>
      {% else %}
        <li class="disabled">
          <a href="#">&hellip;</a>
        </li>
      {% endif %}
    {% endfor %}
    <li {% if not pagination.has_next %}class="disabled hidden"{% endif %}>
      <a href="{% if pagination.has_next %}
                 {{ url_for(endpoint, page=pagination.page+1, **kwargs) }}
               {% else %}#{% endif %}">&raquo;</a>
    </li>
  </ul>
{% endmacro %}

При помощи этого macro я буду отображать постраничные ссылки на всех функциональных страницах сервиса, требующих многостраничного вывода.

Список пользователей я тоже опишу при помощи соответствующего macro, что даст мне возможность не повторять каждый раз длинный html-фрагмент, а замещать его простым вызовом. Пишу новый macro в этот же файл.

{% macro show_society(items, page, view, format_date) %}
  <div class="content-block slidable {% if page == 1 %}next-block{% endif %}">
    <div class="block-header">
      <h3 class="panel-title">
        {% if view == 1 %}
          Список пользователей сервиса
        {% elif view == 2 %}
          Список найденных по запросу пользователей
        {% endif %}
      </h3>
    </div>
    <div class="block-body">
      {% if items %}
        <table class="society-table">
          <tbody>
            <tr>
              <th>Псевдоним</th>
              <th>Группа</th>
              <th class="text-right">Зарегистрирован</th>
            </tr>
            {% for user in items %}
              <tr {% if not loop.last %}class="bordered"{% endif %}>
                <td class="username-field">
                  <a href="{{ url_for(
                              'main.show_profile', username=user.username) }}">
                    {{ user.username }}</a>
                </td>
                <td>{{ user.group }}</td>
                <td class="date-field text-right">
                  {{ format_date(user.registered) }}
                </td>
              </tr>
            {% endfor %}
          </tbody>
        </table>
      {% else %}
        <div class="alert alert-warning">
          В сервисе нет активных аккаунтов.
        </div>
      {% endif %}
    </div>
  </div>
{% endmacro %}

Открываю файл selfish/admin/views.py и переписываю функцию представления show_users следующим образом.

from flask import current_app, render_template, request
from flask_login import current_user, login_required

...
from ..models.auth import User
...


@admin.route('/society')
@login_required
@permission_required(permissions.FOLLOW_USERS)
def show_users():
    pagination = User.query.filter(User.username != current_user.username)\
        .order_by(User.last_visit.desc()).paginate(
        page=request.args.get('page', 1, type=int),
        per_page=current_app.config.get('USERS_PER_PAGE', 3),
        error_out=True)
    return render_template('admin/society.html', pagination=pagination)

Открываю в редакторе файл шаблона admin/society.html и полностью переписываю в нём блок page_content.

{% block page_content %}
  <!-- admin/society.html -->
  {% set messages = get_flashed_messages() %}
  {% if messages %}
    <div class="top-flashed-block">
      {{ get_message(messages) }}
    </div>
  {% endif %}
  {% if pagination.page == 1 %}
    <h4>Страница в стадии разработки</h4>
  {% endif %}
  <div id="center-panel">
    {{ show_society(pagination.items, pagination.page, 1, format_date) }}
    {% if pagination.has_prev or pagination.has_next %}
      <div class="next-block entity-pagination text-center">
        {{ pagination_widget(pagination, 'admin.show_users') }}
      </div>
    {% endif %}
  </div>
{% endblock page_content %}

Поскольку в этом коде использованы новые macro, их нужно импортировать, дописываю соответствующие импорты вверху шаблона.

{% extends "base.html" %}
{% from "macros/_admin.html" import pagination_widget %}
{% from "macros/_admin.html" import show_society %}
{% from "macros/_auth.html" import get_message %}

В режиме отладки сервис должен отображать на странице три учётные записи, всего в базе данных на текущий момент зарегистрировано четыре пользователя, так как текущий пользователь в списке не должен отображаться, чтобы на странице появился виджет постраничного отображения, мне необходим ещё один пользователь. Создаю ещё одного администратора при помощи соответствующей команды менеджера проекта.

TamGdQGVrJ.png

Посмотрим на страницу в браузере, вернее на две страницы.

8l5txBjm3H.png

knp3IP9ClD.png

Выглядит ужасно, над внешним видом я поработаю чуть позже, но функциональные возможности страницы работают уже сейчас. Список зарегистрированных пользователей разделён на две страницы, в нём для каждого пользователя имеется ссылка на профиль пользователя, а внизу страницы появился виджет постраничного отображения с соответствующими ссылками, текущий пользователь в этом списке отсутствует, а другие пользователи показаны в порядке следования в соответствии с данными в поле last_visit базы данных. В режиме отладки на каждой странице будет отображаться не более 3-х пользователей. Это число можно регулировать. Открываю файл config.py и дописываю классу Production новое свойство.

class Production(Config):
    SEND_FILE_MAX_AGE_DEFAULT = int(timedelta(days=28).total_seconds())
    USERS_PER_PAGE = 30    # новое свойство
    SQLALCHEMY_DATABASE_URI = \
        'postgresql+psycopg2://{0}:{1}@192.168.56.101/selfish'.format(
            os.getenv('SDBU', 'newbie'),
            os.getenv('SDBP', 'aa'))

Именно это свойство определяет, сколько пользователей в списке будет отображаться на одной странице.

4. Регистрация аккаунтов администратором сервиса

Администратор selfish - персона в рамках сервиса весьма могущественная, он может изменять разрешения пользователей. Сейчас я дам администратору возможность создавать аккаунты пользователей непосредственно в web-интерфейсе системы.

Для создания аккаунта пользователя администратором сервиса мне потребуется соответствующая форма. Создаю новый файл.

touch selfish/admin/forms.py

Открываю этот файл в редакторе и создаю в нём класс новой формы.

from flask_wtf import FlaskForm
from wtforms.fields import StringField, PasswordField, SubmitField
from wtforms.validators import (
    DataRequired, Email, EqualTo, Length, Regexp, ValidationError)

from ..models.auth import Account, User


class CreateUser(FlaskForm):
    username = StringField(
        'Псевдоним:',
        validators=[DataRequired(message='без псевдонима никак, увы'),
                    Length(min=3, max=16,
                           message='от 3-х до 16-ти знаков'),
                    Regexp(r'^[A-Za-z][a-zA-Z0-9\-_.]*$',
                           message='латинские буквы, цифры, дефис, знак \
                                   подчёркивания, точка, первый символ - \
                                   латинская буква')])
    address = StringField(
        'Адрес эл.почты:',
        validators=[DataRequired(message='без почты никак, увы'),
                    Email(message='нужен адрес электронной почты'),
                    Length(max=128,
                           message='максимальная длина адреса - 128 знаков')])
    password = PasswordField(
        'Новый пароль:',
        validators=[DataRequired(message='без пароля никак, увы'),
                    EqualTo('confirmation', message='пароли не совпадают')])
    confirmation = PasswordField(
        'Повторите:',
        validators=[DataRequired(message='без пароля никак, увы'),
                   EqualTo('password', message='пароли не совпадают')])
    submit = SubmitField('Создать')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('этот псевдоним уже используется')

    def validate_address(self, field):
        account = Account.query.filter_by(address=field.data).first()
        swapped = Account.query.filter_by(swap=field.data).first()
        if (account and account.user) or swapped:
            raise ValidationError('этот адрес уже используется')

Здесь следует обратить внимание, что форма имеет достаточно обширный набор валидаторов, которые ограничивают ввод в эту форму пользователем неверных данных. Теперь я могу импортировать эту форму в файл admin/views.py и правильно её применить в функции представления show_users.

from flask import (
    current_app, flash, redirect, render_template, request, url_for)
...
from .. import db
...
from ..models.auth import Account, Permission, User
...
from .forms import CreateUser


@admin.route('/society', methods=['GET', 'POST'])
@login_required
@permission_required(permissions.FOLLOW_USERS)
def show_users():
    form = None
    if current_user.can(permissions.ADMINISTER):
        form = CreateUser()
        if form.validate_on_submit():
            account = Account.query.filter_by(address=form.address.data).first()
            if not account:
                account = Account(address=form.address.data)
            user = User(
                username=form.username.data,
                password=form.password.data,
                permissions=[permission.permission for permission in
                             Permission.query.filter_by(initial=True)])
            user.account = account
            db.session.add(user)
            db.session.commit()
            flash('Создан аккаунт пользователя {0}.'.format(user.username))
            return redirect(
                url_for('main.show_profile', username=user.username))
    pagination = User.query.filter(User.username != current_user.username)\
        .order_by(User.last_visit.desc()).paginate(
        page=request.args.get('page', 1, type=int),
        per_page=current_app.config.get('USERS_PER_PAGE', 3),
        error_out=True)
    return render_template(
        'admin/society.html', pagination=pagination, form=form)

Логика достаточно проста. Создаётся переменная form в значении None. Если текущий пользователь имеет разрешение ADMINISTER, то переменная form становится экземпляром класса CreateUser. Функция преставления получает возможность обрабатывать запросы по методу POST. Если форма в запросе POST проходит валидацию без ошибок, то создаётся аккаунт пользователя и пользователь в соответствии с данными переданными в форме POST-запроса, созданный пользователь получает разрешения, заданные в соответствующей таблице базы данных с меткой initial. В завершение этих действий система перенаправляет текущего пользователя на страницу профиля созданного пользователя, где ему можно назначить новый набор разрешений.

Чтобы форма появилась на странице, её следует интерпретировать в шаблоне. Возвращаюсь к файлу шаблона admin/society.html в редакторе, нахожу в этом шаблоне такой блок.

{% if pagination.page == 1 %}
    <h4>Страница в стадии разработки</h4>
  {% endif %}

И полностью его переписываю.

  {% if pagination.page == 1 %}
    {% if form %}
      <div class="content-block slidable">
        <div class="block-header">
          <h3 class="panel-title">Создайте новый аккаунт пользователя</h3>
        </div>
        <div class="block-body to-be-hidden">
          <div class="today-field"></div>
          <div class="form-help">
            <p>
              Здесь можно создать новый аккаунт пользователя, для этого
              заполните форму ниже. Имейте ввиду, псевдоним должен быть
              уникальным, может осдержать от 3-х до 16-ти знаков, включая
              латинские буквы, цифры, знак подчёркивания, дефис, точку и должен
              начинаться с латинской буквы. Адрес электронной почты нового
              ползователя должен умещаться в 128 знаков, адрес должен быть
              уникальным и незарегистрированным в этом сервисе.
            </p>
          </div>
          <div class="form-form">
            <form class="form" method="POST" role="form">
              {{ form.hidden_tag() }}
              {{ render_form_group(
                  form, form.username, 'username', 'введите псевдоним') }}
              {{ render_form_group(
                  form, form.address, 'address', 'введите адрес эл. почты') }}
              {{ render_form_group(
                  form, form.password, 'password', 'введите новый пароль') }}
              {{ render_form_group(
                  form, form.confirmation,
                  'confirmation', 'повторите пароль') }}
              <div class="form-group">
                <div class="form-input">
                  {{ form.submit(class="btn btn-primary btn-block") }}
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    {% endif %}
    {% if pagination.has_next %}
      <div class="content-block find-username
                  {% if form %}next-block{% endif %}">
        <div class="form-label text-right">
          <label for="username-input">Найти по имени:</label>
        </div>
        <div class="form-input">
          <input type="text"
                 id="username-input" name="username-input"
                 class="form-control"
                 placeholder="вводите псевдоним пользователя">
        </div>
      </div>
    {% endif %}
  {% endif %}

Дописываю ещё один импорт в набор к уже существующим в шаблоне.

{% from "macros/_auth.html" import render_form_group %}

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

touch selfish/static/css/pagination.css

Открываю его в редакторе и пишу следующие правила.

.entity-pagination {
  border-radius: 3px;
  border: 1px solid #d3ddd3;
  box-shadow: 0 0 6px #f1dbc2;
  background: linear-gradient(to bottom, #fffef2, #f3f2e7);
}

.pagination {
  margin: 4px 0 0 0;
}

Создаю ещё один файл стилей.

mkdir selfish/static/css/admin
touch selfish/static/css/admin/society.css

Открываю его в редакторе и пишу следующие правила.

.find-username {
  padding: 12px;
}

.society-table {
  width: 100%;
  border-collapse: collapse;
}

.society-table th {
  border-bottom: 2px solid #dddddd;
  padding: 8px 4px;
}

.username-field {
  text-shadow: 0 0 3px #d9dcec;
}

.society-table td {
  padding: 8px 4px 4px;
}

.bordered {
  border-bottom: 1px dashed #dddddd;
}

.date-field {
  font-style: italic;
}

Немного JavaScript совсем не помешает, создаю файл для нового сценария.

mkdir selfish/static/js/admin
touch selfish/static/js/admin/society.js

Открываю его в редакторе и пишу такой код.

$(document).ready(function() {
  moment = customizeMoment(moment);
  renderTodayField('.today-field', moment);
  $('.slidable .block-header').on('click', showHideBlock);
  var $form = $('.to-be-hidden');
  if ($form.find('.error').length) {
    $form.siblings('.block-header').trigger('click');
  }
  $('.date-field').each(function() {formatDateTime($(this), moment);});
  $('.close').on('click', closeTopFlashed);
});

Чтобы всё это великолепие заработало как задумано, переписываю в шаблоне admin/society.html блок styles.

{% block styles %}
  {{ super() }}
  {% assets filters='cssmin', output='generic/css/admin/society.css',
            'css/base.css', 'css/content-block.css', 'css/pagination.css',
            'css/labeled-form.css', 'css/top-flashed.css',
            'css/admin/society.css' %}
    <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
  {% endassets %}
{% endblock styles %}

В конец этого шаблона дописываю новый блок scripts.

{% block scripts %}
  {{ super() }}
  {% assets filters='jsmin', output='generic/js/admin/society.js',
            'js/custom_moment.js', 'js/today_field.js',
            'js/scroll_panel.js', 'js/show_hide.js', 'js/show_datetime.js',
            'js/top_flashed.js', 'js/admin/society.js' %}
    <script src="{{ ASSET_URL }}"></script>
  {% endassets %}
{% endblock scripts %}

Вновь запускаю отладочный сервер, иду в браузер и смотрю на результаты.

53rLhy7GM8.png

Ух-ты..! Навожу указатель мыши на блок "Создайте новый аккаунт пользователя" и делаю одиночный клик.

yIsjGqfPPy.png

Поскольку я вошёл в сервис как администратор - webmaster, я получил в своё распоряжение форму для создания аккаунта нового пользователя. Создаю.

bdQoAdR6sh.png

JRAPUiYMem.png

Видно, что в результате у сервиса появился ещё один зарегистрированный пользователь в группе "Блогеры". Вхожу этим пользователем и иду на страницу /admin/society.

UE0W86wg9P.png

На снимке экрана видно, что пользователь из группы "Блогеры" не имеет доступа к форме на этой странице. Что и требовалось.

Ещё не конец. Далее я научу selfish находить пользователей по первым символам псевдонима по мере их ввода в строке Найти по имени.

5. Поиск пользователей по псевдониму

Список зарегистрированных пользователей в зависимости от популярности ресурса может расти, а в связи с тем, что сортировка в этом списке осуществляется не по псевдониму, желательно иметь возможность автоматического поиска и фильтрации списка пользователей по начальным символам псевдонима. На странице /admin/society уже имеется функциональный элемент для ввода псевдонима пользователя, но на данный момент ввод в это поле не сопровождается никаким полезным событием. Сейчас я научу selfish искать пользователей по псевдониму в соответствии с символами введёнными в это поле.

evGPSftksS.png

Создаю новую подпрограмму - ajax.

mkdir selfish/ajax
touch selfish/ajax/__init__.py
touch selfish/ajax/views.py

gntsQjrGtc.png

Открываю в редакторе файл ajax/__init__.py и создаю в этом файле новый экземпляр класса Blueprint.

from flask import Blueprint

ajax = Blueprint('ajax', __name__)

from . import views

Регистрирую эту подпрограмму в функции create_app файла selfish/__init__.py.

...


def create_app(config_name):
    ...
    from .ajax import ajax as ajax_blueprint
    app.register_blueprint(ajax_blueprint, url_prefix='/ajax')
    return app

Открываю файл ajax/views.py и определяю в нём новую функцию представления.

from flask import jsonify, render_template, request
from flask_login import current_user

from ..models.auth import User
from ..models.auth_units import permissions
from . import ajax


@ajax.route('/society/find-username', methods=['POST'])
def find_username():
    result = {'empty': True}
    found = User.query.filter(User.username.like(
        '{}%'.format(request.form.get('value', None, type=str)))).order_by(
        User.last_visit.desc())
    if current_user.can(permissions.FOLLOW_USERS):
        result = {'empty': False,
                  'html': render_template('ajax/find-username.html',
                                          found=found)}
    return jsonify(result)

Логика этой функции представления достаточно проста. Создаётся объект result в значении словаря с единственным ключом empty в значении True. Обрабатывается запрос, а функция может обрабатывать только запросы по методу POST, запросы по методу GET будут возвращать ошибку 405 в соответствии с заданным этой функции преставления url. Из формы запроса находится значение value и по этому значению из базы данных фильтруются пользователи, начальные символы псевдонима которых соответствуют символам переданным в форме запроса (value). Затем проверяется разрешение текущего пользователя FOLLOW_USERS, и если он имеет такое разрешение, то объект result переопределяется соответствующим образом, а затем преобразуется в JSON и возвращается.

Поскольку у меня появился новый тип потенциальной ошибки, следует определить для 405 соответствующий обработчик. Открываю файл main/errors.py и дописываю в конец файла ещё одну функцию.

@main.app_errorhandler(405)
def deny_request_method(e):
    return render_template(
        'error.html', error=e.code, reason='Метод не позволен'), e.code

Создаю шаблон для функции представления find_username.

mkdir selfish/templates/ajax
touch selfish/templates/ajax/find-username.html

Код этого шаблона будет иметь следующий вид.

{% from "macros/_admin.html" import show_society %}

{% if found.first() %}
  {{ show_society(found, 1, 2, format_date) }}
{% else %}
  <div class="content-block next-block">
    <div class="block-header">
      <h3 class="panel-title">Найдено по запросу</h3>
    </div>
    <div class="block-body">
      <div class="alert alert-warning">
        Нет пользователей с таким псевдонимом.
      </div>
    </div>
  </div>
{% endif %}

Здесь macro, созданный ранее, - show_society оказался как нельзя кстати...

Теперь мне необходимо связать соответствующее поле ввода с созданной функцией преставления, для этого открываю в редакторе шаблон admin/society.html и нахожу в нём такой код.

          <input type="text"
                 id="username-input" name="username-input"
                 class="form-control"
                 placeholder="вводите псевдоним пользователя">

Дописываю этому элементу новые атрибуты.

          <input type="text"
                 id="username-input" name="username-input"
                 data-tee="{{ csrf_token() }}"
                 data-url="{{ url_for('ajax.find_username') }}"
                 data-home="{{ url_for('admin.show_users') }}"
                 class="form-control"
                 placeholder="вводите псевдоним пользователя">

Здесь я добавил следующие параметры:

  • data-tee - csrf-брелок;
  • data-url - url-адрес только что созданной функции преставления find_username;
  • data-home - url страницы для отката поиска.

Чтобы связать всё воедино, нужно привязать к этому элементу <input> соответствующее событие. Открываю в редакторе файл admin/society.js и в нём, внутри фигурных скобок формирую новое событие.

$(document).ready(function() {
  ...

  $('#username-input').on('keyup', function(event) {
    var list = [0, 8, 9, 13, 17, 18, 20, 27, 32, 33, 34, 35, 36, 37, 38, 39,
                40, 45, 46, 91, 93, 144];
    var $value = $(this).val();
    var home = $(this).data().home;
    if ($.inArray(event.which, list) == -1 ||
        ((event.which == 8 || event.which == 46) && $value != '')) {
      $.ajax({
        method: "POST",
        url: $(this).data().url,
        data: {
                value: $value,
                csrf_token: $(this).data().tee,
              },
        success: function(data) {
          if (!data.empty) {
            $('#center-panel').empty().append(data.html);
            $('.date-field').each(function() {
              formatDateTime($(this), moment);
            });
          }
        },
        error: function(data) {
          var result = $.trim($(data.responseText).find('h1').text());
          if (result === 'CSRF Error') {
            var html = '<div class="content-block next-block">' +
                       '  <div class="block-header">' +
                       '    <h3 class="panel-title">' +
                              result +
                       '    </h3>' +
                       '  </div>' +
                       '  <div class="block-body">' +
                       '    <div class="alert alert-warning">' +
                       'Обновите страницу в браузере и повторите попытку.' +
                       '    </div>' +
                       '  </div>' +
                       '</div>';
            $('#center-panel').empty().append(html);
          }
        },
        dataType: 'json'
      });
    }
    if ((event.which == 46 || event.which == 8) && $value == '') {
      window.location.replace(home);
    }
  });
});

Таким образом, когда курсор будет находиться в поле ввода Найти по имени, при отпускании пользователем соответствующих клавиш клавиатуры будет осуществляться AJAX-запрос к заданному url-адресу с заданными данными, и в случае успеха возвращаемый сервером ответ, и в частности объект JSON, будет соответствующим образом обрабатываться на клиенте и полученные данные будут подставляться в DOM уже выведенной и обработанной html-страницы.

Запускаю отладочный сервер, иду в браузер, загружаю страницу приложения /admin/society и ставлю курсор в поле ввода - Найти по имени.

3fhjVKPEQJ.png

Начинаю медленно и последовательно вводить псевдоним.

K7a5GFUByd.png

nPaa74efZE.png

Ud5eDrHl6U.png

Если полностью очистить поле ввода, то страница вернётся к первоначальному состоянию.

Если в браузере проследовать по url-адресу функции представления find-username, то сервис покажет соответствующую ошибку.

x9KfiWp71W.png

Можно констатировать, что у selfish появился новый полезный функционал.

6. Подводим итоги

Очередной этап разработки selfish завершен. На текущий момент одна из функциональных страниц selfish получила новый полезный функционал, и теперь администраторы сервиса могут создавать новые пользовательские аккаунты прямо в web-интерфейсе. Кроме этого, все авторизованные пользователи сервиса, имеющие разрешение FOLLOW_USERS, могут видеть на этой странице список всех зарегистрированных в сервисе пользователей и искать в этом списке нужного пользователя по псевдониму. А для перехода в профиль конкретного пользователя этот список содержит соответствующие ссылки.

Текущее состояние кода проекта как всегда можно увидеть в моём профиле на gitlab.com. В случае необходимости задать мне вопросы, к вашим услугам великолепные древовидные комментарии, я обязательно отвечу на ваш вопрос.

Работа над приложение продолжится, и в следующем выпуске блога я начну создавать инструменты для самостоятельной регистрации пользователей и восстановления забытого пароля. Будет интересно, продолжение следует.

Комментарии: