Пишем web на Python3, создаём базовый шаблон приложения

newbie

Опубликован:  2019-02-14T10:51:08.850991Z
Отредактирован:  2019-02-18T09:45:09.504288Z

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

Новым участникам обсуждения следует иметь ввиду, что совсем недавно проект был успешно перенесён в новое окружение Debian buster и теперь серверный код приложения будет основан на Python3.7.2 и PostgreSQL версии 10.

Разработка клиентского кода предполагает необходимость и возможность группирования файлов с кодом (таблиц стилей и JavaScript-сценариев) с учётом повторяемости тех или иных функций на разных страницах web-приложения, принцип DRY никто не отменял. Кроме этого, клиентский код нужно отлаживать и тестировать, это значит, что selfish требует дополнительной конфигурации, которая будет учитывать методологию разработки. Группировать таблицы стилей и JS я буду при помощи webassets, для этого мне потребуется соответствующее расширение Flask.

Вхожу в базовый каталог приложения и активирую виртуальное окружение.

cd ~/workspace/selfhis
source venv/bin/activate

j1DveYohYZ.png

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

Устанавливаю Flask-Assets.

pip install Flask-Assets

ju3CEPppTy.png

Для минимизации клиентского кода потребуются соответствующие фильтры. Устанавливаю jsmin и cssmin.

pip install jsmin cssmin

MNZSIu8uRY.png

Открываю файл selfish/__init__.py в редакторе PyCharm, редактирую его и привожу к следующему виду.

5rvD3NwaC3.png

Здесь я импортировал класс Environment из пакета flask_assets, создал объект assets, который является экземпляром импортированного класса, и инициировал этот объект в теле служебной функции create_app. Нужно обратить внимание, что PyCharm выделил зелёным в поле слева все изменения в файле.

Отладку клиентского кода я буду осуществлять в браузере Chromium, для удобства отладки мне необходимо предотвратить кэширование браузером статических файлов в режиме разработки. Открываю в редакторе PyCharm файл config.py и дописываю в него новый код: импортирую timedelta и вписываю новый атрибут классам Development и Production.

IF12BMAtU5.png

Файл config.py не входит в репозиторий Git, поэтому PyCharm не выделил правки этого файла зелёным, как это случилось в случае с файлом __init__.py, а красные отметки на полях поставил я, чтобы сконцентрировать внимание на новых строчках файла.

Пришло время создать первую страницу приложения - Index Page будущего сайта. Это делается просто, достаточно задать url-адрес и определить для него функцию представления в файле selfish/main/views.py. Открываю этот файл в редакторе и привожу его к следующему виду.

vgWVl8CApd.png

Здесь я определил функцию show_index, при помощи соответствующего декоратора задал этой функции url-адрес, таким образом сделав её функцией представления. Далее я импортировал из пакета flask дополнительно ещё одну полезную функцию - render_template, которая обрабатывает шаблон с учётом переданных ей аргументов и возвращает соответствующую строку. В теле функции show_index я вернул вызов render_template, передав ему только имя шаблона. Возвращаемая функцией представления строка автоматически преобразуется декоратором main.route в экземпляр класса Response.

Если сейчас запустить отладочный сервер и постучаться браузером в заданный url, браузер выведет страницу отладчика, так как шаблон index.html в проекте не существует.

Fw45mpGTTi.png

C8ctZKUujT.png

Шаблоны в приложениях Flask обычно хранятся в специальном каталоге, в данном случае это selfish/templates, если в конфигурации приложения не задан другой адрес. Для начала сделаю шаблон index.html обычным текстовым файлом, содержащим традиционную строчку - Hello, world!.

echo 'Hello, world!' > selfish/templates/index.html

14BKy7FAP9.png

В итоге получаю в браузере очень простую стартовую страницу.

CeTjhJbsut.png

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

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

touch selfish/templates/base.html

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

<!DOCTYPE html>
<html lang="ru">
  <head>
    <title></title>
  </head>
  <body>

  </body>
</html>

Теперь мне необходимо в базовом шаблоне выделить функциональные блоки, которые могут быть переопределены в наследниках базового шаблона. Дописываю следующий код.

<!DOCTYPE html>
<html lang="ru">
  <head>
    {% block metas %}{% endblock metas %}
    <title>{% block title %}{% endblock title %}</title>
    {% block styles %}{% endblock styles %}
  </head>
  <body>
    <nav id="navigation"></nav>
    {% block page_body %}
      {% block page_content %}{% endblock page_content %}
    {% endblock page_body %}
    <footer id="footer"></footer>
    {% block scripts %}{% endblock scripts %}
  </body>
</html>

В базовом шаблоне я выделил следующие функциональные блоки:

  • block metas - будет содержать теги <meta>;
  • block title - будет определять заголовки страниц;
  • block styles - предназначен для стилей страниц;
  • block page_body - будет содержать контент страниц;
  • block page_content - дополнительный блок для контента страниц, даст возможность разделить страницы приложения на несколько видов;
  • block scripts - будет содержать JavaScript-сценарии.

Тело страницы базового шаблона кроме блока page_body получило ещё два тега с заданными id. Эти теги будут определять главное меню приложения и так называемый подвал.

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

{% extends "base.html" %}

{% block title %}Исток{% endblock title %}

{% block styles %}
  {{ super() }}

{% endblock styles %}

{% block page_content %}
  <!-- index.html -->
  <div class="flashed-message">
    <div class="alert alert-warning">
      <div class="today-field"></div>
      <div class="message-text">
        Сайт в стадии разработки, попробуйте зайти позже.
      </div>
    </div>
  </div>
{% endblock page_content %}

{% block scripts %}
  {{ super() }}

{% endblock scripts %}

Здесь я указал, что index.html является наследником базового шаблона base.html, а затем переопределил те блоки index.html, которые будут иметь отличия от соответствующих блоков базового шаблона. Стоит обратить внимание на вызов функции super в блоках styles и scripts, этот вызов позволяет унаследовать код соответствующих блоков базового шаблона и дописать в эти блоки уникальный для index.html код. Без этого вызова данные блоки переопределились бы полностью.

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

NhfWdMHDBc.png

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

zVOwqJ1Q2E.png

У страницы появилась разметка. Далее в процессе разработки этой страницы я буду править сначала базовый шаблон, затем шаблон страницы index.html и демонстрировать изменения отображаемой браузером страницы.

Приступим к правке базового шаблона. Я буду править каждый выделенный блок в порядке следования. Блок metas базового шаблона приобретает следующий вид.

{% block metas %}
  <meta charset="utf-8">
  <meta http-equiv="X-UA-COMPATIBLE" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock metas %}

Для приложения selfish можно определить имя сайта, которое будет транслироваться в заголовок и в подвал всех страниц. Определяю имя сайта в файле config.py, добавляю соответствующее свойство классу Config.

class Config:
    SECRET_KEY = 'My Secret Key'
    SITE_NAME = 'Selfish'

    @staticmethod
    def init_app(app):
        pass

Перехожу в базовый шаблон и привожу тег <title> к виду.

<title>{{ config.SITE_NAME }}: {% block title %}{% endblock title %}</title>

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

{% block styles %}
  {% assets filters='cssmin', output='generic/css/vendor.css',
            'vendor/bootstrap-3.4.0/css/bootstrap.css',
            'vendor/bootstrap-3.4.0/css/bootstrap-theme.css' %}
    <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
  {% endassets %}
{% endblock styles %}

Необходимо заметить, что контекстная переменная assets определена в файле selfish/__init__.py и является экземпляром класса Environment. В аргументах этой переменной в базовом шаблоне при помощи output задан файл, в который будет генерироваться итоговая таблица стилей - generic/css/vendor.css, а также заданы файлы, из которых итоговая таблица стилей будет состоять. Эти файлы уже существуют и скопированы в проект в предыдущем выпуске блога. Аргумент filters задаёт фильтр для минимизации результирующего файла, этот фильтр уже установлен в виртуальное окружение проекта.

Посмотрим, как в итоге изменилась страница в браузере.

cXsWS6ppDl.png

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

{% block scripts %}
  {% assets filters='jsmin', output='generic/js/vendor.js',
            'vendor/jquery-3.3.1.js',
            'vendor/bootstrap-3.4.0/js/bootstrap.js',
            'vendor/moment.js',
            'vendor/ru.js' %}
    <script src="{{ ASSET_URL }}"></script>
  {% endassets %}
{% endblock scripts %}

Модифицирую тег <nav id="navigation"> базового шаблона.

<nav id="navigation">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle"
              data-toggle="collapse" data-target=".navbar-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="{{ url_for('main.show_index') }}">
        <img alt="logo"
             src="{{ url_for('static', filename='images/logo.png') }}"
             width="28" height="28">
      </a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li><a href="">События</a></li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
         <li class="dropdown">
           <a href="#" class="dropdown-toggle" data-toggle="dropdown">
             Действия <b class="caret"></b>
           </a>
           <ul class="dropdown-menu">
             <li><a href="">Войти</a></li>
             <li><a href="">Зарегистрироваться</a></li>
             </ul>
         </li>
      </ul>
    </div>
  </div>
</nav>

Блок page-body базового шаблона привожу к следующему виду.

{% block page_body %}
  <div id="main-container" class="container-fluid">
    <div class="row">
      <div id="content"
           class="col-lg-8 col-md-10 col-sm-10
                  col-lg-offset-2 col-md-offset-1 col-sm-offset-1">
        <div class="row">
          <div id="sub-content"
               class="col-lg-10 col-md-10 col-lg-offset-1 col-md-offset-1">
            {% block page_content %}{% endblock page_content %}
          </div>
        </div>
      </div>
    </div>
  </div>
{% endblock page_body %}

И, наконец, изменяю <footer>.

<footer id="footer">
  <div class="container-fluid">
    <div class="footer-block"></div>
    <div class="footer-content">
      <div class="footer-left text-left">
        <img alt="right finger"
             src="{{ url_for(
                      'static', filename='images/footer-left.png') }}"
             width="24" height="24">
      </div>
      <div class="footer-center text-center">
        <a id="footer-link" href="{{ url_for('main.show_index') }}">
          {{ config.SITE_NAME }}
        </a>
      </div>
      <div class="footer-right text-right">
        <img alt="left finger"
             src="{{ url_for(
                      'static', filename='images/footer-right.png') }}"
             width="24" height="24">
       </div>
       <div class="footer-bottom"></div>
    </div>
    <div class="footer-block"></div>
  </div>
</footer>

Возвращаюсь в браузер и фиксирую изменения в отображении страницы.

KcIlnQWui5.png

Страница пока выглядит достаточно коряво. Но на данный момент меня больше интересует код этой страницы и теги <link> и <script>. Браузер отображает их так.

AVFwf2u0xn.png

rzrb5TWQet.png

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

UwoRKfksJy.png

Каталог generic и его содержимое сгенерированы автоматически. Для полного благополучия и реализации задуманного базовому шаблону не хватает оригинальной таблицы стилей, она появится чуть позже.

Оригинальная таблица стилей базовго шаблона будет храниться в отдельном файле в каталоге selfish/static/css/. Этот каталог уже существует, создаю в нём новый файл.

touch selfish/static/css/base.css

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

html {
    position: relative;
    min-height: 100%;
    }

body {
    margin-bottom: 60px;
    }

#navigation {
    margin: 0;
    background-image: linear-gradient(to bottom, #fffef2, #f3f2e7);
    box-shadow: 0 0 6px #f1dbc2;
    }

.navbar-brand {
    padding-top: 10px;
    }

#main-container {
    padding-top: 8px;
    padding-bottom: 8px;
    color: #83858f;
    }

.error {
    color: #a94442;
    padding-left: 8px;
    margin-bottom: 0;
    }

.alert {
    margin: 0;
    }

#footer {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 60px;
    box-shadow: 0 0 1px #7f8c7f;
    background-color: #687368;
    background-image: linear-gradient(to bottom, #a2a19a, #74736e);
    }

.footer-block {
    height: 15px;
    }

.footer-left {
    width: 5%;
    float: left;
    margin-top: 5px;
    }

.footer-center {
    width: 90%;
    float: left;
    padding-top: 4px;
    color: white;
    }

#footer-link {
    color: white;
    text-decoration: none;
    text-shadow: 0 0 8px #d9dcec;
    }

#footer-link:hover {
    cursor: pointer;
    text-shadow: 0 0 0 #d9dcec;
    }

.footer-right {
    width: 5%;
    float: left;
    margin-top: 5px;
    }

.footer-bottom {
    clear: both;
    }

Каждый новый шаблон selfish будет включать этот файл, в том числе уже созданный шаблон index.html. Открываю его в редакторе и привожу блок styles этого шаблона к следующему виду.

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

Следует обратить внимание на оформление контекстной переменной assets, она получила фильтр для минимизации кода результирующей таблицы стилей, которая будет храниться в файле generic/css/main/index.css, и на данном этапе будет получена из единственного файла css/base.css.

Возвращаюсь в браузер и отмечаю изменения на странице.

8Q6PV6p7mm.png

Если заглянуть в исходный код страницы, можно увидеть, что появился ещё один тег <link>.

jt8CAw45z4.png

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

3E43ssAD1E.png

JavaScript - лекарство от скуки. Настал момент чуть-чуть развлечься. Так как у меня есть сторонняя библиотека Moment.js, которая значительно облегчает жизнь при работе с датами и временем, неплохо было бы начать её использовать уже на первой странице приложения. В дефолтном состоянии эта библиотека имеет англоязычную локаль, что не очень подходит для selfish, и поэтому требуется дополнительная локализация. Русскоязычная локаль Moment.js находится в файле selfish/static/vendor/ru.js, необходимо применить эту локаль и переопределить отображение времени в ней. Создаю новый файл.

touch selfish/static/js/custom_moment.js

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

function customizeMoment(moment) {
  moment.locale('ru');
  moment.updateLocale('ru', {
    longDateFormat : {
      LT : 'HH:mm',
      LTS : 'HH:mm:ss',
      L : 'DD.MM.YYYY',
      LL : 'D MMMM YYYY г.',
      LLL : 'D MMMM YYYY г., HH:mm',
      LLLL : 'dddd, D MMMM YYYY г., HH:mm'
      }
    });
    return moment;
  }

Единственная на текущий момент страница selfish в сущности готова, но при помощи JavaScript и таблиц стилей можно добавить этой странице несколько интересных деталей. Открываю шаблон index.html и пишу в блок scripts этого шаблона следующий код.

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

Создаю новый каталог и в нём новый файл.

mkdir selfish/static/js/main
touch selfish/statis/js/main/index.js

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

$(document).ready(function() {
  moment = customizeMoment(moment);
  $('.today-field')
  .text(moment().format('LL') + ', ' + moment().format('HH:mm:ss'));
  setInterval(
    function() {
      $('.today-field')
      .text(moment().format('LL') + ', ' + moment().format('HH:mm:ss'));
    },
    1000);
});

Затем в блоке styles шаблона index.html вписываю через запятую ещё один файл.

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

Создаю этот файл.

mkdir selfish/static/css/main
touch selfish/static/css/main/index.css

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

.today-field {
    font-style: italic;
    font-weight: 600;
    font-size: 0.9em;
}

.message-text {
    font-style: italic;
}

Возвращаюсь в браузер и обновляю страницу.

ZnvISZB98p.png

На странице появилась текущая дата и тикающие часы, а текст в поле отображается курсивом - это результат работы JavaScript-сценариев и таблиц стилей, только что добавленных в проект и исполняющихся на стороне клиента. Взглянем ещё раз на исходный код страницы, в данном случае меня интересуют теги <link> и <script>. Дело в том, что в режиме отладки приложения мне бы хотелось, чтобы в этих тегах отображались ссылки на реальные файлы, а не автоматически сгенерированные и минимизированные. Открываю файл config.py и классу Development вписываю ещё один атрибут.

class Development(Config):
    DEBUG = True
    SEND_FILE_MAX_AGE_DEFAULT = 0
    ASSETS_DEBUG = True

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

yy2RPGk77L.png

Тегов <link> стало больше, и ссылаются они теперь на исходные неминимизированные файлы. Абсолютно та же картина с тегами <script>.

На текущий момент накопилось достаточно много изменений в базовом каталоге selfish.

tdLVbY8B8x.png

Эти изменения проекта необходимо сохранить в Git, для этого нужно сделать коммит, но перед этим следует зафиксировать изменения виртуального окружения. Изменяю файл requirements.txt.

pip freeze > requirements.txt

Файл config.py подвергся правкам, его изменения тоже нужно сохранить, копирую его в файл шаблона.

cp config.py config.template.py

В каталоге статических файлов появился автоматически генерируемый каталог, который мне не нужен в Git-репозитории. Привожу файл .gitignore к следующему виду.

venv/
.idea/
__pycache__/
config.py
selfish/static/vendor
selfish/static/generic
selfish/static/.webassets-cache

Добавляю изменения в проект.

git add .

Напоминаю, что перед созданием очередного коммита очень неплохо будет ещё раз просмотреть изменения при помощи команды git diff --staged.

Текущая версия кода selfish доступна в моём профиле на gitlab.com.

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

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