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

إنشاء موقع مكتبة محلية باستخدام Express: إنشاء صفحات الموقع


Ola Abbas

سنتعرف في هذا المقال على كيفية إنشاء الصفحة الرئيسية لموقع المكتبة المحلية 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/‎". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:

01 locallibary express home

ملاحظة: لن تتمكّن من استخدام روابط الشريط الجانبي حاليًا لأن عناوين 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". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:

02 new book list

صفحة قائمة نسخ الكتب 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. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:

03 locallibary express bookinstance list

تنسيق التاريخ باستخدام مكتبة 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". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو الصفحة كما يلي:

04 locallibary express author list

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

return this.date_of_birth ? DateTime.fromJSDate(this.date_of_birth).toLocaleString(DateTime.DATE_MED) : '';

صفحة قائمة أنواع الكتب Genre - التحدي

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

05 locallibary express genre list

يجب أن تحصل دالة متحكم قائمة الأنواع على قائمة بجميع نسخ الأنواع Genre، ثم تمرّرها إلى القالب لعرضها باتباع الخطوات التالية:

  1. يجب تعديل التابع genre_list()‎ في الملف "‎/controllers/genreController.js".
  2. يماثل تقديم هذا التابع تقريبًا متحكم التابع author_list()‎، ثم يجب فرز النتائج حسب الاسم بترتيب تصاعدي.
  3. يجب تسمية القالب المراد عرضه بالاسم genre_list.pug.
  4. يجب أن يمرر القالب المراد عرضه متغيرات العنوان title (الذي هو 'Genre List') و genre_list (قائمة أنواع الكتب المُعادة من دالة رد النداء Genre.find()‎).
  5. يجب أن يتطابق العرض مع لقطة الشاشة أو المتطلبات السابقة، إذ يجب أن يكون له بنية أو تنسيق مشابه جدًا لعرض قائمة المؤلفين باستثناء أن أنواع الكتب لا تحتوي على تواريخ.

صفحة تفاصيل أنواع الكتب 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". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي:

06 locallibary express genre detail

ملاحظة: يمكن أن تحصل على خطأ مشابه لما يلي:

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، وحدد أحد هذه الكتب. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي:

07 locallibary express book detail

صفحة تفاصيل المؤلف

يجب أن تعرض صفحة تفاصيل المؤلف معلومات المؤلف 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، ثم حدد أحد المؤلفين. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي:

08 locallibary express author detail

ملاحظة: لا تبدو تواريخ عمر المؤلف جميلة، لذا سنحسّنها في التحدي الأخير من هذا المقال.

صفحة تفاصيل نسخ الكتب 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، وحدّد أحد هذه العناصر. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو موقعك كما يلي:

09 locallibary express bookinstance detail

تحدى نفسك

تستخدم معظم التواريخ المعروضة حاليًا على الموقع تنسيق جافا سكريبت الافتراضي مثل:

Tue Oct 06 2020 15:49:58 GMT+1100 (AUS Eastern Daylight Time)‎

لذا يتمثل التحدي في هذا المقال في تحسين مظهر عرض التاريخ لمعلومات عمر المؤلف Author (تاريخ الوفاة والميلاد) ولصفحات تفاصيل نسخ الكتب لاستخدام التنسيق Oct 6th, 2016.

ملاحظة: يمكنك استخدام نفس الأسلوب الذي استخدمناه لقائمة نسخ الكتب في المقال السابق من خلال إضافة خاصية العمر Lifespan الافتراضية إلى نموذج المؤلف Author واستخدام مكتبة Luxon لتنسيق سلاسل التاريخ.

يجب عليك اتباع الخطوات التالية لإكمال هذا التحدي:

  1. ضع المتغير due_back_formatted مكان due_back في صفحة تفاصيل نسخ الكتاب.
  2. حدّث نموذج المؤلف Author لإضافة خاصية العمر الافتراضية، إذ يجب أن يبدو مثل: date_of_birth - date_of_death، ويكون لكلا القيمتين تنسيق التاريخ BookInstance.due_back_formatted.
  3. استخدم 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.

اقرأ المزيد


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

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

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



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

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

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

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


×
×
  • أضف...