Разработка сайтов в Сватово, ЛНР. Создайте приложение для обмена фотографиями с Django

 
 

Django — наиболее часто используемый фреймворк Python для веб-разработки. Его встроенные функции и надежная структура делают его отличным вариантом при создании веб-приложений. Но существует так много ресурсов, что иногда сложно применить эти знания в реальных проектах. В этом уроке мы собираемся создать полнофункциональное веб-приложение, используя Django в качестве серверной части и шаблоны Django, стилизованные под Bootstrap, во внешней части.

Требования

Чтобы получить максимальную отдачу от этого руководства, в идеале вы должны иметь представление о следующем:

основы Python

объектно-ориентированное программирование на Python

основы веб-фреймворка Django

Если у вас нет опыта работы с Django, не бойтесь продолжать изучение этого руководства. Это будет пошаговый процесс, и каждый шаг будет объяснен.

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

Приложение Django для обмена фотографиями

Весь исходный код этого руководства доступен в этом репозитории GitHub.

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

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

Что мы собираемся строить

В этом руководстве мы создадим полнофункциональное приложение для обмена фотографиями (внутренняя и внешняя разработка). Наше приложение будет включать в себя следующие функции:

Функциональность базы данных CRUD (создание, чтение, обновление, удаление)

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

простой веб-интерфейс, сделанный с помощью Bootstrap

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

Стек технологий

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

На бэкенде Django будет основной структурой приложения. Это позволяет нам определять URL-адреса, определять логику, управлять аутентификацией пользователей и контролировать все операции с базой данных через Django ORM (объектно-реляционный преобразователь).

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

Django-taggit предоставляет нам возможность настроить простую систему тегов за несколько шагов. Pillow — это пакет Python, предоставляющий возможности обработки изображений Django. Наконец, Django-crispy-forms дает нам простой способ отображения форм Bootstrap.

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

Мы также будем использовать Bootstrap 5 (последняя версия на момент написания статьи) для дизайна сайта.

Примечание: вы всегда можете проверить используемые в этом проекте зависимости в файле requirements.txt.

Создайте проект Джанго

Начнем с Джанго!

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

Примечание: в этом руководстве мы будем использовать команды Unix (macOS и Linux). Если вы не можете выполнить их по какой-либо причине, вы можете использовать графический файловый менеджер.

В некоторых дистрибутивах Linux pythonкоманда относится к Python 2. В других pythonвообще не существует.

Давайте посмотрим, какую команду Python вам нужно использовать, чтобы следовать дальше. Откройте терминал (в Unix) или окно командной строки (в Windows) и введите python —version:

python —version

# My result

Python 3.9.5

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

Command 'python’ not found

Python 2.7.18

Команда Python, которую вам нужно выполнить, чтобы следовать этому руководству, будет python3:

python3 —version

Python 3.9.5

Виртуальные среды

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

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

Чтобы создать нативную виртуальную среду, мы будем использовать встроенный модуль venv, доступный в Python 3.6 или более поздней версии.

Следующая команда создаст виртуальную среду с именем.venv (вы можете выбрать другое имя, если хотите):

python -m venv.venv

Если вы используете Ubuntu Linux или любой другой дистрибутив на основе Debian, возможно, вы получите следующее сообщение:

The virtual environment was not created successfully because pip is not available...

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

sudo apt-get install python3-venv

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

virtualenv.venv

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

Все пакеты, которые мы устанавливаем, будут помещены в этот каталог.

Чтобы активировать виртуальную среду, вам нужно выполнить определенную команду в зависимости от вашей ОС. Вы можете обратиться к таблице ниже (извлеченной из документации Python).

Платформа Оболочка Команда для активации виртуальной среды

POSIX баш/зш $ источник.venv/bin/активировать

рыба $ источник.venv/bin/activate.fish

csh/tcsh $ источник.venv/bin/activate.csh

Ядро PowerShell $.venv/bin/Activate.ps1

Окна cmd.exe C: >.venv\Scripts\activate.bat

PowerShell PS C: >.venv\Scripts\Activate.ps1

Поскольку я использую оболочку bash в операционной системе POSIX, я буду использовать это:

source.venv/bin/activate

Обратите внимание, как.venvподпись добавляется в мою оболочку после того, как я активировал файл virtualenv.

Виртуальная среда активирована

Установка Джанго

Django — это внешний пакет, поэтому нам нужно установить его с помощью pip:

pip install django

# Use pip3 if the command above doesn’t work

pip3 install django

Примечание: мы всегда можем взглянуть на пакеты, установленные в нашем файле venvwith pip freeze.

Далее запустим проект Django с именем configс помощью утилиты командной строки django-admin.

django-admin startproject config

Здесь configимя проекта, и оно используется в качестве соглашения об именах, чтобы все ваши проекты имели одинаковую структуру. Например, Django cookiecutter использует это условное имя для запуска проекта.

При этом вы можете создать проект с любым другим именем.

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

Примечание: если вы не можете запустить tree, вам необходимо установить его.

$ tree config/

└── config

├── config

│ ├── asgi.py

│ ├── __init__.py

│ ├── settings.py

│ ├── urls.py

│ └── wsgi.py

└── manage.py

Теперь давайте войдем в папку проекта с помощью cdи запустим сервер, чтобы убедиться, что все настроено правильно:

cd config/

python manage.py runserver

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

Теперь зайдите на localhost:8000 в браузере. Вы должны увидеть культовую страницу поздравления Django.

Страница поздравлений Джанго

Запуск приложения для обмена фотографиями

Файл manage.py имеет те же возможности, что и django-admin, поэтому мы будем использовать его много раз в этом руководстве.

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

Не забывайте всегда перечислять файлы каталога, в котором вы находитесь ls, чтобы проверить, находимся ли мы в правильном месте:

$ ls

Another-files... manage.py

Имея в виду эти советы, пришло время запустить основное приложение проекта. Для этого мы открываем новую оболочку (чтобы локальный сервер все еще работал) и используем manage.pyкоманду startapp.

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

source.venv/bin/activate

cd config

python manage.py startapp photoapp

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

Каждый раз, когда мы создаем приложение, мы должны его установить. Мы можем сделать это в config/settings.pyфайле, добавив photoappв INSTALLED_APPSпеременную:

# config/settings.py

INSTALLED_APPS = [

'django.contrib.admin’,

...

# Custom apps

'photoapp’,

]

Далее мы войдем в каталог приложения и создадим пустой urls.pyфайл. Мы можем сделать это, запустив touchили создав его с помощью графического файлового менеджера:

cd photoapp/

touch urls.py

Наконец, давайте включим все шаблоны URL приложения для обмена фотографиями в общий проект. Для этого воспользуемся django.urls.includeфункцией:

# config/urls.py

from django.urls import path, include # Import this function

urlpatterns = [

path ('admin/', admin.site.urls),

# Main app

path ('', include ('photoapp.urls’)),

]

Приведенный выше код будет включать все шаблоны URL photoapp/urls.pyдля проекта.

Если вы посмотрите на оболочку, в которой работает сервер, вы увидите ошибку:

raise ImproperlyConfigured (msg.format (name=self.urlconf_name))...

Это потому, что мы не создали urlpatternsсписок внутри photopp/urls.pyфайла.

Чтобы решить эту проблему, создайте пустой список с именем urlpatterns. Позже мы собираемся заполнить эту переменную путями Django:

# photoapp/urls.py

# Empty patterns

urlpatterns = [

]

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

Создание фотомодели

В этом разделе мы собираемся построить схему базы данных нашего приложения. Для этой цели мы будем использовать Django ORM.

Django ORM позволяет создавать и управлять таблицами базы данных без необходимости использовать SQL вручную.

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

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

Прежде чем начать, мы собираемся установить некоторые сторонние пакеты django-taggitи файлы Pillow. Мы можем сделать это с помощью следующей команды:

pip install django-taggit Pillow

django-taggit — это приложение Django, поэтому нам нужно установить его так же, как мы это сделали с photoapp:

# config/settings.py

INSTALLED_APPS = [

...

# 3rd party apps

'taggit’,

# Custom apps

'photoapp’,

]

# Django taggit

TAGGIT_CASE_INSENSITIVE = True

Эта TAGGIT_CASE_INSENSITIVEпеременная настраивает теги так, чтобы они были нечувствительны к регистру. Значит PYTHONи pythonбудет так же.

Определим Photoмодель, которая будет основной моделью приложения. Откройте photoapp/models.pyфайл и используйте следующий код:

# photoapp/models.py

from django.db import models

from django.contrib.auth import get_user_model

from taggit.managers import TaggableManager

class Photo (models.Model):

title = models.CharField (max_length=45)

description = models.CharField (max_length=250)

created = models.DateTimeField (auto_now_add=True)

image = models.ImageField (upload_to='photos/')

submitter = models.ForeignKey (get_user_model (), on_delete=models.CASCADE)

tags = TaggableManager ()

def __str__ (self):

return self.title

В приведенном выше блоке кода мы определили Photoмодель. Давайте посмотрим, что делает каждое поле.

Поле titleпредставляет собой CharField и может содержать не более 45 символов.

descriptionэто еще один CharField, но с ограничением в 250 символов.

createdявляется DateTimeField и, как следует из названия, хранит дату и час создания фотографии.

imageявляется ImageField. Он загружает изображения media/photosи сохраняет URL-адрес, по которому находится файл. Позже мы увидим, как настроить медиафайлы.

submitter— это ForeignKey, что означает связь с пользователем и загруженной фотографией. Таким образом, мы можем отфильтровать, какой пользователь загрузил фотографию.

Наконец, tagsэто TaggableManager, который позволяет нам классифицировать темы по тегам.

С другой стороны, __str__метод указывает, как каждый объект будет отображаться в админке. Позже мы настроим администратора и создадим наши первые объекты.

Чтобы создать базу данных на основе созданной нами модели, нам сначала нужно выполнить миграции, а затем запустить их.

Войдите в корневой каталог проекта и используйте manage.pyскрипт со следующими аргументами:

python manage.py makemigrations

python manage.py migrate

Команда makemigrationsсоздаст файл миграции на основе Photoмодели.

Примечание. Миграции — это скрипты Python, которые производят изменения в базе данных на основе моделей.

Мы можем точно увидеть, что происходит с этой миграцией, открыв photoapp/migrations/0001_initial.pyфайл:

# photoapp/migrations/0001_initial.py

# imports...

class Migration (migrations.Migration):

initial = True

dependencies = [

('taggit’, '0003_taggeditem_add_unique_index’),

migrations.swappable_dependency (settings.AUTH_USER_MODEL),

]

operations = [

migrations.CreateModel (

name='Photo’,

fields=[

('id’, models.BigAutoField (auto_created=True, primary_key=True, serialize=False, verbose_name='ID’)),

....

Совет: никогда не изменяйте файл миграции вручную. Все миграции должны быть автоматически сгенерированы Django.

Команда migrateсоздает таблицы базы данных, выполняя все миграции.

После выполнения этих двух команд вы должны увидеть базу данных SQLite в корневой папке проекта. Если мы проверим его с помощью DB Browser, мы увидим все поля, связанные с Photoмоделью.

Визуализатор SQLite

Управление медиафайлами в разработке

Приложение для обмена фотографиями сильно зависит от медиафайлов. Все дело в обмене изображениями, не так ли?

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

Чтобы включить медиафайлы в разработке, мы создаем переменные MEDIA_URL и MEDIA_ROOT внутри файла настроек. Кроме того, нам нужно изменить urlpatternsобщий проект для обслуживания медиафайлов с локального сервера.

Во-первых, нам нужно отредактировать config/settings.pyфайл и добавить следующий код в конец файла:

# config/settings.py

# Other settings...

MEDIA_URL = '/media/'

MEDIA_ROOT = BASE_DIR / 'media/'

MEDIA_URL— это URL-адрес, который обрабатывает все медиафайлы, загруженные в MEDIA_ROOTпапку. В этом случае абсолютный URL-адрес мультимедиа будет выглядеть так: http: //localhost:8000/media/.

С другой стороны, MEDIA_ROOTэто путь, указывающий на папку, в которую будут помещены все медиафайлы.

Помните, что, поскольку мы используем библиотеку pathlib, мы можем объединять пути с /.

Мы можем представить MEDIA_ROOTсебе физическое хранилище, куда будут загружены изображения, и MEDIA_URLURL-адрес, указывающий на это хранилище.

Если мы хотим, чтобы Django управлял медиафайлами, нам нужно изменить URL-адреса проекта:

# config/urls.py

# New imports

from django.conf import settings

from django.conf.urls.static import static

urlpatterns = [

path ('admin/', admin.site.urls),

# Main app

path ('', include ('photoapp.urls’)),

] + static (settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

С учетом этого абсолютный URL загружаемых фотографий будет таким: http: //localhost:8000/media/photos/. Это потому, что мы устанавливаем upload_toатрибут как photos/.

Примечание: принимать загруженные файлы от пользователя может быть опасно. Ознакомьтесь с этим списком соображений безопасности.

При работе с общедоступным приложением мы должны быть осторожны с медиафайлами. Мы можем пострадать от DoS-атак. Пользователи также могут загружать вредоносный контент, поэтому для решения подобных проблем рекомендуется всегда использовать CDN.

На данный момент вы можете забыть о проблемах безопасности, так как мы работаем с проектом разработки и ImageFieldпринимаем только заранее определенный набор расширений.

Вы можете проверить эти действительные расширения, запустив следующий код в оболочке Django (убедившись, что venvон активирован):

$ python manage.py shell

>>> from django.core.validators import get_available_image_extensions

>>> get_available_image_extensions ()

['blp’, 'bmp’, 'dib’, 'bufr’, 'cur’, 'pcx’, 'dcx’, 'dds’, 'ps’, 'eps’, 'fit’, 'fits’, 'fli’, 'flc’, 'ftc’, 'ftu’, 'gbr’, 'gif’, 'grib’, 'h5', 'hdf’, 'png’, 'apng’, 'jp2', 'j2k’, 'jpc’, 'jpf’, 'jpx’, 'j2c’, 'icns’, 'ico’, 'im’, 'iim’, 'tif’, 'tiff’, 'jfif’, 'jpe’, 'jpg’, 'jpeg’, 'mpg’, 'mpeg’, 'mpo’, 'msp’, 'palm’, 'pcd’, 'pdf’, 'pxr’, 'pbm’, 'pgm’, 'ppm’, 'pnm’, 'psd’, 'bw’, 'rgb’, 'rgba’, 'sgi’, 'ras’, 'tga’, 'icb’, 'vda’, 'vst’, 'webp’, 'wmf’, 'emf’, 'xbm’, 'xpm’]

Тестирование моделей с помощью Django Admin

Администратор Django — это встроенный интерфейс, в котором пользователи с правами администратора могут выполнять операции CRUD с зарегистрированными моделями проекта.

Теперь, когда мы создали фотомодель и настроили медиафайлы, пришло время создать наш первый Photoобъект через страницу администрирования.

Для этого мы должны зарегистрировать Photoмодель на странице администратора. Давайте откроем photoapp/admin.py, импортируем модель Photo и передадим ее в качестве параметра admin.site.registerфункции:

# photoapp/admin.py

from django.contrib import admin

from.models import Photo # We import the photo model

# Register your models here.

admin.site.register (Photo)

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

python manage.py createsuperuser

Username: daniel

Email address:

Password:

Password (again):

Superuser created successfully

На данный момент вы можете оставить суперпользователя без электронной почты, так как мы используем пользователя авторизации по умолчанию.

После создания суперпользователя перейдите в браузер и перейдите по адресу http: //localhost:8000/admin.

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

Страница входа администратора Django

После ввода наших учетных данных у нас будет доступ к простой панели инструментов, где мы можем начать создавать фотографии. Просто щелкните раздел «Фотографии», а затем кнопку «Добавить «.

Панель инструментов Джанго

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

Заполнение контента

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

Загрузка изображений

После нажатия кнопки «Сохранить «мы увидим панель со всеми созданными фотографиями.

Фото приборной панели

Обработка веб-ответов с представлениями

Мы определили схему базы данных рабочего приложения и даже создали некоторые объекты с помощью администратора Django. Но мы не коснулись самой важной части любого веб-приложения — взаимодействия с пользователем!

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

В широком смысле представление — это вызываемый объект Python (класс или функция), который принимает запрос и возвращает ответ.

Согласно документации Django, мы должны поместить все наши представления в файл с именем views.pyвнутри каждого приложения. Этот файл уже был создан, когда мы запускали приложение.

У нас есть два основных способа создания представлений: использование представлений на основе функций (FBV) или представлений на основе классов (CBV).

CBV — лучший способ повторного использования кода, применяя возможности наследования классов Python в наших представлениях.

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

Прежде чем начать, мы импортируем все, что нам нужно для создания представлений. Откройте photoapp/views.pyфайл и вставьте код ниже:

# photoapp/views.py

from django.shortcuts import get_object_or_404

from django.core.exceptions import PermissionDenied

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin

from django.urls import reverse_lazy

from.models import Photo

Давайте посмотрим, что мы импортируем сюда:

get_object_or_404— это ярлык, который позволяет нам извлекать объект из базы данных, предотвращая DoesNotExistsошибку и вызывая исключение HTTP 404.

PermissionDeniedвызвать исключение HTTP 403 при вызове.

Готовые genericпредставления помогают нам создавать функциональные возможности CRUD с помощью нескольких строк кода.

Мы будем использовать LoginRequiredMixinи UserPassesTestMixinдля утверждения, что у пользователей есть правильные разрешения при доступе к представлению.

reverse_lazyиспользуется в CBV для перенаправления пользователей на определенный URL-адрес.

Нам нужно импортировать Photo, чтобы получить и обновить строки базы данных (объекты фотографий).

Примечание. Вы можете получить доступ к файлу views.py на GitHub.

Просмотры списков фотографий

Общее представление списка поможет нам отобразить многие объекты модели. Мы сравним его с более DetailViewпоздним.

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

В приведенном ниже коде определяется PhotoListViewнаследование от ListView:

# photoapp/views.py

class PhotoListView (ListView):

model = Photo

template_name = 'photoapp/list.html’

context_object_name = 'photos’

Во-первых, мы наследуем ListViewи, следовательно, получаем все поведение от этого класса.

Помните, вы всегда можете проверить исходный код любого класса Django в официальном репозитории GitHub.

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

Теперь пришло время объявить PhotoTagListView. Это представление немного сложнее, так как мы должны играть с методами get_queryset () и: get_context_data ()

# photoapp/views.py

class PhotoListView (ListView):...

class PhotoTagListView (PhotoListView):

template_name = 'photoapp/taglist.html’

# Custom method

def get_tag (self):

return self.kwargs.get ('tag’)

def get_queryset (self):

return self.model.objects.filter (tags__slug=self.get_tag ())

def get_context_data (self, **kwargs):

context = super ().get_context_data (**kwargs)

context[«tag"] = self.get_tag ()

return context

Здесь мы наследуем все атрибуты файла PhotoListView. Это означает, что мы используем то же самое modelи context_object_name, но меняем template_name.

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

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

Метод get_querysetнастроен на возврат self.model.objects.all () по умолчанию. Мы изменили его, чтобы возвращались только фотообъекты, помеченные слагом, переданным в URL-адрес.

Наконец, get_context_dataбыл изменен, чтобы также возвращать тег, переданный в URL-адрес. Это потому, что мы отобразим его позже в шаблоне.

Подробный просмотр фото

Это простое DetailViewпредставление, которое отображает все данные, относящиеся к уникальной фотографии. Это включает в себя заголовок, описание и теги нужной фотографии:

# photoapp/views.py

class PhotoListView (ListView):...

class PhotoTagListView (PhotoListView):...

class PhotoDetailView (DetailView):

model = Photo

template_name = 'photoapp/detail.html’

context_object_name = 'photo’

Мы делаем почти тот же процесс, что и с представлениями списка. Единственное отличие состоит в том, что мы возвращаем один объект вместо многих и используем другой шаблон.

Создать просмотр фотографий

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

Самый простой способ защитить эту функциональность с помощью Django — создать класс, который наследуется от CreateViewи LoginRequiredMixin. Проверяет, вошел ли пользователь в систему. LoginRequiredMixinЕсли пользователь не вошел в систему, он перенаправляется на страницу входа (которую мы создадим позже):

# photoapp/views.py

class PhotoListView (ListView):...

class PhotoTagListView (PhotoListView):...

class PhotoDetailView (DetailView):...

class PhotoCreateView (LoginRequiredMixin, CreateView):

model = Photo

fields = ['title’, 'description’, 'image’, 'tags’]

template_name = 'photoapp/create.html’

success_url = reverse_lazy ('photo: list’)

def form_valid (self, form):

form.instance.submitter = self.request.user

return super ().form_valid (form)

В этом представлении Django создаст форму с полями, titleи description.imagetags

Мы также используем sucess_urlатрибут. Пользователи будут перенаправлены на панель управления фотографиями, если создание фотографии прошло успешно.

Если мы внимательно посмотрим на этот form_validметод, мы заметим, что он настраивает пользователя, который делает запрос, в качестве отправителя формы с фотографией.

Обновление и удаление просмотров фотографий

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

Обработка условной аутентификации может быть затруднена, если мы используем CBV. Однако мы можем использовать TestMixins для выполнения этой задачи.

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

# photoapp/views.py

class PhotoListView (ListView):...

class PhotoTagListView (PhotoListView):...

class PhotoDetailView (DetailView):...

class PhotoCreateView (LoginRequiredMixin, CreateView):...

class UserIsSubmitter (UserPassesTestMixin):

# Custom method

def get_photo (self):

return get_object_or_404 (Photo, pk=self.kwargs.get ('pk’))

def test_func (self):

if self.request.user.is_authenticated:

return self.request.user == self.get_photo ().submitter

else:

raise PermissionDenied ('Sorry you are not allowed here’)

Во-первых, мы создали собственный метод get_photo, который возвращает объект Photo с первичным ключом, указанным в URL-адресе. Если фото не существует, возникает ошибка HTTP 404.

Затем мы определили тестовую функцию. Он вернет true только в том случае, если пользователь вошел в систему и является отправителем фотографии.

Если пользователь не вошел в систему, это вызовет исключение PermissionDenied.

С другой стороны, PhotoUpdateViewи PhotoDeleteViewявляются дочерними элементами созданного нами миксина, а также UpdateViewи DeleteViewсоответственно:

# photoapp/views.py

class PhotoListView (ListView):...

class PhotoTagListView (PhotoListView):...

class PhotoDetailView (DetailView):...

class PhotoCreateView (LoginRequiredMixin, CreateView):...

class UserIsSubmitter (UserPassesTestMixin):...

class PhotoUpdateView (UserIsSubmitter, UpdateView):

template_name = 'photoapp/update.html’

model = Photo

fields = ['title’, 'description’, 'tags’]

success_url = reverse_lazy ('photo: list’)

class PhotoDeleteView (UserIsSubmitter, DeleteView):

template_name = 'photoapp/delete.html’

model = Photo

success_url = reverse_lazy ('photo: list’)

PhotoUpdateViewнаследует функцию тестирования от UserIsSubmitterмиксина и функцию обновления от UpdateViewфайла.

Атрибут fieldsопределяет поля, которые пользователь сможет редактировать. Мы не хотим, чтобы менялось изображение, ни дата создания, ни отправитель.

С другой стороны, PhotoDeleteViewтакже наследует тестовую функцию, но удаляет фотографию вместо ее обновления.

Оба представления перенаправляют пользователя на URL-адрес списка, если все прошло хорошо.

Это все для просмотров. Теперь давайте создадим простое приложение для аутентификации и завершим проект.

URL-шаблоны

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

Вы помните, когда мы создали пустую urlpatternsпеременную в начале проекта? Пришло время заселить его!

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

# photoapp/urls.py

from django.urls import path

from.views import (

PhotoListView,

PhotoTagListView,

PhotoDetailView,

PhotoCreateView,

PhotoUpdateView,

PhotoDeleteView

)

Функция пути получает два аргумента, routeи viewи необязательный аргумент, nameкоторый используется как часть пространства имен:

# photoapp/urls.py

app_name = 'photo’

urlpatterns = [

path ('', PhotoListView.as_view (), name='list’),

path ('tag//', PhotoTagListView.as_view (), name='tag’),

path ('photo//', PhotoDetailView.as_view (), name='detail’),

path ('photo/create/', PhotoCreateView.as_view (), name='create’),

path ('photo//update/', PhotoUpdateView.as_view (), name='update’),

path ('photo//delete/', PhotoDeleteView.as_view (), name='delete’),

]

Объясняя эту конфигурацию, app_nameпеременная объявляет пространство имен приложения.

Это означает, что независимо от того, используем ли мы reverseфункцию в представлениях или {% url%}тег в шаблонах, нам нужно использовать следующее пространство имен:

photo: <>

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

Система аутентификации

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

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

Сначала мы создаем usersприложение и выполняем тот же процесс установки, что и с photoapp:

python manage.py startapp users

# config/settings.py

INSTALLED_APPS = [

...

# 3rd party apps

'taggit’,

# Custom apps

'photoapp’,

'users’,

]

Далее мы создаем urls.pyфайл так же, как мы делали это с фото-приложением:

cd users/

touch urls.py

Затем мы включаем URL-адреса пользователя в общий проект:

# config/urls.py

urlpatterns = [

path ('admin/', admin.site.urls),

# Main app

path ('', include ('photoapp.urls’)),

# Auth app

path ('users/', include ('users.urls’)),

] + static (settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Затем мы пишем a, SignUpViewчтобы позволить пользователю зарегистрироваться через сайт:

# users/views.py

from django.views.generic import CreateView

from django.contrib.auth import authenticate, login

from django.contrib.auth.forms import UserCreationForm

from django.urls import reverse_lazy

class SignUpView (CreateView):

template_name = 'users/signup.html’

form_class = UserCreationForm

success_url = reverse_lazy ('photo: list’)

def form_valid (self, form):

to_return = super ().form_valid (form)

user = authenticate (

username=form.cleaned_data["username"],

password=form.cleaned_data["password1»],

)

login (self.request, user)

return to_return

Это представление является CreateView и работает со встроенной формой UserCreationForm для создания нового пользователя.

Мы используем этот form_validметод для входа пользователей перед перенаправлением их на панель управления фотографиями.

Мы создадим представление входа, потому что мы хотим использовать настраиваемый шаблон для отображения страницы входа. Для этого импортируем встроенный LoginViewи наследуемся от него:

# Previous imports

from django.contrib.auth.views import LoginView

class SignUpView (CreateView):...

class CustomLoginView (LoginView):

template_name = 'users/login.html’

Наконец, пришло время создать маршрутизацию URL:

# users/urls.py

from django.urls import path

from django.contrib.auth.views import LogoutView

from.views import SignUpView, CustomLoginView

app_name = 'user’

urlpatterns = [

path ('signup/', SignUpView.as_view (), name='signup’),

path ('login/', CustomLoginView.as_view (), name='login’),

path ('logout/', LogoutView.as_view (), name='logout’),

]

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

user: <>

Мы настраиваем три URL. И signup/используют login/созданные нами пользовательские представления, но logout/URL-адрес использует встроенный Django LogoutView.

Прежде чем продолжить, давайте настроим перенаправления аутентификации в config/settings.pyфайле:

# Other settings...

USE_TZ = True

# Django Authentication

LOGIN_URL = 'user: login’

LOGIN_REDIRECT_URL = 'photo: list’

LOGOUT_REDIRECT_URL = 'photo: list’

Это сообщает Django, что URL-адрес входа является пользовательским URL-адресом входа пользователя, и что когда пользователи входят в систему, они должны быть перенаправлены на панель управления фотографиями.

Передняя часть

После создания серверной части (то, что пользователь не может видеть) с помощью Django пришло время создать переднюю часть (то, что видит пользователь).

Для этой цели мы будем использовать язык шаблонов Django и Bootstrap 5. Это позволяет нам динамически генерировать HTML и выводить разные выходные данные в зависимости от состояния нашей базы данных. Мы можем сэкономить много кода, работая с наследованием шаблонов. Использование Bootstrap 5 означает, что мы не будем использовать статические файлы.

Пишем базовый шаблон

В этом разделе мы собираемся создать base.htmlфайл, который является шаблоном, от которого будут наследоваться все остальные.

Для этого мы должны изменить DIRSключ внутри TEMPLATESпеременной, расположенной в файле настроек:

# config/settings.py

TEMPLATES = [

{

# Options...

'DIRS’: [BASE_DIR / 'templates’],

'APP_DIRS’: True,

# More options

},

]

По умолчанию Django ищет файлы шаблонов в templates/папке каждого приложения.

Например, шаблоны приложения для обмена фотографиями можно найти в файлах photoapp/templates. То же самое и с приложением пользователей (users/templates).

Назначая DIRSключ [BASE_DIR / 'templates’], мы говорим Django также искать шаблоны внутри папки с именем templates.

Создайте каталог templatesв корне проекта (где находится manage.pyфайл) и коснитесь шаблонов base.htmlи: navbar.html

ls

# manage.py

mkdir templates && cd templates

touch base.html navbar.html

Заключительные шаблоны нашего проекта можно найти в любом из этих трех каталогов:

.

├── photoapp

│ └── templates

│ └── photoapp

├── templates

└── users

└── templates

└── users

Помните, что вы всегда можете проверить структуру проекта в репозитории GitHub.

Внутри base.htmlшаблона мы настроим базовую структуру HTML, некоторые метатеги, ссылки на загрузочную CDN и блоки, которые будут использовать другие шаблоны:

<! DOCTYPE html>

 

 

http-equiv="X-UA-Compatible" content="IE=edge" />

 

<link

href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css"

rel="stylesheet"

integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0"

crossorigin="anonymous"

/>

<link

rel="stylesheet"

href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"

integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w=="

crossorigin="anonymous"

/>

 

 

 

{% include 'navbar.html’%}

 

 

{% block body%}

{% endblock body%}

 

 

crossorigin="anonymous" >

 

 

Тег {% include%} (как следует из названия) включает в себя весь код выбранного шаблона внутри base.htmlфайла.

Поэтому весь код, присутствующий внутри navbar.html, будет помещен в начало тела.

Примечание: здесь много HTML и Bootstrap. Не стесняйтесь копировать все это, так как это не основная цель урока.

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

 

 

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

Navbar, когда пользователь вошел в систему

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

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

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

Шаблоны для обмена фотографиями

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

Все эти шаблоны будут расширять base.htmlшаблон и будут расположены в photoapp/templates/photoappкаталоге.

Но прежде чем работать с формами в шаблонах, мы воспользуемся хрустящими формами Django для стилизации нашего приложения:

pip install django-crispy-forms

Еще раз, crispy_formsэто приложение Django, и нам нужно включить его в INSTALLED_APPSсписок:

# config/settings.py

INSTALLED_APPS = [

...

# 3rd party apps

'taggit’,

'crispy_forms’,

# Custom apps

'photoapp’,

'users’,

]

# Indicates the frontend framework django crispy forms will use

CRISPY_TEMPLATE_PACK = 'bootstrap4'

Мы используем пакет шаблонов Bootstrap 4, потому что классы форм Bootstrap совместимы между 4-й и 5-й версиями (на момент написания).

Возможно, вы помните, что мы использовали следующие имена шаблонов в photoapp/views.py:

'photoapp/list.html’

'photoapp/taglist.html’

'photoapp/detail.html’

'photoapp/create.html’

'photoapp/update.html’

'photoapp/delete.html’

Это означает, что все эти шаблоны будут расположены в photoapp/templates/photoapp.

Чтобы создать эту папку, перейдите в приложение для обмена фотографиями и создайте каталог templates/, а внутри него создайте еще одну папку с именем photoapp/:

cd photoapp/

mkdir -p templates/photoapp/

cd templates/photoapp/

Теперь создайте все шаблоны, которые мы объявили в представлениях:

touch list.html taglist.html detail.html create.html update.html delete.html

Шаблоны списков

Будет list.htmlнаследоваться от base.htmlшаблона, и поэтому вся структура HTML появится в исходном коде:

{% extends 'base.html’%}

{% block body%}

 

 

{% for photo in photos%}

 

 

 

 

 

 

{% endfor%}

 

 

{% endblock body%}

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

Не забудьте создать несколько фотообъектов в админке Django.

Посетите localhost:8000/, чтобы увидеть, как выглядит шаблон.

Шаблон списка

Шаблон taglist.htmlунаследуется от list.htmlтолько что созданного нами:

{% extends 'photoapp/list.html’%}

{% block body%}

 

 

Photos with the tag {{tag}}

 

 

{{ block.super }}

{% endblock body%}

Мы просто немного модифицируем этот шаблон. Вот почему мы вызываем {{ block.super }}, который содержит весь код внутри блока body list.htmlшаблона.

codeПрежде чем продолжить, создайте пару объектов с тегом.

Перейдите на localhost:8000/tag/code/, где код — это слаг тега.

Тег шаблона списка

Помните, что taglistURL-адрес имеет следующую форму:

'localhost: //8000/tag//'

Здесь относится имя тега.

Детальный фотошаблон

Давайте отредактируем detail.htmlшаблон, чтобы увидеть наши фотографии в деталях:

{% extends 'base.html’%}

{% block body%}

 

 

 

{{ photo.title }}

 

 

Uploaded on: {{photo.created}}
By {{photo.submitter.username}}

{% if user == photo.submitter%}

 

 

{% endif%}

 

 

 

 

 

 

 

 

 

 

More about this photo:

 

 

{% for tag in photo.tags.all%}

{% endfor%}

  • {{tag.name}}

{{ photo.description }}

 

 

 

 

{% endblock body%}

Давайте посмотрим, как выглядит шаблон, прежде чем углубляться в функциональность. Подпишитесь на локальный хост: 8000/photo/1.

Фото в деталях

Здесь мы получаем доступ к свойствам фотографии из шаблонов через точечную нотацию. Это потому, что photo.submitter.usernameравно daniel.

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

Наконец, мы показываем все теги фотографии, повторяющиеся photo.tags.all.

Создайте шаблон фотографии

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

{% extends 'base.html’%}

{% load crispy_forms_tags%}

{% block body%}

 

 

 

Add photo

 

 

 

 

 

 

 

{% csrf_token%}

{{ form|crispy }}

 

 

 

 

{% endblock body%}

Каждый раз, когда мы используем хрустящие формы, нам нужно загружать теги с{% load crispy_forms_tags%}.

Крайне важно включить enctype="multipart/form-data", потому что если мы этого не сделаем, файлы не будут загружены. Вот действительно хороший ответ на последствия его использования в формах.

Каждая форма Django должна включать в себя {% csrf_token%}внутреннюю часть. Вы можете узнать больше об этом теге на странице «Защита от подделки межсайтовых запросов «.

Обратите внимание, как мы просто отображаем форму с помощью {{form|crispy}}. Если вы знаете, что такое каналы в Linux, мы делаем именно это, перенаправляя форму, предоставленную представлением, на crispyфильтр.

Перейдите по URL- адресу добавления фотографии, чтобы проверить, загружена ли фотография.

Загрузка фото

Если все прошло хорошо, мы должны увидеть добавленную фотографию в панели инструментов.

Добавлено фото

Обновление и удаление шаблонов

Давайте закончим приложение для обмена фотографиями, прежде чем перейти к шаблонам аутентификации.

Следующий updateшаблон представляет собой простую форму, в которой пользователь может обновить title, descriptionи tagsфотографии:

{% extends 'base.html’%}

{% load crispy_forms_tags%}

{% block body%}

 

 

 

Edit photo {{photo}}

 

 

 

 

 

 

 

{% csrf_token%}

{{ form|crispy }}

 

 

 

 

{% endblock body%}

Мы можем посмотреть, как это выглядит на localhost:8000/photo/1/update.

Обновление фотографии

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

{% extends 'base.html’%}

{% block body%}

 

 

 

 

 

You are going to delete: «<i

>{{ photo }}</i

>

«

 

Are you sure, you want to delete the photo?

 

 

<form

action=""

method="post"

class="d-flex flex-column align-items-center justify-content-center"

>

{% csrf_token%}

 

 

 

 

<a href="{% url 'photo:detail' photo.id %}" class="btn btn-primary"

>Cancel</a

>

 

 

 

 

 

 

 

 

This action is irreversible

 

 

 

 

 

{% endblock body%}

Страница удаления будет выглядеть так.

Удалить шаблон

Если пользователь решит отменить, он будет перенаправлен на страницу сведений об этой фотографии.

Шаблоны аутентификации пользователя

Цель этого раздела — написать все шаблоны, связанные с аутентификацией. Мы напишем signup.htmlи login.htmlшаблоны.

Как и в приложении для обмена фотографиями, все следующие шаблоны будут расположены в двойной структуре папок: users/templates/users/.

Войдите в приложение пользователей и создайте папки, в которых будут находиться шаблоны:

# Enter to the project root directory

cd... /... /... /

cd users/

mkdir -p templates/users/

Создайте файлы шаблонов регистрации и входа в эту папку:

cd templates/users/

touch signup.html login.html

Ниже приведен код шаблона для signup.htmlшаблона:

{% extends 'base.html’%}

{% load crispy_forms_tags%}

{% block body%}

 

 

 

 

 

 

{% csrf_token%}

{{ form|crispy }}

 

 

 

 

{% comment%} Already Registered {% endcomment%}

 

 

 

 

Already Registered?

Login

 

 

 

 

{% endblock body%}

Мы можем проверить это в браузере по адресу localhost:8000/users/signup.

Страница регистрации

И последнее, но не менее важное: напишите шаблон входа в систему:

{% extends 'base.html’%}

{% load crispy_forms_tags%}

{% block body%}

 

 

 

 

 

 

{% csrf_token%}

{{ form|crispy }}

 

 

 

 

{% comment%} Already Registered {% endcomment%}

 

 

 

 

Don’t have an account?

Create account

 

 

 

 

{% endblock body%}

Страница авторизации

Шаблоны Django позволяют нам сэкономить много времени, повторно используя один и тот же HTML-код несколько раз. Только представьте, сколько времени вы потратили бы на копирование и вставку одного и того же HTML снова и снова.

Идеально! Теперь у вас есть полностью работающее приложение. Попробуйте использовать его, модифицировать или даже расширить его функциональность.

Подводя итоги

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

Django — наиболее часто используемый веб-фреймворк Python. Он позволяет быстро создавать сложные веб-приложения.

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

Django также предлагает несколько сторонних пакетов, которые дают вам возможность использовать чужое приложение. Например, проект работает с формами Django taggit и Django crispy.

В этом уроке мы рассмотрели следующее:

Джанго CRUD-операции

Встроенная система аутентификации Django

как управлять медиафайлами в Django

использование Django taggit для классификации контента

реализация форм Django с хрустящими формами

написание шаблонов Django с помощью Bootstrap 5

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

3D-печать5GABC-анализAndroidAppleAppStoreAsusCall-центрChatGPTCRMDellDNSDrupalExcelFacebookFMCGGoogleHuaweiInstagramiPhoneLinkedInLinuxMagentoMicrosoftNvidiaOpenCartPlayStationPOS материалPPC-специалистRuTubeSamsungSEO-услугиSMMSnapchatSonyStarlinkTikTokTwitterUbuntuUp-saleViasatVPNWhatsAppWindowsWordPressXiaomiYouTubeZoomАвдеевкаАктивные продажиАкцияАлександровск ЛНРАлмазнаяАлчевскАмвросиевкаАнализ конкурентовАнализ продажАнтимерчандайзингАнтрацитАртемовскАртемовск ЛНРАссортиментная политикаБелгородБелицкоеБелозерскоеБердянскБизнес-идеи (стартапы)БрендБрянкаБукингВахрушевоВендорВидеоВикипедияВирусная рекламаВирусный маркетингВладивостокВнутренние продажиВнутренний маркетингВолгоградВолновахаВоронежГорловкаГорнякГорскоеДебальцевоДебиторкаДебиторская задолженностьДезинтермедитацияДзержинскДивизионная система управленияДизайнДимитровДирект-маркетингДисконтДистрибьюторДистрибьюцияДобропольеДокучаевскДоменДружковкаЕкатеринбургЕнакиевоЖдановкаЗапорожьеЗимогорьеЗолотоеЗоринскЗугрэсИжевскИловайскИрминоКазаньКалининградКировскКировскоеКомсомольскоеКонстантиновкаКонтент-маркетингКонтент-планКопирайтингКраматорскКрасноармейскКрасногоровкаКраснодарКраснодонКраснопартизанскКрасный ЛиманКрасный ЛучКременнаяКураховоКурскЛисичанскЛуганскЛутугиноМакеевкаМариупольМаркетингМаркетинговая информацияМаркетинговые исследованияМаркетинговый каналМаркетинг услугМаркетологМарьинкаМедиаМелекиноМелитопольМенеджментМерчандайзерМерчандайзингМиусинскМолодогвардейскМоскваМоспиноНижний НовгородНиколаевНиколаевкаНишевой маркетингНовоазовскНовогродовкаНоводружескНовосибирскНумерическая дистрибьюцияОдессаОмскОтдел маркетингаПартизанский маркетингПервомайскПеревальскПетровскоеПлата за кликПоисковая оптимизацияПопаснаяПравило ПаретоПривольеПрогнозирование продажПродвижение сайтов в ДонецкеПроизводство видеоПромоПромоушнПрямой маркетингРабота для маркетологаРабота для студентаРазработка приложенийРаспродажаРегиональные продажиРекламаРеклама на асфальтеРемаркетингРетро-бонусРибейтРитейлРовенькиРодинскоеРостов-на-ДонуРубежноеСамараСанкт-ПетербургСаратовСватовоСвердловскСветлодарскСвятогорскСевастопольСеверодонецкСеверскСедовоСейлз промоушнСелидовоСимферопольСинергияСколковоСлавянскСнежноеСоздание сайтов в ДонецкеСоледарСоциальные сетиСочиСтаробельскСтаробешевоСтахановСтимулирование сбытаСуходольскСчастьеТелемаркетингТельмановоТираспольТорговый представительТорезТрейд маркетингТрейд промоушнТюменьУглегорскУгледарУкраинскХабаровскХарцызскХерсонХостингЦелевая аудиторияЦифровой маркетингЧасов ЯрЧелябинскШахтерскЮжно-СахалинскЮнокоммунаровскЯндексЯсиноватая