سنتعرف في هذا المقال على كيفية إنشاء الصفحة الرئيسية لموقع المكتبة المحلية LocalLibrary وصفحات قوائم الكتب والمؤلفين ونسخ الكتب وأنواعها، وسنوضح كيفية تنسيق التاريخ باستخدام المكتبة Luxon، كما سنتعرف على كيفية إنشاء صفحات تفاصيل الكتب وأنواعها ونسخها وتفاصيل المؤلفين.
صفحة الموقع الرئيسية
ستكون الصفحة الأولى التي سننشئها هي الصفحة الرئيسية لموقع الويب، والتي يمكن الوصول إليها إما من جذر الموقع /
أو من الدليل catalog/
، مما سيؤدي إلى عرض بعض النصوص الثابتة التي تصف الموقع، مع الأعداد المحسوبة ديناميكيًا لأنواع السجلات المختلفة في قاعدة البيانات.
أنشأنا مسبقًا وجهةً Route للصفحة الرئيسية، ولإكمال هذه الصفحة، يجب تحديث دالة المتحكم لجلب أعداد السجلات من قاعدة البيانات وإنشاء عرض (قالب) يمكننا استخدامه لعرض الصفحة.
ملاحظة: سنستخدم مكتبة Mongoose للحصول على معلومات قاعدة البيانات، لذا لا بد من إعادة قراءة قسم البحث عن السجلات من مقال استخدام قاعدة البيانات باستخدام مكتبة Mongoose.
الوجهة Route
أنشأنا وِجهات صفحة الفهرس في مقال الوجهات والمتحكمات، إذ عرّفنا جميع دوال الوجهات في الملف "/routes/catalog.js":
// الحصول على صفحة الدليل الرئيسية router.get("/", book_controller.index); //يُربَط مع /catalog/ لأننا نستورد الوجهة مع البادئة /catalog
تملك دالة متحكم الكتاب index
المُمرَّرة بوصفها معاملًا (book_controller.index
) تقديم بديل placeholder implementation مُعرَّف في الملف /controllers/bookController.js:
exports.index = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Site Home Page"); });
تُعَد هذه الدالة هي دالة المتحكم التي نوسّعها للحصول على معلومات من نماذجنا ثم عرضها باستخدام عرض (قالب).
المتحكم Controller
تحتاج دالة المتحكم index
إلى جلب معلومات حول عدد سجلات الكتب Book
ونسخ الكتب BookInstance
(جميعها) ونسخ الكتب BookInstance
(المتاحة) والمؤلفين Author
وأنواع الكتب Genre
الموجودة في قاعدة البيانات، وعرض هذه البيانات في قالب لإنشاء صفحة HTML، ثم إعادتها في استجابة HTTP.
افتح الملف /controllers/bookController.js، إذ سترى الدالة index()
المستورَدة بالقرب من أعلى الملف.
const Book = require("../models/book"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Site Home Page"); });
ضع جزء الشيفرة التالي مكان الشيفرة السابقة، فأول شيء تفعله هذه الشيفرة هو استيراد (أو طلب require()
) جميع النماذج Models، إذ نحتاج إلى ذلك لأننا سنستخدمها للحصول على عدد المستندات. تطلب الشيفرة أيضًا "express-async-handler" الذي يوفّر مغلِّفًا Wrapper لالتقاط الاستثناءات التي تُرمَى في دوال معالج الوِجهة.
const Book = require("../models/book"); const Author = require("../models/author"); const Genre = require("../models/genre"); const BookInstance = require("../models/bookinstance"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل عدد الكتب ونسخها والمؤلفين وأنواع الكتب (على التوازي) const [ numBooks, numBookInstances, numAvailableBookInstances, numAuthors, numGenres, ] = await Promise.all([ Book.countDocuments({}).exec(), BookInstance.countDocuments({}).exec(), BookInstance.countDocuments({ status: "Available" }).exec(), Author.countDocuments({}).exec(), Genre.countDocuments({}).exec(), ]); res.render("index", { title: "Local Library Home", book_count: numBooks, book_instance_count: numBookInstances, book_instance_available_count: numAvailableBookInstances, author_count: numAuthors, genre_count: numGenres, }); });
نستخدم التابع countDocuments()
للحصول على عدد نسخ كل نموذج، ويُستدعَى هذا التابع في نموذج مع مجموعة اختيارية من الشروط لمطابقتها وإعادة كائن استعلام Query
. يمكن تنفيذ الاستعلام من خلال استدعاء التابع exec()
، والذي يعيد وعدًا Promise
إما يجري الوفاء به مع نتيجة أو يُرفض إذا كان هناك خطأ في قاعدة البيانات.
تُعَد الاستعلامات الخاصة بأعداد المستندات مستقلة عن بعضها بعضًا، لذا نستخدم التابع Promise.all()
لتنفيذها على التوازي، ويعيد هذا التابع وعدًا جديدًا ننتظره باستخدام await
حتى ينتهي، إذ يتوقف التنفيذ مؤقتًا في هذه الدالة عند await
. يجري الوفاء بالوعد الذي يعيده all()
عند اكتمال جميع الاستعلامات، ويستمر تنفيذ دالة معالج الوجهة، ويملأ هذا الوعد المصفوفةَ بنتائج استعلامات قاعدة البيانات.
نستدعي بعد ذلك التابع res.render()
، ونحدّد عرضًا (قالبًا) بالاسم "index" وكائنات تربط نتائج استعلامات قاعدة البيانات مع قالب العرض، إذ تتوفر البيانات على شكل أزواج مفتاح-قيمة key-value، ويمكن الوصول إليها في القالب باستخدام المفتاح.
لاحظ أن الشيفرة البرمجية بسيطة جدًا لأنه يمكننا افتراض نجاح استعلامات قاعدة البيانات، وإذا فشلت أيٌّ من عمليات قاعدة البيانات، فسيكتشف asyncHandler()
الاستثناء المُرمَى ويمرّره إلى معالج البرمجية الوسيطة التالية next
في السلسلة.
العرض View
افتح العرض "/views/index.pug" وضع فيه المحتوى التالي بدلًا من محتواه الموجود مسبقًا:
extends layout block content h1= title p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network. h1 Dynamic content p The library has the following record counts: ul li #[strong Books:] !{book_count} li #[strong Copies:] !{book_instance_count} li #[strong Copies available:] !{book_instance_available_count} li #[strong Authors:] !{author_count} li #[strong Genres:] !{genre_count}
يُعَد العرض واضحًا، إذ نوسّع القالب الأساسي layout.pug من خلال تعديل الكتلة block
التي اسمها 'content'. سيكون العنوان h1
الأول هو النص المُهرَّب للمتغير title
المُمرَّر إلى التابع render()
، ولاحظ استخدام 'h1=
' بحيث يجري التعامل مع النص الذي يليه بوصفه تعبير جافا سكريبت، ثم نضمّن فقرةً تعرّف بالمكتبة المحلية LocalLibrary.
نسرد عدد النسخ من كل نموذج تحت عنوان المحتوى الديناميكي Dynamic content. لاحظ أن قيم القالب للبيانات هي المفاتيح المُحدَّدة عند استدعاء التابع render()
في دالة معالج الوِجهة.
ملاحظة: لم نهرّب قيم العد، إذ استخدمنا صيغة {}!
، لأن قيم العد محسوبة، ولكن إذا وفّر المستخدمون النهائيون المعلومات، فسنهرّب المتغير لعرضه.
كيف تبدو الصفحة الرئيسية؟
أنشأنا حتى الآن كل ما هو مطلوب لعرض صفحة الفهرس، لذا شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:
ملاحظة: لن تتمكّن من استخدام روابط الشريط الجانبي حاليًا لأن عناوين URL والعروض والقوالب الخاصة بهذه الصفحات غير مُعرَّفة بعد، وإذا حاولت الوصول إليها، فستحصل على أخطاء مثل "NOT IMPLEMENTED: Book list" اعتمادًا على الرابط الذي تنقر عليه، إذ تُحدَّد هذه السلسلة النصية الحرفية -التي سنضع مكانها البيانات المناسبة- في المتحكمات المختلفة التي تكون موجودة ضمن الملف "controllers".
صفحة قائمة الكتب
سنقدّم الآن صفحة قائمة الكتب، إذ تعرض هذه الصفحة قائمةً بجميع الكتب الموجودة في قاعدة البيانات مع مؤلفها والعنوان الخاص بكل كتاب، والذي يكون رابطًا تشعبيًا hyperlink لصفحة تفاصيل الكتاب المرتبطة به.
المتحكم
يجب أن تحصل دالة متحكم قائمة الكتب على قائمة بجميع كائنات الكتاب Book
في قاعدة البيانات، ثم تفرزها وتمرّرها إلى القالب لعرضها.
افتح الملف "/controllers/bookController.js"، وابحث عن تابع المتحكم book_list()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض قائمة جميع الكتب exports.book_list = asyncHandler(async (req, res, next) => { const allBooks = await Book.find({}, "title author") .sort({ title: 1 }) .populate("author") .exec(); res.render("book_list", { title: "Book List", book_list: allBooks }); });
يستدعي معالج الوجهة Route Handler الدالة find()
للنموذج Book
، ويختار إعادة العنوان title
والمؤلف author
فقط لأننا لسنا بحاجة إلى الحقول الأخرى (سيعيد أيضًا الحقل _id
والحقول الافتراضية)، ويفرز النتائج حسب العنوان أبجديًا باستخدام التابع sort()
. نستدعي أيضًا التابع populate()
للنموذج Book
مع تحديد الحقل author
، مما سيؤدي إلى استبدال معرّف مؤلف الكتاب المُخزَّن بتفاصيل المؤلف الكاملة، ثم يُستدعَى التابع exec()
في نهاية السلسلة التعاقبية daisy-chained لتنفيذ الاستعلام وإعادة الوعد.
يستخدم معالج الوِجهة await
لانتظار الوعد، مما يؤدي إلى إيقاف التنفيذ حتى استقراره. إذا جرى الوفاء بالوعد، فستُحفَظ نتائج الاستعلام في المتغير allBooks
ويستمر المعالج في التنفيذ.
يستدعي الجزء الأخير من معالج الوِجهة التابع render()
، ويحدد القالب book_list (.pug) ويمرر قيم title
و book_list
إلى القالب.
العرض View
أنشئ العرض "/views/book_list.pug" وضع فيه المحتوى التالي:
extends layout block content h1= title ul each book in book_list li a(href=book.url) #{book.title} | (#{book.author.name}) else li There are no books.
يوسّع العرض القالبَ الأساسي layout.pug ويعدّل الكتلة block
التي اسمها 'content'، ويعرض العنوان title
الذي مرّرناه من المتحكم (باستخدام التابع render()
) ويتكرر عبر المتغير book_list
باستخدام صيغة each
-in
-else
.
يُنشَأ عنصر قائمة لكل كتاب، إذ تعرض هذه القائمة عنوان الكتاب كرابط إلى صفحة تفاصيل الكتاب متبوعًا باسم المؤلف، وإذا لم يكن هناك كتب في book_list
، فستُنفَّذ تعليمة else
، ويعرض نص عدم وجود كتب "There are no books".
ملاحظة: نستخدم book.url
لتوفير رابط إلى سجل تفاصيل كل كتاب (قدّمنا هذه الوجهة، ولكن لم نقدّم الصفحة الخاصة بها بعد)، وهي خاصية افتراضية لنموذج الكتاب Book
الذي يستخدم الحقل _id
الخاص بنسخة النموذج لإنشاء مسار عنوان URL فريد.
من المهم تعريف كل كتاب على سطرين، باستخدام الشريط العمودي |
في السطر الثاني، ويُعَد هذا الأسلوب ضروريًا لأنه إذا كان اسم المؤلف موجودًا في السطر السابق، فسيكون جزءًا من الرابط التشعبي.
كيف تبدو صفحة قائمة الكتب؟
شغّل التطبيق (راجع قسم اختبار الوجهات من مقال الوجهات والمتحكمات للاطلاع على الأوامر ذات الصلة) وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط "جميع الكتب All books". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:
صفحة قائمة نسخ الكتب BookInstance
سنقدّم الآن قائمة بجميع نسخ الكتب BookInstance
في المكتبة، إذ يجب أن تتضمن الصفحة عنوان الكتاب Book
المرتبط بكل نسخة كتاب BookInstance
(مرتبطة برابط إلى صفحة تفاصيلها) مع المعلومات الأخرى في نموذج BookInstance
، بما في ذلك الحالة والناشر والمعرِّف الفريد لكل نسخة، ويجب ربط نص المعرّف الفريد برابط إلى صفحة تفاصيل نسخة الكتاب BookInstance
.
المتحكم
يجب أن يحصل متحكم قائمة BookInstance
على قائمة بجميع نسخ الكتاب، ويجب ملء معلومات الكتاب المرتبطة بها، ثم تمرير القائمة إلى القالب لعرضها.
افتح الملف "/controllers/bookinstanceController.js"، وابحث عن تابع المتحكم bookinstance_list()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض قائمة جميع نسخ الكتب exports.bookinstance_list = asyncHandler(async (req, res, next) => { const allBookInstances = await BookInstance.find().populate("book").exec(); res.render("bookinstance_list", { title: "Book Instance List", bookinstance_list: allBookInstances, }); });
يستدعي معالج الوجهة الدالةَ find()
للنموذج BookInstance
، ثم يتبعه في سلسلة تعاقبية استدعاءٌ للتابع populate()
مع حقل الكتاب book
، والذي سيضع مستند كتاب Book
كامل مكان معرّف الكتاب المُخزَّن لكل BookInstance
، ثم يُستدعَى التابع exec()
في نهاية السلسلة التعاقبية لتنفيذ الاستعلام وإعادة الوعد.
يستخدم معالج الوِجهة await
لانتظار الوعد، مما يؤدي إلى إيقاف التنفيذ حتى استقراره. إذا جرى الوفاء بالوعد، فستُحفَظ نتائج الاستعلام في المتغير allBookInstances
ويستمر المعالج في التنفيذ.
يستدعي الجزء الأخير من الشيفرة البرمجية التابع render()
، ويحدد القالب bookinstance_list (.pug) ويمرر قيم title
و bookinstance_list
إلى القالب.
العرض View
أنشئ العرض "/views/bookinstance_list.pug" وضع فيه المحتوى التالي:
extends layout block content h1= title ul each val in bookinstance_list li a(href=val.url) #{val.book.title} : #{val.imprint} - if val.status=='Available' span.text-success #{val.status} else if val.status=='Maintenance' span.text-danger #{val.status} else span.text-warning #{val.status} if val.status!='Available' span (Due: #{val.due_back} ) else li There are no book copies in this library.
يماثل هذا العرض العروض الأخرى، إذ يوسّع التخطيط، ويبدّل كتلة المحتوى content
، ويعرض العنوان title
المُمرَّر من المتحكم، ويتكرر عبر جميع نسخ الكتاب في bookinstance_list
. نعرض لكل نسخة حالتها، وإن لم يكن الكتاب متاحًا، فسيُعرَض تاريخ استرجاعه المتوقع.
قدّمنا ميزة جديدة، إذ يمكننا استخدام الصيغة النقطية بعد الوسم لإسناد صنف، لذلك سيُصرَّف span.text-success
إلى <span class="text-success">
، ويمكن كتابته أيضًا في Pug بالشكل التالي:
span(class="text-success")
كيف تبدو صفحة قائمة نسخ الكتب؟
شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/" ثم حدد رابط جميع نسخ الكتاب All book-instances. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:
تنسيق التاريخ باستخدام مكتبة Luxon
لا يبدو العرض الافتراضي للتواريخ في نماذجنا جميلًا:
Mon Apr 10 2020 15:49:58 GMT+1100 (AUS Eastern Daylight Time)
لذا سنوضح في هذا القسم كيفية تحديث صفحة قائمة نسخ الكتاب BookInstance List من القسم السابق من أجل عرض حقل due_date
بتنسيق أكثر ملاءمة مثل: Apr 10th, 2023.
الطريقة التي سنستخدمها هي إنشاء خاصية افتراضية في نموذج BookInstance
، والتي تعيد التاريخ المُنسَّق. سنطبّق التنسيق الفعلي باستخدام مكتبة Luxon، وهي مكتبة قوية وحديثة وسهلة الاستخدام لتحليل التواريخ والتحقق من صحتها ومعالجتها وتنسيقها وجعلها تواريخًا محلية.
ملاحظة: يمكن استخدام مكتبة Luxon لتنسيق السلاسل النصية مباشرةً في قوالب Pug، أو يمكننا تنسيق السلسلة النصية في عدد من الأماكن الأخرى. يتيح استخدام خاصية افتراضية الحصول على التاريخ المُنسَّق باستخدام الطريقة نفسها للحصول على تاريخ الاسترجاع due_date
حاليًا.
تثبيت مكتبة Luxon
أدخل الأمر التالي في جذر المشروع:
npm install luxon
إنشاء الخاصية الافتراضية
افتح الملف "./models/bookinstance.js"، ثم استورد مكتبة Luxon في أعلى هذا الملف كما يلي:
const { DateTime } = require("luxon");
ضِف الخاصية الافتراضية due_back_formatted
بعد خاصية URL مباشرةً.
BookInstanceSchema.virtual("due_back_formatted").get(function () { return DateTime.fromJSDate(this.due_back).toLocaleString(DateTime.DATE_MED); });
ملاحظة: يمكن لمكتبة Luxon استيراد السلاسل النصية بتنسيقات متعددة وتصديرها إلى كلٍّ من التنسيقات المُعرَّفة مسبقًا والتنسيقات الحرة free-form formats، إذ نستخدم في حالتنا التابع fromJSDate()
لاستيراد سلسلة تاريخ جافا سكريبت و toLocaleString()
لإخراج التاريخ بتنسيق DATE_MED باللغة الإنجليزية: Apr 10th, 2023. اطلع على توثيق مكتبة Luxon الخاص بالتنسيق للحصول على معلومات حول التنسيقات الأخرى وجعل سلسلة التاريخ عالمية.
تحديث العرض
افتح العرض "/views/bookinstance_list.pug" وضع due_back_formatted
بدلًا من due_back
.
if val.status != 'Available' //span (Due: #{val.due_back} ) span (Due: #{val.due_back_formatted} )
إذا انتقلت إلى جميع نسخ الكتاب All book-instances في الشريط الجانبي، فيجب أن ترى أن جميع تواريخ الاسترجاع أجمل بكثير.
صفحة قائمة المؤلفين
يجب أن تعرض صفحة قائمة المؤلفين قائمة بجميع المؤلفين في قاعدة البيانات، مع ربط اسم كل مؤلف برابط إلى صفحة تفاصيل المؤلف المرتبطة به، ويجب إدراج تاريخ الميلاد وتاريخ الوفاة بعد الاسم الموجود على السطر نفسه.
المتحكم
يجب أن تحصل دالة متحكم قائمة المؤلفين على قائمة بجميع نسخ المؤلف Author
، ثم تمرّرها إلى القالب لعرضها. افتح الملف "/controllers/authorController.js"، ثم ابحث عن تابع المتحكم المُصدَّر author_list()
بالقرب من أعلى الملف وضع مكانه الشيفرة البرمجية التالية:
// عرض قائمة جميع المؤلفين exports.author_list = asyncHandler(async (req, res, next) => { const allAuthors = await Author.find().sort({ family_name: 1 }).exec(); res.render("author_list", { title: "Author List", author_list: allAuthors, }); });
تتبع دالة متحكم الوجهة النمط نفسه المُتبَع في صفحات القوائم الأخرى، إذ تعرّف استعلامًا لنموذج المؤلف Author
باستخدام دالة find()
للحصول على جميع المؤلفين والتابع sort()
لفرزهم حسب family_name
بترتيب أبجدي، ثم يُستدعَى التابع exec()
في نهاية السلسلة التعاقبية لتنفيذ الاستعلام وإعادة الوعد الذي يمكن أن تنتظره الدالة باستخدام await
.
يعرض معالج الوجهة قالب author_list(.pug) بعد الوفاء بالوعد، ويمرر عنوان title
الصفحة وقائمة المؤلفين (allAuthors
) باستخدام مفاتيح القالب.
العرض
أنشئ العرض "/views/author_list.pug" وضع المحتوى التالي بدل المحتوى الموجود مسبقًا:
extends layout block content h1= title ul each author in author_list li a(href=author.url) #{author.name} | (#{author.date_of_birth} - #{author.date_of_death}) else li There are no authors.
شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع المؤلفين "All authors". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو الصفحة كما يلي:
ملاحظة: لا يُعَد مظهر تواريخ العمر الافتراضي للمؤلف جميلًا، ويمكنك تحسينه باستخدام الأسلوب نفسه الذي استخدمناه في قائمة نسخ الكتب BookInstance
من خلال إضافة الخاصية الافتراضية للعمر الخاص بنموذج المؤلف Author
. يمكن ألا يكون المؤلف ميتًا أو يمكن أن يكون لديه بيانات ولادة أو وفاة مفقودة، لذا يجب تجاهل التواريخ المفقودة أو المراجع التي تشير إلى خاصيات غير موجودة. تتمثل إحدى طرق التعامل مع ذلك في إعادة إما تاريخ مُنسَّق أو سلسلة فارغة اعتمادًا على ما إذا كانت الخاصية مُعرَّفةً أم لا، مثل:
return this.date_of_birth ? DateTime.fromJSDate(this.date_of_birth).toLocaleString(DateTime.DATE_MED) : '';
صفحة قائمة أنواع الكتب Genre - التحدي
يجب عليك في هذا القسم تقديم صفحة قائمة أنواع الكتب، إذ يجب أن تعرض الصفحة قائمة بجميع أنواع الكتب الموجودة في قاعدة البيانات، مع ربط كل نوع برابط إلى صفحة التفاصيل المرتبطة به بحيث تحصل على الصفحة التالية:
يجب أن تحصل دالة متحكم قائمة الأنواع على قائمة بجميع نسخ الأنواع Genre
، ثم تمرّرها إلى القالب لعرضها باتباع الخطوات التالية:
-
يجب تعديل التابع
genre_list()
في الملف "/controllers/genreController.js". -
يماثل تقديم هذا التابع تقريبًا متحكم التابع
author_list()
، ثم يجب فرز النتائج حسب الاسم بترتيب تصاعدي. - يجب تسمية القالب المراد عرضه بالاسم genre_list.pug.
-
يجب أن يمرر القالب المراد عرضه متغيرات العنوان
title
(الذي هو 'Genre List') وgenre_list
(قائمة أنواع الكتب المُعادة من دالة رد النداءGenre.find()
). - يجب أن يتطابق العرض مع لقطة الشاشة أو المتطلبات السابقة، إذ يجب أن يكون له بنية أو تنسيق مشابه جدًا لعرض قائمة المؤلفين باستثناء أن أنواع الكتب لا تحتوي على تواريخ.
صفحة تفاصيل أنواع الكتب Genre
يجب أن تعرض صفحة تفاصيل أنواع الكتب المعلومات الخاصة بنسخة نوع كتاب معينة باستخدام قيمة الحقل _id
المُولَّد تلقائيًا بوصفه المعرِّف. يُرمَّز معرّف سجل نوع الكتاب المطلوب في نهاية عنوان URL ويُستخرَج تلقائيًا بناءً على تعريف الوجهة (/genre/:id)، ثم يجري الوصول إليه ضمن المتحكم باستخدام معاملات الطلب: req.params.id
.
يجب أن تعرض الصفحة اسم نوع الكتاب وقائمة بجميع الكتب في هذا النوع مع روابط إلى صفحة تفاصيل كل كتاب.
المتحكم
افتح الملف "/controllers/genreController.js" واطلب وحدة الكتاب Book
في بداية الملف، إذ يجب أن يطلب الملف باستخدام الدالة require()
الوحدة Genre
و "express-async-handler".
const Book = require("../models/book");
ابحث عن تابع المتحكم genre_detail()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض صفحة التفاصيل لنوع كتاب معين exports.genre_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل نوع الكتاب وجميع الكتب المرتبطة به (على التوازي) const [genre, booksInGenre] = await Promise.all([ Genre.findById(req.params.id).exec(), Book.find({ genre: req.params.id }, "title summary").exec(), ]); if (genre === null) { // لا توجد نتائج const err = new Error("Genre not found"); err.status = 404; return next(err); } res.render("genre_detail", { title: "Genre Detail", genre: genre, genre_books: booksInGenre, }); });
نستخدم التابع Genre.findById()
أولًا للحصول على معلومات نوع كتاب لمعرّف معين، والتابع Book.find()
للحصول على جميع سجلات الكتب التي لها نفس معرّف نوع الكتاب المرتبط بها. لا يعتمد هذان الطلبان على بعضهما، لذا نستخدم التابع Promise.all()
لتشغيل استعلامات قاعدة البيانات على التوازي، إذ وضّحنا هذا الأسلوب لتشغيل الاستعلامات على التوازي عندما أنشأنا [الصفحة الرئيسية](رابط المقال السابق)).
ننتظر باستخدام await
الوعد المُعاد، ثم نتحقق من النتائج عندما يستقر؛ فإذا لم يكن نوع الكتاب موجودًا في قاعدة البيانات (أي يمكن أن يكون محذوفًا)، فسيعيد التابع findById()
بنجاح بدون نتائج، ونريد في هذه الحالة عرض صفحة عدم وجوده "not found"، لذلك ننشئ كائن خطأ Error
ونمرّره إلى دالة البرمجية الوسيطة التالية next
في السلسلة.
ملاحظة: تنتقل الأخطاء -المُمرَّرة إلى دالة البرمجية الوسيطة التالية next
- إلى شيفرة معالجة الأخطاء التي جرى إعدادها عندما أنشأنا التطبيق الهيكلي (اطلع على قسم معالجة الأخطاء لمزيد من المعلومات).
إذا عُثِر على نوع الكتاب genre
، فسنستدعي التابع render()
لتقديم العرض، إذ يكون قالب العرض هو genre_detail (.pug). تُمرَّر قيم title
و genre
و booksInGenre
إلى القالب باستخدام المفاتيح المقابلة (title
و genre
و genre_books
).
العرض
أنشئ العرض "/views/genre_detail.pug" وضع فيه المحتوى التالي:
extends layout block content h1 Genre: #{genre.name} div(style='margin-left:20px;margin-top:20px') h4 Books dl each book in genre_books dt a(href=book.url) #{book.title} dd #{book.summary} else p This genre has no books
يشابه هذا العرض جميع قوالبنا الأخرى، ولكن يتمثل الاختلاف الرئيسي في أننا لا نستخدم title
المُمرَّر إلى العنوان الأول، بالرغم من استخدامه في قالب layout.pug
الأساسي لضبط عنوان الصفحة.
كيف تبدو صفحة تفاصيل نوع الكتاب؟
شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع أنواع الكتب "All genres"، ثم حدّد أحد هذه الأنواع مثل النوع "Fantasy". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي:
ملاحظة: يمكن أن تحصل على خطأ مشابه لما يلي:
Cast to ObjectId failed for value " 59347139895ea23f9430ecbb" at path "_id" for model "Genre"
وهو خطأ Mongoose سببه عدم تحويل req.params.id
أو أي معرّف ID آخر إلى ()mongoose.Types.ObjectId
، ويعود السبب الأكثر شيوعًا لذلك إلى اختلاف الرقم المعرّف عن المعرّف الحقيقي، ويمكن حل هذه المشكلة من خلال استخدام ()Mongoose.prototype.isValidObjectId
لفحص المعرّف إذا كان صالحًا.
صفحة تفاصيل الكتاب
يجب أن تحصل صفحة تفاصيل الكتاب على المعلومات الخاصة بكتابٍ Book
معين يُحدَّد باستخدام قيمة الحقل _id
المُولَّد تلقائيًا، إضافةً إلى معلومات حول كل نسخة مرتبطة به في المكتبة BookInstance
. ينبغي ربط أيّ مؤلف أو نوع أو نسخة كتاب نعرضها برابط إلى صفحة التفاصيل المرتبطة بهذا العنصر.
المتحكم
افتح الملف "/controllers/bookController.js"، وابحث عن تابع المتحكم book_detail()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض صفحة تفاصيل كتاب معين exports.book_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل الكتب ونسخ الكتب لكتاب معين const [book, bookInstances] = await Promise.all([ Book.findById(req.params.id).populate("author").populate("genre").exec(), BookInstance.find({ book: req.params.id }).exec(), ]); if (book === null) { // لا توجد نتائج const err = new Error("Book not found"); err.status = 404; return next(err); } res.render("book_detail", { title: book.title, book: book, book_instances: bookInstances, }); });
ملاحظة: لا نحتاج إلى طلب أيّ وحدات إضافية حاليًا، إذ استوردنا مسبقًا الاعتماديات عندما قدّمنا متحكم الصفحة الرئيسية.
استخدمنا الأسلوب نفسه الموضح في صفحة تفاصيل نوع الكتاب، إذ تستخدم دالةُ متحكم الوجهة التابعَ Promise.all()
للاستعلام عن الكتاب Book
المُحدَّد والنسخ المرتبطة به BookInstance
على التوازي. إذا لم يُعثَر على كتاب مطابق، فسيُعاد كائن خطأ مع الخطأ "404: Not Found"، وإذا عُثِر على الكتاب، فستُعرَض معلومات قاعدة البيانات المسترجَعة باستخدام قالب "book_detail".
نمرر results.book.title
أثناء عرض صفحة الويب، نظرًا لاستخدام المفتاح "title" لإعطاء اسم لصفحة الويب كما هو مُعرَّف في ترويسة القالب "layout.pug".
العرض
أنشئ العرض "/views/book_detail.pug" وضِف إليه المحتوى التالي:
extends layout block content h1 Title: #{book.title} p #[strong Author:] a(href=book.author.url) #{book.author.name} p #[strong Summary:] #{book.summary} p #[strong ISBN:] #{book.isbn} p #[strong Genre:] each val, index in book.genre a(href=val.url) #{val.name} if index < book.genre.length - 1 |, div(style='margin-left:20px;margin-top:20px') h4 Copies each val in book_instances hr if val.status=='Available' p.text-success #{val.status} else if val.status=='Maintenance' p.text-danger #{val.status} else p.text-warning #{val.status} p #[strong Imprint:] #{val.imprint} if val.status!='Available' p #[strong Due back:] #{val.due_back} p #[strong Id:] a(href=val.url) #{val._id} else p There are no copies of this book in the library.
وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة.
ملاحظة: يمكن تقديم قائمة أنواع الكتب المرتبطة بالكتاب في القالب على النحو التالي، إذ تُضاف فاصلة بعد كل نوع مرتبط بالكتاب باستثناء النوع الأخير:
p #[strong Genre:] each val, index in book.genre a(href=val.url) #{val.name} if index < book.genre.length - 1 |,
كيف تبدو صفحة تفاصيل الكتاب؟
شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع الكتب All books، وحدد أحد هذه الكتب. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي:
صفحة تفاصيل المؤلف
يجب أن تعرض صفحة تفاصيل المؤلف معلومات المؤلف Author
، والذي يُحدَّد باستخدام قيمة الحقل _id
المُولَّد تلقائيًا، إضافةً إلى قائمة بجميع كائنات الكتاب Book
المرتبطة بهذا المؤلف Author
.
المتحكم
افتح الملف "/controllers/authorController.js"، ثم ضِف السطر التالي إلى بداية الملف لطلب -باستخدام الدالة require()
- الوحدة Book
التي تحتاجها صفحة تفاصيل المؤلف، ويجب أن تكون الوحدات الأخرى مثل "express-async-handler" موجودة مسبقًا.
const Book = require("../models/book");
ابحث عن تابع المتحكم author_detail()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض صفحة تفاصيل مؤلف مُحدَّد exports.author_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل المؤلف وجميع كتبه (على التوازي) const [author, allBooksByAuthor] = await Promise.all([ Author.findById(req.params.id).exec(), Book.find({ author: req.params.id }, "title summary").exec(), ]); if (author === null) { // لا توجد نتائج const err = new Error("Author not found"); err.status = 404; return next(err); } res.render("author_detail", { title: "Author Detail", author: author, author_books: allBooksByAuthor, }); });
يماثل هذا الأسلوب المتبع تمامًا الأسلوب الموضح في صفحة تفاصيل نوع الكتاب، إذ تستخدم دالةُ متحكم الوجهة التابعَ Promise.all()
للاستعلام عن المؤلف Author
ونسخ الكتب Book
المرتبطة بها على التوازي. إذا لم يُعثَر على مؤلف مطابق، فسيُرسَل كائن خطأ إلى برمجية Express الوسيطة لمعالجة الأخطاء، وإذا عُثِر على المؤلف، فستُعرَض معلومات قاعدة البيانات المُسترجَعة باستخدام قالب تفاصيل المؤلف "author_detail".
العرض
أنشئ العرض "/views/author_detail.pug" وضع فيه المحتوى التالي:
extends layout block content h1 Author: #{author.name} p #{author.date_of_birth} - #{author.date_of_death} div(style='margin-left:20px;margin-top:20px') h4 Books dl each book in author_books dt a(href=book.url) #{book.title} dd #{book.summary} else p This author has no books.
وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة.
كيف تبدو صفحة تفاصيل المؤلف؟
شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع المؤلفين All Authors، ثم حدد أحد المؤلفين. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:
ملاحظة: لا تبدو تواريخ عمر المؤلف جميلة، لذا سنحسّنها في التحدي الأخير من هذا المقال.
صفحة تفاصيل نسخ الكتب BookInstance
يجب أن تعرض صفحة تفاصيل نسخ الكتاب BookInstance
المعلومات الخاصة بكل نسخة كتاب والمُحدَّدة باستخدام قيمة الحقل _id
المُولَّد تلقائيًا، إذ تتضمن هذه المعلومات اسم الكتاب Book
بوصفه رابطًا إلى صفحة تفاصيل الكتاب، إضافةً إلى معلومات أخرى في السجل.
المتحكم
افتح الملف "/controllers/bookinstanceController.js"، ثم ابحث عن تابع المتحكم bookinstance_detail()
المُصدَّر وضع مكانه الشيفرة البرمجية التالية:
// عرض صفحة تفاصيل نسخة كتاب معينة exports.bookinstance_detail = asyncHandler(async (req, res, next) => { const bookInstance = await BookInstance.findById(req.params.id) .populate("book") .exec(); if (bookInstance === null) { // لا توجد نتائج const err = new Error("Book copy not found"); err.status = 404; return next(err); } res.render("bookinstance_detail", { title: "Book:", bookinstance: bookInstance, }); });
يشابه هذا التقديم ما استخدمناه لصفحات تفاصيل النماذج الأخرى، إذ تستدعي دالة متحكم الوجهة التابع BookInstance.findById()
مع معرّف نسخة كتاب معين مُستخرَج من عنوان URL (باستخدام الوجهة Route)، ويمكن الوصول إليه في المتحكم باستخدام معاملات الطلب: req.params.id
، ثم تستدعي التابع populate()
للحصول على تفاصيل الكتاب Book
المرتبط به. إذا لم يُعثَر على نسخة كتاب BookInstance
مطابقة، فسيُرسَل خطأ إلى برمجية Express الوسيطة، وإلّا فستُعرَض البيانات المُعادة باستخدام طريقة العرض bookinstance_detail.pug.
العرض
أنشئ العرض "/views/bookinstance_detail.pug" وضع فيه المحتوى التالي:
extends layout block content h1 ID: #{bookinstance._id} p #[strong Title:] a(href=bookinstance.book.url) #{bookinstance.book.title} p #[strong Imprint:] #{bookinstance.imprint} p #[strong Status:] if bookinstance.status=='Available' span.text-success #{bookinstance.status} else if bookinstance.status=='Maintenance' span.text-danger #{bookinstance.status} else span.text-warning #{bookinstance.status} if bookinstance.status!='Available' p #[strong Due back:] #{bookinstance.due_back}
وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة.
كيف تبدو صفحة تفاصيل نسخ الكتاب؟
شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط جميع نسخ الكتب All book-instances، وحدّد أحد هذه العناصر. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو موقعك كما يلي:
تحدى نفسك
تستخدم معظم التواريخ المعروضة حاليًا على الموقع تنسيق جافا سكريبت الافتراضي مثل:
Tue Oct 06 2020 15:49:58 GMT+1100 (AUS Eastern Daylight Time)
لذا يتمثل التحدي في هذا المقال في تحسين مظهر عرض التاريخ لمعلومات عمر المؤلف Author
(تاريخ الوفاة والميلاد) ولصفحات تفاصيل نسخ الكتب لاستخدام التنسيق Oct 6th, 2016.
ملاحظة: يمكنك استخدام نفس الأسلوب الذي استخدمناه لقائمة نسخ الكتب في المقال السابق من خلال إضافة خاصية العمر Lifespan الافتراضية إلى نموذج المؤلف Author
واستخدام مكتبة Luxon لتنسيق سلاسل التاريخ.
يجب عليك اتباع الخطوات التالية لإكمال هذا التحدي:
-
ضع المتغير
due_back_formatted
مكانdue_back
في صفحة تفاصيل نسخ الكتاب. -
حدّث نموذج المؤلف
Author
لإضافة خاصية العمر الافتراضية، إذ يجب أن يبدو مثل: date_of_birth - date_of_death، ويكون لكلا القيمتين تنسيق التاريخBookInstance.due_back_formatted
. -
استخدم
Author.lifespan
في جميع العروض حيث تستخدم حاليًاdate_of_birth
وdate_of_death
صراحةً.
ترجمة -وبتصرُّف- للمقالات Home page و Book list page و BookInstance list page و Date formatting using luxon و Author list page and Genre list page challenge و Genre detail page و Book detail page و Author detail page و BookInstance detail page and challenge.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.