Пятый этап
Зарегистрированные пользователи сайта ITfinder должны иметь возможность обмениваться сообщениями и оставлять отзывы о проектах. И то, и другое реализовать на Django несложно.
Мессенджер
Начнем работу с создания модели Message
– сохраните этот код в users/models.py:
class Message(models.Model):
sender = models.ForeignKey(
Profile, on_delete=models.SET_NULL, null=True, blank=True)
recipient = models.ForeignKey(
Profile, on_delete=models.SET_NULL, null=True, blank=True, related_name="messages")
name = models.CharField(max_length=200, null=True, blank=True)
email = models.EmailField(max_length=200, null=True, blank=True)
subject = models.CharField(max_length=200, null=True, blank=True)
body = models.TextField()
is_read = models.BooleanField(default=False, null=True)
created = models.DateTimeField(auto_now_add=True)
id = models.UUIDField(default=uuid.uuid4, unique=True,
primary_key=True, editable=False)
def __str__(self):
return self.subject
class Meta:
ordering = ['is_read', '-created']
Подготовьте и примените миграции:
python manage.py makemigrations
python manage.py migrate
После этого добавьте импорт Message и новый класс в users/form.py:
class MessageForm(ModelForm):
class Meta:
model = Message
fields = ['name', 'email', 'subject', 'body']
def __init__(self, *args, **kwargs):
super(MessageForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs.update({'class': 'input'})
Шаблоны, которые понадобятся для мессенджера:
Сохраните эти шаблоны в templates/users. Еще мы добавим ссылку на входящие в шаблоне navbar.html – {% url 'inbox' %}
.

Отзывы о проектах
В первой части туториала мы уже частично реализовали функциональность для отзывов о проектах. Теперь нам нужно связать отзывы с пользователями. Для этого мы внесем дополнения в модель Project
:
@property
def reviewers(self):
queryset = self.review_set.all().values_list('owner__id', flat=True)
return queryset
@property
def getVoteCount(self):
reviews = self.review_set.all()
upVotes = reviews.filter(value='up').count()
totalVotes = reviews.count()
ratio = (upVotes / totalVotes) * 100
self.vote_total = totalVotes
self.vote_ratio = ratio
self.save()
Дополним класс Review
:
class Review(models.Model):
VOTE_TYPE = (
('up', 'Положительная оценка'),
('down', 'Отрицательная оценка'),
)
owner = models.ForeignKey(Profile, on_delete=models.CASCADE, null=True)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
body = models.TextField(null=True, blank=True)
value = models.CharField(max_length=200, choices=VOTE_TYPE)
created = models.DateTimeField(auto_now_add=True)
id = models.UUIDField(default=uuid.uuid4, unique=True,
primary_key=True, editable=False)
class Meta:
unique_together = [['owner', 'project']]
def __str__(self):
return self.value
В шаблоне single-projects.html изменим ссылки на авторов отзывов: {% url 'user-profile' review.owner %}
, после чего добавим форму отзыва в projects/forms.py:
class ReviewForm(ModelForm):
class Meta:
model = Review
fields = ['value', 'body']
labels = {
'value': 'Оцените проект',
'body': 'Добавьте комментарий'
}
def __init__(self, *args, **kwargs):
super(ReviewForm, self).__init__(*args, **kwargs)
for name, field in self.fields.items():
field.widget.attrs.update({'class': 'input'})
Осталось внести небольшие изменения в представление проекта в файле projects/views.py. Добавим import ReviewForm
и код:
def project(request, project_slug):
project = Project.objects.get(slug=project_slug)
tags = project.tags.all()
form = ReviewForm()
if request.method == 'POST':
form = ReviewForm(request.POST)
review = form.save(commit=False)
review.project = project
review.owner = request.user.profile
review.save()
project.getVoteCount
messages.success(request, 'Ваш отзыв был добавлен!')
return redirect('project', project_slug=project.slug)
return render(request, 'projects/single-project.html', {'project': project, 'form': form})
Готово, теперь на сайте есть система отзывов:

Шестой этап
На заключительном этапе мы сделаем пагинацию профилей и проектов, а затем реализуем систему поиска.
Пагинация
Сначала сделаем пагинацию для вывода проектов. Создайте файл projects/utils.py, а в файл projects/views.py добавьте импорт:
from django.core import paginator
from .utils import paginateProjects
В конец шаблона projects.html добавьте:
{% include 'pagination.html' with queryset=projects custom_range=custom_range %}
После этого в папку с глобальными шаблонами положите файл pagination.html.
Запустите сервер и проверьте: проекты теперь выводятся постранично.
Таким же образом сделаем пагинацию для постраничного вывода профилей в приложении Users:
- создадим файл utils.py;
- добавим изменения в шаблон profiles.html;
- внесем дополнения в users/views.py.
Готово – теперь профили выводятся по 6 штук на странице:

Поиск
Мы реализуем поиск по проектам и профилям с помощью модуля Q. Сначала добавим импорт from django.db.models import Q
и эту функцию в projects/utils.py:
def searchProjects(request):
search_query = ''
if request.GET.get('search_query'):
search_query = request.GET.get('search_query')
tags = Tag.objects.filter(name__icontains=search_query)
projects = Project.objects.distinct().filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query) |
Q(owner__name__icontains=search_query) |
Q(tags__in=tags)
)
return projects, search_query
Использование модуля Q значительно изменит представление для вывода проектов:
def projects(request):
projects, search_query = searchProjects(request)
custom_range, projects = paginateProjects(request, projects, 6)
context = {'projects': projects,
'search_query': search_query, 'custom_range': custom_range}
return render(request, 'projects/projects.html', context)
Вызов Q в шаблоне projects.html производит {{ search_query }}
. Все готово – поиск по проектам работает:

То же самое сделаем для поиска по профилям. Добавим функцию поиска в users/utils.py:
def searchProfiles(request):
search_query = ''
if request.GET.get('search_query'):
search_query = request.GET.get('search_query')
skills = Skill.objects.filter(name__icontains=search_query)
profiles = Profile.objects.distinct().filter(
Q(name__icontains=search_query) |
Q(short_intro__icontains=search_query) |
Q(skill__in=skills)
)
return profiles, search_query
Изменим представление для вывода профилей в users/views.py:
def profiles(request):
profiles, search_query = searchProfiles(request)
custom_range, profiles = paginateProfiles(request, profiles, 6)
context = {'profiles': profiles, 'search_query': search_query,
'custom_range': custom_range}
return render(request, 'users/profiles.html', context)
И добавим {{ search_query }}
в profiles.html. Готово – поиск по профилям работает:

На этом разработка сайта завершена. Код готового проекта можно взять в репозитории ITfinder.
Материалы по теме
- 🐍🚀 Django с нуля. Часть 1: пишем многопользовательский блог для клуба любителей задач Python
- 🐍🚀 Django с нуля. Часть 2: регистрация, авторизация, ограничение доступа
- 🐍🚀 Django с нуля. Часть 3: создание профилей, сжатие изображений, CRUD и пагинация
Комментарии
Наталья, добрый день. У меня возникли проблемы с подсчетом голосов и положительных оценок. На сайте показывает что отзыв добавлен, в админке все это видно, но в базу не добавляет новых значений и соответсвенно на сайте так все и остаться по "0". Вы сможете подсказать куда копать? Или можно с вами связаться через другой канал связи, чтобы была возможность код показать ?
Добрый день, Алексей. Напишите в ТГ - natkaida, или на ВК - natalia.kaida. Или загрузите свой код на Гитхаб, если так удобнее.
Наталья, есть еще вопрос: На гитхабе в файлике projects.html, 31 строчка: Мы разве в маршрут 'project' не должны пробрасывать не слаг а uuid? Тоесть вот так project.id? Потому что со слагом джанго выкидывает ошибку мол он ожидал uuid. Но при таком раскладе смысл slug ов теряется т.к. в строке появляется uuid. Либо я что то не доделал. Подскажите плз
Может, не добавили маршрут в urls?
Да, надо было изменить немного. На первых этапах тут на сайте разногласия небольшие с кодом который на гитхабе в последнем шаге step 6. Спасибо за помощь !)
Всегда рада:). С большим проектом иногда можно увлечься и накодить чуть больше, чем освещено в статье. Но код по каждому этапу в отдельности всегда железно работает - если что-то глючит, можно скопировать весь этап сразу, и все заработает:).
Спасибо за статью. Подскажите, мы в этот проект куда-нибудь асинхронку можем добавить? Если да, то куда?
В этот проект я добавила бы async/await на стороне фронтенда. То есть асинхронная функциональность была бы реализована на JS.
Спасибо
Подскажите пожалуйста, на гитхаб сейчас в main.js - только заготовка c fetch для этого? html с api/remove-tag не добавляли или я не там ищу и как использовать let tags = document.getElementsByClassName('course-tag') и блок кода под ним?
Ага, заготовка. Допилю на досуге :)