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

إنشاء مكتبة محلية باستخدام Express:استمارات حذف المؤلفين والتعديل على الكتب


Ola Abbas

يوضح هذا المقال كيفية تعريف صفحة لحذف كائن المؤلف وتحديث كائن الكتاب للتعرّف على المزيد حول الاستمارات Forms في إطار عمل Express.

استمارة حذف مؤلف

سنوضح كيفية تعريف صفحة لحذف كائنات المؤلف Author، إذ ستكون استراتيجيتنا -كما ناقشنا في قسم تصميم الاستمارة- هي السماح فقط بحذف الكائنات التي لا تشير إليها كائنات أخرى، وهذا يعني أننا لن نسمح بحذف المؤلف Author إذا أشار إليه كتاب Book، ويعني ذلك من حيث التقديم أن الاستمارة يجب أن تؤكد عدم وجود كتب مرتبطة بالمؤلف قبل حذفه. tإذا كان هناك كتب مرتبطة به، فيجب أن تعرضها وتوضح أنه يجب حذفها قبل حذف كائن المؤلف Author.

متحكم وجهة Get

افتح الملف ‎/controllers/authorController.js، ثم ابحث عن تابع المتحكم author_delete_get()‎ المُصدَّر وضع مكانه الشيفرة البرمجية التالية:

// عرض استمارة حذف مؤلف‫ في طلب GET
exports.author_delete_get = 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) {
    // لا توجد نتائج
    res.redirect("/catalog/authors");
  }

  res.render("author_delete", {
    title: "Delete Author",
    author: author,
    author_books: allBooksByAuthor,
  });
});

يحصل المتحكم على معرّف نسخة المؤلف Author لحذفه من معامل عنوان URL (وهو req.params.id)، ويستخدم await لانتظار الوعد الذي أعاده التابع Promise.all()‎ للانتظار بطريقة غير متزامنة لسجل المؤلف المُحدَّد وجميع الكتب المرتبطة به على التوازي، ويصيّر العرض author_delete.pug عند اكتمال كلتا العمليتين، مع تمرير متغيرات title و author و author_books.

ملاحظة: إن لم يُعِد التابع findById()‎ أيّ نتائج، فلن يكون المؤلف موجودًا في قاعدة البيانات، وبالتالي لا يوجد شيء يمكن حذفه، لذا نعيد التوجيه مباشرةً إلى قائمة جميع المؤلفين.

if (author === null) {
  // لا توجد نتائج
  res.redirect("/catalog/authors");
}

متحكم طلب وجهة Post

ابحث عن تابع المتحكم author_delete_post()‎ المُصدَّر وضع مكانه الشيفرة البرمجية التالية:

// ‫معالجة حذف مؤلف في طلب POST
exports.author_delete_post = 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 (allBooksByAuthor.length > 0) {
    // المؤلف لديه كتب، لذا اعرض باست‫خدام طريقة وجهة GET نفسها
    res.render("author_delete", {
      title: "Delete Author",
      author: author,
      author_books: allBooksByAuthor,
    });
    return;
  } else {
    // المؤلف ليس لديه كتب، لذا احذف الكائن وأعِد التوجيه إلى قائمة المؤلفين
    await Author.findByIdAndRemove(req.body.authorid);
    res.redirect("/catalog/authors");
  }
});

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

ملاحظة: يمكننا التحقق من أن استدعاء التابع findById()‎ يعيد أيّ نتائج، فإن لم يكن الأمر كذلك، نعرض قائمة جميع المؤلفين مباشرةً. لقد تركنا الشيفرة كما هي موضحة أعلاه للإيجاز، إذ ستظل تعيد قائمة المؤلفين إن لم يُعثَر على المعرّف، ولكن ذلك سيحدث بعد التابع findByIdAndRemove()‎.

العرض View

أنشئ العرض ‎/views/author_delete.pug وضع فيه النص التالي:

extends layout

block content
  h1 #{title}: #{author.name}
  p= author.lifespan

  if author_books.length

    p #[strong Delete the following books before attempting to delete this author.]

    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 Do you really want to delete this Author?

    form(method='POST' action='')
      div.form-group
        input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id )

      button.btn.btn-primary(type='submit') Delete

يوسّع هذا العرض قالب التخطيط Layout من خلال تعديل الكتلة content، إذ يعرض تفاصيل المؤلف في أعلى الملف، ثم يتضمن تعليمة شرطية تعتمد على عدد كتب المؤلف author_books (تعليمات if و else) كما يلي:

  • إذا كان هناك كتب مرتبطة بالمؤلف، فستسرد الصفحة الكتب وتشير إلى أنه يجب حذفها قبل حذف هذا المؤلف Author.
  • إن لم يكن هناك كتب، فستعرض الصفحة طلبًا لتأكيد الحذف.
  • إذا نُقِر على زر الحذف Delete، فسيُرسَل معرّف المؤلف إلى الخادم في طلب POST وسيُحذَف سجل المؤلف.

إضافة عنصر تحكم الحذف Delete Control

سنضيف الآن عنصر تحكم الحذف إلى عرض تفاصيل المؤلف، إذ تُعَد صفحة التفاصيل مكانًا جيدًا لحذف سجل منه.

ملاحظة: سيكون عنصر التحكم مرئيًا فقط للمستخدمين المُصرَّح لهم عند التقديم الكامل، ولكن ليس لدينا نظام تصريح حاليًا.

افتح العرض author_detail.pug وأضِف الأسطر التالية إلى نهايته:

hr
p
  a(href=author.url+'/delete') Delete author

يجب أن يظهر عنصر التحكم الآن بوصفه رابطًا في صفحة تفاصيل المؤلف كما يلي:

01 locallibary express author detail delete

كيف تبدو استمارة حذف المؤلف؟

شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/‎"، ثم حدّد رابط جميع المؤلفين All authors، واختر مؤلفًا معينًا، ثم حدّد رابط حذف المؤلف Delete author.

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

02 locallibary express author delete nobooks

إذا كان لدى المؤلف كتب، فسيُقدَّم عرض يشبه ما يلي، ثم يمكنك حذف الكتب من صفحات تفاصيلها (بعد تقديم الشيفرة البرمجية المتعلقة بذلك):

03 locallibary express author delete withbooks

ملاحظة: يمكن تقديم الصفحات الأخرى الخاصة بحذف الكائنات باستخدام الطريقة نفسها، لذا تركناها كتحدٍ لك.

استمارة تحديث كتاب

سنوضح كيفية تعريف صفحة لتحديث كائنات الكتاب Book، إذ تشبه معالجة استمارة تحديث كتاب إلى حدٍ كبير معالجة استمارة إنشاء كتاب، باستثناء أنه يجب عليك ملء الاستمارة في وِجهة GET بقيمٍ من قاعدة البيانات.

متحكم وجهة Get

افتح الملف "‎/controllers/bookController.js"، ثم ابحث عن تابع المتحكم book_update_get()‎ المُصدَّر وضع مكانه الشيفرة البرمجية التالية:

// عرض استمارة تحديث كتاب‫ في طلب GET
exports.book_update_get = asyncHandler(async (req, res, next) => {
  // الحصول على الكتاب والمؤلفين وأنواع الكتب للاستمارة
 const [book, allAuthors, allGenres] = await Promise.all([
    Book.findById(req.params.id).populate("author").populate("genre").exec(),
    Author.find().sort({ family_name: 1 }).exec(),
    Genre.find().sort({ name: 1 }).exec(),
  ]);

  if (book === null) {
    // لا توجد نتائج
    const err = new Error("Book not found");
    err.status = 404;
    return next(err);
  }

  // ميّز أنواع الكتب المختارة بوصفّها مُحدَّدة
  for (const genre of allGenres) {
    for (const book_g of book.genre) {
      if (genre._id.toString() === book_g._id.toString()) {
        genre.checked = "true";
      }
    }
  }

  res.render("book_form", {
    title: "Update Book",
    authors: allAuthors,
    genres: allGenres,
    book: book,
  });
});

يحصل المتحكم على معرّف الكتاب Book لتحديثه من معامل عنوان URL (هو req.params.id)، وينتظر (باستخدام await) الوعد الذي أعاده التابع Promise.all()‎ للحصول على سجل الكتاب Book المحدد (مع ملء حقلي نوع الكتاب والمؤلف) وجميع سجلات المؤلف Author ونوع الكتاب Genre. تتحقق الدالة عند اكتمال العمليات من العثور على أيّ كتب، فإن لم يُعثَر على أيٍّ منها، فسترسل خطأ عدم العثور على الكتاب "Book not found" إلى البرمجية الوسيطة لمعالجة الخطأ.

ملاحظة: لا يُعَد عدم العثور على أيّ كتاب كنتيجة خطأً في البحث، ولكنه كذلك في هذا التطبيق لأننا نعلم أنه يجب أن يكون هناك سجل كتاب مطابق. تطبّق الشيفرة السابقة اختبار المقارنة (book===null) في دالة رد النداء، ولكن يمكن أن تضيف التابع orFail()‎ إلى السلسلة التعاقبية الخاصة بالاستعلام.

نميّز بعد ذلك أنواع الكتب المختارة حاليًا بوصفها مُحدَّدة، ثم نقدّم العرض book_form.pug، ونمرر متغيرات title والكتاب وجميع المؤلفين authors وجميع أنواع الكتب genres.

متحكم وجهة Post

ابحث عن تابع المتحكم book_update_post()‎ المُصدَّر وضع مكانه الشيفرة البرمجية التالية:

// معالجة تحديث كتاب‫ في طلب POST
exports.book_update_post = [
  // تحويل نوع الكتاب إلى مصفوفة
  (req, res, next) => {
    if (!Array.isArray(req.body.genre)) {
      req.body.genre =
        typeof req.body.genre === "undefined" ? [] : [req.body.genre];
    }
    next();
  },

  // التحقق من صحة الحقول وتطهيرها
  body("title", "Title must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("author", "Author must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("summary", "Summary must not be empty.")
    .trim()
    .isLength({ min: 1 })
    .escape(),
  body("isbn", "ISBN must not be empty").trim().isLength({ min: 1 }).escape(),
  body("genre.*").escape(),

  // طلب العملية بعد التحقق من صحة البيانات وتطهيرها
  asyncHandler(async (req, res, next) => {
    // استخراج أخطاء التحقق من صحة البيانات من الطلب
    const errors = validationResult(req);

    // إنشاء كائن كتاب مع بيانات مُهرَّبة أو محذوف منها المسافات والمعرّف القديم
    const book = new Book({
      title: req.body.title,
      author: req.body.author,
      summary: req.body.summary,
      isbn: req.body.isbn,
      genre: typeof req.body.genre === "undefined" ? [] : req.body.genre,
      _id: req.params.id, // هذا مطلوب، أو سيجري إسناد معرّف جديد
    });

    if (!errors.isEmpty()) {
      // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ

      // الحصول على جميع المؤلفين وأنواع الكتب للاستمارة
      const [allAuthors, allGenres] = await Promise.all([
        Author.find().exec(),
        Genre.find().exec(),
      ]);

      // ميّز أنواع الكتب المختارة بوصفها مُحدَّدة
     for (const genre of allGenres) {
        if (book.genre.indexOf(genre._id) > -1) {
          genre.checked = "true";
        }
      }
      res.render("book_form", {
        title: "Update Book",
        authors: allAuthors,
        genres: allGenres,
        book: book,
        errors: errors.array(),
      });
      return;
    } else {
      // البيانات الواردة من الاستمارة صالحة، لذا حدّث السجل
      const thebook = await Book.findByIdAndUpdate(req.params.id, book, {});
      // أعِد التوجيه إلى صفحة تفاصيل الكتاب
      res.redirect(thebook.url);
    }
  }),
];

تُعَد هذه الشيفرة البرمجية مشابهة جدًا لشيفرة وِجهة Post التي استخدمناها لإنشاء كتاب Book، إذ نتحقق أولًا من صحة بيانات الكتاب القادمة من الاستمارة ونطهرها ونستخدمها لإنشاء كائن كتاب Book جديد مع ضبط قيمة ‎_id الخاصة به على معرّف الكائن المُراد تحديثه. إذا كان هناك أخطاء عند التحقق من صحة البيانات، فسنعيد تصيير الاستمارة، بالإضافة إلى عرض البيانات التي أدخلها المستخدم والأخطاء وقوائم أنواع الكتب والمؤلفين. إذا لم يكن هناك أخطاء، فسنستدعي التابع Book.findByIdAndUpdate()‎ لتحديث مستند الكتاب Book، ثم نعيد التوجيه إلى صفحة تفاصيله.

العرض View

ليست هناك حاجة لتغيير عرض الاستمارة (‎/views/book_form.pug)، إذ يناسب هذا القالب إنشاء الكتاب وتحديثه.

إضافة زر تحديث

افتح العرض book_detail.pug وتأكّد من وجود روابط لكل من حذف وتحديث الكتب أسفل الصفحة كما يلي:

hr
  p
    a(href=book.url+'/delete') Delete Book
  p
    a(href=book.url+'/update') Update Book

يجب أن تكون قادرًا الآن على تحديث الكتب من صفحة تفاصيل الكتاب.

كيف تبدو استمارة تحديث الكتاب؟

شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/‎"، ثم حدّد رابط جميع الكتب All books، واختر كتابًا معينًا، ثم حدّد رابط تحديث الكتاب Update Book. يجب أن تبدو الاستمارة تمامًا مثل صفحة إنشاء كتاب Create book، ولكن مع العنوان "Update book"، وتكون الاستمارة مملوءة مسبقًا بقيم السجل.

04 locallibary express book update noerrors

ملاحظة: يمكن تقديم الصفحات الأخرى الخاصة بتحديث الكائنات باستخدام الطريقة نفسها، لذا تركناها كتحدٍ لك.

ترجمة -وبتصرُّف- للمقالين Delete Author form و Update Book form.

اقرأ المزيد


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

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

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



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

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

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

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


×
×
  • أضف...