🐍🎸 Курс Django: Веб-приложение на основе XLSX вместо базы данных

Покажем, как Django работает с файлами JSON, CSV, YAML, XLSX, разберемся с настройками media, и напишем приложение, которое использует XLSX (или Google Sheets) для загрузки данных.

Приложения Django работают с файлами любых форматов – растровыми и векторными изображениями, аудио, видео, архивами и всевозможными документами. Путь к файлам можно указывать точно так же, как в обычных Python скриптах, но проще воспользоваться встроенным инструментом для работы с пользовательскими файлами – папкой media.

Зачем нужна папка media в Django

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

Заметим, что media используется для размещения именно пользовательских файлов, а не статических. Статические файлы являются неотъемлемой частью приложения, не претерпевают никаких изменений со стороны пользователя и хранятся в папке static. Подробнее со статическими файлами мы разберемся в следующей главе.

🐍 Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Как настроить media в Django

Прежде всего, нужно создать папку media в корневой директории проекта, на одном уровне с приложениями:

|-- config/
   |-- asgi.py
   |-- settings.py
   |-- urls.py
   |-- wsgi.py
   |-- __init__.py
   |-- __pycache__/
|-- media/
|-- students/
   |-- admin.py
   |-- apps.py
   |-- migrations/
      |-- __init__.py
   |-- models.py
   |-- tests.py
   |-- views.py
   |-- __init__.py
|-- venv/
|-- manage.py

Затем нужно добавить import os и эти строки в файл настроек config/settings.py:

config/settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Кроме того, надо внести дополнения в в главный файл маршрутов config/urls.py:

config/urls.py
from django.conf import settings
from django.conf.urls.static import static
…
urlpatterns += static(settings.MEDIA_URL, 
	document_root=settings.MEDIA_ROOT)

Файлы вместо базы данных в Django

Хотя веб-приложения обычно работают с базами данных, иногда возникает необходимость сохранять информацию в файлах. Например, конфигурационные файлы JSON и YAML используют для хранения настроек, а Markdown файлы заменяют базу данных в генераторах статических сайтов. Кроме того, веб-приложения часто используют файлы JSON, XLSX, CSV и для загрузки информации в базу данных, и для экспорта данных из БД.

В простых веб-приложениях файлы JSON, XLSX, CSV и YAML вполне могут заменить базу данных. Рассмотрим использование данных из различных файлов на примере приложения students.

Работа с YAML-файлами в Django

В этом примере мы будем использовать YAML файл, поэтому нам нужно установить PyYAML:

pip install pyyaml

Сохраните файл students.yaml в папке media. Поскольку Django знает, где находится media, путь к файлу выглядит как 'media/students.yaml'. Функция представления students/views.py загружает сведения о студентах из YAML-файла и передает данные в шаблон student_list.html:

import yaml
from django.shortcuts import render

def student_list(request):
    with open('media/students.yaml', encoding='utf-8') as file:
        students = yaml.safe_load(file)
    return render(request, 'student_list.html', {'students': students})

В шаблоне для создания списка используется HTML тег <ul></ul>, а для вывода пунктов – тег <li></li>:

<!DOCTYPE html>
<html>
<head>
    <title>Список студентов</title>
</head>
<body>
    <h1>Наши студенты</h1>
    <ul>
    {% for student in students %}
        <li>имя: {{ student.name }}, фамилия: {{ student.lastname }}, возраст: {{ student.age }},
        курс: {{ student.year }}, факультет: {{ student.faculty }}</li>
    {% endfor %}
    </ul>
</body>
</html>

Добавьте маршрут для вызова функции в файл students/urls.py:

from django.urls import path
from students.views import student_list

urlpatterns = [
    path('students/', student_list, name='student_list'),
]

И включите маршруты приложения students в список маршрутов в config/urls.py:

config/urls.py
path('', include('students.urls')),

Запустите сервер, перейдите по адресу http://localhost:8000/students/ – приложение успешно загружает данные из файла students.yaml:

🐍🚀 Django с нуля. Часть 1: пишем многопользовательский блог для клуба любителей задач Python

Как использовать CSV-файлы в Django

Во втором примере мы воспользуемся файлом students.csv. Для загрузки данных нужно переписать функцию представления в students/views.py, а шаблон и urls.py останутся неизменными:

import csv
from django.shortcuts import render

def student_list(request):
    students = []
    with open('media/students.csv', 'r', encoding='utf-8') as file:
        csv_data = csv.DictReader(file)
        for row in csv_data:
            students.append(row)
    return render(request, 'student_list.html', {'students': students})

Результат будет аналогичен первому примеру:

Как работать с JSON-файлами в Django

Для загрузки данных из JSON-файлов Django использует модуль json из стандартной библиотеки Python, поэтому для этого примера не придется ничего устанавливать. Функция представления будет выглядеть так:

import json
from django.shortcuts import render

def student_list(request):
    with open('media/students.json', 'r', encoding='utf-8') as file:
        students = json.load(file)
    return render(request, 'student_list.html', {'students': students})

Как загружать данные из Excel в Django

Для работы с Excel таблицами в Django можно использовать библиотеку Pandas, которая позволяет загружать данные из XLSX-файлов максимально просто:

import pandas as pd
from django.shortcuts import render

def student_list(request):
    file_path = 'media/students.xlsx' 
    df = pd.read_excel(file_path)
    students = df.to_dict('records')
    return render(request, 'student_list.html', {'students': students})

Другой способ немного сложнее, зато он позволяет обойтись компактным модулем openpyxl:

from django.shortcuts import render
from openpyxl import load_workbook

def student_list(request):
    file_path = 'media/students.xlsx'
    workbook = load_workbook(filename=file_path)
    worksheet = workbook.active

    students = []
    skip_first_row = True
    for row in worksheet.iter_rows(values_only=True, min_row=2 if skip_first_row else 1):
        student = {
            'name': row[0],
            'lastname': row[1],
            'age': row[2],
            'faculty': row[3],
            'year': row[4],
            'average': row[5]
        }
        students.append(student)

    return render(request, 'student_list.html', {'students': students})

Результат будет одинаковым:

Проект 1: Приложение Books для хранения информации о книгах

Мы разработаем приложение, которое будет использовать XLSX-файл вместо базы данных. Код готового приложения находится здесь.

Хотя Django может работать с практически любыми файлами, удобнее всего использовать именно XLSX, поскольку такие файлы можно редактировать с помощью любого приложения для работы с электронными таблицами – Excel, Google Sheets, LibreOffice Calc и так далее. Таблица, в которой хранятся данные о книгах, выглядит так:

Обзор проекта

Приложение будет выводить данные из файла books.xlsx, который хранится в папке media. Для каждой книги выводится одинаковый набор данных:

  • название;
  • автор;
  • жанр;
  • год издания;
  • краткое содержание;
  • обложка.

Обложки находятся в media/covers.

Предусмотрено переключение темы со светлой на темную:

Приступаем к работе

Создайте виртуальное окружение и активируйте его:

python -m venv myproject\venv
cd myproject
venv\scripts\activate

Поместите файл requirements.txt в myproject и установите все нужные зависимости:

pip install -r requirements.txt

Создайте проект config и приложение books:

django-admin startproject config . 
manage.py startapp books

Теперь нужно зарегистрировать приложение books в config/settings.py и добавить туда же настройки для папки media:

config/settings.py
INSTALLED_APPS = [
…
    'books.apps.BooksConfig',
]
…
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Не забудьте добавить настройки media в файл config/urls.py:

config/urls.py
urlpatterns += static(settings.MEDIA_URL, 
	document_root=settings.MEDIA_ROOT)

Напомним, что папка media располагается на одном уровне с приложением books. В media находится файл books.xlsx и вложенная директория covers, в которой хранятся обложки книг.

Функции представления views.py

Все основные действия по извлечению из Excel-файла информации о книгах выполняет get_books(). Остальные функции, которые выводят список всех книг, а также книги конкретных жанров, полагаются на get_books():

from django.shortcuts import render
from openpyxl import load_workbook

def get_books(genre=None):
    file_path = 'media/books.xlsx'
    workbook = load_workbook(filename=file_path)
    worksheet = workbook.active

    books = []
    skip_first_row = True
    for row in worksheet.iter_rows(values_only=True, min_row=2 if skip_first_row else 1):
        genre_check = row[3].lower() == genre.lower() if genre else True
        if genre_check:
            book = {
               'book_id': int(row[0]),           
               'title': row[1],
               'author': row[2],
               'genre': row[3],
               'year': int(row[4]),
               'description': row[5],
               'cover': row[6]
           }
            books.append(book)
    return books

def all_books(request):
    books = get_books()
    return render(request, 'books.html', {'books': books})

def book_detail(request, book_id):
    books = get_books()
    book = books[int(book_id) - 1] 
    return render(request, 'book_detail.html', {'book': book})

def thriller(request):
    books = get_books('триллер')
    return render(request, 'books.html', {'books': books})

def mystery(request):
    books = get_books('детектив')
    return render(request, 'books.html', {'books': books})

def fantasy(request):
    books = get_books('фэнтези')
    return render(request, 'books.html', {'books': books})

def programming(request):
    books = get_books('программирование')
    return render(request, 'books.html', {'books': books})

При обработке данных из файла media/books.xlsx функция пропускает первую строку с названиями столбцов. Порядковый номер и год выпуска преобразуются в целые числа int(), так как по умолчанию модуль openpyxl считает их float().

Если приложение использует не локальный файл books.xlsx из папки media, а таблицу Google Sheets, то функции будут выглядеть так:

from django.shortcuts import render
import csv
import requests

def get_books(request, genre=None):
   sheet_id = "1IkfPkI5ZBtzVJlyomD21nFg4ZUMnJgqLqVrNbiTWkME"
   sheet_name = "Books"
   url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/gviz/tq?tqx=out:csv&sheet={sheet_name}"

   response = requests.get(url)
   if response.ok:
       book_data = response.text
       reader = csv.DictReader(book_data.splitlines())

       books = [
           {
              'book_id': int(row['Номер']),
              'title': row['Название'],
              'author': row['Автор'],
              'genre': row['Жанр'],
              'year': int(row['Год издания']),
              'description': row['Краткое содержание'],
              'cover': row['Обложка']
           }
           for row in reader if genre is None or row['Жанр'].lower() == genre.lower()
       ]

       return render(request, 'books.html', {'books': books})
   else:
       return render(request, 'error.html', {'message': 'Ошибка получения данных'})


def all_books(request):
    return get_books(request)

def book_detail(request, book_id):
    books = get_books(request)
    book = books[int(book_id)] 
    return render(request, 'book_detail.html', {'book': book})

def thriller(request):
    return get_books(request, 'триллер')

def mystery(request):
    return get_books(request, 'детектив')

def fantasy(request):
    return get_books(request, 'фэнтези')

def programming(request):
    return get_books(request, 'программирование')

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

Данные передаются в шаблон books.html. Обратите внимание, что шаблон books.html наследует базовый шаблон base.html, в котором:

  • Определена общая для всех страниц разметка.
  • Подключен Bootstrap – CSS/HTML/JS фреймворк.
  • Заданы дополнительные CSS стили <style></style>.
  • Содержится JS-скрипт, который переключает тему со светлой на темную и запоминает выбор пользователя в localStorage браузера.

Подробнее механизмы наследования и включения шаблонов мы рассмотрим в главе, посвященной шаблонизатору Django. Заметим, что дополнительные CSS стили и JS-скрипты для крупных проектов обычно хранятся в отдельных файлах в папке static – с этой концепцией мы подробно разберемся в разделе «Статические файлы».

Вывод данных в шаблоне происходит в цикле:

{% for book in books %}
<div class="card mb-3 px-0">
  <div class="row g-0">
    <div class="col-md-4 card-image">
      <img src="/media/covers/{{book.cover}}.jpeg" class="img-fluid rounded" alt="{{ book.cover }}">
    </div>
    <div class="col-md-8">
      <div class="card-body">
        <h5 class="card-title">{{ book.title }}</h5>
        <h5 class="card-title text-muted">{{ book.author }}</h5>
        <span class="badge text-bg-danger">{{ book.genre }}</span>
        <p class="card-text">{{ book.description|truncatechars:"120"|linebreaks }}</p>
        <p class="text-end"><a href="{% url 'book_detail' book_id=book.book_id %}" class="btn btn-primary">Подробнее</a></p>
      </div>
    </div>
  </div>
</div>
{% endfor %}

Для ограничения описания книги до 120 символов используется конструкция |truncatechars:"120"|linebreaks. Ссылка, привязанная к кнопке «Подробнее», обеспечивает передачу в функцию book_detail() порядкового номера книги – так Django узнает, о какой именно книге нужно вывести информацию на отдельной странице:

Верхнее меню содержит ссылки на другие функции представления, которые обеспечивают вывод книг по жанрам:

               <ul class="navbar-nav ml-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'thriller' %}">Триллеры</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'programming' %}">Программирование</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'fantasy' %}">Фэнтези</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'mystery' %}">Детективы</a>
                    </li>
                </ul>

При нажатии на эти ссылки происходит запуск соответствующих функций представления в books/views.py:

  • thriller() – выбирает из books.xlsx все триллеры;
  • mystery() – выводит детективы;
  • fantasy() – отбирает книги в жанре фэнтези;
  • programming() – выбирает книги по программированию.

Маршруты, которые приводят в действие все эти функции, определены в books/urls.py:

urlpatterns = [
    path('books/', views.all_books, name='all_books'),
    path('book/<int:book_id>/', views.book_detail, name='book_detail'),
    path('programming/', views.programming, name='programming'),
    path('thriller/', views.thriller, name='thriller'),
    path('mystery/', views.mystery, name='mystery'),
    path('fantasy/', views.fantasy, name='fantasy'),
]

Все классы, используемые в шаблонах (например, class="card-text">) являются классами Bootstrap. Классы позволяют применять к элементам шаблона наборы CSS стилей, которые обеспечивают тексту и изображениям привлекательный внешний вид. Стили можно дополнять, а при необходимости – полностью переопределять. Например, в шаблоне base.html мы определяем два стиля, которые обеспечивают фиксированную позицию для футера – внизу страницы:

    .content-wrapper {
        min-height: 100%;
        position: relative;
        padding-bottom: 60px; 
    }

    footer {
        position: absolute;
        bottom: 0;
        width: 100%;
        height: 60px; 
        background-color: #343a40;
        color: #fff;
        text-align: center;
        padding: 15px;
    }

Без этих стилей футер не будет «прилипать» к низу страницы, если на ней мало контента:

Подведем итоги

Небольшие веб-приложения вполне могут обойтись без базы данных. К тому же использование файла вместо БД может быть оптимальным решением в том случае, если над сбором данных работают несколько человек: предоставить общий доступ к XLSX-файлу гораздо проще, чем к базе.

***

Содержание курса

  1. Часть 1: Django — что это? Обзор и установка фреймворка, структура проекта
  2. Проект 1: Веб-приложение на основе XLSX вместо базы данных
  3. Часть 2: ORM и основы работы с базами данных
  4. Проект 2: Портфолио разработчика

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...