QuerySets

سلام

در این قسمت می خواهیم APIهای مدل ها را واکاوی کنیم. در ابتدا قصد داریم ویوی مربوط به صفحۀ اصلی را بهبود بخشیم.

باید سه قسمت را درست کنیم.

  • نمایش تعداد پست های ارسالی در یک بورد.
  • نمایش تعداد تاپیک های ارسالی در یک بورد.
  • نمایش آخرین کاربری که پستی را ارسال نموده به همراه تاریخ و ساعت آن.

برای اینکار از ترمینال پایتون (و نه کدزنی مستقیم در برنامه) شروع می کنیم و سپس تغییرات لازم را در برنامه پیاده سازی می کنیم.

بهتر است پیش از استفاده از ترمینال پایتون برای تمام مدل های خود متد __str__ را اضافه کنیم.

boards/models.py (مشاهده کد کامل)

from django.db import models
from django.utils.text import Truncator

class Board(models.Model):
    # ...
    def __str__(self):
        return self.name

class Topic(models.Model):
    # ...
    def __str__(self):
        return self.subject

class Post(models.Model):
    # ...
    def __str__(self):
        truncated_message = Truncator(self.message)
        return truncated_message.chars(30)

در مدل پست از کلاس و ابزار Truncator استفاده می کنیم. با این کار می توانیم رشته های طولانی را به سایز مشخصی (در اینجا ۳۰ کاراکتر) محدود و کوتاه کنیم.

بسیار عالی! ترمینال پایتون را باز می کنیم.

python manage.py shell

سپس درون شل دستورات زیر را وارد می کنیم.

from board.models import Board

# First get a board instance from the database
board = Board.objects.get(name='Django')

راحت ترین کار برای انجام ۳ هدف قبلی این است که تعداد تاپیک های فعلی را به دست آوریم. به خاطر اینکه تاپیک ها و بوردها مستقیما با هم در ارتباط اند.

board.topics.all()
<QuerySet [<Topic: Hello everyone!>, <Topic: Test>, <Topic: Testing a new post>, <Topic: Hi>]>

board.topics.count()
۴

اما تعداد پست های یک بورد نیاز به کمی حقه دارد! چراکه پست ها مستقیما با یک بورد در ارتباط نیستند.

from boards.models import Post

Post.objects.all()
<QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: Hi everyone!>,
  <Post: New test here!>, <Post: Testing the new reply feature!>, <Post: Lorem ipsum dolor sit amet,...>,
  <Post: hi there>, <Post: test>, <Post: Testing..>, <Post: some reply>, <Post: Random random.>
]>

Post.objects.count()
۱۱

ما در اینجا تعداد ۱۱ پست داریم اما تمام آن ها به بورد «Django» تعلق ندارند. برای این کار به شیوۀ زیر پست ها را فیلتر می کنیم.

from boards.models import Board, Post

board = Board.objects.get(name='Django')

Post.objects.filter(topic__board=board)
<QuerySet [<Post: This is my first topic.. :-)>, <Post: test.>, <Post: hi there>,
  <Post: Hi everyone!>, <Post: Lorem ipsum dolor sit amet,...>, <Post: New test here!>,
  <Post: Testing the new reply feature!>
]>

Post.objects.filter(topic__board=board).count()
۷

استفاده از دو علامت آندرلاین (__) پشت سر هم در topic__board برای بهره برداری از روابط تعریف‌شده میان مدل های مختلف است.

جنگو میان پست، تاپیک و بورد پلی را ایجاد کرده و کوئری SQLای را می سازد تا صرفاً پست های متعلق به یک بورد خاص را نمایش دهد.

و حالا آخرین مأموریت ما آن است که آخرین پست ارسالی را مشخص کنیم.

# order by the `created_at` field, getting the most recent first
Post.objects.filter(topic__board=board).order_by('-created_at')
<QuerySet [<Post: testing>, <Post: new post>, <Post: hi there>, <Post: Lorem ipsum dolor sit amet,...>,
  <Post: Testing the new reply feature!>, <Post: New test here!>, <Post: Hi everyone!>,
  <Post: test.>, <Post: This is my first topic.. :-)>
]>

# we can use the `first()` method to just grab the result that interest us
Post.objects.filter(topic__board=board).order_by('-created_at').first()
<Post: testing>

و تمام…! حالا که همه چیز در شل پایتون درست کار می کند باید آن ها را به برنامۀ خود اضافه کنیم.

boards/models.py (مشاهده کد کامل)

from django.db import models

class Board(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=100)

    def __str__(self):
        return self.name

    def get_posts_count(self):
        return Post.objects.filter(topic__board=self).count()

    def get_last_post(self):
        return Post.objects.filter(topic__board=self).order_by('-created_at').first()

دقت کنید که از self استفاده کرده ایم. برای اینکه این متد توسط بورد استفاده خواهد شد و به این معنا خواهد بود که ما از این مورد برای فیلتر کردن کوئری خود استفاده خواهیم کرد.

گام بعدی بروزرسانی قالب صفحۀ خانه برای نمایش اطلاعات جدید است.

templates/home.html

{% extends 'base.html' %}

{% block breadcrumb %}
  <li class="breadcrumb-item active">Boards</li>
{% endblock %}

{% block content %}
  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>Board</th>
        <th>Posts</th>
        <th>Topics</th>
        <th>Last Post</th>
      </tr>
    </thead>
    <tbody>
      {% for board in boards %}
        <tr>
          <td>
            <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
            <small class="text-muted d-block">{{ board.description }}</small>
          </td>
          <td class="align-middle">
            {{ board.get_posts_count }}
          </td>
          <td class="align-middle">
            {{ board.topics.count }}
          </td>
          <td class="align-middle">
            {% with post=board.get_last_post %}
              <small>
                <a href="{% url 'topic_posts' board.pk post.topic.pk %}">
                  By {{ post.created_by.username }} at {{ post.created_at }}
                </a>
              </small>
            {% endwith %}
          </td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
{% endblock %}

و خروجی به شکل زیر خواهد شد:

نکته خیلی مهم: اگر در قطعه کد بالا تمام بوردها دارای تاپیک نباشند صفحه خطا می دهد که راه بر طرف کردن آن در ادامۀ همین آموزش آمده است…

فرمان تست را اجرا می کنیم.

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......................................................EEE......................
======================================================================
ERROR: test_home_url_resolves_home_view (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']

======================================================================
ERROR: test_home_view_contains_link_to_topics_page (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']

======================================================================
ERROR: test_home_view_status_code (boards.tests.test_view_home.HomeTests)
----------------------------------------------------------------------
django.urls.exceptions.NoReverseMatch: Reverse for 'topic_posts' with arguments '(1, '')' not found. 1 pattern(s) tried: ['boards/(?P<pk>\\d+)/topics/(?P<topic_pk>\\d+)/$']

----------------------------------------------------------------------
Ran 80 tests in 5.663s

FAILED (errors=3)
Destroying test database for alias 'default'...

به نظر می رسد که با پیاده سازی جدید مشکل داریم و اگر هیچ پستی ارسال نشده باشد برنامه کرش و فروپاشی می کند.

templates/home.html

{% with post=board.get_last_post %}
  {% if post %}
    <small>
      <a href="{% url 'topic_posts' board.pk post.topic.pk %}">
        By {{ post.created_by.username }} at {{ post.created_at }}
      </a>
    </small>
  {% else %}
    <small class="text-muted">
      <em>No posts yet.</em>
    </small>
  {% endif %}
{% endwith %}

فرمان تست را دوباره اجرا می کنیم.

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
................................................................................
----------------------------------------------------------------------
Ran 80 tests in 5.630s

OK
Destroying test database for alias 'default'...

حل است!

من یک بورد جدید ولی بدون هیچ پیامی ایجاد کرده ام تا «پیام های خالی» را چک کنم.

حالا نوبت آن رسیده تا ویوی لیست تاپیک ها را بهبود دهیم.

طبق قولی که داده بودم می خواهم راه دیگری را برای نمایش تعداد پاسخ های یک پست معرفی کنم که از لحاظ پردازش و بار سرور به صرفه تر باشد.

مثل معمول با شل پایتون شروع می کنیم.

python manage.py shell
from django.db.models import Count
from boards.models import Board

board = Board.objects.get(name='Django')

topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts'))

for topic in topics:
    print(topic.replies)

۲
۴
۲
۱

در اینجا از متد annotate در کوئری استفاده کرده ایم که با این کار یک ستون جدید به صورت موقتی و در هوا می سازیم.

این ستون جدید قرار است توسط topic.replies قابل دسترسی بوده و شامل تعداد پست های یک تاپیک باشد. فقط لازم است که یک ترمیم جزئی انجام دهیم چراکه تعداد پاسخ ها نباید شامل اولین پست (پست اصلی) شوند.

خیلی خوب این کار به راحتی قابل انجام است و کافی است از تعداد خروجی یک عدد کم کنیم.

topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)

for topic in topics:
    print(topic.replies)

۱
۳
۱
۰

دیگه چی از این بهتر؟!

boards/views.py (مشاهده کد کامل)

from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from .models import Board

def board_topics(request, pk):
    board = get_object_or_404(Board, pk=pk)
    topics = board.topics.order_by('-last_updated').annotate(replies=Count('posts') - 1)
    return render(request, 'topics.html', {'board': board, 'topics': topics})

templates/topics.html (مشاهده کد کامل)

{% for topic in topics %}
  <tr>
    <td><a href="{% url 'topic_posts' board.pk topic.pk %}">{{ topic.subject }}</a></td>
    <td>{{ topic.starter.username }}</td>
    <td>{{ topic.replies }}</td>
    <td>0</td>
    <td>{{ topic.last_updated }}</td>
  </tr>
{% endfor %}

گام بعدی شمارش تعداد بازدیدهاست اما قبل از آن باید یک فیلد جدید را به مدل های قبلی خود اضافه کنیم که در بخش بعدی به آن پرداخته می شود.

ترجمۀ اختصاصی توسط تمدن

 

مطلب بعدی: مهاجرت

مطلب قبلی:ویوی پاسخ به پست

0 پاسخ

دیدگاه خود را ثبت کنید

تمایل دارید در گفتگو شرکت کنید؟
نظری بدهید!

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *