Журнал LinuxFormat - перейти на главную

LXF167:До­бав­лять фор­мы

Материал из Linuxformat
Перейти к: навигация, поиск

Кар­кас для web-при­ло­же­ний. До­ба­вим функ­цио­наль­но­сти

Содержание

Django: Дан­ные и фор­мы

Во вто­рой час­ти се­рии Джо­но Бэ­кон по­ка­зы­ва­ет, как за­ра­нее за­гру­зить в про­ект дан­ные и дать поль­зо­ва­те­лям вне­сти свой вклад с по­мо­щью форм. [[Файл: |100px|left|thumb|Наш эксперт Джо­но Бэ­кон – ме­нед­жер со­об­ще­ст­ва Ubuntu, ав­тор кни­ги «Ис­кус­ст­во со­об­ще­ст­ва» и ос­но­ва­тель еже­год­но­го сам­ми­та ру­ко­во­ди­те­лей со­об­ще­ст­ва. ]]

В LXF165/166 я на­пи­сал вве­дение в Django, и ока­за­лось, что мно­гим из вас, на­ших до­ро­гих чи­та­те­лей, он по­нра­вил­ся, и вы хо­ти­те уз­нать о нем боль­ше. По­это­му я рад со­об­щить, что в на­шей се­рии бу­дут еще три пре­крас­ных ста­тьи. По окон­чании се­рии у вас бу­дет все необ­хо­ди­мое, что­бы на­пи­сать пол­но­цен­ное web-при­ло­жение Django, ко­то­рое изу­мит ва­ших дру­зей.

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

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

Пред­за­груз­ка про­ек­та дан­ны­ми

Од­на из са­мых при­ят­ных черт Django – ав­то­ма­ти­зи­ро­ван­ный ин­тер­фейс ад­минист­ра­то­ра. В дру­гих фрейм­вор­ках обыч­но при­хо­дит­ся соз­да­вать его вруч­ную, на что ухо­дит мно­го вре­мени и ко­да. В Django вы по­лу­чае­те его да­ром, и с ним очень удоб­но до­бав­лять в ба­зу но­вые дан­ные, уда­лять за­пи­си и вы­пол­нять дру­гие дей­ст­вия.

Во мно­гих про­ек­тах Django удоб­но ав­то­ма­ти­че­­ски за­гру­зить в про­ект дан­ные за­ранее. В на­шем при­ме­ре с лом­бар­дом у нас есть таб­ли­ца Category. Ес­ли бы мы уда­ли­ли ба­зу дан­ных и сно­ва соз­да­ли ее, при­шлось бы сно­ва до­бав­лять все ка­те­го­рии. В иде­аль­ном слу­чае хо­ро­шо иметь на­бор ка­те­го­рий по умол­чанию, ко­то­рый бу­дет ав­то­ма­ти­че­­ски соз­да­вать­ся при соз­дании ба­зы дан­ных. Для это­го нуж­но соз­дать кое-ка­кие при­спо­соб­ления – фик­сту­ры [fixtures].

Фик­сту­ра – это про­сто го­то­вая запись, до­бав­ляе­мая в ба­зу дан­ных. Когда мы соз­да­дим фик­сту­ры, Django ав­то­ма­ти­че­­ски им­пор­ти­ру­ет их при соз­дании но­вой ба­зы дан­ных. Вот мы их и по­на­де­ла­ем.

На это есть раз­ные спо­со­бы, но я по­ка­жу два про­стых под­хо­да. Пер­вый – восполь­зо­вать­ся YAML, про­стым фор­ма­том се­риа­ли­за­ции. Для на­ча­ла соз­да­дим ка­та­лог fixtures в при­ло­жении Django (в дан­ном слу­чае, в ка­та­ло­ге inventory). Django ищет ка­та­лог fixtures в ка­ж­дом при­ло­жении и ав­то­ма­ти­че­­ски за­гру­жа­ет най­ден­ные фик­сту­ры при соз­дании ба­зы дан­ных.

Соз­да­дим файл initial_data.yaml в ка­та­ло­ге fixtures и до­ба­вим в него сле­дую­щие стро­ки:

- model: inventory.category

pk: 1

fields:

name: Watches

- model: inventory.category

pk: 2

fields:

name: Electronics

- model: inventory.category

pk: 3

fields:

name: Instruments

- model: inventory.category

pk: 4

fields:

name: Memorabilia

Здесь мы до­ба­ви­ли че­ты­ре за­пи­си в таб­ли­цу Category. Для каж­до­го бло­ка мы ука­зы­ва­ем имя мо­де­ли (таб­ли­ца Category внут­ри при­ло­жения inventory). За­тем в по­ле pk мы ука­зы­ва­ем пер­вич­ный ключ для ка­ж­до­го бло­ка. Он дол­жен быть уникаль­ным чис­лом. Обыч­но мы про­сто на­чи­на­ем с 1 и уве­ли­чи­ва­ем его на единицу с ка­ж­дым бло­ком. На­конец, мы ука­зы­ва­ем ка­ж­дое по­ле в те­ку­щей мо­де­ли бло­ка (Category) и зна­чение для это­го по­ля. По­сколь­ку на­ша мо­дель Category име­ет все­го од­но по­ле (name), это до­воль­но про­сто; мы про­сто ука­зы­ва­ем его зна­чение (на­при­мер, Watches [На­руч­ные ча­сы]).

Что­бы за­гру­зить фик­сту­ры, про­сто уда­ли­те ба­зу дан­ных, уда­лив файл pawnstore.db в корневом ка­та­ло­ге про­ек­та и вы­полнив ко­ман­ду

python manage.py syncdb

При ге­не­ра­ции ба­зы дан­ных вы долж­ны уви­деть сле­дую­щую стро­ку:

Installed 4 object(s) from 1 fixture(s)

Она го­во­рит о том, что на­ши че­ты­ре фик­сту­ры бы­ли за­гру­же­ны в один файл fixture. Хо­тя этот под­ход соз­дания фик­стур удо­бен, он бы­ва­ет скуч­ным и при­во­дит к ошиб­кам. Часть про­бле­мы в том, что мож­но невер­но ука­зать пер­вич­ные клю­чи, и для мо­де­лей с боль­шим ко­ли­че­­ст­вом по­лей на соз­дание ка­ж­дой фикс­туры ухо­дит боль­ше вре­мени. Дру­гой под­ход – до­ба­влять на­ши дан­ные че­рез ин­тер­фейс ад­минист­ра­то­ра и экс­пор­ти­ро­вать фик­сту­ры.

Что­бы при­менить этот под­ход, сна­ча­ла уда­ли­те ба­зу дан­ных и соз­дай­те ее сно­ва, что­бы у нас бы­ла све­жая ба­за дан­ных. Те­перь до­бавь­те дан­ные фик­стур че­рез ин­тер­фейс ад­минист­ра­то­ра. До­бав­ляй­те толь­ко те дан­ные, ко­то­рые нуж­но за­гру­зить за­ранее, так как все, что вы до­ба­ви­те, экс­пор­ти­ру­ет­ся в фик­сту­ры. Когда вы покончите с этим, скон­вер­ти­руй­те со­дер­жи­мое ба­зы дан­ных в файл YAML, за­пустив ко­ман­ду:

python manage.py dumpdata inventory format yaml > initial_data.yaml

Эта ко­ман­да бе­рет все дан­ные из при­ло­жения inventory и за­пи­сы­ва­ет их в файл initial_data.yaml. Когда ко­ман­да от­ра­бо­та­ет, мо­же­те за­гру­зить файл и по­смот­реть на сгенери­ро­ван­ные дан­ные. За­тем про­сто по­мес­ти­те его в ка­та­лог inventory/fixtures, и он бу­дет им­пор­ти­ро­ван при сле­дую­щем за­пуске ко­ман­ды python manage.py syncdb.

До­бав­ля­ем дан­ные че­рез фор­мы

По­ка в на­ших при­клю­чениях с Django мы до­бав­ля­ли дан­ные в ба­зу толь­ко че­рез ин­тер­фейс ад­минист­ра­то­ра. Это не са­мый удоб­ный спо­соб, да к то­му же по­зво­лен­ный толь­ко ад­минист­ра­то­ру. Ес­ли вы хо­ти­те, что­бы ин­фор­ма­цию мог­ли до­бав­лять несколь­ко поль­зо­ва­те­лей, или хо­ти­те из­менить спо­соб до­бав­ления ин­фор­ма­ции в ба­зу дан­ных, нуж­но соз­дать фор­му.

Для но­вич­ков в web-про­грам­ми­ро­вании ска­жу, что фор­мы – это ин­те­рак­тив­ные эле­мен­ты управ­ления, че­рез ко­то­рые мож­но до­бав­лять ин­фор­ма­цию на сайт. На­при­мер, в поч­то­вом кли­ен­те (ска­жем, в Google Mail) фор­ма применяет­ся в окне соз­дания пись­ма, где вво­дят­ся по­лу­ча­тель, те­ма и со­об­щение. Эти эле­мен­ты управ­ления вме­сте с кноп­ка­ми Send/Cancel/Save Draft (От­пра­вить/От­ме­на/Со­хранить чер­но­вик) пред­став­ля­ют со­бой фор­му.

К сча­стью, соз­да­вать фор­мы в Django до­воль­но про­сто. Рас­смот­рим при­мер до­бав­ления фор­мы для до­бав­ления но­во­го пред­ме­та в лом­бард.

Пер­вым де­лом до­бавь­те в файл urls.py стро­ку, ко­то­рая бу­дет со­от­вет­ст­во­вать ад­ре­су до­бав­ления но­во­го пред­ме­та. От­крой­те urls.py и вставь­те в него сле­дую­щую стро­ку:

url(r’^newitem/$’, ‘inventory.views.newitem’, name=”newitem”),

В этой стро­ке мы свя­зы­ва­ем ад­рес http://127.0.0.1:8000/newitem/ с пред­став­лением newitem. Мы соз­да­дим его немно­го поз­же. Воз­мож­но, вы так­же за­ме­ти­ли, что name=“newitem” есть толь­ко в этой стро­ке. Бла­го­да­ря это­му ат­ри­бу­ту, поз­же мы смо­жем ссы­лать­ся на этот ад­рес по имени newitem. Тогда, ес­ли мы ре­шим из­менить ад­рес, шаб­ло­ны ме­нять не при­дет­ся.

Ад­рес за­дан, те­перь соз­да­дим фор­му. Ве­те­ра­ны web-раз­ра­бот­ки сей­час по­ду­ма­ли, что мы на­пи­шем фор­му на HTML. К сча­стью, в Django есть пре­крас­ный спо­соб из­бе­жать этой ра­бо­ты.

Од­на из функ­ций Django – ModelForms. Ес­ли вкрат­це, с ModelForms мож­но ука­зать мо­дель, ку­да нуж­но до­ба­вить дан­ные (на­при­мер, на­шу мо­дель Item), и Django соз­даст фор­му на осно­ве ти­пов по­лей. На­при­мер, в на­шей мо­де­ли Item по­ле Category ссы­ла­ет­ся на эле­мент мо­де­ли Category, а ModelForm ав­то­ма­ти­че­­ски до­ба­вит раз­лич­ные ка­те­го­рии в вы­па­даю­щий спи­сок. Мы так­же за­да­ем тип по­ля Name в CharField, ко­то­рое бу­дет до­бав­ле­но в ви­де од­но­строч­но­го тек­сто­во­го по­ля. За­тем мы за­да­ем по­ле Description с ти­пом TextField, оно бу­дет до­бав­ле­но в ви­де тек­сто­во­го по­ля боль­ше­го раз­ме­ра.

Что­бы восполь­зо­вать­ся ModelForms, оп­ре­де­лим на­шу но­вую фор­му. Для это­го соз­дай­те но­вый файл forms.py в при­ло­жении inventory и до­бавь­те в него сле­дую­щий код:

from django.forms import ModelForm

from inventory.models import Item

class NewItemForm(ModelForm):

class Meta:

model = Item

В этом фай­ле мы сна­ча­ла им­пор­ти­ру­ем мо­дуль ModelForm, а за­тем им­пор­ти­ру­ем на­шу мо­дель Item из при­ло­жения inventory. За­тем соз­да­ем класс но­вой фор­мы NewItemForm ти­па ModelForm. Мы бу­дем соз­да­вать свой класс для ка­ж­дой фор­мы, ко­то­рую нуж­но соз­дать в про­ек­те Django.

За­тем мы соз­да­ем класс Meta как часть на­шей фор­мы и ука­зы­ва­ем, что восполь­зу­ем­ся мо­де­лью Item. Это оз­на­ча­ет, что при дальней­шем об­ра­щении к NewItemForm в на­шем про­ек­те мы бу­дем об­ра­щать­ся к фор­ме ModelForm, сгенери­ро­ван­ной из на­шей мо­де­ли Item.

Соз­да­ем пред­став­ление

Те­перь соз­да­дим пред­став­ление, ко­то­рое бу­дет за­гру­же­но при от­кры­тии ад­ре­са http://127.0.0.1:8000/newitem/. От­крой­те файл views.py и об­но­ви­те стро­ку django.shortcuts, что­бы за­од­но им­пор­ти­ро­вать мо­дуль redirect:

from django.shortcuts import render_to_response, redirect

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

Те­перь до­ба­вим сле­дую­щую стро­ку в на­ча­ло фай­ла:

from django.template import RequestContext

Здесь мы сна­ча­ла им­пор­ти­ру­ем RequestContext, это но­вый спо­соб об­ра­бот­ки на­ше­го пред­став­ления. Бо­лее под­роб­но об этом поз­же. Те­перь до­ба­вим весь код пред­став­ления:

def newitem(request):

form = NewItemForm(request.POST or None)

if form.is_valid():

cmodel = form.save()

cmodel.save()

return redirect(index)

return render_to_response(‘inventory/newitem.html’, {‘item_form’: form}, context_instance=RequestContext(request))

Прой­дем­ся по ка­ж­дой стро­ке это­го пред­став­ле­ния. Сна­ча­ла идет та­кая:

form = NewItemForm(request.POST or None)

Здесь мы соз­да­ем но­вый эк­зем­п­ляр ModelForm. Мы пе­ре­да­дим его на­ше­му шаб­ло­ну поз­же. За­тем мы про­ве­ря­ем, бы­ла ли фор­ма про­ве­ре­на (когда поль­зо­ва­тель от­пра­вил дан­ные).

if form.is_valid():

Ес­ли фор­ма бы­ла от­прав­ле­на, мы соз­да­ем объ­ект из дан­ных фор­мы и со­хра­ня­ем его в ба­зу дан­ных:

cmodel = form.save()

cmodel.save()

Те­перь объ­ект со­хра­нен в ба­зе дан­ных. Мы пе­ре­на­прав­ля­ем поль­зо­ва­те­ля на глав­ную стра­ни­цу:

return redirect(index)

Ес­ли фор­ма не бы­ла от­прав­ле­на и про­ве­ре­на (и, сле­до­ва­тель­но, form.is_valid() не рав­но True), мы с по­мо­щью ме­то­да render_to_response() за­тем за­гру­жа­ем шаб­лон из фор­мы и пе­ре­да­ем его на­ше­му объ­ек­ту фор­мы:

return render_to_response(‘inventory/newitem.html’, {‘item_form’: form}, context_instance=RequestContext(request))

В кон­це стро­ки вы ви­ди­те осо­бый фраг­мент context_instance =RequestContext(request). Од­но из пре­иму­ществ RequestContext в том, что он пре­достав­ля­ет за­щи­ту от атак ме­то­дом подделки межсетевых запросов [cross-site request forgery], так­же из­вест­ных как ата­ка в один клик или захват сессии [session riding]. До­ба­вив к на­ше­му шаб­ло­ну то­кен CSRF, Django по­мо­жет за­щи­тить ва­ше при­ло­жение от по­доб­ных атак.

Те­перь пред­став­ление го­то­во, и по­ра соз­дать шаб­лон для об­ра­бот­ки фор­мы. В ка­та­ло­ге pawnstore/templates/inventory соз­дай­те файл newitem.html и до­бавь­те в него сле­дую­щий код:

Add New Item

<form action=”{% url newitem %}” method=”post”>{% csrf_token %}

Шаблон:Item form.as table

<input type=”submit” value=”Save” />

</form>

В этом фай­ле мы соз­да­ем тэг <form> и пе­ре­да­ем код шаб­ло­на, ко­то­рый бу­дет для вас но­вым:

<form action=”{% url newitem %}” method=”post”>

Син­так­сис { % url newitem %} – это ко­рот­кая ссыл­ка на стро­ку newitem в фай­ле urls.py. Вы помните, что в этой стро­ке мы ука­за­ли имя name=“newitem”; ну, а те­перь мы на него со­сла­лись. Как я отметил, для защиты от атак нам ну­жен то­кен CSRF, и теперь мы его до­бавим:

{% csrf_token %}

За­тем мы до­бав­ля­ем са­му глав­ную фор­му:

Шаблон:Item form.as table
Здесь мы про­сто соз­да­ем тэг и из него об­ра­ща­ем­ся к item_form, ко­то­рая бы­ла пе­ре­да­на шаб­ло­ну, ис­поль­зуя as_table для ото­бра­жения со­дер­жи­мо­го фор­мы в ви­де таб­ли­цы. На­конец, мы до­бав­ля­ем кноп­ку Save (Со­хранить) и за­кры­ва­ем фор­му: <input type=”submit” value=”Save” /> </form> Те­перь фор­ма долж­на ра­бо­тать, и с ее по­мо­щью вы смо­же­те до­ба­вить но­вый объ­ект в ба­зу дан­ных. По­сле до­бав­ления эле­мента про­ис­хо­дит пе­ре­на­прав­ление на ин­декс объ­ек­тов.

На­страи­ва­ем фор­му

Те­перь фор­ма го­то­ва, и мож­но кое-что из­менить. На­при­мер, сей­час в на­шей фор­ме да­ту нуж­но вво­дить в осо­бом фор­ма­те:

YYYY-MM-DD

Это мо­жет за­пу­тать поль­зо­ва­те­лей, к то­му же в раз­ных стра­нах фор­ма­ты да­ты раз­ные.

Иде­аль­но бы­ло бы под­ста­вить в это по­ле те­ку­щую да­ту. Так­же сто­ит уста­но­вить ко­ли­че­­ст­во по умол­чанию в 1, поскольку, ско­рее все­го, ка­ж­дый пред­мет бу­дет в един­ст­вен­ном чис­ле.

Что­бы под­ста­вить в по­ле да­ту, нуж­но оп­ре­де­лить те­ку­щую да­ту, для че­го восполь­зу­ем­ся мо­ду­лем datetime. Сна­ча­ла им­пор­ти­ру­ем мо­дуль в на­ча­ле фай­ла views.py:

import datetime

Те­перь за­полним фор­му. Для это­го соз­да­дим но­вый эк­зем­п­ляр Item и за­полним неко­то­рые по­ля. За­тем пе­ре­да­дим его фор­ме при ее соз­дании.

Это оз­на­ча­ет, что фор­ма за­пол­ня­ет­ся за­ранее, и лю­бые из­менения в фор­ме при­ме­ня­ют­ся к это­му эк­зем­п­ля­ру. Мы так­же со­хра­ня­ем его в ба­зе дан­ных.

В функ­ции view объ­ек­та newitem(), соз­дай­те эк­зем­п­ляр объ­екта с та­кой пер­вой стро­кой функ­ции:

formdata = Item(dateadded = datetime.date.today().isoformat(), quantity = 1)

Здесь мы соз­да­ем но­вый эк­зем­п­ляр Item и за­да­ем зна­чения по умол­чанию для ка­ж­до­го из по­лей. Те­ку­щая да­та для по­ля «да­та» у нас за­да­ется с по­мо­щью функ­ции isoformat() мо­ду­ля datetime.

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

form = NewItemForm(request.POST or None, instance = formdata)

Мы здесь соз­да­ем эк­зем­п­ляр на­ше­го эле­мен­та Item(). Ес­ли те­перь пе­ре­за­гру­зить фор­му, на ней поя­вит­ся да­та.

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

В этом, несо­мнен­но, есть смысл, да и в дру­гих слу­ча­ях то­же мож­но скры­вать неко­то­рые по­ля фор­мы. Для это­го об­но­вим класс NewItemForm() в forms.py. Внут­ри клас­са, сра­зу по­сле стро­ки class Meta: до­бавь­те сле­дую­щее:

exclude = [ ‘dateadded’ ]

По­том пе­ре­за­гру­зи­те фор­му и по­ра­дуй­тесь то­му, что по­ле с дан­ны­ми ис­чез­ло!

Да­лее, пе­рей­дем к дру­го­му эта­пу об­ра­бот­ки фор­мы, ко­то­рый мы еще не ис­сле­до­ва­ли. За­гру­зи­те фор­му и за­полните все по­ля, кро­ме од­но­го (ска­жем, Description) – его не тро­гай­те.

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

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

И на­стро­ить это, к сча­стью, до­воль­но лег­ко. Нуж­но лишь немно­го из­менить на­ши мо­де­ли дан­ных в models.py.

Что­бы по­ле Description мог­ло быть пустым, про­сто до­бавь­те сле­дую­щий код к оп­ре­де­лению по­ля в клас­се Item():

description = models.TextField(blank=True)

При до­бав­лении blank=True по­ле бу­дет принимать пустые зна­чения; это бу­дет от­ра­же­но в ба­зе дан­ных.

В за­клю­чение

На на­шем уро­ке мы пе­ре­кры­ли важ­ный уча­сток пу­ти к нир­ване Django. Как и с лю­бой тех­но­ло­ги­ей, что­бы сде­лать что-то хо­ро­шее, нуж­но прак­ти­ко­вать­ся. Про­дол­жай­те пи­сать код, вы­хо­дить из труд­ных си­туа­ций – и со­всем ско­ро вы бу­де­те соз­да­вать фан­та­сти­че­­ские web-при­ло­жения.

На сле­дую­щем уро­ке я по­ка­жу, как, не изу­чая CSS, ук­ра­сить свое при­ло­жение про­фес­сио­наль­ны­ми те­ма­ми. Уда­чи, при­ят­но­го про­грам­ми­ро­вания, и – уви­дим­ся! |
Персональные инструменты
купить
подписаться
Яндекс.Метрика