flask_cms تقسيم السجلات إلى صفحات Pagination في Flask-SQLAlchemy


عبدالهادي الديوري

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

كيف تعمل خاصية الصفحات في Flask-SQLAlchemy

قبل أن ننتقل إلى كيفيّة تقسيم الصّفحات إلى سجلّات لا بد أن تعرف المبدأ الرّئيسي للعمل، بدءًا من معنى كلمة Pagination (تحويل مجموعة من السّجلات إلى صفحات مُرقّمة، كل صفحة تحمل عددا مُعيّنا من السّجلات، والعدد الذي تُحدّده لكل صفحة يكون ثابتا إلى آخر مجموعة من السّجلات). فمثلا لنفترض بأنّ الأسماء التّاليّة مُسجلة لدينا في قاعدة البيانات:

  • 1 عمر
  • 2 عبد الرّحمن
  • 3 خالد
  • 4 يوسف
  • 5 أحمد
  • 6 إبراهيم
  • 7 كمال
  • 8 كريم
  • 9 مُعاذ
  • 10 جواد

لنُقسّمها إلى صفحات مُرقّمة، كل صفحة ستحتوي على 3 أسماء أو أقل:

الصّفحة 1:

  • 1 عمر
  • 2 عبد الرّحمن
  • 3 خالد

الصّفحة 2:

  • 4 يوسف
  • 5 أحمد
  • 6 إبراهيم

الصّفحة 3:

  • 7 كمال
  • 8 كريم
  • 9 مُعاذ

الصّفحة 4:

  • 10 جواد

بهذه الطّريقة، عندما تطلب من قاعدة البيانات السّجلات المُتواجدة في الصّفحة رقم 1، ستحصل على الأسماء: عمر، عبد الرّحمن، خالد. وإن طلبت السّجلات في الصّفحة رقم 4 فستحصل على الاسم "جواد"، وهكذا ستتمكّن من تقسيم السّجلات المُتواجدة في قاعدة البيانات لتفادي الاستعلام عنها كلّها في مرّة واحدة ومن ثم تقسيمها بنفسك باستعمال لغة Python، وهذا يكون بطيئا جدّا إذا ما كانت لديك الكثير من السّجلات في قاعدة البيانات، وعوضا عن الحصول عليها جميعا في مرّة واحدة، تطلب سجلّات صفحة واحدة فقط.

إذا كنت تستخدم كلّا من مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy للتّعامل مع قاعدة البيانات فيُمكنك استعمال خاصيّة تجميع عدد من السّجلات في عدد من الصّفحات عبر استعمال التّابع paginate الذي يأخذ مُعاملين أساسيّين، المُعامل page والذي سيُعبّر عن رقم الصّفحة، والمُعامل per_page الذي سيُعبّر عن عدد السّجلات التّي تتواجد بكل صفحة، وكمثال على هذين المعاملين، فقيمة per_page في مثال الأسماء سابقا هي 3 وكلّما غيّرت قيمة المُعامل page من 1 إلى 4 ستحصل على الأسماء المتواجدة بكل صفحة حسب رقم الصّفحة.

السّطر التّالي مثال بسيط على كيفيّة استخدام التّابع paginate:

>>> from project.models import Post

>>> pagination = Post.query.paginate(page=1,per_page=3)

>>> pagination
<flask_sqlalchemy.Pagination object at 0x7f0b4a3cc910>

ستُلاحظ بعد تنفيذك للأسطر السّابقة، أنّ المُتغيّر pagination عبارة عن كائن من الصّنف Pagination وليس عبارة عن قائمة سجلّات تُمثّل المقالات، ذلك أن SQLAlchemy توفّر لنا العديد من الخصائص والتّوابع التّي يُمكننا استخدامها مع أي كائن من الصّنف Pagination، فيُمكنك مثلا أن تتحقّق من أنّ الصّفحة الحاليّة هي آخر صفحة أو العكس أو أن تحصل على عدد الصّفحات الإجمالي والعديد من التّوابع والخصائص الأخرى التّي سنتطرّق إليها والتّي ستُساعدك حتما عندما ترغب في العمل مع صفحات مُتعدّدة أو استخدام تنسيقات مُحدّدة حسب شروط مُعيّنة.

سنُغطّي في الفقرات التّاليّة بعضا من أهم الأمور التّي يُمكنك القيام بها مع خاصيّة ترقيم الصّفحات لتُقدّم لمُستخدمي تطبيقك أفضل تجربة استخدام مُمكنة.

الوصول إلى السجلات الموجودة بالصفحة

نفّذنا الأسطر التّالية قبل قليل لنحصل على كائن باسم pagination من الصّنف Pagination:

>>> from project.models import Post
>>> pagination = Post.query.paginate(page=1, per_page=3)

ولكن الأهم أن نحصل على المقالات المُتواجدة في الصّفحة كي نتمكّن من عرض عنوان كل مقال ومحتواه أو مُعالجة بيانات كل مقال على حدة. وللوصول إلى السّجلات المُتواجدة بالصّفحة، أضف خاصيّة items للكائن pagination، وبالتّالي ستتمكّن من الدّوران حول المقالات الثّلاثة بحلقة for بسيطة ومنه ستتمكّن من الوصول إلى عنوان كل مقال ومُحواه وكاتبه وما إلى ذلك (تماما كما تتعامل مع المقالات التّي تحصل عليها عن طريق ()query.all أو أي استعلام مُشابه).

والمثال التّالي يوضح هذا:

>>> posts = pagination.items

>>> posts
[<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >]

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

الانتقال إلى الصفحة التاليّة

بعد أن تعرّفنا على كيفيّة الوصول إلى الصّفحة الأولى وعناصر الصّفحة، حان الوقت للانتقال إلى الصّفحة التّاليّة والوصول إلى عناصرها.

للوصول إلى الصّفحة التّاليّة، كل ما عليك القيام به هو إضافة التّابع next إلى مُتغيّر الصّفحة الحاليّة والذي يجب أن يكون كائنا من الصّنف Pagination، انظر:

>>> page_1 = Post.query.paginate(page=1, per_page=3)

>>> page_1.items
[<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >]

>>> page_2 = page_1.next()

>>> page_2.items
[<title a post from dyouri | other content >, <title Post from user4 | Content 4 >, <title Post from user5 | Content 5 >]

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

الصفحة السابقة

تتشابه طريقة الوصول إلى الصّفحة السّابقة مع طريقة الوصول إلى الصّفحة التالية، فقط استعمل التّابع prev عوضا عن التّابع next، انظر:

>>> page_2 = Post.query.paginate(page=2, per_page=3)

>>> page_2.items
[<title a post from dyouri | other content >, <title Post from user4 | Content 4 >, <title Post from user5 | Content 5 >]

>>> page_1 = page_2.prev()

>>> page_1.items
[<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >]

نستعمل التّابع paginate أولا مع تمرير القيمة 2 إلى المُعامل page للحصول على الصّفحة الثّانيّة، ومن ثمّ نقوم بإنشاء مُتغيّر باسم page_1 بالقيمة ()page_2.prev (أي الصّفحة التّي تسبق الصّفحة الثّانيّة)، وهكذا نصل إلى الصّفحة الأولى مرة أخرى.

التحقق من وجود صفحة تالية للصفحة الحالية

قد ترغب أحيانا بمعرفة ما إذا كانت هذه الصّفحة التّي تتعامل معها هي الأخيرة أم لا، وتستطيع ذلك عن طريق الخاصيّة has_next التّي تُرجع إمّا القيمة المنطقيّة True أو القيمة False، بحيث يكون الشرط صحيحًا إذا كانت للصّفحة صفحة مُواليّة والعكس إذا كانت آخر صفحة. مثال على الحالة الأولى:

>>> page_1 = Post.query.paginate(page=1, per_page=3)

>>> page_1.items
[<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >]

>>> page_1.has_next
True

كما تُلاحظ، فالصّفحة الأولى ليست آخر صفحة لأنّ عدد المقالات يتجاوز 3 مقالات، وعليه فالخاصيّة has_next تُرجع القيمة المنطقيّة True، وهذا يعني بأنّ القيمة كانت لتكون False لو كانت عدد المقالات المُتواجدة بقاعدة البيانات ثلاثة أو أقل من ذلك. مثال على الحالة الثّانيّة:

>>> page_5 = Post.query.paginate(page=5, per_page=3)

>>> page_5.items
[<title New title for post from user8 | Content 8 >]

>>> page_5.has_next
False

في هذه الحالة تكون الصّفحة الخامسة هي آخر صفحة وليس بعدها أية صفحة أخرى، حتى أنّ بها مقالا واحدا فقط، وبالتّالي فالنّتيجة هي القيمة False عند استعمالنا للخاصيّة has_next.

التحقق من أن للصفحة الحالية صفحة تسبقها

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

>>> from project.models import Post

>>> page_1 = Post.query.paginate(page=1, per_page=3)

>>> page_1.items
[<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >]

>>> page_1.has_prev
False

>>> page_1.has_next
True

نحصل هنا على الصّفحة الأولى ونتحقّق من أنّ لها صفحة تسبقها، والنّتيجة هي القيمة False كما هو مُتوقّع، بعدها نقوم بالتّحقق ممّا إذا كانت للصّفحة صفحة تالية بعدها، والنّتيجة مُتوقّعة كذلك. لنجمع الآن كلّا من التّابع next والخاصيّة has_prev ونرى النّتيجة:

>>> page_1.next().has_prev
True

النّتيجة هي القيمة True لأنّ قيمة ()page_1.next هي الصّفحة الثّانيّة، ومن المُؤكّد بأنّ للصّفحة الثّانيّة صفحة تسبقها.

عدد الصفحات وكيفية الدوران حول قائمة الصفحات

للوصول إلى عدد الصّفحات الكليّ عند استخدام التّابع paginate، أضف الخاصيّة pages إلى الكائن، وسيعمل مهما كانت الصّفحة الحاليّة. مثال:

>>> from project.models import Post
>>> page_1 = Post.query.paginate(page=1, per_page=3)
>>> page_1.pages
5

>>> page_3 = Post.query.paginate(page=3, per_page=3)

>>> page_3.pages
5

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

الدوران حول قائمة أرقام الصفحات

قد ترغب أحيانًا في الوصول إلى رقم كل صفحة على حدة، وأغلب الظّن أنّك تعمل على شريط لتمكين المُستخدم من الانتقال بين الصّفحات دون ترتيب مُحدّد (الانتقال من الصّفحة الثّالثة إلى الخامسة على سبيل المثال). يُمكنك استخدام التّابع iter_pages للحصول على كائن مُولّد (generator) والذي يسمح لك بالوصول إلى كل رقم على حدة، ويُمكنك التّعامل معه باستخدام حلقة for كما تتعامل مع قوائم python عاديّة، ويُمكنك كذلك الحصول على قائمة عبر استخدام الدّالة list لكنّ الأمر ليس ضروريا. انظر المثال التالي:

>>> from project.models import Post

>>> page_1 = Post.query.paginate(page=1, per_page=3)

>>> page_1.iter_pages()
<generator object iter_pages at 0x7f4d96115230>

>>> for page in page_1.iter_pages():
...     print(page)
... 
1
2
3
4
5

>>> list(page_1.iter_pages())
[1, 2, 3, 4, 5]

عندما طبّقنا التّابع ()iter_pages على المُتغيّر page_1 حصلنا على معلومة بسيطة تُشير إلى أنّ هذا الكائن من النّوع generator، أي أنّه يُولّد كل عنصر عندما تحتاج إليه فقط، وكما تُلاحظ، فعندما استخدمنا حلقة for وطبعنا كل عنصر على حدة، استطعنا الوصول إلى أرقام الصّفحات من واحد إلى خمسة، وفي السّطر الأخير، ينتج استدعاء الدّالة list وتمرير المُولّد إليها قائمةً عاديّة، وهذا السّطر الأخير غير مفيد إلا في التّوضيح فقط، واستخدام المُولّد مُباشرة أمر طبيعي.

فهذا المُولّد يُستخدم بشكل شائع لتوفير شريط تصفّح لتمكين الزّائر من الانتقال من صفحة إلى أخرى، ولا بدّ أنّك صادفت مثل هذا الشّريط من قبل. ولتوفير الشّريط لمُستخدمي تطبيقك، استخدم ماكرو خاص مع مُحرّك القوالب Jinja، وهو كالتّالي:

{% macro render_pagination(pagination, endpoint) %}
  <div class=pagination>
  {%- for page in pagination.iter_pages() %}
    {% if page %}
      {% if page != pagination.page %}
        <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
      {% else %}
        <strong>{{ page }}</strong>
      {% endif %}
    {% else %}
      <span class=ellipsis>…</span>
    {% endif %}
  {%- endfor %}
  </div>
{% endmacro %}

يأخذ الماكرو render_pagination مُعاملين اثنين، الأول هو الكائن الذي يُمثّل صفحة مُعيّنة، ويجب أن يكون من الصّنف Pagination، مثل page_1 أو page_2 في الأمثلة السّابقة، أمّا المُعامل الثّاني فهو رابط المُوجّه الذي سيكون مسؤولا عن عرض الصّفحات في المُتصفّح، ويجب أن يكون الرّابط على شكل اسم الطّبعة الزّرقاء (إن وُجدت) ثمّ نقطة ثمّ اسم الدّالة المُرتبطة بالمُوجّه.

فمثلا إن كان الموجّه الخاصّ بك يقع تحت طبعة زرقاء باسم posts وكانت اسم الدّالة المُرتبطة بهذا الموجّه هي (pages(page بحيث page رقم الصّفحة، فقيمة المُعامل endpoint في الماكرو أعلاه يجب أن تكون كما يلي: 'posts.pages'، والسّبب الرّئيسي لتوفير هذا المُعامل هو أنّ الماكرو يستخدم الدّالة url_for لتوليد رابط للصّفحة حسب رقمها، والتّالي بعض الرّوابط التّي يُمكن أن تُولّد:

/posts/page/1
/posts/page/2
/posts/page/3
...

وعليه يكون الشريط في نفس المكان في كل مرّة تصل فيها إلى صفحة مُعيّنة. لاحظ بأنّنا نستعمل في الماكرو حلقة for مع التّابع ()iter_pages الذي تحدّثنا عنه سابقا، وذلك لعرض أرقام الصّفحات وإضافة رابط لكل صفحة، وهكذا سيكون الشّريط كما يلي: 1 2 3 4 5

مع رابط تحت كل رقم ليُوجّه إلى الصّفحة المُناسبة في التّطبيق.

تتحقق الجملة الشّرطيّة if إن كانت الصّفحة الحاليّة تحمل نفس رقم الصّفحة المُتواجد في pagination.page وذلك لعرض الصّفحة الحاليّة بشكل مُميّز (في هذه الحالة سيكون خطّا عريضا مع إزالة الرابط) ليعرف الزّائر أية صفحة يتواجد فيها. تستطيع كذلك أن تنسقه كما تشاء، فقط أضف خاصيّة css وسيكون التّنسيق هو نفسه في جميع أنحاء التّطبيق ما دمت تستدعي هذا الماكرو في المكان الذي تُريد به الشّريط.

إذا كنت تستخدم إطار العمل Bootstrap فإن الماكرو التّالي يُوفّر تنسيقا جميلا لشريط التّصفّح مع زرّين إضافيّين، واحد للانتقال إلى الصّفحة السّابقة والآخر للانتقال إلى الصّفحة التّاليّة.

{% macro render_pagination(pagination, endpoint) %}
  <div class=pagination>

  <nav aria-label="Page navigation">
  <ul class="pagination">

  {# انتقل إلى الصّفحة السّابقة إن كانت مُتواجدة #}

  {% if pagination.has_prev %}
    <li>
      <a href="{{ url_for(endpoint, page=pagination.prev_num) }}" aria-label="Previous">
        <span aria-hidden="true">«</span>
      </a>
    </li>
  {% else %}
      <li class='disabled'>
      <a href="#" aria-label="Previous">
        <span aria-hidden="true">«</span>
      </a>
    </li>
  {% endif %}

{# عرض أزرار الصّفحات (<< 1, 2, 3 >>)#}

  {%- for page in pagination.iter_pages() %}
    {% if page %}

    {% if page != pagination.page %}
      <li><a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a></li>
      {% else %}
        <li class="active"><a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a></li>
      {% endif %}

    {% else %}
      <span class=ellipsis>…</span>
    {% endif %}
  {%- endfor %}

{# انتقل إلى الصّفحة المُواليّة إن كانت مُتواجدة #}
  {% if pagination.has_next %}
  <li>
      <a href="{{ url_for(endpoint, page=pagination.next_num) }}" aria-label="Next">
        <span aria-hidden="true">»</span>
      </a>
  </li>
    {% else %}
      <li class='disabled'>
      <a href="#" aria-label="Next">
        <span aria-hidden="true">»</span>
      </a>
    </li>
  {% endif %}
</ul>
</nav>
  </div>
{% endmacro %}

لاحظ جيّدا بأنّ هذا الماكرو يستخدم كذلك كلّا من الخاصيّتين has_next و has_prev، وذلك لتعطيل زرّ الانتقال إلى الصّفحة السّابقة إذا لم تكن تتواجد وتعطيل زر الانتقال إلى الصّفحة التّالية في نفس الحالة كذلك.

كل ما عليك فعله لاستخدام الماكرو هو وضعه في أعلى ملفّ HTML واستدعائه كما يلي:

{{ render_pagination(pagination, 'posts.pages') }}

يفترض المثال أعلاه أنّ المُتغيّر الذي يحمل كائنا من الصّنف Pagination يُسمّى pagination والذي سيتوجّب عليك تمريره إلى ملفّ HTML عن طريق الدّالة render_template ورابط الموجّه المسؤول عن عرض الصّفحات هو posts.pages والذي سيُمرّر إلى الدّالة url_for لتوليد رابط كل صفحة على حدة.

الحصول على رقم الصفحة التالية

تطرّقنا من قبل إلى أنّك تستطيع الوصول إلى الكائن الذي يُمثّل الصّفحة التّاليّة عن طريق استخدام التّابع next، ولكن ماذا لو أردت أن تصل إلى رقم الصّفحة التّالية فقط؟ يُمكنك الحصول على رقم الصّفحة التّاليّة عن طريق استخدام الخاصيّة next_num، انظر:

>>> from project.models import Post

>>> page_1 = Post.query.paginate(page=1, per_page=3)

>>> page_1.next_num
2

في المثال، نحصل على الصّفحة الأولى ثمّ نستخدم الخاصيّة next_num للحصول على رقم الصّفحة التالية للصّفحة الأولى والذي هو الرّقم 2 وهذا طبيعي لأنّ رقم الصّفحة الأولى هو 1.

الحصول على رقم الصفحة الحالية

للوصول إلى رقم الصّفحة الحاليّة كل ما عليك القيام به هو استخدام الخاصيّة page وستحصل على رقم الصّفحة المرتبطة بالكائن من الصّنف Pagination. انظر المثال التالي لكيفية الحصول على رقم الصّفحة الحاليّة:

>>> from project.models import Post
>>> page_1 = Post.query.paginate(page=1, per_page=3)
>>> page_1.page
1

الحصول على رقم الصّفحة السّابقة

تشبه طريقة الوصول إلى رقم الصّفحة السّابقة ما سبق، فقط استخدم الخاصيّة prev_num عوضا عن next_num و page. يغطي المثال التّالي كيفيّة الحصول على أرقام الصّفحات التّي ذكرناها إلى الآن.

>>> from project.models import Post
>>> page_2 = Post.query.paginate(page=2, per_page=3)
>>> page_2.page
2
>>> page_2.prev_num
1
>>> page_2.next_num
3

خاتمة

هكذا نكون قد وصلنا إلى جزء مُهم من مشوارنا لتعلّم كيفيّة التّعامل مع قاعدة بياناتنا باستخدام كل من مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy، وتبقى لنا بضعة أمور مُهمّة لتعلّم كيفيّة إجراء عمليّات CRUD كلّها، إذ أنّنا لم نتعرّف بعد على كلّ من كيفيّة تعديل وحذف السّجلات إلّا أنّها أمور بسيطة لذا لن تأخذ الكثير من المساحة في هذه السّلسلة من الدّروس.

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





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن