اذهب إلى المحتوى

سنوسّع في هذا المقال موقع المكتبة المحلية من خلال إضافة صفحات قائمة وتفاصيل للكتب والمؤلفين، إذ سنتعرف على العروض المُعمَّمة المستندة إلى الأصناف Generic Class-based Views، ونوضح كيف يمكنها تقليل كمية الشيفرة البرمجية التي يجب عليك كتابتها لحالات الاستخدام الشائعة. سننتقل أيضًا إلى معالجة عناوين URLs بمزيد من التفصيل، ونوضح كيفية إجراء مطابقة الأنماط الأساسية.

سنكمل في هذا المقال النسخة الأولى من موقع المكتبة المحلية LocalLibrary من خلال إضافة صفحات قائمة وصفحات تفصيلية للكتب والمؤلفين، إذ سنعرض كيفية تقديم صفحات الكتاب، ونجعلك تنشئ صفحات المؤلفين بنفسك. تشبه هذه العملية إنشاء صفحة الفهرس التي تعلّمناها في المقال السابق، إذ سنحتاج إلى إنشاء روابط Maps لعناوين URL والعروض والقوالب. يتمثل الاختلاف الرئيسي في أننا سنواجه تحديًا إضافيًا بالنسبة لصفحات التفاصيل يتمثل في استخراج المعلومات من الأنماط الموجودة في عنوان URL وتمريرها إلى العرض، إذ سنعرض بالنسبة لهذه الصفحات نوعًا مختلفًا تمامًا من العروض هو عروض القائمة المُعمَّمة المستندة إلى الأصناف والعروض التفصيلية التي يمكن أن تقلل بصورة كبيرة من مقدار الشيفرة البرمجية للعرض المطلوب، مما يسهل كتابته وصيانته.

سيوضح الجزء الأخير من هذا المقال كيفية ترقيم صفحات Pagination بياناتك عند استخدام عروض القائمة المُعمَّمة المستندة إلى الأصناف.

تتألف هذه السلسلة الفرعية من السلسلة الأشمل تعلم تطوير الويب من المقالات التالية:

صفحة قائمة الكتب

ستعرض صفحة قائمة الكتب قائمةً بجميع سجلات الكتب المتاحة في الصفحة، والتي يمكن الوصول إليها باستخدام عنوان URL هو "catalog/books/‎"، وستعرض الصفحة عنوانًا ومؤلفًا لكل سجل على أن يكون العنوان رابطًا تشعبيًا لصفحة تفاصيل الكتاب المرتبطة به. سيكون للصفحة البنية والتنقل نفسه لجميع الصفحات الأخرى في الموقع، وبالتالي يمكننا توسيع القالب الأساسي base_generic.html الذي أنشأناه سابقًا.

ربط عناوين URLs

افتح الملف "‎/catalog/urls.py" وانسخ فيه السطر الذي يضبط مسار 'books/‎'. تعرّف الدالة path()‎ -كما هو الحال بالنسبة لصفحة الفهرس Index- نمطًا لمطابقة عنوان URL الذي هو 'books/‎'، ودالة عرض ستُستدعَى عند التطابق مع عنوان URL وهي views.BookListView.as_view()‎، واسمًا لهذا الربط المحدَّد كما يلي:

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
]

يجب أن يطابق عنوان URL عنوان "‎/catalog" كما ناقشنا سابقًا، لذلك سيُستدعَى العرض لعنوان URL هو "/catalog/books/".

لدالة العرض تنسيق مختلف عن ذي قبل، لأن هذا العرض سيُقدَّم بوصفه صنفًا، إذ سنرث من دالة عرض مُعمَّمة حالية تنفّذ معظم ما نريد أن تنفّذه دالة العرض بدلًا من كتابة دالتنا من الصفر. يمكننا الوصول إلى دالة عرض مناسبة من خلال استدعاء تابع الصنف as_view()‎ بالنسبة لعروض جانغو المستندة إلى الأصناف، إذ يطبّق هذا التابع كل العمل لإنشاء نسخة من الصنف والتأكد من استدعاء التوابع المعالجة الصحيحة لطلبات HTTP الواردة.

العرض المستند إلى الأصناف

يمكننا بسهولة كتابة عرض قائمة الكتب بوصفه دالة عادية، مثل عرض الفهرس في المقال السابق، إذ يمكن لهذا العرض الاستعلام في قاعدة البيانات عن جميع الكتب، ثم استدعاء التابع render()‎ لتمرير القائمة إلى قالب محدد، ولكن سنستخدم بدلًا من ذلك عرض قائمة مُعمَّمًا ومستندًا إلى الأصناف ListView، إذ يرث هذا الصنف من العرض الموجود مسبقًا. يقدّم العرض المُعمَّم معظم الوظائف التي نحتاجها ويتبع أفضل ممارسات جانغو، لذا سنكون قادرين على إنشاء عرض قائمة أقوى مع شيفرة وتكرار أقل وصيانة أقل.

افتح الملف catalog/views.py، وانسخ الشيفرة البرمجية التالية في نهاية الملف:

from django.views import generic

class BookListView(generic.ListView):
    model = Book

سيستعلم العرض المُعمَّم في قاعدة البيانات للحصول على جميع السجلات للنموذج المحدد Book، ثم يعرض قالبًا موجودًا في الملف "‎/locallibrary/catalog/templates/catalog/book_list.html"، الذي سننشئه لاحقًا. يمكنك ضمن القالب الوصول إلى قائمة الكتب باستخدام متغير القالب المسمى object_list أو book_list؛ أي "‎list_<اسم النموذج>" عمومًا.

ملاحظة: ليس هذا المسار الغريب لموقع القالب خطأ مطبعيًا، إذ تبحث العروض المعمَّمة عن القوالب في الملف "‎/application_name/the_model_name_list.html" (في حالتنا "catalog/book_list.html") في المجلد "/application_name/templates/" الخاص بالتطبيق ("/catalog/templates/").

يمكنك إضافة سمات لتغيير السلوك الافتراضي السابق، إذ يمكنك مثلًا تحديد ملف قالب آخر إذا كنت بحاجة إلى عروض متعددة تستخدم النموذج نفسه، أو يمكن أن ترغب في استخدام اسم متغير قالب مختلف إذا لم يكن الاسم book_list معبرًا عن حالة استخدام قالبك. يُحتمَل أن يكون الاختلاف الأكثر فائدة هو تغيير أو ترشيح المجموعة الفرعية من النتائج المُعادة، لذا يمكنك سرد أفضل 5 كتب قرأها المستخدمون الآخرون بدلًا من سرد جميع الكتب.

class BookListView(generic.ListView):
    model = Book
    context_object_name = 'book_list'   # اسمك الخاص للقائمة بوصفه متغير قالب
    queryset = Book.objects.filter(title__icontains='war')[:5] # احص‫ل على 5 كتب تحتوي على العنوان war
    template_name = 'books/my_arbitrary_template_name_list.html'  # حدد اسم/موقع قالبك

تعديل التوابع في العروض المستندة إلى الأصناف

يمكنك تعديل بعض توابع الصنف بالرغم من أننا لا نحتاج إلى ذلك حاليًا، فمثلًا يمكننا تعديل التابع get_queryset()‎ لتغيير قائمة السجلات المُعادة، إذ يُعَد ذلك أكثر مرونةً من ضبط السمة queryset كما فعلنا في جزء الشيفرة البرمجية السابق، بالرغم من عدم وجود فائدة حقيقية في هذه الحالة:

class BookListView(generic.ListView):
    model = Book

    def get_queryset(self):
        # ا‫حصل على 5 كتب تحتوي على العنوان war
        return Book.objects.filter(title__icontains='war')[:5] 

يمكننا تعديل التابع get_context_data()‎ لتمرير متغيرات سياق إضافية إلى القالب، مثل تمرير قائمة الكتب افتراضيًا. يوضح جزء الشيفرة التالي كيفية إضافة متغير بالاسم "some_data" إلى السياق، وسيكون متاحًا بعد ذلك بوصفه متغير قالب:

class BookListView(generic.ListView):
    model = Book

    def get_context_data(self, **kwargs):
        # استدعِ التقديم الأساسي أولًا للحصول على السياق
        context = super(BookListView, self).get_context_data(**kwargs)
        # أنشئ بيانات وأضِفها إلى السياق
        context['some_data'] = 'This is just some data'
        return context

من المهم اتباع النمط المستخدم السابق عند تطبيق ذلك كما يلي:

  • احصل أولًا على السياق الحالي من الصنف الأب.
  • ضِف بعد ذلك معلومات السياق الجديدة.
  • ثم أعِد السياق الجديد (المُحدَّث).

ملاحظة: اطلع على العروض المعمَّمة المستندة إلى الأصناف المبنية مسبقًا في توثيق جانغو للحصول على مزيد من الأمثلة لما يمكنك فعله باستخدامها.

إنشاء قالب عرض القائمة

أنشئ ملف HTML بالاسم التالي:

/locallibrary/catalog/templates/catalog/book_list.html

وانسخ فيه النص الآتي، وهو ملف القالب الافتراضي الذي يتوقعه عرض القائمة المُعمَّمة المستند إلى الأصناف (للنموذج Book في التطبيق catalog).

تتشابه قوالب العروض المُعمَّمة مع أيّ قوالب أخرى بالرغم من اختلاف السياق أو المعلومات الممررة إلى القالب. سنوسّع القالب الأساسي في السطر الأول، ثم سنستبدل كتلة المحتوى content كما هو الحال في قالب الفهرس index.

{% extends "base_generic.html" %}

{% block content %}
  <h1>Book List</h1>
  {% if book_list %}
    <ul>
      {% for book in book_list %}
      <li>
        <a href="{{ book.get_absolute_url }}">{{ book.title }}</a>
        ({{book.author}})
      </li>
      {% endfor %}
    </ul>
  {% else %}
    <p>There are no books in the library.</p>
  {% endif %}
{% endblock %}

يمرر العرضُ السياقَ (قائمة الكتب) افتراضيًا بوصفه اسمًا بديلًا لمتغير القالب object_list و book_list (سيلبي أيّ منهما الغرض).

التنفيذ المشروط

نستخدم وسوم القالب if و else و endif للتحقق من تعريف المتغير book_list وأنه ليس فارغًا. إذا كان book_list فارغًا، فستعرض تعليمة else نصًا يوضح عدم وجود كتب لسردها؛ وإذا لم يكن فارغًا، سنكرر المرور على قائمة الكتب.

{% if book_list %}
  <!-- تسرد هذه الشيفرة البرمجية الكتب -->
{% else %}
  <p>There are no books in the library.</p>
{% endif %}

يتحقق الشرط السابق من حالة واحدة فقط، ولكن يمكنك اختبار شروط إضافية باستخدام وسم القالب elif، مثل {% elif var2 %}. اطلع على if و ifequal/ifnotequal و ifchanged في وسوم ومرشحات القوالب المبنية مسبقًا في توثيق جانغو لمزيد من المعلومات حول المعاملات الشرطية.

حلقات for

يستخدم القالب وسمي القالب for و endfor للتكرار عبر قائمة الكتب كما هو موضح في المثال التالي، إذ يملأ كل تكرار متغير القالب book بمعلومات عن عنصر القائمة الحالية:

{% for book in book_list %}
  <li> <!-- تحصل هذه الشيفرة البرمجية على المعلومات من كل عنصر كتاب --> </li>
{% endfor %}

يمكنك أيضًا استخدام وسم القالب {% empty %} لتحديد ما يحدث إذا كانت قائمة الكتب فارغة (بالرغم من أن قالبنا يختار استخدام شرط بدلًا من ذلك):

<ul>
  {% for book in book_list %}
    <li><!-- تحصل هذه الشيفرة البرمجية على المعلومات من كل عنصر كتاب --></li>
  {% empty %}
    <p>There are no books in the library.</p>
  {% endfor %}
</ul>

سينشئ جانغو أيضًا ضمن الحلقة متغيرات أخرى يمكنك استخدامها لتتبع التكرار بالرغم من عدم استخدامه هنا، فمثلًا يمكنك اختبار المتغير forloop.last لإجراء معالجة شرطية لآخر مرة لتشغيل الحلقة.

الوصول إلى المتغيرات

تنشئ الشيفرة البرمجية الموجودة ضمن الحلقة عنصر قائمة لكل كتاب يُظهِر المؤلف والعنوان كأنه رابط للعرض التفصيلي الذي لم ننشئه بعد كما يلي:

<a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})

يمكن الوصول إلى حقول سجل الكتاب المرتبط بها باستخدام "الصيغة النقطية Dot Notation" مثل book.title و book.author، إذ يكون النص الذي يأتي بعد العنصر book هو اسم الحقل كما هو مُحدَّد في النموذج.

يمكننا أيضًا استدعاء دوال في النموذج من القالب، إذ نستدعي في هذه الحالة الدالة Book.get_absolute_url()‎ للحصول على عنوان URL الذي يمكنك استخدامه لعرض سجل التفاصيل المرتبط به. تعمل هذه الدالة بشرط ألّا تحتوي على أي وسطاء، إذ لا توجد طريقة لتمريرها.

ملاحظة: يجب أن نكون حذرين بعض الشيء من "الآثار الجانبية" لاستدعاء الدوال في القوالب، إذ حصلنا في مثالنا فقط على عنوان URL للعرض، ولكن الدالة يمكنها فعل أي شيء، فلا نريد حذف قاعدة بياناتنا عند عرض النموذج مثلًا.

تحديث القالب الأساسي

افتح النموذج الأساسي التالي وأدخل {% url 'books'‎ %} في ارتباط عنوان URL لجميع الكتب All books، مما يؤدي إلى تفعيل هذا الارتباط في جميع الصفحات، ويمكننا وضع ذلك في مكانه الصحيح بنجاح الآن بعد أن أنشأنا رابط Mapper عنوان URL للكتب.

  • الملف ‎/locallibrary/catalog/templates/base_generic.html:
<li><a href="{% url 'index' %}">Home</a></li>
<li><a href="{% url 'books' %}">All books</a></li>
<li><a href="">All authors</a></li>

لن تتمكن من إنشاء قائمة الكتب بعد، لأننا ما زلنا نفتقد اعتمادية هي ربط عنوان URL لصفحات تفاصيل الكتب، والتي تُعَد ضروريةً لإنشاء ارتباطات تشعبية لكل كتاب. سنعرض كلًا من عروض القائمة والعروض التفصيلية بعد القسم التالي.

صفحة تفاصيل الكتاب

ستعرض صفحة تفاصيل الكتاب معلومات حول كتاب معين يمكن الوصول إليه باستخدام عنوان URL هو catalog/book/<id>‎، إذ يمثل <id> المفتاح الرئيسي للكتاب. سندرج أيضًا تفاصيل النسخ المتاحة BookInstances بما في ذلك الحالة وموعد الإعادة المتوقع والناشر والمعرّف، إضافةً إلى الحقول الموجودة في النموذج Book (المؤلف والملخص ورقم ISBN واللغة والنوع)، مما يسمح لقرائنا ليس فقط بالتعرف على الكتاب، ولكن لتأكيد ما إذا كان متوفرًا أو موعد توفره.

ربط عناوين URLs

افتح الملف ‎/catalog/urls.py وضِف المسار المُسمَّى 'book-detail'، إذ تعرّف الدالة path()‎ نمطًا، وعرضًا تفصيليًا معمَّمًا مستندًا إلى الأصناف مرتبطًا به، واسمًا.

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
    path('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),
]

يستخدم نمط عنوان URL -بالنسبة للمسار book-detail- صيغةً خاصةً لالتقاط المعرّف المحدد للكتاب الذي نريد رؤيته، إذ تكون هذه الصيغة بسيطةً جدًا، إذ تحدّد أقواس الزاوية جزء عنوان URL الذي سيُلتقط، مع تضمين اسم المتغير الذي يمكن أن يستخدمه العرض للوصول إلى البيانات الملتقطة، فمثلًا سيلتقط النمط المحدد ويمرر القيمة إلى العرض بوصفه المتغير "something". يمكنك اختياريًا أن تسبق اسم المتغير بمواصفات المحوّل Converter Specification التي تحدد نوع البيانات (int و str و slug و uuid و path).

نستخدم في حالتنا '<int:pk>' لالتقاط معرّف الكتاب، والذي يجب أن يكون سلسلةً نصيةً منسقةً بصورة خاصة ويجب تمريره إلى العرض بوصفه معاملًا بالاسم pk (اختصار المفتاح الرئيسي Primary Key)، وهذا هو المعرّف الذي يُستخدَم لتخزين الكتاب بصورة فريدة في قاعدة البيانات كما هو مُحدَّد في النموذج Book.

ملاحظة: عنوان URL المطابق هو "catalog/book/‎"، لأننا في التطبيق catalog أو /catalog/ كما يُفترض.

تحذير: يتوقع العرض التفصيلي المعمَّم المستند إلى الأصناف تمرير معاملٍ بالاسم pk، فإذا أردت كتابة دالة العرض الخاصة بك، فيمكنك استخدام أيّ اسم معامل تريده، أو تمرير المعلومات في وسيط بدون اسم.

مطابقة المسار أو التعبير النمطي المتقدم

ملاحظة: لن تحتاج هذا القسم لإكمال هذا المقال، ولكننا نقدّمه لأن معرفة هذا الخيار يُحتمَل أن يكون مفيدًا في مستقبلك المرتكز على إطار عمل جانغو.

مطابقة النمط التي يوفرها التابع path()‎ بسيطة ومفيدة للحالات الشائعة جدًا إذ تريد فقط التقاط أيّ سلسلة أو عدد صحيح، ولكن إذا كنت بحاجة إلى مزيدٍ من الترشيح المُحسَّن مثل ترشيح السلاسل النصية التي تحتوي على عدد معين من المحارف فقط، فيمكنك استخدام التابع re_path()‎ الذي يُستخدَم تمامًا مثل التابع path()‎ باستثناء أنه يسمح لك بتحديد نمط باستخدام تعبير نمطي Regular Expression، فمثلًا يمكن كتابة المسار السابق كما يلي:

re_path(r'^book/(?P<pk>\d+)$', views.BookDetailView.as_view(), name='book-detail'),

تُعَد التعابير النمطية أداةً فعالة جدًا لربط الأنماط، وهي غير بسيطة ويمكن أن تكون مخيفة للمبتدئين. سنوضّح فيما يلي مقدمةً تمهيديةً عنها، فأول شيء يجب معرفته هو أنه يجب التصريح عن التعابير النمطية باستخدام صياغة سلسلة نصية مجردة (أي تُضمَّن بالشكل: '<يظهر نص تعبيرك النمطي هنا>'r‎).

الأجزاء الرئيسية من الصياغة التي ستحتاج إلى معرفتها للتصريح عن تطابقات الأنماط هي:

الرمز معناه
^ يطابق بداية النص.
$ يطابق نهاية النص.
‎\d يطابق رقمًا (0 و 1 و 2 و ... 9).
‎\w يطابق محرفًا بطول كلمة مثل أي حرف كبير أو صغير في الأبجدية أو رقمًا أو محرف الشرطة السفلية (_).
+ يطابق محرفًا أو أكثر من المحارف السابقة، فمثلًا يمكنك استخدام ‎\d+‎ لمطابقة رقم واحد أو أكثر، ويمكنك استخدام a+‎ لمطابقة حرف "a" واحد أو أكثر.
* يطابق صفرًا أو أكثر من المحارف السابقة، فمثلًا يمكنك مطابقة لا شيء أو كلمة من خلال استخدام ‎\w*‎
( ) يلتقط جزء النمط الموجود بين الأقواس، وتُمرَّر أيّ قيم مُلتقَطة إلى العرض بوصفات معاملات دون اسم، فإن اُلتقِطت أنماطٌ متعددة، فستُوفَّر المعاملات المرتبطة بها بترتيب التصريح عن هذه الالتقاطات.
‎(?P...)‎ التقاط النمط (الذي تشير إليه ...) بوصفه متغيرًا مُسمًّى (في هذه الحالة "name")، وتُمرَّر القيم المُلتقَطة إلى العرض باستخدام الاسم المُحدَّد، لذلك يجب أن يصرّح العرض عن معامل بالاسم نفسه.
[ ] يطابق محرفًا واحدًا في المجموعة، فمثلًا سيطابَق [abc] مع 'a' أو 'b' أو 'c'، وسيطابَق [‎-\w] مع المحرف '-' أو أي محرف بحجم كلمة.

يمكن أن تؤخَذ معظم المحارف الأخرى حرفيًا.

لنطّلع الآن على بعض الأمثلة الحقيقية عن الأنماط:

النمط الوصف
r'^book/(?P<pk>\d+)$'‎ هذا هو التعبير النمطي RE المُستخدَم في رابط عنوان URL الخاص بنا، حيث يجري مطابقة مع سلسلة نصية تحتوي على book/‎ في بداية السطر (‎^book/‎)، ثم يحتوي على رقم واحد أو أكثر (‎\d+‎)، ثم ينتهي (بمحارف غير رقمية قبل علامة نهاية السطر). يلتقط هذا النمط أيضًا جميع الأرقام (‎?P<pk>\d+‎) ويمررها إلى العرض ضمن معامل يسمى 'pk'، إذ تُمرَّر دائمًا القيم المُلتقَطة بوصفها سلسلة نصية، فمثلًا سيطابق هذا النمط book/1234، ويرسل المتغير pk='1234'‎ إلى العرض.
r'^book/(\d+)$'‎ يجري هذا النمط مطابقة مع عناوين URL للحالة السابقة نفسها، وستُرسَل المعلومات المُتقَطة بوصفها وسيطًا غير مُسمَّى إلى العرض.
r'^book/(?P<stub>[-\w]+)$'‎ يجري هذا النمط مطابقة مع سلسلة نصية تحتوي على book/‎ في بداية السطر (‎^book/‎)، ثم يحتوي على حرف واحد أو أكثر يكون إما '-' أو محرف بحجم كلمة (‎[‎-\w]+‎)، ثم ينتهي النمط، إذ يلتقط هذه المجموعة من المحارف ويمرّرها إلى العرض ضمن معامل بالاسم 'stub'. يُعَد هذا النمط نموذجيًا إلى حد ما لمفاتيح ‎"Stub"‎، التي تُعَد مفاتيحًا رئيسية للبيانات وتعتمد على الكلمات ومناسبة لعناوين URL، إذ يمكنك استخدامها إذا أردتَ أن يكون عنوان URL للكتاب يتضمن معلومات أكثر مثل استخدام ‎/catalog/book/the-secret-garden بدلًا من ‎/catalog/book/33.

يمكنك التقاط أنماط متعددة في تطابق واحد، مما يؤدي إلى تشفير الكثير من المعلومات المختلفة في عنوان URL.

ملاحظة: ضع في حساباتك كتحدٍ لك كيفية تشفير عنوان URL لسرد جميع الكتب الصادرة في سنة وشهر ويوم محددين والتعبير النمطي RE الذي يمكن استخدامه لمطابقتها.

تمرير خيارات إضافية في روابط URL

إحدى الميزات التي لم نستخدمها حتى الآن -لكنها تُعَد قيّمة- هي أنه يمكنك تمرير قاموس Dictionary يحتوي على خيارات إضافية إلى العرض باستخدام الوسيط الثالث غير المسمَّى للدالة path()‎. يمكن أن يكون هذا الأسلوب مفيدًا إذا أردت استخدام العرض نفسه لموارد متعددة، وتمرير البيانات لإعداد سلوكها في كل حالة، فمثلًا سيستدعي جانغو:

views.my_view(request, fish=halibut, my_template_name='some_path')‎

بالنسبة إلى المسار التالي لطلب /myurl/halibut/:

path('myurl/<int:fish>', views.my_view, {'my_template_name': 'some_path'}, name='aurl'),

ملاحظة: تُمرَّر كلٌّ من الأنماط المُلتقَطة وخيارات القاموس إلى العرض بوصفها وسائطًا لها اسم، فإذا استخدمتَ الاسم نفسه لكل من نمط الالتقاط ومفتاح القاموس، فسيُستخدَم خيار القاموس.

العرض المستند إلى الأصناف

افتح "catalog/views.py"، وانسخ الشيفرة البرمجية التالية في أسفل الملف:

class BookDetailView(generic.DetailView):
    model = Book

هذا كل شيء، وكل ما عليك فعله الآن هو إنشاء قالب يُسمَّى:

‎/locallibrary/catalog/templates/catalog/book_detail.html

وسيمرّر العرض إليه معلومات قاعدة البيانات للسجل Book المحدد الذي يستخرجه رابط عنوان URL، إذ يمكنك الوصول إلى تفاصيل الكتاب ضمن النموذج باستخدام متغير القالب المسمى object أو book (أي "اسم النموذج" بصورة عامة). يمكنك تغيير القالب المُستخدَم واسم كائن السياق المُستخدَم للإشارة إلى الكتاب في القالب إذا احتجتَ إلى ذلك، ويمكنك تعديل التوابع لإضافة معلومات إضافية إلى السياق مثلًا.

ماذا يحدث إذا كان السجل غير موجود؟

إذا لم يكن السجل المطلوب موجودًا، فسيرفع العرض التفصيلي المُعمَّم المستند إلى الأصناف استثناءَ Http404 تلقائيًا، وسيعرض في مرحلة الإنتاج صفحة مناسبة تحتوي على النص "لم يُعثَر على المورد resource not found"، والتي يمكنك تخصيصها إذا رغبت في ذلك.

سنعطيك فكرةً عن كيفية عمل ذلك فقط، إذ يوضح جزء الشيفرة التالي كيفية تقديم العرض المستند إلى الأصناف بوصفه دالة إن لم تستخدم العرض التفصيلي المُعمَّم المستند إلى الأصناف:

def book_detail_view(request, primary_key):
    try:
        book = Book.objects.get(pk=primary_key)
    except Book.DoesNotExist:
        raise Http404('Book does not exist')

    return render(request, 'catalog/book_detail.html', context={'book': book})

يحاول العرض أولًا الحصول على سجل الكتاب المحدد من النموذج، وإذا فشل في ذلك، فيجب أن يرفع العرض استثناء "Http404" للإشارة إلى أن الكتاب غير موجود، والخطوة الأخيرة هي كالعادة استدعاء الدالة render()‎ مع اسم القالب وبيانات الكتاب في المعامل context بوصفه قاموسًا.

يمكننا بدلًا من ذلك استخدام الدالة get_object_or_404()‎ بوصفها اختصارًا لرفع استثناء "Http404" إذا لم يُعثَر على السجل كما يلي:

from django.shortcuts import get_object_or_404

def book_detail_view(request, primary_key):
    book = get_object_or_404(Book, pk=primary_key)
    return render(request, 'catalog/book_detail.html', context={'book': book})

إنشاء نموذج العرض التفصيلي

أنشئ ملف HTML بالاسم:

‎/locallibrary/catalog/templates/catalog/book_detail.html 

وضع فيه المحتوى التالي، وهذا هو اسم ملف القالب الافتراضي الذي يتوقعه العرض التفصيلي المُعمَّم المستند إلى الأصناف (للنموذج Book في التطبيق المُسمى catalog).

{% extends "base_generic.html" %}

{% block content %}
  <h1>Title: {{ book.title }}</h1>

  <p><strong>Author:</strong> <a href="">{{ book.author }}</a></p>
  <!-- ارتباط تفاصيل المؤلف لم يُعرَّف بعد -->
  <p><strong>Summary:</strong> {{ book.summary }}</p>
  <p><strong>ISBN:</strong> {{ book.isbn }}</p>
  <p><strong>Language:</strong> {{ book.language }}</p>
  <p><strong>Genre:</strong> {{ book.genre.all|join:", " }}</p>

  <div style="margin-left:20px;margin-top:20px">
    <h4>Copies</h4>

    {% for copy in book.bookinstance_set.all %}
      <hr />
      <p
        class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">
        {{ copy.get_status_display }}
      </p>
      {% if copy.status != 'a' %}
        <p><strong>Due to be returned:</strong> {{ copy.due_back }}</p>
      {% endif %}
      <p><strong>Imprint:</strong> {{ copy.imprint }}</p>
      <p class="text-muted"><strong>Id:</strong> {{ copy.id }}</p>
    {% endfor %}
  </div>
{% endblock %}

ملاحظة: لارتباط المؤلف في النموذج السابق عنوان URL فارغ، لأننا لم ننشئ بعد صفحة تفاصيل المؤلف للارتباط بها. يمكننا الحصول على عنوان URL الخاص بصفحة التفاصيل بمجرد وجودها باستخدام أيٍّ من الطريقتين التاليتين:

  • أولًا، استخدم وسم القالب url لعكس عنوان URL الخاص بتفاصيل المؤلف 'author-detail' المُعرَّف في رابط عنوان URL، ومرّره إلى نسخة المؤلف الخاص بالكتاب:
<a href="{% url 'author-detail' book.author.pk %}">{{ book.author }}</a>
  • استدعِ التابع get_absolute_url()‎ الخاص بنموذج المؤلف، إذ يجري هذا التابع عملية الانعكاس نفسها:
<a href="{{ book.author.get_absolute_url }}">{{ book.author }}</a>

يطبّق كلا التابعين الشيء نفسه بفعالية، ولكن يُفضَّل استخدام التابع get_absolute_url()‎ لأنه يساعدك على كتابة شيفرة برمجية أكثر تناسقًا وقابلية للصيانة، إذ يجب إجراء التغييرات في مكان واحد فقط هو نموذج المؤلف.

شرحنا مسبقًا كل شيء تقريبًا في هذا القالب بالرغم من أنه أكبر قليلًا الآن:

  • نوسّع القالب الأساسي ونعدّل كتلة "المحتوى content".
  • نستخدم المعالجة المشروطة لتحديد عرض محتوًى معين أم لا.
  • نستخدم حلقات for للتكرار عبر قوائم الكائنات.
  • نصل إلى حقول السياق باستخدام الصيغة النقطية، إذ سُمِّي السياق بالاسم book لأننا استخدمنا العرض المُعمَّم التفصيلي، ولكن يمكننا أيضًا استخدام الاسم "object".

أول شيء مهم لم نره من قبل هو التابع book.bookinstance_set.all()‎ الذي ينشئه جانغو تلقائيًا لإعادة مجموعة سجلات BookInstance المرتبطة بكتاب Book معين:

{% for copy in book.bookinstance_set.all %}
  <!-- شيفرة للتكرار على كل نسخة من الكتاب -->
{% endfor %}

يُعَد هذا التابع ضروريًا لأنك تصرّح عن حقل ForeignKey (واحد إلى متعدد) فقط في جانب "المتعدد" من العلاقة (جانب BookInstance). بما أنك لا تفعل أي شيء للتصريح عن العلاقة في النموذج الآخر ("واحد")، فلن يحتوي هذا الجانب (النموذج Book) على أيّ حقل للحصول على مجموعة السجلات المرتبطة به. يتغلب جانغو على هذه المشكلة من خلال بناء دالة "بحث عكسي" مسمّاة بطريقة مناسبة لاستخدامها، إذ يُبنَى اسم الدالة من خلال جعل حروف اسم النموذج حروفًا صغيرة في مكان التصريح عن ForeignKey ويتبعه ‎_set، فمثلًا يكون اسم الدالة المُنشَأة في Book هي bookinstance_set()‎.

ملاحظة: استخدمنا هنا الدالة all()‎ للحصول على جميع السجلات افتراضيًا، ويمكنك استخدام الدالة filter()‎ للحصول على مجموعة فرعية من السجلات في الشيفرة البرمجية، ولكن لا يمكنك ذلك مباشرةً في القوالب لأنه لا يمكن تحديد وسطاء للدوال.

احذر أيضًا، ففي حال لم تحدّد طلبًا في العرض المستند إلى الأصناف أو النموذج، فسترى أخطاءً من خادم التطوير مثل الخطأ التالي:

[29/May/2017 18:37:53] "GET /catalog/books/?page=1 HTTP/1.1" 200 1637
/foo/local_library/venv/lib/python3.5/site-packages/django/views/generic/list.py:99: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <QuerySet [<Author: Ortiz, David>, <Author: H. McRaven, William>, <Author: Leigh, Melinda>]>
  allow_empty_first_page=allow_empty_first_page, **kwargs)

يحدث هذا الخطأ لأن كائن ترقيم الصفحات يتوقع أن يرى تنفيذ تعليمة الترتيب ORDER BY على قاعدة بياناتك الأساسية، فبدونها لا يمكن التأكد من أن السجلات المُعادة بالترتيب الصحيح.

لم يغطي هذا المقال ترقيم الصفحات Pagination حتى الآن، ولكن بما أنه لا يمكنك استخدام الدالة sort_by()‎ وتمرير معامل كما هو الحال مع الدالة filter()‎ سابقًا، فيجب عليك الاختيار من بين ثلاثة خيارات هي:

  1. أضف السمة ordering ضمن التصريح عن الصنف class Meta في نموذجك.
  2. أضف السمة queryset في عرضك المخصص المستند إلى الأصناف مع تحديد الدالة order_by()‎.
  3. أضف التابع get_queryset في عرضك المخصص المستند إلى الأصناف مع تحديد الدالة order_by()‎.

إذا قررت استخدام الصنف class Meta للنموذج Author (ربما لن يكون مرنًا مثل تخصيص العرض المستند إلى الأصناف، ولكنه سهل بما فيه الكفاية)، فستحصل على ما يلي:

class Author(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get_absolute_url(self):
        return reverse('author-detail', args=[str(self.id)])

    def __str__(self):
        return f'{self.last_name}, {self.first_name}'

    class Meta:
        ordering = ['last_name']

ليس ضروريًا أن يكون الحقل هو last_name، إذ يمكن أن يكون أيّ حقل آخر.

أخيرًا، يجب عليك الفرز حسب السمة أو العمود الذي يحتوي على فهرس (فريد أو غير فريد) في قاعدة البيانات لتجنب مشاكل الأداء. لن يكون ذلك ضروريًا في هذا المشروع، فلن نحتاج مستقبلًا إلّا عددًا قليلًا من الكتب والمستخدمين، ولكنه شيء يستحق أن نضعه في الحسبان بالنسبة للمشاريع المستقبلية.

الشيء الثاني المهم وغير الواضح في القالب هو المكان الذي نعرض فيه نص الحالة لكل نسخة كتاب ("متاح Available" و"في الصيانة Maintenance" …إلخ.). سيلاحظ القراء المتمرسون أن التابع BookInstance.get_status_display()‎ الذي نستخدمه للحصول على نص الحالة لا يظهر في أي مكان آخر من الشيفرة البرمجية.

<p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">
 {{ copy.get_status_display }} </p>

تُنشَأ هذه الدالة تلقائيًا لأن الحقل BookInstance.status هو حقل اختيارات choices، إذ ينشئ جانغو تلقائيًا التابع get_FOO_display()‎ لكل حقل اختيارات "Foo" في النموذج، والذي يمكن استخدامه للحصول على قيمة الحقل الحالية.

أنشأنا حتى الآن كل ما هو مطلوب لعرض كلِّ من صفحات قائمة الكتب وتفاصيل الكتاب. شغّل الخادم باستخدام الأمر python3 manage.py runserver وافتح المتصفح على العنوان "http://127.0.0.1:8000/‎".

ملاحظة: لا تنقر على أيّ ارتباطات للمؤلف أو تفاصيله حتى الآن، إذ ستنشِئ هذه الارتباطات في التحدي الذي سنوكله إليك في نهاية المقال.

انقر على ارتباط جميع الكتب All books لعرض قائمة الكتب.

01_book_list_page_no_pagination.png

انقر بعد ذلك على ارتباط إلى أحد كتبك. إن سار كل شيء بصورة صحيحة، فسترى شيئًا مثل لقطة الشاشة التالية:

02_book_detail_page_no_pagination.png

ترقيم الصفحات Pagination

إذا كان لديك عدد قليل من السجلات، فستبدو صفحة قائمة الكتب جيدة، ولكن ستستغرق الصفحة وقتًا أطول للتحميل وستحتوي على الكثير من المحتوى لتصفحه عندما تصل إلى عشرات أو مئات السجلات. يمكن حل هذه المشكلة من خلال إضافة ترقيم الصفحات إلى عروض القائمة، مما يقلل عدد العناصر المعروضة في كل صفحة.

يتمتع جانغو بدعم ممتاز لترقيم الصفحات، وهو مُضمَّن في عروض القائمة المُعمَّمة المستندة إلى الأصناف، لذا لا يجب عليك فعل الكثير لتفعيله.

العروض Views

افتح الملف catalog/views.py، وأضِف سطر paginate_by التالي:

class BookListView(generic.ListView):
    model = Book
    paginate_by = 10

سيبدأ العرض مع هذه الإضافة بترقيم صفحات البيانات التي يرسلها إلى القالب بمجرد أن يكون لديك أكثر من 10 سجلات. يمكن الوصول إلى الصفحات المختلفة باستخدام معاملات GET، فمثلًا ستستخدم ‎/catalog/books/?page=2 للوصول إلى الصفحة 2.

القوالب Templates

يجب الآن إضافة دعم إلى القالب للتمرير عبر مجموعة النتائج بعد أن أصبحت البيانات ذات صفحات مرقَّمة، إذ سنضيف ذلك إلى القالب الأساسي لأننا يمكن أن نرغب في ترقيم صفحات جميع عروض القائمة.

افتح الملف التالي وابحث عن "كتلة المحتوى content block" كما يلي:

  • الملف ‎/locallibrary/catalog/templates/base_generic.html:
{% block content %}{% endblock %}

انسخ فيه كتلة ترقيم الصفحات التالية بعد {% endblock %} مباشرةً، إذ تتحقق هذه الشيفرة البرمجية أولًا من تفعيل ترقيم الصفحات في الصفحة الحالية. إذا كان الأمر كذلك، فستضيف ارتباطات التالي next والسابق previous كما هو مناسب (ورقم الصفحة الحالية).

{% block pagination %}
    {% if is_paginated %}
        <div class="pagination">
            <span class="page-links">
                {% if page_obj.has_previous %}
                    <a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
                {% endif %}
                <span class="page-current">
                    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
                </span>
                {% if page_obj.has_next %}
                    <a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
                {% endif %}
            </span>
        </div>
    {% endif %}
  {% endblock %}

يُعَد الكائن page_obj كائن ترقيم Paginator، والذي يكون موجودًا في حالة استخدام ترقيم الصفحات في الصفحة الحالية، ويسمح لك بالحصول على جميع المعلومات حول الصفحة الحالية والصفحات السابقة وعدد الصفحات الموجودة وغير ذلك.

نستخدم {{ request.path }} للحصول على عنوان URL للصفحة الحالية لإنشاء ارتباطات ترقيم الصفحات، ويُعَد ذلك مفيدًا لأنه مستقل عن الكائن الذي نرقّم صفحاته.

توضح لقطة الشاشة الآتية كيف يبدو ترقيم الصفحات؛ فإذا لم تدخِل أكثر من 10 عناوين في قاعدة بياناتك، فيمكنك اختبارها بسهولة أكبر من خلال تقليل العدد المُحدَّد في السطر paginate_by في الملف "catalog/views.py"، فمثلًا يمكنك الحصول على النتيجة التالية من خلال التغيير إلى paginate_by = 2، وتُعرَض ارتباطات ترقيم الصفحات في الجزء السفلي مع عرض ارتباطات next أو previous بناءً على الصفحة الموجود فيها:

03_book_list_paginated.png

تحدى نفسك

يتمثل التحدي في هذا المقال في إنشاء تفاصيل المؤلف وعروض القائمة المطلوبة لإكمال المشروع، إذ يجب توفيرها على عناوين URL التالية:

  • catalog/authors/‎: قائمة جميع المؤلفين.
  • catalog/author/<id>‎: العرض التفصيلي للمؤلف المُحدَّد باستخدام حقل المفتاح الرئيسي الذي يُسمَّى <id>.

يجب أن تكون الشيفرة البرمجية المطلوبة لروابط عناوين URL والعروض متطابقة تقريبًا مع العروض التفصيلية وعروض قائمة Book التي أنشأناها سابقًا، وستكون القوالب مختلفة ولكنها ستشترك في سلوك مماثل.

ملاحظة: ستحتاج إلى تحديث ارتباط جميع المؤلفين All authors في القالب الأساسي بعد إنشاء رابط عنوان URL لصفحة قائمة المؤلفين، لذا اتبع العملية نفسها عندما حدّثنا ارتباط جميع الكتب All books. يجب أيضًا تحديث قالب عرض تفاصيل الكتاب ‎/locallibrary/catalog/templates/catalog/book_detail.html بعد إنشاء رابط عنوان URL لصفحة تفاصيل المؤلف، بحيث يؤشّر ارتباط المؤلف إلى صفحة تفاصيل المؤلف الجديدة بدلًا من أن يكون عنوان URL فارغ، والطريقة الموصى بها لذلك هي استدعاء التابع get_absolute_url()‎ في نموذج المؤلف كما يلي:

<p>
  <strong>Author:</strong>
  <a href="{{ book.author.get_absolute_url }}">{{ book.author }}</a>
</p>

يجب بعد ذلك أن تبدو صفحاتك كما يلي:

04_author_list_page_no_pagination.png

05_author_detail_page_no_pagination.png

الخلاصة

تهانينا، لقد اكتملت وظائف مكتبتنا الأساسية، إذ تعلمنا في هذا المقال كيفية استخدام عروض القائمة المعمَّمة المستندة إلى الأصناف والعرض التفصيلية واستخدمناها لإنشاء صفحات لعرض الكتب والمؤلفين، وتعرّفنا على مطابقة الأنماط مع التعابير النمطية، وكيفية تمرير البيانات من عناوين URLs إلى العروض، وتعلمنا بعض الحيل الأخرى لاستخدام القوالب. أخيرًا، عرضنا كيفية ترقيم صفحات عروض القائمة بحيث يمكن إدارة القوائم حتى عند وجود العديد من السجلات.

سنوسّع في المقالات القادمة هذه المكتبة لدعم حسابات المستخدمين، وبالتالي سنوضح استيثاق Authentication المستخدمين والأذونات والجلسات والاستمارات.

ترجمة -وبتصرُّف- للمقال Django Tutorial Part 6: Generic list and detail views.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...