Перейти к содержанию

Категории

Создание контроллера

Все контроллеры по умолчанию расположены в каталоге app/controllers и Masonite продвигает идею один контролер, один файл. Легко запомнить, где именно находится контроллер, потому что имя файла — это контроллер.

Конечно, вы можете перемещать контроллеры куда угодно, но командна craft по умолчанию поместит их в отдельные файлы.

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

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

python craft controller Category

Команда craft создаст контроллер app/controllers/CategoryController.py, который выглядит следующим образом:

app/controllers/CategoryController.py
1
2
3
4
5
6
7
8
9
from masonite.controllers import Controller
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def show(self, view: View):
        return view.render("")
Вы можете заметить, что в контроллере уже есть один метод, show(). Данный метод мы рассмотрим позже.

Создание категорий

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

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def show(self, view: View):
        return view.render("")

    def store(self, request: Request, response: Response):
        Category.create(name=request.input("name"))
        return response.redirect('/')

Бизнес логика

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

Service Container

Обратите внимание, что мы сейчас использовали request: Request и response: Response. Это объекты Request и Response. В этом сила и красота Masonite, и ваше первое знакомство с Service Container. Service Container — чрезвычайно мощная реализация, позволяющая запросить у Masonite объект (в данном случае Request или Response) и получить этот объект. Это называется «внедрением зависимостей», важная концепция для понимания, поэтому обязательно прочитайте документацию.

С помощью метода create() модели Category создадим категорию. В create() передаем название столбца и данные которые хотим записать.

Для получения данных из запроса используем метод input(). Masonite не обращает внимание на методы запроса, поэтому для получения данных по запросу GET, POST и т.д. мы используем метод .input().

Валидация

В данный момент мы не проверяем пришедшие данные. Добавим валидацию.

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def show(self, view: View):
        return view.render("")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')
На 14-ой строке указываем, что name обязателен. Более подробно про валидацию можете прочитать в документации.

Далее если у нас есть ошибки, пользователь будет перенаправлен на страницу с которой был отправлен запрос.

View

Добавим еще один метод .create(), данный метод будет рендерить шаблон добавления категории:

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def show(self, view: View):
        return view.render("")

    def create(self, view: View):
        return view.render("category.create")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')
Обратите внимание, что здесь мы также "типизируем" класс View. Это то, что Masonite называет "внедрением зависимостей", о чём говорилось ранее.

В метод .render() передаем путь к шаблону через точку. Далее мы создадим этот шаблон и он будет доступен по пути templates/category/create.html.

Добавление routes

В файле routes/web.py добавим два маршрута.

routes/web.py
1
2
3
4
5
6
7
from masonite.routes import Route


ROUTES = [
    Route.get("/create", "CategoryController@create"),
    Route.post("/create", "CategoryController@store"),
]
При get запросе будет вызван метод контроллера create() и который отобразит страницу.

При post запросе будет вызван метод контроллера store(), который примет отправленные данные формы.

Создание html шаблона

Теперь в директории templates создадим директорию category, а в ней файл create.html.

В файл templates/category/create.html добавим следующий код:

templates/category/create.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Title</title>
</head>
<body>
<section>

  <h2>Создать категорию</h2>

  <form action="/create" method="POST">
    {{ csrf_field }}
    <input type="text" name="name" placeholder="Название категории">
    <button class="btn" type="submit">Создать</button>
  </form>

</section>
</body>
</html>

Обратите внимание, здесь есть тег {{ csrf_field }} под тегом <form>. Masonite поставляется с защитой от CSRF, поэтому нам нужен токен для отображения скрытого поля с CSRF токеном.

templates/category/create.html
1
2
3
4
5
6
7
8
9
...
  <h2>Создать категорию</h2>

  <form action="/create" method="POST">
    {{ csrf_field }}
    <input type="text" name="name" placeholder="Название категории">
    <button class="btn" type="submit">Создать</button>
  </form>
...
На 4-й строке в атрибуте action указываем ссылку куда отправить данные из формы. Данную ссылку мы описали ранее в routes/web.py. Так же указали метод http запроса, используем POST.

templates/category/create.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Title</title>
</head>
<body>
<section>

  <h2>Создать категорию</h2>

  <form action="/create" method="POST">
    {{ csrf_field }}
    <input type="text" name="name" placeholder="Название категории">
    <button class="btn" type="submit">Создать</button>
  </form>

</section>
</body>
</html>
Здесь указываем ссылку на файл стилей, ниже мы его создадим.

Статические файлы

В директории storage создайте папку static. Создайте файл style.css в директории storage/static/ и добавим в него следующий код.

storage/static/style.css
section {
    width: 700px;
    margin: 0 auto;
    padding: 1px 15px 30px 15px;
    box-shadow: 0 3px 5px 0 grey;
    text-align: center;
}

a {
    text-decoration: none;
}

.list-item {
    display: flex;
    justify-content: space-between;
    margin: 15px 0 5px 0;
    padding: 5px 10px;
    box-shadow: 1px 1px 4px grey;
}

.list-item:hover {
    background-color: #f7f7f7;
}

.btn {
    background-color: #199319;
    color: white;
    padding: 10px 10px;
    text-decoration: none;
    border: none;
    cursor: pointer;
}

.btn:hover {
    background-color: #223094;
}

.btn-del {
    background-color: #e32323;
    color: white;
    padding: 10px 20px;
    text-decoration: none;
}

.btn-del:hover {
    background-color: #c91f1f;
}

.done::after {
    content: '\2713';
    color: #199319;
}

.work::after {
    content: '\25CF';
    color: #c91f1f;
}

Список категорий

Теперь выведем список категорий которые мы создаем.

Контроллер

В файле app/controllers/CategoryController.py добавим следующий код:

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def index(self, view: View):
        categories = Category.all()
        return view.render('category.list', {'categories': categories})

    def show(self, view: View):
        return view.render("")

    def create(self, view: View):
        return view.render("category.create")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')
Здесь мы получаем все категории из БД. Вызвав метод all() модели, мы получим все записи из БД. Затем указываем какой шаблон рендерить и передаем контекст в виде словаря. Таким образом передаем данные в шаблон. По ключу словаря будем обращаться к данным в самом шаблоне.

Routes

В файле routes/web.py добавим маршрут для вывода списка категорий. Перейдя на главную страницу сайта будем получать все категории.

routes/web.py
1
2
3
4
5
6
7
8
from masonite.routes import Route


ROUTES = [
    Route.get("/", "CategoryController@index"),
    Route.get("/create", "CategoryController@create"),
    Route.post("/create", "CategoryController@store"),
]

Шаблон html

В директории templates/category создадим файл list.html и добавим следующий код.

templates/category/list.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Категории</title>
</head>
<body>
<section>

  <div class="center">
    <h2>Список категорий</h2>
  </div>

  @for category in categories
  <div class="list-item">
    <a href="/task/{{category.id}}">{{category.name}}</a>
    <a class="edit" href="/single/{{category.id}}">Редактировать</a>
  </div>
  @endfor

</section>
</body>
</html>

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

Наследование шаблонов

Jinja2 поддерживает расширение (наследование) шаблонов, чтобы избежать повторения кода. В Masonite уже предустановлен базовый шаблон, templates/base.html. В данном руководстве мы не будем использовать наследование шаблонов, для упрощения задачи.

templates/category/list.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Категории</title>
</head>
<body>
<section>

  <div class="center">
    <h2>Список категорий</h2>
  </div>

  @for category in categories
  <div class="list-item">
    <a href="/task/{{category.id}}">{{category.name}}</a>
    <a class="edit" href="/single/{{category.id}}">Редактировать</a>
  </div>
  @endfor

</section>
</body>
</html>

Здесь с помощью шаблонизатора, описываем цикл for. Который переберет все переданные категории. В фигурных скобках указываем объект категории и через точку обращаемся к атрибуту (столбцу таблицы) name и id.

На строке 17 указываем ссылку на список задач конкретной категории, передаем id категории. В данный момент у нас еще нет логики обработки этого url, реализуем позже.

На 18-й строке формируем ссылку на странице редактирования. Ниже реализуем данный функционал.

Запуск сервера разработки

Выполним команду для запуска сервера разработки:

python craft serve
И перейдите по адресу http://127.0.0.1:8000/create

Вы увидите форму, после отправки которой вас перенаправит на http://127.0.0.1:8000/

Чтение, редактирование и удаление категорий

Также сделаем, чтобы можно было редактировать и удалять категорию.

Вывод одной категории

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

Контроллер одной категории

Доработаем метод show() контроллера CategoryController.

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def index(self, view: View):
        categories = Category.all()
        return view.render('category.list', {'categories': categories})

    def show(self, view: View, request: Request):
        category = Category.find_or_fail(request.param("id"))
        return view.render('category.single', {'category': category})

    def create(self, view: View):
        return view.render("category.create")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')

В метод show() добавил получение request. Чтобы получить параметр из маршрута. Для этого используем метод param() с указанием имени параметра.

Затем ищем категорию по id, который будет передаваться в url, например, /single/2. Здесь 2 и есть наш id. Если такая категория не будет найдена, мы увидим ошибку 404. Если вы не хотите получать эту ошибку, можете вызвать метод find() у модели.

Затем мы указываем какой шаблон будем использовать и передаем в контекст объект категории.

Route одной категории

routes/web.py
1
2
3
4
5
6
7
8
9
from masonite.routes import Route


ROUTES = [
    Route.get("/", "CategoryController@index"),
    Route.get("/create", "CategoryController@create"),
    Route.post("/create", "CategoryController@store"),
    Route.get("/single/@id", "CategoryController@show").name("category_single"),
]
Здесь в url указан параметр id. Чтобы указать параметр, его нужно прикрепить к символу @.

Также я указал имя маршрута, оно используется для получения информации о маршруте в других частях проекта. Имя более статично, чем URL-адрес.

Шаблон одной категории

В директории templates/category создадим файл single.html.

templates/category/single.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Редактирование категории</title>
</head>
<body>
<section>

  <h2>Редактирование категории</h2>

  <form action="/update/{{category.id}}" method="POST">
    {{ csrf_field }}
    <input type="text" name="name" value="{{category.name}}">
    <p>
      <button class="btn" type="submit">Сохранить</button>
    </p>
  </form>

</section>
</body>
</html>

В форме используя атрибутaction указываем url по которому будет идти отправка формы, контролер и маршрут напишем позже.

В value тега input передаю значение имени категории. Таким образом форма будет заполнена.

Контроллер обновления категории

Добавим метод update() в наш контроллер. Он будет отвечать за обновление категории.

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def index(self, view: View):
        categories = Category.all()
        return view.render('category.list', {'categories': categories})

    def show(self, view: View, request: Request):
        category = Category.find_or_fail(request.param("id"))
        return view.render('category.single', {'category': category})

    def create(self, view: View):
        return view.render("category.create")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')

    def update(self, request: Request, response: Response):
        category = Category.find_or_fail(request.param("id"))

        errors = request.validate({"name": "required"})
        if errors:
            return response.redirect(name='category_single', params={"id": request.param("id")})

        category.name = request.input('name')
        category.save()
        return response.redirect('/')
На 32-й строке указываем, что name обязателен. Если name будет отсутствовать, то пользователь будет перенаправлен на ту же страницу.

Я здесь не использую back(), для того чтобы показать как работать с redirect().

В метод redirect() передаем имя маршрута и в параметрах виде словаря передаем id категории. Таким образом будет построен нужный нам url, на страницу редактирования категории.

app/controllers/CategoryController.py
...
class CategoryController(Controller):
    ...

    def update(self, request: Request, response: Response):
        category = Category.find_or_fail(request.param("id"))

        errors = request.validate({"name": "required"})
        if errors:
            return response.redirect(name='category_single', params={"id": request.param("id")})

        category.name = request.input('name')
        category.save()
        return response.redirect('/')
После получения объекта категории, атрибуту name присваиваем полученное значение от пользователя и сохраняем значение в БД.

Route обновления категории

routes/web.py
from masonite.routes import Route


ROUTES = [
    Route.get("/", "CategoryController@index"),
    Route.get("/create", "CategoryController@create"),
    Route.post("/create", "CategoryController@store"),
    Route.get("/single/@id", "CategoryController@show").name("category_single"),
    Route.post("/update/@id", "CategoryController@update").name("category_update"),
]
Для обновления категории добавляем новый маршрут и указываем метод контроллера update(). Здесь используем метод post.

Запуск сервера

Теперь можно запустить сервер разработки и проверить как работает редактирование категории.

python craft serve
Перейдите по адресу http://localhost:8000. Если вы уже создавали категорию, то кликнув по ссылке Редактировать вас перенаправит на страницу редактирования категории.

Или перейдите по ссылке http://localhost:8000/single/1

У вас должна отобразиться форма с уже заполненными данными.

Если из поля ввода удалить текст и сохранить, вас должно перенаправить на эту же страницу.

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

Удаление категории

Настало время реализовать удаление категории.

Контроллер удаления категории

В контроллер добавим метод destroy() для удаления категории.

app/controllers/CategoryController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.response import Response
from masonite.views import View

from app.models.Category import Category


class CategoryController(Controller):
    def index(self, view: View):
        categories = Category.all()
        return view.render('category.list', {'categories': categories})

    def show(self, view: View, request: Request):
        category = Category.find_or_fail(request.param("id"))
        return view.render('category.single', {'category': category})

    def create(self, view: View):
        return view.render("category.create")

    def store(self, request: Request, response: Response):
        errors = request.validate({"name": "required"})
        if errors:
            return response.back()

        Category.create(name=request.input("name"))
        return response.redirect('/')

    def update(self, request: Request, response: Response):
        category = Category.find_or_fail(request.param("id"))

        errors = request.validate({"name": "required"})
        if errors:
            return response.redirect(name='category_single', params={"id": request.param("id")})

        category.name = request.input('name')
        category.save()
        return response.redirect('/')

    def destroy(self, request: Request, response: Response):
        post = Category.find_or_fail(request.param("id"))
        post.delete()
        return response.redirect('/')
Здесь ищем категорию и если она существует, то удаляем. Для этого используем метод delete().

Маршрут удаления категории

routes/web.py
from masonite.routes import Route


ROUTES = [
    Route.get("/", "CategoryController@index"),
    Route.get("/create", "CategoryController@create"),
    Route.post("/create", "CategoryController@store"),
    Route.get("/single/@id", "CategoryController@show").name("category_single"),
    Route.post("/update/@id", "CategoryController@update").name("category_update"),
    Route.get("/delete/@id", "CategoryController@destroy"),
]
Добавляю еще один маршрут для удаления категории. Также будем передавать id той категории которую хотим удалить. Для удаления будем использовать http метод get.

Шаблон удаления категории

В шаблоне templates/category/single.html добавим ссылку на удаление.

templates/category/single.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Редактирование категории</title>
</head>
<body>

<section>
  <h2>Редактирование категории</h2>

  <form action="/update/{{category.id}}" method="POST">
    {{ csrf_field }}
    <input type="text" name="name" value="{{category.name}}">
    <p>
      <button class="btn" type="submit">Сохранить</button>
      <a class="btn-del" href="/delete/{{category.id}}">Удалить</a>
    </p>
  </form>

</section>
</body>
</html>
Просто добавляем ссылку на удаления категории и подставляем id категории.

После нажатия по ссылке, категория должна удалиться и вы будете перенаправлены на главную страницу.

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

Ссылка на создание категории

Доработаем шаблон списка категорий templates/category/list.html и добавим ссылку на страницу создания категории.

templates/category/list.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="/static/style.css" rel="stylesheet">
  <title>Категории</title>
</head>
<body>
<section>

  <div class="center">
    <h2>Список категорий</h2>

    <a class="btn" href="{{ route('category_create') }}">Создать категорию</a>
  </div>

  @for category in categories
  <div class="list-item">
    <a href="/task/{{category.id}}">{{category.name}}</a>
    <a class="edit" href="/single/{{category.id}}">Редактировать</a>
  </div>
  @endfor

</section>
</body>
</html>

Тут я также добавил просто ссылку, но для построения url использовал route(). В данный метод передаем имя маршрута в виде строки.

Чтобы данную ссылку Masonite смог построить, нужно добавить name нашему маршруту создания категории.

routes/web.py
from masonite.routes import Route


ROUTES = [
    Route.get("/", "CategoryController@index"),
    Route.get("/create", "CategoryController@create").name('category_create'),
    Route.post("/create", "CategoryController@store"),
    Route.get("/single/@id", "CategoryController@show").name("category_single"),
    Route.post("/update/@id", "CategoryController@update").name("category_update"),
    Route.get("/delete/@id", "CategoryController@destroy"),
]

Теперь можете запустить сервер и проверить как это работает.

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

Часть 5