Статьи

Быстрая итерация с Django & Heroku

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

  • Строить слишком много рано : терять время и тратить деньги на создание сложного продукта. По пути быть демотивированным, потерять веру в продукт и отказаться от проекта.
  • Слишком много веря в идею : застрять в оригинальной идее и не повторять ее, даже если клиенты не появляются, не платят или не удовлетворены.
  • Неспособность начать : Когда кто-то начинает идти по пути создания веб-проекта, он / она может быть поражен, казалось бы, бесконечными решениями и выборами, которые необходимо сделать. Какой хостинг использовать? Какая платформа? Какая тема WordPress? Как построить высоко конвертирующуюся целевую страницу? Какой язык программирования и какая база данных? Стоит ли использовать веб-фреймворк? Ванильный JavaScript или jQuery для внешнего интерфейса? Может быть, более сложный интерфейс, потому что он понадобится проекту, когда он станет достаточно зрелым?
  • Невозможность запуска : при создании веб-проекта, даже если вы определились со своим технологическим стеком, вы можете быть поражены обратной связью, которую получаете. Противоположным образом считается ошибкой слушать слишком много отзывов. Это могут быть отзывы людей, которые не будут использовать ваш продукт в любом случае. Люди склонны иметь мнение обо всем, даже если они не совсем осведомлены в этой области.

Учитывая множество способов потерпеть неудачу на дороге, очень важно:

  • Создавайте как можно меньше и как можно быстрее, демонстрируя это людям, которых вы считаете потенциальными клиентами : минимизируйте затраты и усилия.
  • Разместите его в Интернете как можно скорее : получайте отзывы от людей о продукте, а не о вашей абстрактной идее.
  • Быстро вносите изменения . Когда вы узнаете, чего хочет клиент, важно быть гибким и хорошо обслуживать своих первых платящих клиентов.

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

Существует множество идей о том, что такое прототип и как его следует создавать. Некоторые говорят, что это должна быть только целевая страница, другие — что это должна быть урезанная версия конечного продукта. Я больше фанат второго. Использование только целевой страницы может привести к мошенничеству. Кроме того, вы не можете получить обратную связь о том, как вы решаете проблему, а только о том, стоит ли решать проблему.

Вот инструментальный пояс умного предпринимателя по созданию прототипов:

  • Фреймворки внешнего интерфейса: Bootstrap, Foundation, jQuery, Vue и т. Д. Использование внешнего интерфейса позволит вашим приложениям работать на экранах разных размеров и в разных браузерах с достойным дизайном.
  • Бэк-фреймворки : Django, Ruby on Rails, Laravel. Использование внутренних сред поможет вам легко иметь дело с шаблонами HTML, формами HTTP, доступом к базе данных, схемами URL и т. Д.
  • Платформа как услуга : Heroku, Google App Engine, AWS Elastic Beanstalk. Выбор PaaS может избавить вас от необходимости управлять серверами, агрегацией журналов, мониторингом доступности, инфраструктурой развертывания и многим другим.

В этом уроке мы будем создавать простое приложение в духе быстрого прототипирования. Мы будем использовать Django, Bootstrap CSS и Heroku. Основное внимание будет уделено внутренней части, а не передней части.

Мы собираемся воспользоваться платформой Heroku, чтобы быстро что-то подключить к сети и быстро развернуть новые функции. Мы собираемся использовать Django для построения сложных моделей баз данных и функциональности. Bootstrap CSS даст нам разумный стиль по умолчанию для наших страниц. Хватит говорить, поехали.

Убедитесь, что вы садитесь за это. Идея выбьет ваши носки. Вот в чем суть: неужели вы просто ненавидите, как получаете все эти коды скидок, но забыли их использовать, и срок их действия истек?

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

В этом уроке я собираюсь использовать Python 3. Если вы используете Python 2.7, изменения должны быть достаточно простыми. Я также предполагаю, что вы знакомы с setuptools , Python virtualenvs и Git. Еще одна вещь, прежде чем идти вперед: убедитесь, что у вас есть учетная запись GitHub и Heroku. Чтобы использовать Heroku, вам также нужно установить Heroku CLI .

Давайте начнем с создания virtualenv:

1
$ mkvirtualenv coupy

Как вы, наверное, поняли, наше приложение называется Coupy . Давайте переключимся на новый virtualenv, $ workon coupy и установим Django:

1
$ pip install Django

Зайдите в свою учетную запись GitHub и создайте новый проект. Теперь давайте клонируем этот проект:

1
2
$ git clone https://github.com/<GITHUB_USERNAME>/<GITHUB_PROJECT_NAME>.git
$ cd <GITHUB_PROJECT_NAME>

Следующим логическим шагом является создание проекта Django. Чтобы развернуть проект Django в Heroku, нам нужно следовать некоторым рекомендациям. К счастью, мы можем использовать шаблон проекта для этого. Вот как это сделать:

1
$ django-admin.py startproject —template=https://github.com/heroku/heroku-django-template/archive/master.zip —name=Procfile coupy

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

01
02
03
04
05
06
07
08
09
10
11
12
13
.
├── Procfile
├── README.md
├── coupy
│ ├── __init__.py
│ ├── settings.py
│ ├── static
│ │ └── humans.txt
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
└── runtime.txt

Давайте теперь установим требования, предусмотренные шаблоном:

1
$ pip install -r requirements.txt

Теперь мы хотим отправить недавно созданные файлы на GitHub:

1
2
3
$ git add .
$ git commit -m»Init Django project»
$ git push origin master

Давайте посмотрим, работает ли то, что мы сделали до сих пор:

1
$ python manage.py runserver

Теперь откройте окно браузера и перейдите по адресу http: // localhost: 8000 . Если все хорошо, вы должны увидеть классическую страницу приветствия Django. Чтобы убедиться, что все хорошо с точки зрения Heroku, мы также можем запустить приложение следующим образом:

1
$ heroku local web

Чтобы доказать, насколько быстро мы можем выходить в интернет, давайте сделаем наше первое развертывание в Heroku:

1
2
$ heroku login
$ heroku create

Теперь мы создали приложение Heroku, но не отправили код Heroku. Обратите внимание, что Heroku создал удобный идентификатор приложения. Вот результат, который вы должны получить:

1
2
Creating app… done, ⬢ <HEROKU_APP_ID>
https://<HEROKU_APP_ID>.herokuapp.com/ |

Теперь нам нужно связать наше репо с недавно созданным приложением Heroku:

1
2
3
$ heroku git:remote -a <HEROKU_APP_ID>
$ git push heroku master
$ heroku open

Круто, вы только что развернули приложение в Heroku. Это ничего не значит, но вы размещаете что-то в сети в рекордно короткие сроки. Молодец.

Вы, вероятно, никогда не создадите нетривиальное веб-приложение без базы данных. База данных является частью хранилища данных веб-приложения. Здесь веб-приложение сохраняет свое состояние (по крайней мере, большую его часть). Здесь мы храним учетные записи пользователей, данные для входа и многое, многое другое. Heroku предоставляет управляемый сервис PostgreSQL.

Это то, что мы собираемся использовать. Убедитесь, что вы установили Postgres на свой компьютер и создайте экземпляр базы данных для использования в нашем приложении. Heroku необходимо установить переменную среды, чтобы иметь возможность подключаться к службе базы данных. Переменная, которую нам нужно установить — DATABASE_URL :

1
$ export DATABASE_URL=»postgres://<USERNAME>:<PASSWORD>@localhost:5432/<DATABASE_NAME>»

Теперь давайте скажем Django применить миграцию и создать необходимые таблицы:

1
$ ./manage.py migrate

Давайте создадим суперпользователя и войдем в интерфейс администратора по адресу http: // localhost: 8000 / admin :

1
2
$ ./manage.py createsuperuser
$ ./manage.py runserver

Мы видим, что таблицы действительно были созданы. Heroku по умолчанию уже связал экземпляр базы данных с вашим приложением. Вы можете убедиться в этом, проверив в Heroku HEROKU_APP_ID > Settings > Config Variables в онлайн-консоли Heroku. Здесь вы должны увидеть, что DATABASE_URL настроен на сгенерированный Heroku адрес базы данных.

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

1
2
$ heroku run python manage.py migrate
$ heroku run python manage.py createsuperuser

Если все прошло хорошо, если мы посетим https://<HEROKU_APP_ID>.herokuapp.com/admin/ , мы сможем войти в систему с использованием только что предоставленных учетных данных.

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

1
$ ./manage.py startapp main

Внутри нового приложения мы собираемся создать файл urls.py :

01
02
03
04
05
06
07
08
09
10
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from django.views.generic.base import RedirectView
 
 
urlpatterns = [
    url(‘^$’, RedirectView.as_view(url=’login’), name=’index’),
    url(r’^login$’, auth_views.LoginView.as_view(), name=’login’),
    url(r’^logout$’, auth_views.LogoutView.as_view(), name=’logout’),
]

Здесь мы используем три общих вида Django:

  • RedirectView : поскольку базовый URL-адрес приложения ничего не делает, мы перенаправляем на страницу входа.
  • LoginView : предопределенное представление Django, которое создает форму входа в систему и реализует процедуру аутентификации пользователя.
  • LogoutView : предопределенное представление Django, которое выводит пользователя из системы и перенаправляет на определенную страницу.

Добавьте main приложение в список INSTALLED_APPS :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
INSTALLED_APPS = [
    ‘django.contrib.admin’,
    ‘django.contrib.auth’,
    ‘django.contrib.contenttypes’,
    ‘django.contrib.sessions’,
    ‘django.contrib.messages’,
    # Disable Django’s own staticfiles handling in favour of WhiteNoise, for
    # greater consistency between gunicorn and `./manage.py runserver`.
    # http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development
    ‘whitenoise.runserver_nostatic’,
    ‘django.contrib.staticfiles’,
 
    ‘main’,
]

main.urls к корневой схеме URL:

1
2
3
4
5
6
7
from django.conf.urls import url, include
from django.contrib import admin
 
urlpatterns = [
    url(r’^’, include(‘main.urls’)),
    url(r’^admin/’, admin.site.urls),
]

Чтобы правильно отображать формы со стилями, классами и всем остальным, нам нужно установить django-widget-tweaks :

1
2
$ pip install django-widget-tweaks
$ pip freeze > requirements.txt

Добавьте django-widget-tweaks в INSTALLED_APPS :

1
2
3
4
5
INSTALLED_APPS = [
    # …
    ‘main’,
    ‘widget_tweaks’,
]

Теперь добавим эти два конфига в settings.py :

  • LOGIN_REDIRECT_URL : сообщает Django, куда перенаправить пользователя после успешной аутентификации.
  • LOGOUT_REDIRECT_URL : сообщает Django, куда перенаправить пользователя после его выхода из системы.
1
2
3
4
# settings.py
 
LOGIN_REDIRECT_URL = ‘dashboard’
LOGOUT_REDIRECT_URL = ‘login’

Давайте напишем простой основной шаблон base.html и шаблон dashboard.html , расширяющий его. Мы вернемся к приборной панели позже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<!DOCTYPE html>
<html>
<head lang=»en»>
    <meta charset=»UTF-8″>
    <link rel=»stylesheet» href=»https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css» />
 
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <div class=»container»>
    {% block content %}{% endblock %}
    </div><!— /container—>
</body>
</html>
1
2
3
4
5
6
7
{% extends ‘base.html’ %}
 
{% block title %}Dashboard{% endblock %}
 
{% block content %}
<h1>Dashboard</h1>
{% endblock %}

Напишите представление, которое отображает шаблон dashboard.html :

1
2
3
4
5
6
7
from django.shortcuts import render
from django.core.urlresolvers import reverse_lazy
 
 
@login_required(login_url=reverse_lazy(‘login’))
def dashboard(request):
    return render(request, ‘dashboard.html’)

У нас все готово. http://localhost:8000/login/ на http://localhost:8000/login/ и проверьте, работает ли аутентификация. Далее сохраните ваш прогресс:

1
2
$ git add .
$ git commit -m»Login/Logout/Dashboard views»

Теперь мы подошли к самой важной части нашего приложения — разработке модели Coupon. Мы будем устанавливать django-model-utils чтобы добавить некоторые дополнительные свойства в наши модели.

1
2
$ pip install django-model-utils
$ pip freeze > requirements.txt

Напишите модель Coupon :

01
02
03
04
05
06
07
08
09
10
from model_utils.models import TimeStampedModel, TimeFramedModel
from django.db import models
from django.contrib.auth.models import User
 
 
class Coupon(TimeStampedModel, TimeFramedModel):
    owner = models.ForeignKey(User)
    discount_code = models.CharField(«Discount Code», max_length=100)
    website = models.URLField(«Website»)
    description = models.TextField(«Coupon Description»)

Модели django-model-utils которые мы расширили, позволяют нам:

  • TimeStampedModel помогает нам отслеживать, когда модель была помещена в базу данных, через created поле.
  • TimeFramedModel добавляет start и end поля в нашу модель. Мы используем эти поля, чтобы отслеживать доступность купона.

Подключите модель к администратору:

1
2
3
4
5
6
7
from django.contrib import admin
from .models import Coupon
 
 
@admin.register(Coupon)
class CouponAdmin(admin.ModelAdmin):
    pass

Создайте и примените миграции:

1
2
$ ./manage.py makemigrations
$ ./manage.py migrate

Сохранить прогресс:

1
2
$ git add .
$ git commit -m»Create Coupon model»

Одна из замечательных особенностей Django — возможность создавать формы из классов моделей. Мы собираемся создать такую ​​форму, которая позволит пользователям создавать купоны. Давайте создадим файл forms.py внутри main приложения:

1
2
3
4
5
6
7
8
from django.forms import ModelForm
from .models import Coupon
 
 
class CouponForm(ModelForm):
    class Meta:
        model = Coupon
        exclude = (‘owner’, ) # We’re setting this field ourselves

Давайте добавим эту форму на панель инструментов. Нам нужно изменить как вид, так и шаблон:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
# views.py
 
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse_lazy
from .forms import CouponForm
 
 
@login_required(login_url=reverse_lazy(‘login’))
def dashboard(request):
    if request.method == ‘POST’:
        form = CouponForm(request.POST)
        if form.is_valid():
            coupon = form.save(commit=False)
            coupon.owner = request.user
            coupon.save()
            return redirect(‘dashboard’)
    else:
        form = CouponForm()
 
    return render(request, ‘dashboard.html’, context={‘create_form’: form})
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{% extends ‘base.html’ %}
 
{% load widget_tweaks %}
 
{% block title %}Dashboard{% endblock %}
 
{% block content %}
<h1>Dashboard</h1>
 
<form method=»post»>
    {% csrf_token %}
    <div class=»form-group»>
        <label for=»discount_code»>Discount Code</label>
        {% render_field create_form.discount_code class=»form-control» placeholder=»Discount Code» %}
    </div>
 
    <div class=»form-group»>
        <label for=»website»>Website</label>
        {% render_field create_form.website class=»form-control» placeholder=»Website» %}
    </div>
 
    <div class=»form-group»>
        <label for=»description»>Description</label>
        {% render_field create_form.description class=»form-control» placeholder=»Description» %}
    </div>
 
    <div class=»form-group»>
        <label for=»start»>Available From</label>
        {% render_field create_form.start class=»form-control» placeholder=»Available From (MM/DD/YYYY)» %}
    </div>
 
    <div class=»form-group»>
        <label for=»end»>Expires on</label>
        {% render_field create_form.end class=»form-control» placeholder=»Expires On (MM/DD/YYYY)» %}
    </div>
 
    <button type=»submit» class=»btn btn-primary»>Save</button>
</form>
 
 
{% endblock %}

Теперь у нас есть способ создавать купоны из панели инструментов. Иди попробуй. У нас нет возможности увидеть купоны на приборной панели, но мы можем сделать это в панели администратора. Давайте сохраним прогресс:

1
2
$ git add .
$ git commit -m»Coupon creation form in dashboard»

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

Добавьте django.contrib.humanize в INSTALLED_APPS для django.contrib.humanize отображения дат в шаблонах.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from datetime import timedelta
 
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse_lazy
from django.utils import timezone
 
from .forms import CouponForm
from .models import Coupon
 
 
@login_required(login_url=reverse_lazy(‘login’))
def dashboard(request):
    expiring_coupons = Coupon.objects.filter(
        end__gte=timezone.now(),
        end__lte=timezone.now() + timedelta(days=7))
 
    if request.method == ‘POST’:
        form = CouponForm(request.POST)
        if form.is_valid():
            coupon = form.save(commit=False)
            coupon.owner = request.user
            coupon.save()
            return redirect(‘dashboard’)
    else:
        form = CouponForm()
 
    return render(request, ‘dashboard.html’, context={
        ‘create_form’: form,
        ‘expiring_coupons’: expiring_coupons})

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{% extends ‘base.html’ %}
 
{% load widget_tweaks %}
{% load humanize %}
 
{% block title %}Dashboard{% endblock %}
 
{% block content %}
<h1>Dashboard</h1>
<div class=»row»>
    <div class=»col-md-6″>
        [The form code]
    </div>
    <div class=»col-md-6″>
        {% if expiring_coupons %}
            <table class=»table»>
                <thead>
                    <tr>
                        <th>Discount Code</th>
                        <th>Website</th>
                        <th>Expire Date</th>
                    </tr>
                </thead>
            {% for coupon in expiring_coupons %}
                <tr>
                    <td>{{coupon.discount_code}}</td>
                    <td>{{coupon.website}}</td>
                    <td>{{coupon.end|naturalday }}</td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <div class=»alert alert-success» role=»alert»>No coupons expiring soon</div>
        {% endif %}
 
        {% endblock %}
    </div>
</div>

Хорошо смотритесь. Сохраните ваш прогресс:

1
2
$ git add .
$ git commit -m»Implementing the expiring coupon list»

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

01
02
03
04
05
06
07
08
09
10
11
12
# views.py
 
# …
from django.views.generic.list import ListView
from django.db.models import Q
 
 
class CouponListView(ListView):
    model = Coupon
 
    def get_queryset(self):
        return Coupon.objects.filter(Q(end__gte=timezone.now()) | Q(end__isnull=True)).order_by(‘-end’)

Теперь свяжите представление в вашей схеме URL:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
# main/urls.py
 
from django.conf.urls import url
from django.contrib.auth import views as auth_views
from django.views.generic.base import RedirectView
from .views import dashboard, CouponListView
 
 
urlpatterns = [
    url(‘^$’, RedirectView.as_view(url=’login’), name=’index’),
    url(r’^login/$’, auth_views.LoginView.as_view(), name=’login’),
    url(r’^logout/$’, auth_views.LogoutView.as_view(), name=’logout’),
    url(r’^dashboard/$’, dashboard, name=’dashboard’),
    url(r’^catalogue/$’, CouponListView.as_view(template_name=’catalogue.html’), name=’catalogue’),
 
]

Создайте шаблон catalogue.html :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{% extends ‘base.html’ %}
 
{% load humanize %}
 
{% block title %}Catalogue{% endblock %}
 
{% block content %}
<h1>Catalogue</h1>
<div class=»row»>
 
    <div class=»col-md-12″>
        {% if object_list %}
            <table class=»table»>
                <thead>
                    <tr>
                        <th>Discount Code</th>
                        <th>Website</th>
                        <th>Expire Date</th>
                    </tr>
                </thead>
            {% for coupon in object_list %}
                <tr>
                    <td>{{coupon.discount_code}}</td>
                    <td>{{coupon.website}}</td>
                    <td>{{coupon.end|naturalday }}</td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <div class=»alert alert-success» role=»alert»>
                No coupons yet.
            </div>
        {% endif %}
 
        {% endblock %}
    </div>
</div>

Так как мы все подключили, зайдите на http://localhost:8000/catalogue/ чтобы просмотреть каталог ваших купонов.

Сохранить прогресс:

1
2
$ git add .
$ git commit -m»Creating the catalogue view»

Это довольно близко к MVP. Я рекомендую вам выполнить некоторые тонкие настройки, такие как создание навигационной панели, кнопок входа / выхода / регистрации и т. Д. Важно то, что вы понимаете процесс создания прототипа и выпускаете свой продукт, чтобы его увидели люди. Кстати, наш продукт еще не в сети. Мы не давили последнюю версию Heroku. Давайте сделаем это, а затем возьмем трубку и позвоним инвесторам.

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

Вот некоторые выводы, которые мы можем сделать:

  • Выбор правильных инструментов может значительно ускорить процесс разработки.
  • Инструменты, используемые для создания прототипов, не всегда являются лучшим выбором для более зрелых проектов. Помня об этом, лучше использовать более гибкие инструменты на ранних этапах и использовать их, а не заблудиться в мельчайших деталях реализации на ранних этапах.
  • Использование преимуществ PaaS означает, что приложения должны соблюдать несколько шаблонов проектирования. Обычно эти шаблоны имеют смысл, и они заставляют нас писать еще лучший код.
  • У Django есть много ярлыков, которые делают нашу жизнь проще:
    • Django ORM помогает с доступом к базе данных. Не нужно беспокоиться о написании правильного SQL и об осторожности с синтаксическими ошибками.
    • Миграции помогают нам создавать версии и повторять схему базы данных. Не нужно идти и писать SQL для создания таблиц или добавления столбцов.
    • Django имеет плагин-дружественную архитектуру. Мы можем установить приложения, которые помогут нам быстрее достичь наших целей.
    • Общие представления и формы моделей могут внедрять некоторые из наиболее распространенных вариантов поведения: перечисление моделей, создание моделей, аутентификация, перенаправление и т. Д.
  • При запуске важно быть стройным и быстрым. Не тратьте время и не тратьте деньги.