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

سنضيف في هذا المقال بعض الميزات المتقدمة الاختيارية لموقع مدونة جانغو Django الخاص بنا، والذي أنشأناه في المقالات السابقة من هذه السلسلة، بما في ذلك ميزة ترقيم الصفحات Pagination، والمنشورات ذات الصلة Related Posts، وميزة البحث Search.

إنشاء ترقيم الصفحات Pagination في جانغو

01 التنقل بين الصفحات

قد يكون إنشاء مرقّم صفحات فكرة جيدة عند إضافة الكثير من المنشورات إلى مدونتنا، فنحن لا نريد عرض عدد كبير جدًا من المنشورات في صفحة واحدة، لذا يجب إضافة شيفرة برمجية إضافية إلى دوال العرض view functions. لنأخذ على سبيل المثال عرض الصفحة الرئيسية home، حيث يجب أولًا أن نستورد بعض الحزم الضرورية لتفعيل الترقيم كما يلي:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

نحدّث بعد ذلك العرض home كما يلي:

def home(request):
   site = Site.objects.first()
   categories = Category.objects.all()
   tags = Tag.objects.all()
   featured_post = Post.objects.filter(is_featured=True).first()

   # إضافة مرقم الصفحات
   page = request.GET.get("page", "")  # الحصول على رقم الصفحة الحالية
   posts = Post.objects.all().filter(is_published=True)
   paginator = Paginator(posts, n)  # ‫عرض n منشور لكل صفحة

   try:
       posts = paginator.page(page)
   except PageNotAnInteger:
       posts = paginator.page(1)
   except EmptyPage:
       posts = paginator.page(paginator.num_pages)

   return render(
       request,
       "home.html",
       {
           "site": site,
           "posts": posts,
           "categories": categories,
           "tags": tags,
           "featured_post":featured_post
       },
   )

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

بعد أن انتهينا من إعداد الترقيم في الكود البرمجي للعرض، يتوجب علينا إضافة مرقّم الصفحات في القالب Template لكي يظهر في صفحة قائمة المنشورات، أي سنضيفه في الملف templates/vendor/list.html كما يلي:

<!-- مرقم الصفحات -->
<nav
 class="isolate inline-flex -space-x-px rounded-md mx-auto my-5 max-h-10"
 aria-label="Pagination"
>
 {% if posts.has_previous %}
 <a
   href="?page={{ posts.previous_page_number }}"
   class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
 >
   <span class="sr-only">Previous</span>
   <!-- Heroicon name: mini/chevron-left -->
   <svg
     class="h-5 w-5"
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 20 20"
     fill="currentColor"
     aria-hidden="true"
   >
     <path
       fill-rule="evenodd"
       d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
       clip-rule="evenodd"
     />
   </svg>
 </a>
 {% endif %}

 {% for i in posts.paginator.page_range %}
 {% if posts.number == i %}
 <a
   href="?page={{ i }}"
   aria-current="page"
   class="relative z-10 inline-flex items-center border border-blue-500 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 focus:z-20"
   >{{ i }}</a
 >
 {% else %}
 <a
   href="?page={{ i }}"
   aria-current="page"
   class="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
   >{{ i }}</a
 >
 {% endif %}
 {% endfor %}

 {% if posts.has_next %}
 <a
   href="?page={{ posts.next_page_number }}"
   class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
 >
   <span class="sr-only">Next</span>
   <!-- Heroicon name: mini/chevron-right -->
   <svg
     class="h-5 w-5"
     xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 20 20"
     fill="currentColor"
     aria-hidden="true"
   >
     <path
       fill-rule="evenodd"
       d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
       clip-rule="evenodd"
     />
   </svg>
 </a>
 {% endif %}
</nav>

علينا تطبيق الشيء نفسه لجميع الصفحات التي تعرض قائمة منشورات مثل صفحة الوسوم والفئات وصفحة البحث.

02 المشاركات ذات الصلة

نريد الآن إضافة ميزة  عرض المنشورات ذات الصلة بالمنشور الحالي، أي المنشورات التي لها نفس وسوم هذا المنشور ويمكن القيام بذلك من خلال إضافة الشيفرة التالية:

def post(request, slug):
   site = Site.objects.first()
   requested_post = Post.objects.get(slug=slug)
   categories = Category.objects.all()
   tags = Tag.objects.all()

   # المنشورات ذات الصلة
   ## الحصول على جميع الوسوم المتعلقة بهذا المقال
   post_tags = requested_post.tag.all()
   ## ترشيح جميع المنشورات التي تحتوي على وسوم متعلقة بالمنشور الحالي، واستبعاد المنشور الحالي
   related_posts_ids = (
       Post.objects.all()
       .filter(tag__in=post_tags)
       .exclude(id=requested_post.id)
       .values_list("id")
   )

   related_posts = Post.objects.filter(pk__in=related_posts_ids)

   return render(
       request,
       "post.html",
       {
           "site": site,
           "post": requested_post,
           "categories": categories,
           "tags": tags,
           "related_posts": related_posts,
       },
   )

قد تكون الشيفرة البرمجية السابقة صعبة الفهم بعض الشيء، دعنا نحللها ونشرحها بالتفصيل، حيث يمثل السطر 3 الحصول على المنشور المطلوب باستخدام المتغير slug، ويمثل السطر 9 الحصول على جميع الوسوم Tags التي تعود إلى المنشور المطلوب.

تصبح الأمور أعقد في الأسطر من 11 إلى 16، حيث يسترد التابع Post.objects.all()‎ جميع المنشورات من قاعدة البيانات، ثم يسترد التابع filter(tag__in=post_tags)‎ جميع المنشورات التي تحتوي على وسوم مرتبطة بالمنشور الحالي، ولكن لدينا مشكلتان، إذ سيُضمَّن المنشور الحالي في مجموعة الاستعلام، لذا سنستخدم التابع exclude(id=requested_post.id)‎ لاستبعاد المنشور الحالي.

لنبسّط الآن المشكلة الثانية، ولنفترض أن لدينا السيناريو التالي مع وجود ثلاث منشورات وثلاثة وسوم:

معرّف الوسم
Tag ID
اسم الوسم
Tag Name
1 Tag 1
2 Tag 2
3 Tag 3
معرّف المنشور
Post ID
اسم المنشور
Post Name
1 Post 1
2 Post 2
3 Post 3

وتكون العلاقة بين المنشورات والوسوم علاقة متعدد إلى متعدد Many-to-Many.

معرّف الوسم
Tag ID
معرّف المنشور
Post ID
1 2
1 3
1 1
2 1
2 2
2 3
3 2
معرّف المنشور
Post ID
معرّف الوسم
Tag ID
1 1
1 2
2 1
2 2
2 3
3 1
3 2

لنفترض أن المنشور الحالي هو المنشور 2، وبالتالي ستكون الوسوم المتعلقة به هي 1 و 2 و 3، حيث سيذهب جانغو أولًا إلى الوسم 1 عند استخدام التابع filter(tag__in=post_tags)‎، ثم سيجد المنشورات المتعلقة بالوسم 1، والتي هي المنشورات 2 و 3 و 1، ثم يذهب إلى الوسم 2، ويجد المنشورات المتعلقة بالوسم 2، وينتقل أخيرًا إلى الوسم 3.

يعيد التابع filter(tag__in=post_tags)‎ بعد ذلك في النهاية القائمة [2,3,1,1,2,3,2]، وستُعاد القائمة [3,1,1,3] بعد تنفيذ التابع exclude()‎، ولا نريد ذلك أيضًا، حيث نحتاج لإيجاد طريقة للتخلص من التكرارات، لذا يجب استخدام التابع values_list('id')‎ لتمرير معرّفات المنشورات إلى المتغير related_posts_ids، ثم نستخدم هذا المتغير لاسترداد المنشورات ذات الصلة، وبذلك نتخلص من التكرار.

يمكننا عرض المنشورات ذات الصلة في القالب المقابل كما يلي:

   <!-- المنشورات ذات الصلة -->

   <div class="grid grid-cols-3 gap-4 my-5">
     {% for post in related_posts %}
     <!-- المنشور -->
     <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
       <a href="{% url 'post' post.slug %}"
         ><img
           class="rounded-t-md object-cover h-60 w-full"
           src="{{ post.featured_image.url }}"
           alt="..."
       /></a>
       <div class="m-4 grid gap-2">
         <div class="text-sm text-gray-500">
           {{ post.created_at|date:"F j, o" }}
         </div>
         <h2 class="text-lg font-bold">{{ post.title }}</h2>
         <p class="text-base">
           {{ post.content|striptags|truncatewords:30 }}
         </p>
         <a
           class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring"
           href="{% url 'post' post.slug %}"
           >Read more →</a
         >
       </div>
     </div>
     {% endfor %}
   </div>

تطبيق عملية البحث في جانغو

سنعمل أخيرًا على إضافة ميزة البحث لمدونتنا، حيث نحتاج إلى استمارة بحث Search Form في الواجهة الأمامية، والتي ترسل استعلام بحث إلى العرض، تسترد دالة العرض السجلات الملائمة من قاعدة البيانات، وتعيد صفحة بحث تعرض النتيجة.

استمارة البحث Search Form

لنضيف أولًا استمارة بحث إلى الشريط الجانبي Sidebar في الملف templates/vendor/sidebar.html كما يلي:

<div class="col-span-1">
 <div class="border rounded-md mb-4">
   <div class="bg-slate-200 p-4">Search</div>
   <div class="p-4">
     <form action="{% url 'search' %}" method="POST" class="grid grid-cols-4 gap-2">
       {% csrf_token %}
       <input
         type="text"
         name="q"
         id="search"
         class="border rounded-md w-full focus:ring p-2 col-span-3"
         placeholder="Search something..."
       />
       <button
         type="submit"
         class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1"
       >
         Search
       </button>
     </form>
   </div>
 </div>
 . . .
</div>

لاحظ في الأسطر من 7 إلى 13 السمة name لحقل الإدخال input، حيث سنسميها q، سيُربط دخل المستخدم بالمتغير  q ويُرسَل للواجهة الخلفية.

إذا نقرنا على الزر في السطر 5، فسيُوجَّه المستخدم إلى عنوان URL بالاسم search، لذا يجب تسجيل نمط عنوان URL المقابل كما يلي:

path('search', views.search, name='search'),

عرض البحث Search View

سيكون عرض البحث Search View كما يلي:

def search(request):
   site = Site.objects.first()
   categories = Category.objects.all()
   tags = Tag.objects.all()

   query = request.POST.get("q", "")
   if query:
       posts = Post.objects.filter(is_published=True).filter(title__icontains=query)
   else:
       posts = []
   return render(
       request,
       "search.html",
       {
           "site": site,
           "categories": categories,
           "tags": tags,
           "posts": posts,
           "query": query,
       },
   )

قالب البحث Search Template

لإنشاء قالب البحث لنكتب الكود التالي في الملف templates/search.html:

{% extends 'layout.html' %}

{% block title %}
<title>Page Title</title>
{% endblock %}

{% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
 <div class="col-span-3 grid grid-cols-1">

   {% include "vendor/list.html" %}

 </div>
 {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

بعدها نحاول البحث عن شيء ما في استمارة البحث، ويجب إعادة المنشورات التي نطلبها فقط.

الخلاصة

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

ترجمة -وبتصرّف- للمقال Django for Beginners #5 - Some Advanced Features لصاحبه Eric Hu.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...