django

Создаем Todo приложение c помощью Django. Часть 1

  • вторник, 18 февраля 2020 г. в 00:26:25
https://habr.com/ru/company/otus/blog/488748/
  • Блог компании OTUS. Онлайн-образование
  • Разработка веб-сайтов
  • Django


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




Джанго это мощный фреймворк для создания веб-приложений. Изначально Django был создан для того, чтобы быстро создавать, например, новостные сайты (или другие сайты, который нужно создавать максимально быстро). И после нативного PHP не покидает ощущение, что ты едешь на очень быстрой машине разработки. Чтобы посмотреть все его возможности для быстрой разработки, мы с вами попробуем создать простое Todo — приложение.



Начнем с формулировки краткого т.з. У нас будет небольшое веб-приложение с версткой на Bulma (да, я очень люблю Bulma. Возможно, когда-нибудь я сверну на Bootstrap или Picnic, но всему свое время). У нас (пока) нет авторизаций и пользователь может создавать, редактировать и удалять либо категорию дел, либо карточку todo, которая связана с какой-либо категорией, которую создал пользователь. Todo карточку или категорию можно удалить, поставив галочку на checkbox и нажав кнопку удалить.

Основные концепции Django


Немного поговорим о Django. Django реализует архитектурный паттерн MVT (Модель Представление Шаблон), которая немного отличается от знакомого большинству MVC (Модель Представление Контроллер) на котором работает Ruby on Rails и Laravel.

Модель (Model) Модель в Django описывает схему данных в БД. С Django ORM, вы можете самостоятельно описывать поля и любые другие типы данных, и совершать миграции для упрощения разработки.

Представление (View) В представлении в Django вы задаете основную логику и алгоритмы приложения, получаете различные данные из базы данных или манипулируете ими. Представление обычно базируется на функциях request\response. Response представляет из себя обычно HTTP redirect, HTTP error(404), MimeTypes или какой-либо шаблон.

Шаблон Шаблон в Django это простой HTML код со специальным шаблонным языком Django. DTL (Django Template Language) — это язык, с помощью которого вы можете динамически менять содержимое страницы (к примеру, изменять имя пользователя на странице, в зависимости от того, как зовут авторизовавшегося пользователя).

Настройки Файл настроек в Django, в котором находятся все настройки вашего веб-приложения. Он включает в себя секретный ключ, папки с шаблонами, middlewares (которые отвечают, например за то, чтобы ваши приватные альбомы не увидели другие пользователи), подключение к базе данных, и много всего остального.

Url Файл настройки роутинга — примерно то же самое, что и в Angular или Laravel. Это связывает представление с url запросами.

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

Установка Python и Django


Творческое отступление
Автор этой статьи (то есть я) кроме написания статей занимается еще и обучением основам питона детей. По долгу службы я ставил Python и его пакеты на достаточно большое количество разнообразных машин. И кроме переустановок питона, один раз для установки пакета глобально сработал даже даунгрейд версии языка. Да, так тоже бывает.

Версии Python


До последнего времени активно поддерживались и развивались две основные ветки Python: 2.7 и 3.x. Я буду использовать версию 3.7.3 в данной статье, но на самом деле это не так важно. Если вы правда хотите знать разницу между ними, есть специальная вики. С другой стороны, сейчас уже нет никакого смысла использовать Python версии 2.7 — обновление языка остановилось на 2.7.17 (если я правильно понимаю документацию на официальном сайте). Это означает, что есть смысл переводить проекты написанные на Python 2.7.x на новую ветку, а вот новые писать на 2 версии совсем бессмысленно.

Инсталляция Python


Если вы работаете на Mac или Ubuntu — у вас уже скорее всего установлен Python, однако 2 версии. Python третьей версии придется скачивать отдельно, и вызывать его в командной строке вы сможете через python3. В любом случае, лучше всего скачать последний релиз здесь.

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


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

Так как же использовать virtual env?



1) Самый простой вариант. Вы можете скачать замечательный IDE от JET BRAINS PyCharm Community Edition отсюда. После установки PyCharm создайте новый проект, и Pycharm по умолчанию предложит вам создать Virtual Env, в котором будет возможность установить нужную версию Django (или по умолчанию последнюю, которая на момент написания данной статьи 3.0.2):

pip3 install django

2) Чуть более хардкорный вариант:

А что, если вы хотите запустить Django в virtual env, к примеру, в любимой папке?
Во, первых, создаем папку, в которой мы будет творить:

	mkdir myfirstdjango && cd myfirstdjango

Дальше вводим следующие команды для активации venv, где django_env имя нашего виртуального окружения:

	python3 -m venv django_env
	source django_env/bin/activate

Далее наше виртуальное окружение активировалось. Можем поставить необходимые пакеты. В нашем случае это Django:

	pip3 install django

Если вы захотите выключить виртуальное окружение, чтобы вернуться в ваш глобальный python (вернуться в контекст system), введите следующую команду:

    deactivate

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

Создание самого проекта


Допустим вы выбрали какой-то из способов создания своего виртуального окружения (или даже делаете все глобально, что же, никто не запрещает вам это делать). Теперь проходим в папку проекта и начинаем его создание:

    django-admin startproject todo  #создаем корневую папку вашего проекта
    cd todo #проходим в нее
    python manage.py startapp todolist  #как бы подприложение todolist
	python3 manage.py runserver 8100 #поднимаем сервер на нашем любимом незанятом порте, 8000 по умолчанию
 

Так, после того как Django открыл стартовую страницу, необходимо проинсталлировать наше приложение todolist в основное приложение. Открываем settings.py и добавляем в уже имеющийся список приложений наш собственный todolist:

INSTALLED_APPS = [
	#предустановленные админки, аутентификации и остальное из коробки, не вижу смысла здесь перечислять
    'todolist',
]

Следующим шагом будет связывание приложения с базой данных. Если базы данных — это не то, с чем вы хотите возиться, вам стоит воспользоваться решением по умолчанию — SQlite. Я же решил воспользоваться PostgreSQL — она популярна и классически связана с Django, кроме того, потом мы можем захотеть увеличить производительность приложения. Инструкций как устанавливать PostgreSQL на все операционные системы достаточно много. Я разрабатываю под MacOS и не без небольших танцев с бубном я поставил эту базу данных, скачав сам Postgres.app отсюда . Что касается интерфейсов для БД, то здесь я воспользовался Postico и пробной версии для разработки приложения нам вполне хватит (хотя в принципе можно обойтись и без неё, потому что все наше взаимодействие с базой данных будет построено через само веб-приложение и миграции). Кроме того, пришлось поставить psycopg2 в виртуальное окружение проекта (без этого драйвера с БД ваше приложение работать не будет).

Дальше нужно настроить работу статики. По-прежнему редактируем файл settings.py, теперь в самом конце добавляем работу со статикой:

STATIC_URL = '/static/'
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage'

Для того, чтобы у вас заработала статика, проверьте что в списке INSTALLED_APPS находился пакет, отвечающий за статику:

django.contrib.staticfiles, на тот случай, если произойдет ошибка.
И последнее в подготовительных работах, нам нужно ещё настроить базовую работу url в проекте:

	from django.conf.urls import url
	from django.contrib import admin
	from todolist.views import todo
	from todolist.views import category
	from todolist.views import redirect_view
 
urlpatterns = [
	url(r'$^', redirect_view ),
	url(r'^admin/', admin.site.urls),
	url(r'^todo/', todo, name="TodoList"),
	url(r'^category/', category, name="Category"),
]

Я добавил редирект так как хочу, чтобы с дефолтной страницы localhost сразу переходил на подстраницу category(чтобы не дай бог пользователь не потерялся). Также у нас есть роутинг на две страницы: категорий и дел.

Итак, надеюсь ваше приложение не упало. Далее мы можем наконец-то переходить к созданию самого приложения:

Создание модели Todo и Категорий


Дальше приступим к созданию модели, которая будет базово взаимодействовать с нашей базой данных. Для создания модели открываем файл models.py в нашем todolist и начинаем творить. Начнем с того, что создадим таблицу категорий:

from django.utils import timezone #мы будем получать дату создания todo
from django.db import models
 
class Category(models.Model): # Таблица категория которая наследует models.Model
    name = models.CharField(max_length=100) #varchar.Нам потребуется только имя категории
	class Meta:
    	verbose_name = ("Category") # человекочитаемое имя объекта
    	verbose_name_plural = ("Categories")  #человекочитаемое множественное имя для Категорий
    def __str__(self):
        return self.name  # __str__ применяется для отображения объекта в интерфейсе

Отлично! Да, здесь у нас будет только две колонки в таблице Категорий: id и name. Дальше создадим таблицу для наших дел. Думаю, из комментариев все понятно:

class TodoList(models.Model):
	title = models.CharField(max_length=250)
	content = models.TextField(blank=True) #текстовое поле
	created = models.DateField(default=timezone.now().strftime("%Y-%m-%d")) # дата создания
	due_date = models.DateField(default=timezone.now().strftime("%Y-%m-%d")) #до какой даты нужно было сделать дело
	category = models.ForeignKey(Category, default="general",on_delete=models.PROTECT) # foreignkey с помощью которой мы будем осуществлять связь с таблицей Категорий
	class Meta: #используем вспомогательный класс мета для сортировки наших дел
        ordering = ["-created"] #сортировка дел по времени их создания
	def __str__(self):
    	return self.title

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

	python3 manage.py makemigrations

И потом запускаете сами миграции:

	python3 manage.py migrate

Создание view


Откроем файл view.py в todolist и отредактируем его. Для начала добавим необходимые импорты и редирект с главной на category:

from django.shortcuts import render, redirect #для отображения и редиректа берем необходимые классы
from django.http import HttpResponse
from .models import TodoList, Category #не забываем наши модели
 
def redirect_view(request):
	return redirect("/category") # редирект с главной на категории

Потом начинаем создание нашего дела. У экземпляра дела будут поля самого текста, даты, до которой должно быть закончено дело, категория дела, и объединенный контент:

def todo(request):
	todos = TodoList.objects.all() #запрашиваем все объекты todo через менеджер объектов
    categories = Category.objects.all() #так же получаем все Категории

После этого добавим функции добавления и удаления дел:

if request.method == "POST": #проверяем то что метод именно POST
	if "Add" in request.POST: #проверяем метод добавления todo
    	title = request.POST["description"] #сам текст
    	date = str(request.POST["date"]) #дата, до которой должно быть закончено дело
        category = request.POST["category_select"] #категория, которой может выбрать или создать пользователь.
        content = title + " -- " + date + " " + category # полный склеенный контент
    	Todo = TodoList(title=title, content=content, due_date=date, category=Category.objects.get(name=category))
    	Todo.save() # сохранение нашего дела
        return redirect("/todo") # перегрузка страницы (ну вот так у нас будет устроено очищение формы)
    if "Delete" in request.POST: #если пользователь собирается удалить одно дело
        checkedlist = request.POST.getlist('checkedbox') # берем список выделенные дел, которые мы собираемся удалить
        for i in range(len(checkedlist)): #мне почему-то не нравится эта конструкция
            todo = TodoList.objects.filter(id=int(checkedlist[i]))
        	todo.delete() #удаление дела
return render(request, "todo.html", {"todos": todos, "categories": categories})

С тудушками все. Дальше можем перейти к странице Категорий. Создаем функцию категорий, в которой у нас тоже будет функция добавления и удаления категории. Принципиально здесь ничего нового не будет, у нас так же здесь будет возможность добавления и удаления:

def category(request):
	categories = Category.objects.all()  #запрашиваем все объекты Категорий
	if request.method == "POST": #проверяем что это метод POST
    	if "Add" in request.POST: #если собираемся добавить
        	name = request.POST["name"] #имя нашей категории
        	category = Category(name=name) #у нашей категории есть только имя
            category.save() # сохранение нашей категории
        	return redirect("/category")
    	if "Delete" in request.POST: # проверяем есть ли удаление
            check = request.POST.getlist('check') #немного изменил название массива в отличии от todo, что бы было меньше путаницы в коде
            for i in range(len(check)):
            	try:
                	сateg = Category.objects.filter(id=int(check[i]))
                	сateg.delete()   #удаление категории
            	except BaseException: # вне сомнения тут нужно нормально переписать обработку ошибок, но на первое время хватит и этого
                	return HttpResponse('<h1>Сначала удалите карточки с этими категориями)</h1>')
	return render(request, "category.html", {"categories": categories})

На этом мы заканчиваем с файлом view и можем переходить к шаблонам:

Работа с шаблонами


Как вы помните, чтобы не писать css лишний раз, я воспользовался bulma.css для упрощения верстки. Т.к. наши страницы категорий и todo буду очень схожи, я создал три файла:
base.html, который будет включать в себя все одинаковое, что у нас есть на страницах, а в category.html, todo.html будут располагаются отличия:



Создаем base.html и редактируем его:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title> Приложение для дел</title>
  {% load static %}
	<link rel="shortcut icon" type="image/png" href="{% static 'favicon.png' %}"/>
   <link rel="stylesheet" type="text/css" href="{% static 'bulma.min.css' %}">
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<div django-app="TaskManager">
	<nav class ="navbar is-success" role="navigation" aria-label="main navigation">
     	<div class="navbar-menu ">
        	<div class="navbar-start">
        	<a class="navbar-item" href="../category"> Категории</a>
        	<a class="navbar-item" href="../todo"> Список дел </a>
        	</div>
         </div>
	</nav>
<!--     в блок контент мы будем подгружать наши странички категорий и тудушек-->
{% block content %}
{% endblock %}
</div>
</body>
</html>

Дальше у нас пойдут страницы todo.html и category.html:

Тудушка:

{% extends 'base.html' %}
{% block content %}
<div class="columns has-background-black-ter is-centered has-text-white-bis" style="min-height:101vh;">
	<!-- на самом деле можно было обойтись и hero, но я обратился к inline-css -->
	<div class="column is-half">
    	<h1 class="is-size-3 has-text-centered"> Список дел </h1>
    	<form action="" method="post">
        	{% csrf_token %}
        	<!-- csrf для базовой безопасности нашего приложения -->
            <div class="field has-text-centered">
            	<label for="description" class="label has-text-white-bis">Введите дело</label>
            	<div class="control">
             	   <input type="text" id="description" class="input" placeholder="Чем собираетесь заняться?"
                    	name="description" required>
            	</div>
        	</div>
        	<div class="columns">
            	<div class="column">
                	<label for="category">Категории</label>
                	<div class="control">
                    	<div class="select">
                        	<select id="category" class="select" name="category_select" required>
                            	<!--поставишь такой required, и не надо пустые поляв бд валидизировать. Не повторять в продакшене-->
                            	<option class="disabled" value="">Выберите категорию дела</option>
                    	        {% for category in categories %}
 	                           <option class="" value="{{ category.name }}" name="{{ category.name }}">
                                    {{ category.name }}</option>
                            	{% endfor %}
                        	</select>
    	                </div>
                	</div>
            	</div>
            	<div class="column">
                	<label for="dueDate">Выберите дату</label>
                	<input type="date" id="dueDate" class="input calendar" name="date" required>
            	</div>
        	</div>
        	<div class="column">
            	<div class="field">
                	<button class="button is-primary" name="Add" type="submit">
                    	<span class="icon is-small">
                        	<i class="fa fa-plus"></i>
                    	</span>
                    	<span>Добавить задание</span>
                	</button>
             	   <button class="button is-link" name="Delete" formnovalidate="" type="submit">
                    	<span class="icon is-small">
                        	<i class="fa fa-trash-o"></i>
                    	</span>
                    	<span>
                        	Удалить дело
                    	</span>
                	</button>
            	</div>
        	</div>
        	<div class="list is-hoverable">
            	{% for todo in todos %}
            	<!-- шаблонный язык django- for loop -->
            	<div class="list-item">
                	<label class="checkbox">
                    	<input type="checkbox" class=" checkbox" name="checkedbox" value="{{ todo.id }}">
                    	<span class="complete-">{{ todo.title }}</span>
                	</label>
                	<span class=" category-{{ todo.category }} has-text-info">{{ todo.category }}</span>
                	<strong class="is-pulled-right"><i class="fa fa-calendar"></i>{{ todo.created }} -
                    	{{ todo.due_date }}</strong>
            	</div>
            	{% endfor %}
        	</div>
    	</form>
	</div>
</div>
{% endblock %}

И category.html. В ней у нас не особо много чего меняется, принципиально никак не отличается от todo.html:

{% extends 'base.html' %}
{% block content %}
<div class="columns has-background-link has-text-white is-centered" style="min-height: 101vh;">
	<div class="column is-half">
    	<h1 class="is-size-4 has-text-centered"> Отредактируйте ваши категории </h1>
    	<form action="" method="post">
        	{% csrf_token %}
        	<!-- csrf для базовой безопасности нашего приложения -->
            <div class="field has-text-centered">
            	<label for="description" class="label has-text-white-bis"> Введите категории </label>
            	<div class="control">
     	           <input type="text" id="description" class="input" placeholder="Какого рода у вас дела?"
                    	name="name" required>
            	</div>
            	<div class="field">
                	<button class="button is-primary" name="Add" type="submit">
                    	<span class="icon is-small">
                        	<i class="fa fa-plus"></i>
                    	</span>
                    	<span>Добавить категорию</span>
                	</button>
                	<button class="button is-danger" name="Delete" formnovalidate="" type="submit">
                    	<span class="icon is-small">
           	             <i class="fa fa-trash-o"></i>
                    	</span>
	                    <span> Удалить категорию </span>
                	</button>
            	</div>
            </div>
 
            <!--  cписок наших категорий -->
            <div class="list is-hoverable">
            	{% for category in categories %}
            	<div class="list-item">
                	<label class="checkbox">
                    	<input type="checkbox" class="checkbox" name="check" value="{{category.id}}">
                    	<span class="complete-">{{ category.name }}</span>
                	</label>
            	</div>
            	{% endfor %}
 
        	</div>
    	</form>
	</div>
    {% endblock %}
 

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

  1. Официальная документация Django, если вы хотите разрабатывать на новой 3 версии
  2. В русскоязычной интернете не так много хороший гайдов по поводу ООП, но этот мне кажется самым полезным
  3. Русскоязычная документация на PostgreSQL