В качестве хобби, я решил открыть для себя мир веб-разработки, попутно освещая свои успехи, а может и неудачи на пути к просветлению…

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

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

Создание веб-сайта с нуля на Django и Bootstrap. Книги. Список книг и их содержимое. Шаблоны

Сергей Серов

Базовый шаблон проекта

Переходим в директорию шаблонов проекта и приводим base.html к следующему виду:

{% load static %}

<!DOCTYPE html>
<html lang="ru">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <meta name="author" content="{{ SITE_AUTHOR }}">

        <title>{% block title %}{% endblock %} | {{ SITE_TITLE }}</title>

        <!-- Bootstrap -->
        <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
        <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

        <!-- Style -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
        <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700%7CUbuntu:700&amp;subset=cyrillic" rel="stylesheet">
        <link rel="stylesheet" href="{% static 'style.css' %}">

        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
            <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
            <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->

        <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
        <!-- Include all compiled plugins (below), or include individual files as needed -->
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

        {% block head_extra %}{% endblock %}
    </head>

    <body>
        <header>
            <nav class="navbar navbar-default navbar-static-top">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
                            <span class="sr-only">Переключить навигацию</span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                        </button>

                        <a class="navbar-brand" href="/">{{ SITE_AUTHOR }}</a>
                    </div>

                    <div class="collapse navbar-collapse" id="navbar-collapse">
                        <ul class="nav navbar-nav">
                            <li><a href="{% url 'books:index' %}">Книги</a></li>
                            <li><a href="{% url 'articles:index' %}">Статьи</a></li>
                            <li><a href="{% url 'questions:index' %}">Вопросы</a></li>
                        </ul>

                        <ul class="nav navbar-nav navbar-right">
                            {% if user.is_authenticated %}
                                <li class="dropdown">
                                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                                        <i class="fa fa-user fa-fw" aria-hidden="true"></i>
                                        {{ user.username }}
                                        <span class="caret"></span>
                                    </a>

                                    <ul class="dropdown-menu">
                                        <li>
                                            <a href="{% url 'accounts:change_password' %}">
                                                <i class="fa fa-key fa-fw" aria-hidden="true"></i>
                                                Смена пароля
                                            </a>
                                        </li>

                                        <li>
                                            <a href="{% url 'accounts:change_email' %}">
                                                <i class="fa fa-envelope-o fa-fw" aria-hidden="true"></i>
                                                Смена почты
                                            </a>
                                        </li>

                                        <li>
                                            <a href="{% url 'accounts:logout' %}">
                                                <i class="fa fa-sign-out fa-fw" aria-hidden="true"></i>
                                                Выход
                                            </a>
                                        </li>
                                    </ul>
                                </li>
                            {% else %}
                                <li>
                                    <a href="{% url 'accounts:login' %}">
                                        <i class="fa fa-sign-in fa-fw" aria-hidden="true"></i>
                                        Войти
                                    </a>
                                </li>
                            {% endif %}
                        </ul>
                    </div>
                </div>
            </nav>

            <div class="container">
                {% block header %}
                    <div class="about">
                        <h2 class="title">{{ SITE_TITLE }}</h2>
                    </div>
                {% endblock %}
            </div>
        </header>

        <main>
            <div class="container">
                {% block main %}{% endblock %}
            </div>
        </main>

        <footer>
            <div class="container">
                {% block footer %}
                    <p>&#169; {{ SITE_AUTHOR }}, 2017&#8212;{% now "Y" %}</p>
                    <p>&#169; Разработка и поддержка веб-сайта. <a href="https://serov.space">Сергей Серов</a>, 2017&#8212;{% now "Y" %}</p>
                {% endblock %}
            </div>
        </footer>
    </body>
</html>

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

Базовый шаблон статей

В директории articles создаем файл base.html:

{% extends 'base.html' %}

{% load static %}

{% block head_extra %}
    <link rel="alternate" type="application/atom+xml" title="{{ SITE_TITLE }}" href="{% url 'articles:feed' %}">
{% endblock %}

{% block header %}
    <div class="about">
        <div class="row">
            <div class="col-sm-8">
                <h2 class="title">{{ SITE_TITLE }}</h2>
                <h3 class="subtitle"><a href="{% url 'articles:index' %}">{{ ARTICLES_SUBTITLE }}</a></h3>
            </div>

            <div class="col-sm-4">
                <address class="contacts">
                    <div class="articles-mail">
                        <a href="#">
                            <span class="sr-only">Адрес электронной почты</span>
                            <i class="fa fa-envelope-o fa-fw" aria-hidden="true"></i>
                            Почта
                        </a>
                    </div>

                    <div class="cv">
                        <a rel="nofollow" href="#">
                            <i class="fa fa-id-card-o fa-fw" aria-hidden="true"></i>
                            Резюме
                        </a>
                    </div>
                </address>
            </div>
        </div>

        <div class="description">
            <p>
                В течение десяти лет работы на крупных производственных предприятиях заполучил опыт создания служб с нуля,
                по направлениям: охрана труда, пожарная, промышленная и экологическая безопасность.
            </p>

            <p>
                Проведу для Вас полный аудит или разработаю комплекты документов по всем вышеперечисленным направлениям.
            </p>
        </div>
    </div>
{% endblock %}

В файлах article.html и index.html нам необходимо заменить наследование от базового шаблона проекта на вышеописанный шаблон приложения статей.

Базовый шаблон книг

В директории templates проекта создаем папку books, а в ней base.html:

{% extends 'base.html' %}

{% block header %}
    <div class="about">
        <div class="row">
            <div class="col-sm-8">
                <h2 class="title">{{ SITE_TITLE }}</h2>
                <h3 class="subtitle"><a href="{% url 'books:index' %}">{{ BOOKS_SUBTITLE }}</a></h3>
            </div>

            <div class="col-sm-4">
                <address class="contacts">
                    <div class="mail">
                        <a href="#">
                            <span class="sr-only">Адрес электронной почты</span>
                            <i class="fa fa-envelope-o fa-fw" aria-hidden="true"></i>
                            Почта
                        </a>
                    </div>

                    <div class="donate">
                        <a rel="nofollow" href="#">
                            <span class="sr-only">Поблагодарить автора</span>
                            <i class="fa fa-credit-card fa-fw" aria-hidden="true"></i>
                            Поблагодарить
                        </a>
                    </div>
                </address>
            </div>
        </div>

        <div class="description">
            <p>
                С детства у меня была мечта — освоить писательское ремесло, но каждый раз меня что-то останавливало.
                Теперь я достиг всего, чего хотел и настало время последнего аккорда — мокнуть в чернила свое перо.
            </p>

            <p>
                Все жанры хороши, без исключения, но меня претит от унылой фабулы, где занудный протагонист плетется
                в хвосте еще менее красочных сюжетных переплетений. Даже настоящий детектив можно спеленать
                в атмосфере истового драйва.
            </p>

            <p>
                Lights! Camera! Action!
            </p>
        </div>
    </div>
{% endblock %}

Шаблон списка книг

Создаем файл с названием index.html:

{% extends 'books/base.html' %}

{% block head_extra %}
    <link rel="alternate" type="application/atom+xml" title="{{ SITE_TITLE }}" href="{% url 'books:feed' %}">
{% endblock %}

{% block title %}{{ BOOKS_SUBTITLE }}{% endblock %}

{% block main %}
    <div class="books-index">
        {% if books %}
            <h1>
                Книги
                <a href="{% url 'books:feed' %}" title="Лента обновлений">
                    <i class="fa fa-rss fa-fw" aria-hidden="true"></i>
                </a>
            </h1>

            <div class="row">
                {% for book in books %}
                    <div class="col-sm-3">
                        <a href="{{ book.get_absolute_url }}" class="thumbnail">
                            <img src="{{ book.image.url }}" alt="Обложка книги" title="{{ book.title }}">

                            <h3 class="sr-only">{{ book.title }}</h3>
                            <address class="sr-only">{{ SITE_AUTHOR }}</address>
                            <p class="sr-only">{{ book.slogan }}</p>
                        </a>
                    </div>
                {% endfor %}
            </div>
        {% else %}
            <h1>Видимо что-то пошло не так и все рукописи сгорели...</h1>
        {% endif %}
    </div>
{% endblock %}

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

Шаблон содержимого книги

Создаем файл с названием book.html:

{% extends 'books/base.html' %}

{% block head_extra %}
    {% if chapters %}
        <link rel="alternate" type="application/atom+xml" title="{{ book.title }} | {{ SITE_TITLE }}" href="{% url 'books:book_feed' book.pk %}">
    {% endif %}
{% endblock %}

{% block title %}{{ book.title }}{% endblock %}

{% block main %}
    <div class="books-book">
        <h1>
            {{ book.title }}

            {% if chapters %}
                <a href="{% url 'books:book_feed' book.pk %}" title="Лента обновлений">
                    <i class="fa fa-rss fa-fw" aria-hidden="true"></i>
                </a>
            {% endif %}

            {% if not book.completed %}
                <small>В процессе</small>
            {% endif %}
        </h1>

        <address class="sr-only">{{ SITE_AUTHOR }}</address>

        <h2 class="subtitle">{{ book.slogan }}</h2>

        <div>
            <img src="{{ book.image.url }}" alt="Обложка книги" class="img-thumbnail img-responsive">
        </div>

        <div class="description">
            {{ book.description|safe }}
        </div>

        {% if book.redirect %}
            <p class="redirect">
                <a href="{{ book.redirect }}" class="btn btn-primary btn-block" role="button">Подробнее</a>
            </p>
        {% endif %}

        {% if chapters %}
            <div class="chapters">
                <h2>Оглавление</h2>

                <div class="list-group">
                    {% for chapter in chapters %}
                        <a href="{{ chapter.get_absolute_url }}" class="list-group-item">{{ chapter.title }}</a>
                    {% endfor %}
                </div>
            </div>
        {% endif %}
    </div>
{% endblock %}

Выводим полное описание книги.

Добавляем ленту обновлений глав и выводим их список если присутствуют таковые.

Если книга не имеет даты окончания, значит выводим статус: в процессе.

Показываем кнопку со ссылкой на другой ресурс, если задано перенаправление (книга выложена на стороннем ресурсе).

Шаблон содержимого главы

Создаем файл с названием chapter.html:

{% extends 'books/base.html' %}

{% block title %}{{ chapter.title }} | {{ chapter.book.title }}{% endblock %}

{% block main %}
    <div class="books-chapter">
        <h2 class="book-title">
            <a href="{{ chapter.book.get_absolute_url }}">{{ chapter.book.title }}</a>
        </h2>

        <address class="sr-only">{{ SITE_AUTHOR }}</address>

        <h1 class="subtitle">
            {{ chapter.title }}

            {% if user.is_superuser %}
                <a href="{{ chapter.get_admin_url }}" title="Редактировать">
                    <i class="fa fa-pencil fa-fw" aria-hidden="true"></i>
                </a>

                {% if not chapter.published %}
                    <small>Неопубликованная глава</small>
                {% endif %}
            {% endif %}
        </h1>

        {% if not page.has_previous %}
            {% with chapter.prev as prev %}
                {% if prev  %}
                    <div class="prev-chapter">
                        <a href="{{ prev.get_absolute_url }}" title="{{ prev.title }}">&#8592; Перейти к предыдущей главе</a>
                    </div>
                {% endif %}
            {% endwith %}
        {% endif %}

        <div class="text">
            {{ page|first|safe }}
        </div>

        {% if page.has_other_pages %}
            <nav aria-label="Навигация по страницам">
                <ul class="pagination">
                    {% if page.has_previous %}
                        <li>
                            <a href="?page={{ page.previous_page_number }}" title="Предыдущая страница">
                                <i class="fa fa-chevron-left" aria-hidden="true"></i>
                            </a>
                        </li>
                    {% endif %}

                    {% for number in page.paginator.page_range %}
                        <li{% if number == page.number %} class="active" aria-label="Текущая страница"{% endif %}>
                            <a href="?page={{ number }}">{{ number }}</a>
                        </li>
                    {% endfor %}

                    {% if page.has_next %}
                        <li>
                            <a href="?page={{ page.next_page_number }}" title="Следующая страница">
                                <i class="fa fa-chevron-right" aria-hidden="true"></i>
                            </a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endif %}

        {% if not page.has_next %}
            {% with chapter.next as next %}
                {% if next %}
                    <div class="next-chapter">
                        <a href="{{ next.get_absolute_url }}" title="{{ next.title }}">Перейти к следующей главе &#8594;</a>
                    </div>
                {% endif %}
            {% endwith %}
        {% endif %}
    </div>
{% endblock %}

Выводим название книги и главы, а так же ее содержимое.

Если просматривает сайт супер-пользователь: отображаем административный блок.

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

Если текущая страница главы не имеет предыдущей или следующей страницы: выводим ссылки на переключение глав.

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

Стили

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

body {
    font-family: 'Open Sans', sans-serif;
    font-size: 15px;
    line-height: 160%;
    color: black;
}

h1, h2, h3, h4, h5, h6 {
    font-family: 'Ubuntu', sans-serif;
    text-transform: uppercase;
    font-weight: bold;
}

h1 {
    font-size: 22px;
    margin-bottom: 30px;
}

h2 {
    font-size: 20px;
    margin-bottom: 20px;
}

a:link {
    text-decoration: none;
}

.subtitle {
    font-size: 19px;
    font-family: 'Open Sans', sans-serif;
    font-weight: normal;
    text-transform: none;
}

.pagination {
    margin-bottom: 0;
}

.container {
    max-width: 750px;
}

header .about .title {
    font-size: 28px;
    margin: 0;
}

header .about a {
    color: black;
}

header .about .subtitle {
    font-size: 18px;
    text-transform: uppercase;
    margin-top: 0;
}

header .about .contacts {
    text-align: right;
    font-size: 18px;
    margin-top: 5px;
    line-height: 130%;
}

header .about .contacts .articles-mail {
    margin-right: 6px;
}

header .about .description {
    margin-top: 15px;
}

footer {
    color: #666666;
    margin-bottom: 30px;
}

footer p {
    margin-bottom: 0;
}

main {
    margin-top: 10px;
    margin-bottom: 20px;
}

main a {
    font-weight: bold;
}

main form .errorlist {
    list-style-type: none;
    padding-left: 5px;
}

main form button {
    margin-top: 10px;
}

main blockquote {
    font-size: 14px;
    border-color: #e6e6e6;
    background-color: #f7f7f7;
}

main .django-ckeditor-widget {
    width: 100%;
}

main .accounts-login .links {
    margin-top: 25px;
}

main .questions-index .question .title {
    font-size: 18px;
    margin-top: 3px;
}

main .questions-index .pagination {
    margin-top: 25px;
    margin-bottom: 5px;
}

main .questions-index .your-question h2 {
    margin-top: 40px;
}

main .questions-question .title {
    margin-bottom: -30px;
}

main .questions-question .article {
    margin-top: 50px;
}

main .questions-question .answer {
    color: darkgreen;
}

main .questions-question .article .expert {
    margin-right: 5px;
}

main .questions-question .article .author {
    font-weight: bold;
    margin-bottom: 5px;
}

main .questions-question .article .created {
    color: #666666;
    margin-right: 5px;
}

main .questions-question .article .text {
    margin-top: 20px;
}

main .questions-question .your-answer h2 {
    margin-top: 50px;
}

main .questions-question .your-answer form {
    margin-top: -25px;
}

main .questions-question .your-answer label {
    visibility: hidden;
}

main .articles-index {
    margin-bottom: 10px;
}

main .articles-index .article .title {
    font-size: 18px;
    margin-top: 3px;
    margin-bottom: 10px;
}

main .articles-article .title {
    margin-bottom: 5px;
}

main .articles-article .published {
    color: #666666;
    margin-right: 5px;
}

main .articles-article .text {
    margin-top: 20px;
}

main .articles-article .text h2 {
    margin-top: 25px;
    margin-bottom: 15px;
}

main .articles-article .text h3 {
    font-size: 18px;
    margin-top: 25px;
    margin-bottom: 15px;
}

main .articles-article .text ul {
    margin-bottom: 20px;
}

main .articles-article .text p {
    margin-bottom: 15px;
}

/* books-index */

main .books-index .thumbnail h3 {
    font-size: 19px;
    text-transform: none;
    color: black;
}

main .books-index .thumbnail p {
    font-size: 17px;
    font-weight: normal;
    margin-top: -7px;
    color: black;
}

/* books-book */

main .books-book h1 small {
    text-transform: none;
}

main .books-book .subtitle {
    margin-top: -26px;
    margin-bottom: 35px;
}

main .books-book .description {
    margin-top: 35px;
}

main .books-book .redirect {
    margin-top: 30px;
}

main .books-book .chapters h2 {
    margin-top: 35px;
    margin-bottom: 25px;
}

/* books-chapter */

main .books-chapter .book-title {
    font-size: 22px;
}

main .books-chapter .subtitle {
    font-weight: bold;
    margin-top: -14px;
}

main .books-chapter .prev-chapter {
    margin-top: -5px;
    margin-bottom: 20px;
}

main .books-chapter .next-chapter {
    margin-top: 20px;
}


/* Маленькие устройства (телефоны) (<768px) */

@media (max-width: 767px) {
    header .about .contacts {
        margin-top: 25px;
        text-align: left;
    }

    header .about .description {
        margin-top: 10px;
    }
}