Django入门与实践-第26章:个性化工具

我觉得只添加内置的人性化(humanize)包就会很不错。它包含一组为数据添加“人性化(human touch)”的工具集。

例如,我们可以使用它来更自然地显示日期和时间字段。我们可以简单地显示:“2分钟前”,而不是显示整个日期。

我们来实践一下!首先,添加 django.contrib.humanize 到配置文件的 INSTALLED_APPS 中。

myproject/settings.py

  1. INSTALLED_APPS = [
  2. 'django.contrib.admin',
  3. 'django.contrib.auth',
  4. 'django.contrib.contenttypes',
  5. 'django.contrib.sessions',
  6. 'django.contrib.messages',
  7. 'django.contrib.staticfiles',
  8. 'django.contrib.humanize', # <- 这里
  9. 'widget_tweaks',
  10. 'accounts',
  11. 'boards',
  12. ]

现在我们就可以在模板中使用它了。首先来编辑 topics.html 模板:

templates/topics.html 查看完整文件

  1. {% extends 'base.html' %}
  2. {% load humanize %}
  3. {% block content %}
  4. <!-- 代码被压缩 -->
  5. <td>{{ topic.last_updated|naturaltime }}</td>
  6. <!-- 代码被压缩 -->
  7. {% endblock %}

我们所要做的就是在模板中加载 {%load humanize%} 这个模板标签,然后在模板中使用过滤器: {{ topic.last_updated|naturaltime }}

Django入门与实践-第26章:个性化设置 - 图1

你当然可以将它添加到其他你需要的地方。

Gravatar(添加头像用的库)

给用户个人信息添加图片的一种非常简单的方法就是使用 Gravatar

boards/templatetags 文件夹内,创建一个名为 gravatar.py 的新文件:

boards/templatetags/gravatar.py

  1. import hashlib
  2. from urllib.parse import urlencode
  3. from django import template
  4. from django.conf import settings
  5. register = template.Library()
  6. @register.filter
  7. def gravatar(user):
  8. email = user.email.lower().encode('utf-8')
  9. default = 'mm'
  10. size = 256
  11. url = 'https://www.gravatar.com/avatar/{md5}?{params}'.format(
  12. md5=hashlib.md5(email).hexdigest(),
  13. params=urlencode({'d': default, 's': str(size)})
  14. )
  15. return url

基本上我们可以使用官方提供的代码片段。我只是做了一下适配,使得它可以在python 3环境中运行。

很好,现在我们可以将它加载到我们的模板中,就像之前我们使用人性化模板过滤器一样:

templates/topic_posts.html 查看完整文件

  1. {% extends 'base.html' %}
  2. {% load gravatar %}
  3. {% block content %}
  4. <!-- code suppressed -->
  5. <img src="{{ post.created_by|gravatar }}" alt="{{ post.created_by.username }}" class="w-100 rounded">
  6. <!-- code suppressed -->
  7. {% endblock %}

Django入门与实践-第26章:个性化设置 - 图2

最后调整

也许你已经注意到了,如果有人回复帖子时有一个小问题。我们没有更新 last_update 字段,因此主题的排序被打乱顺序了。

我们来修一下:

boards/views.py

  1. @login_required
  2. def reply_topic(request, pk, topic_pk):
  3. topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
  4. if request.method == 'POST':
  5. form = PostForm(request.POST)
  6. if form.is_valid():
  7. post = form.save(commit=False)
  8. post.topic = topic
  9. post.created_by = request.user
  10. post.save()
  11. topic.last_updated = timezone.now() # <- 这里
  12. topic.save() # <- 这里
  13. return redirect('topic_posts', pk=pk, topic_pk=topic_pk)
  14. else:
  15. form = PostForm()
  16. return render(request, 'reply_topic.html', {'topic': topic, 'form': form})

接下来我们要做的事是需要控制一下页面访问统计系统。我们不希望相同的用户再次刷新页面的时候被统计为多次访问。为此,我们可以使用会话(sessions):

boards/views.py

  1. class PostListView(ListView):
  2. model = Post
  3. context_object_name = 'posts'
  4. template_name = 'topic_posts.html'
  5. paginate_by = 20
  6. def get_context_data(self, **kwargs):
  7. session_key = 'viewed_topic_{}'.format(self.topic.pk) # <--这里
  8. if not self.request.session.get(session_key, False):
  9. self.topic.views += 1
  10. self.topic.save()
  11. self.request.session[session_key] = True # <--直到这里
  12. kwargs['topic'] = self.topic
  13. return super().get_context_data(**kwargs)
  14. def get_queryset(self):
  15. self.topic = get_object_or_404(Topic, board__pk=self.kwargs.get('pk'), pk=self.kwargs.get('topic_pk'))
  16. queryset = self.topic.posts.order_by('created_at')
  17. return queryset

现在我们可以在主题列表中提供一个更好一点的导航。目前唯一的选择是用户点击主题标题并转到第一页。我们可以实践一下这么做:

boards/models.py

  1. import math
  2. from django.db import models
  3. class Topic(models.Model):
  4. # ...
  5. def __str__(self):
  6. return self.subject
  7. def get_page_count(self):
  8. count = self.posts.count()
  9. pages = count / 20
  10. return math.ceil(pages)
  11. def has_many_pages(self, count=None):
  12. if count is None:
  13. count = self.get_page_count()
  14. return count > 6
  15. def get_page_range(self):
  16. count = self.get_page_count()
  17. if self.has_many_pages(count):
  18. return range(1, 5)
  19. return range(1, count + 1)

然后,在 topics.html 模板中,我们可以这样实现:

templates/topics.html

  1. <table class="table table-striped mb-4">
  2. <thead class="thead-inverse">
  3. <tr>
  4. <th>Topic</th>
  5. <th>Starter</th>
  6. <th>Replies</th>
  7. <th>Views</th>
  8. <th>Last Update</th>
  9. </tr>
  10. </thead>
  11. <tbody>
  12. {% for topic in topics %}
  13. {% url 'topic_posts' board.pk topic.pk as topic_url %}
  14. <tr>
  15. <td>
  16. <p class="mb-0">
  17. <a href="{{ topic_url }}">{{ topic.subject }}</a>
  18. </p>
  19. <small class="text-muted">
  20. Pages:
  21. {% for i in topic.get_page_range %}
  22. <a href="{{ topic_url }}?page={{ i }}">{{ i }}</a>
  23. {% endfor %}
  24. {% if topic.has_many_pages %}
  25. ... <a href="{{ topic_url }}?page={{ topic.get_page_count }}">Last Page</a>
  26. {% endif %}
  27. </small>
  28. </td>
  29. <td class="align-middle">{{ topic.starter.username }}</td>
  30. <td class="align-middle">{{ topic.replies }}</td>
  31. <td class="align-middle">{{ topic.views }}</td>
  32. <td class="align-middle">{{ topic.last_updated|naturaltime }}</td>
  33. </tr>
  34. {% endfor %}
  35. </tbody>
  36. </table>

就像每个主题的小分页一样。请注意,我在 table 标签里还添加了 table-striped 类,使得表格有一个更好的样式。

Django入门与实践-第26章:个性化设置 - 图3

在回复页面中,我们现在是列出了所有的回复。我们可以将它限制在最近的十个回复。

boards/models.py

  1. class Topic(models.Model):
  2. # ...
  3. def get_last_ten_posts(self):
  4. return self.posts.order_by('-created_at')[:10]

templates/reply_topic.html

  1. {% block content %}
  2. <form method="post" class="mb-4" novalidate>
  3. {% csrf_token %}
  4. {% include 'includes/form.html' %}
  5. <button type="submit" class="btn btn-success">Post a reply</button>
  6. </form>
  7. {% for post in topic.get_last_ten_posts %} <!-- here! -->
  8. <div class="card mb-2">
  9. <!-- code suppressed -->
  10. </div>
  11. {% endfor %}
  12. {% endblock %}

Django入门与实践-第26章:个性化设置 - 图4

另一件事是,当用户回复帖子时,我们现在是会再次将用户重定向到第一页。我们可以通过将用户送回到最后一页来改善这个问题。

我们可以在帖子上添加一个ID:

templates/topic_posts.html

  1. {% block content %}
  2. <div class="mb-4">
  3. <a href="{% url 'reply_topic' topic.board.pk topic.pk %}" class="btn btn-primary" role="button">Reply</a>
  4. </div>
  5. {% for post in posts %}
  6. <div id="{{ post.pk }}" class="card {% if forloop.last %}mb-4{% else %}mb-2{% endif %} {% if forloop.first %}border-dark{% endif %}">
  7. <!-- code suppressed -->
  8. </div>
  9. {% endfor %}
  10. {% include 'includes/pagination.html' %}
  11. {% endblock %}

这里的重要点是 <div id="{{ post.pk }}" ...>

然后我们可以在视图中像这样使用它:

boards/views.py

  1. @login_required
  2. def reply_topic(request, pk, topic_pk):
  3. topic = get_object_or_404(Topic, board__pk=pk, pk=topic_pk)
  4. if request.method == 'POST':
  5. form = PostForm(request.POST)
  6. if form.is_valid():
  7. post = form.save(commit=False)
  8. post.topic = topic
  9. post.created_by = request.user
  10. post.save()
  11. topic.last_updated = timezone.now()
  12. topic.save()
  13. topic_url = reverse('topic_posts', kwargs={'pk': pk, 'topic_pk': topic_pk})
  14. topic_post_url = '{url}?page={page}#{id}'.format(
  15. url=topic_url,
  16. id=post.pk,
  17. page=topic.get_page_count()
  18. )
  19. return redirect(topic_post_url)
  20. else:
  21. form = PostForm()
  22. return render(request, 'reply_topic.html', {'topic': topic, 'form': form})

topic_post_url 中,我们使用最后一页来构建一个url,添加一个锚点id等于帖子id的元素。

有了这个,这要求我们需要更新下面的这些测试用例:

boards/tests/test_view_reply_topic.py

  1. class SuccessfulReplyTopicTests(ReplyTopicTestCase):
  2. # ...
  3. def test_redirection(self):
  4. '''
  5. A valid form submission should redirect the user
  6. '''
  7. url = reverse('topic_posts', kwargs={'pk': self.board.pk, 'topic_pk': self.topic.pk})
  8. topic_posts_url = '{url}?page=1#2'.format(url=url)
  9. self.assertRedirects(self.response, topic_posts_url)

Django入门与实践-第26章:个性化设置 - 图5

下一个问题,正如你在前面的截图中看到的,要解决分页时页数太多的问题。

最简单的方法是调整 pagination.html 模板:

templates/includes/pagination.html

  1. {% if is_paginated %}
  2. <nav aria-label="Topics pagination" class="mb-4">
  3. <ul class="pagination">
  4. {% if page_obj.number > 1 %}
  5. <li class="page-item">
  6. <a class="page-link" href="?page=1">First</a>
  7. </li>
  8. {% else %}
  9. <li class="page-item disabled">
  10. <span class="page-link">First</span>
  11. </li>
  12. {% endif %}
  13. {% if page_obj.has_previous %}
  14. <li class="page-item">
  15. <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
  16. </li>
  17. {% else %}
  18. <li class="page-item disabled">
  19. <span class="page-link">Previous</span>
  20. </li>
  21. {% endif %}
  22. {% for page_num in paginator.page_range %}
  23. {% if page_obj.number == page_num %}
  24. <li class="page-item active">
  25. <span class="page-link">
  26. {{ page_num }}
  27. <span class="sr-only">(current)</span>
  28. </span>
  29. </li>
  30. {% elif page_num > page_obj.number|add:'-3' and page_num < page_obj.number|add:'3' %}
  31. <li class="page-item">
  32. <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
  33. </li>
  34. {% endif %}
  35. {% endfor %}
  36. {% if page_obj.has_next %}
  37. <li class="page-item">
  38. <a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
  39. </li>
  40. {% else %}
  41. <li class="page-item disabled">
  42. <span class="page-link">Next</span>
  43. </li>
  44. {% endif %}
  45. {% if page_obj.number != paginator.num_pages %}
  46. <li class="page-item">
  47. <a class="page-link" href="?page={{ paginator.num_pages }}">Last</a>
  48. </li>
  49. {% else %}
  50. <li class="page-item disabled">
  51. <span class="page-link">Last</span>
  52. </li>
  53. {% endif %}
  54. </ul>
  55. </nav>
  56. {% endif %}

Django入门与实践-第26章:个性化设置 - 图6

总结

在本教程中,我们完成了Django board项目应用的实现。我可能会发布一个后续的实现教程来改进代码。我们可以一起研究很多事情。例如数据库优化,改进用户界面,文件上传操作,创建审核系统等等。

下一篇教程将着重于部署。它将是关于如何将你的代码投入到生产中以及需要关注的一些重要细节的完整指南。

我希望你会喜欢本系列教程的第六部分!最后一部分将于下周2017年10月16日发布。如果你希望在最后一部分发布时收到通知,可以订阅我们的邮件列表

该项目的源代码在github上找到。当前状态的该项目的可以在发布标签 v0.6-lw 下找到。或者直接点击下面的链接:

https://github.com/sibtc/django-beginners-guide/tree/v0.6-lw