إن كانت لدينا آلاف المقالات فلا يُمكننا أن نعرضها جميعها في الصّفحة الرّئيسية، ولو كانت تحتوي على آلاف الصّور ومقاطع الفيديو فمن المؤكّد بأنّ الصّفحة ستأخذ عدّة دقائق لتنهي التّحميل، والحل الشّائع حاليّا في تطبيقات الويب هو تجميع عدد محدّد من السّجلات في كل صفحة، مثلا خمسة سجلّات في الصّفحة الأولى وخمسة أخرى في الصّفحة الثّانيّة، وهكذا… بهذه الطّريقة يُمكنك عرض شريط أسفل الصّفحة لتوفير طريقة للمُستخدم للتنقل بين الصّفحات، كذلك يُمكنك استعمال لغة جافا سكربت لوضع زر لتحميل المزيد من المقالات كما هو شائع في الكثير من المواقع على الانترنت.
كيف تعمل خاصية الصفحات في 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 كلّها، إذ أنّنا لم نتعرّف بعد على كلّ من كيفيّة تعديل وحذف السّجلات إلّا أنّها أمور بسيطة لذا لن تأخذ الكثير من المساحة في هذه السّلسلة من الدّروس.
لذا سنُنهي جزء التّعامل مع قاعدة البيانات لنتمكّن بعدها من العودة إلى تطبيق "كلمة" لنستمرّ في تطويره بإنشاء نظام لتسجيل مُستخدمين جدد وتسجيل دخولهم وتسجيل خروجهم، إضافة إلى عرض المقالات المتواجدة بقاعدة البيانات وكذا تمكين كل مُستخدم من إضافة مقالات خاصّة به، وسنستغلّ ما تعلّمناه من طرق لتقسيم السّجلات إلى صفحات مُتعدّدة ومُختلف المعلومات التّي اكتسبناها لتُعرض كل المقالات بشكل مُناسب للمُستخدمين مع شريط تصفّح للانتقال بين الصّفحات وما إلى ذلك.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.