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

إنشاء تطبيق ويب باستخدام إطار عمل فلاسك Flask من لغة بايثون


محمد الخضور

يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا العديد من الأدوات والميزات التي من شأنها جعل إنشاء تطبيقات الويب في لغة بايثون أسهل، مانحًا المطورين المرونة في العمل، كما أنّه أبسط للاستخدام من قِبل المطورين المبتدئين نظرًا لإمكانية إنشاء تطبيق ويب كامل بسرعة باستخدام ملفٍ وحيدٍ مكتوب بلغة بايثون. إضافةً لما سبق، يتميز فلاسك بكونه قابلًا للتوسّع والوراثة دون أن يفرض أي بنية هرمية لطريقة عرض الملفات، كما أنّه لا يتطلب أي شفرات برمجية معقدّة استهلالية قبل البدء باستخدامه.

سنتعرّف في هذا المقال أيضًا على إطار العمل بوتستراب bootstrap لإضافة التنسيقات على التطبيق ليبدو أكثر جاذبية بصريًا، كما سيساعدنا على تمكين ميزة الصفحات المتوافقة مع المتصفحات في تطبيق الويب، ما يضمن عمله بصورةٍ جيدة في المتصفحات الخاصة بالجوال، دون كتابة شفرات HTML و CSS و JavaScript لتحقيق هذه الغاية، وبالتالي سيساعدك بوتستراب في صبِّ تركيزك على تعلُّم كيفية عمل فلاسك نفسه.

يستخدم فلاسك محرّك القوالب jinja لبناء صفحات HTML ديناميكيًا باستخدام مفاهيم بايثون المألوفة، من متغيراتٍ وحلقاتٍ تكرارية وقوائم وغيرها، وبالتالي ستُستخدم هذه القوالب جزءًا من هذا المشروع.

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

مستلزمات العمل

قبل المتابعة في هذا المقال لابدّ من:

  • توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو flask_blog.
  • فهم مبادئ بايثون 3 مثل أنماط البيانات والجمل الشرطية وحلقة for التكرارية والدوال، وغيرها من المفاهيم المشابهة في بايثون 3.

الخطوة الأولى – تثبيت فلاسك

في هذه الخطوة سنفعّل بيئة بايثون ونثبّت فلاسك باستخدام أمر تثبيت الحزم pip، وفي حال كون بيئة البرمجة غير مفعّلة بعد، سنستخدم الأمر التالي لتفعيلها بعد التأكّد من كون موجه الأوامر يشير إلى مسار مجلد المشروع flask_blog:

$ source env/bin/activate

وبمجرّد تفعيل بيئة البرمجة، سيُظهِر موجّه الأوامر البادئة env والتي تظهر على النحو التالي:

(env)user@localhost:$

تشير هذه البادئة إلى أن بيئة العمل env فعّالةٌ حاليًا، والتي قد تكون باسمٍ آخر لديك وذلك حسب الاسم الذي اخترته لها خلال إنشائها.

اقتباس

ملاحظة: يمكنك استخدام نظام إدارة الإصدارات Git لإدارة ومتابعة عملية تطوير المشروع بفعالية، وفي حال استخدامك له، فمن الجيد أن تستثني المجلد env الذي أنشأته للتو في ملف "gitignore." لتفادي تتبّع الملفات التي ليس لها صلةٌ بالمشروع.

الآن سنثبّت حزم بايثون وسنعزل شيفرة المشروع بعيدًا عن نظام بايثون المُثبت أساسًا باستخدام أوامر pip و python.

ولتثبيت فلاسك، سنشغّل الأمر التالي:

(env)user@localhost:$ pip install flask

وعند انتهاء التثبيت، سنشغّل الأمر التالي للتحقُّق من إتمام العملية:

(env)user@localhost:$ python -c "import flask; print(flask.__version__)"

وبذلك نكون قد استخدمنا واجهة سطر أوامر بايثون ذات الخيار c- لتنفّيذ شيفرة بايثون، ومن ثم استوردنا مكتبة فلاسك باستخدام الأمر import flask، ثمّ طبعنا إصدار فلاسك المُثبت باستخدام المتغير flask.__version__ variable.

وسيتضمّن الخرج رقم الإصدار على النحو التالي:

1.1.2

وبذلك نكون قد أنشأنا مجلد المشروع وبيئة العمل الافتراضية، كما أنّنا ثبَّتنا فلاسك، وبذلك أصبحنا مُستعدين لإنشاء تطبيق أساسي.

الخطوة الثانية – كيفية إنشاء تطبيق أساسي

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

الآن سنفتح الملف hello.py الموجود في مجلد flask_blog للتعديل عليه باستخدام محرّر النصوص نانو nano، أو أي محرّر آخر تفضِّله:

(env)user@localhost:$ nano hello.py

سيشكّل ملف hello.py مثالًا مبسّطًا لكيفية التعامل مع طلبات HTTP، إذ سنستورد في هذا الملف كائن فلاسك ونكتب تابعًا لتوليد الاستجابة لطلبات بروتوكول HTTP، ولتحقيق ذلك نكتب الشيفرة التالية في الملف hello.py:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

استوردنا في الجزء السابق من الشيفرة كائن فلاسك من حزمة فلاسك، ثم استخدمناه لإنشاء نسخةٍ فعليةٍ موجودةٍ في الذاكرة لتطبيق فلاسك Flask application instance باسم app، وليست مجرد كائن برمجي، ومن ثمّ مررنا المتغير الخاص __name__، الذي سيخزّن اسم وحدة بايثون الحالية ليُعلم تطبيق فلاسك بمكان وجود هذه الوحدة، إذ لا بُدّ من إجراء هذه الخطوة كون فلاسك يهيّئ بعض المسارات اللازمة في الخلفية. وبمجرد إنشاء هذا التطبيق app، يمكنك استخدامه في معالجة طلبات الويب القادمة وإرسال الردود إلى المُستخدم.

يكون المزخرف app.route@ مسؤولًا عن تعديل دوال بايثون المألوفة لتصبح دوالًا عاملةً في فلاسك، والتي تحوّل القيمة المعادة من قِبل التابع إلى استجابةٍ من نوع HTTP تُعرض لدى عميل HTTP الذي قد يكون متصفحًا مثلًا؛ فبمجرد تمرير القيمة / وسيطًا للتابع app.route@، سينشئ الردود على طلبات الويب الواردة إلى الرابط /، والذي يمثّل الرابط الرئيسي في التطبيق، وبذلك سيعيد التابع ()hello السلسلة النصية '!Hello, World' ردًا على الطلب.

الآن، اِحفظ الملف وأغلقه. ولتشغيل تطبيق الويب الذي أنشأناه، لا بُدّ من إرشاد فلاسك إلى موقعه (في حالتنا الملف ذو الاسم hello.py) باستخدام متغير بيئة فلاسك FLASK_APP على النحو التالي:

(env)user@localhost:$ export FLASK_APP=hello

ومن ثم تشغيله بوضع التطوير باستخدام متغير بيئة فلاسك Flask_ENV على النحو التالي:

(env)user@localhost:$ export FLASK_ENV=development

وفي النهاية، سنشغّل التطبيق باستخدام الأمر flask run:

(env)user@localhost:$ flask run

وبمجرّد تشغيل التطبيق، سيكون الخرج مشابهًا لما يلي:

* Serving Flask app "hello" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 813-894-335

يحتوي الخرج السابق على عدة معلومات، مثل:

  • اسم التطبيق المُشغَّل.
  • بيئة التشغيل الحالية التي يعمل عليها التطبيق.
  • عبارة Debug mode:on التي تشير إلى أن مصحّح أخطاء فلاسك قيد التشغيل، وهو ذو فوائد عديدة أثناء عملية التطوير كونه يقدم رسائل خطأ مفصّلة عندما يحدث أي خلل، ما يجعل عملية تنقيح الأخطاء أسهل وأيسر.
  • التطبيق يعمل على الحاسب المحلي وذلك على الرابط /http://127.0.0.1:5000، إذ أن 127.0.0.1 هو عنوان IP الذي يمثِّل الخادم المحلي localhost على حاسبك، و 5000: هو رقم المنفذ.

افتح المتصفح واكتب عنوان URL التالي "/http://127.0.0.1:5000"، ستظهر عبارة !Hello, World استجابةً لهذا العنوان، وهذا ما يؤكد أن التطبيق يعمل بنجاح.

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

يمكنك في هذه المرحلة الإبقاء على خادم التطوير قيد التشغيل في الطرفية terminal الخاصة به، ومن ثم افتح نافذة طرفية جديدة وغيّر المسار فيها إلى مسار مجلد المشروع حيث يتواجد ملف hello.py، ثم فعّل البيئة الافتراضية (السبب مبيّنٌ في الملاحظة أدناه)، وهيّئ متغيرات البيئة FLASK_ENV و FLASP_APP، ومن ثم تابع الخطوات التالية. (ذُكرت هذه الأوامر سابقًا في هذه الخطوة).

ملاحظة: من الضروري تفعيل البيئة الافتراضية لدى فتح طرفية جديدة، ولا بدّ من إعداد متغيرات البيئة FLASK_ENV و FLASK_APP.

لا يمكن تشغيل تطبيق فلاسك آخر باستخدام الأمر flask run نفسه خلال فترة عمل خادم تطوير تطبيقات فلاسك، كونه يستخدم المنفذ رقم 5000 افتراضيًا، وحالما يُحجَز هذا المنفذ يصبح غير متاحًا لتشغيل أي تطبيقٍ آخر، وفي حال فعلت ذلك ستظهر رسالة خطأ مشابهةٍ لما يلي:

OSError: [Errno 98] Address already in use

ويمكن حل لهذه المشكلة، إمّا بإيقاف الخادم العامل حاليًا عن طريق الضغط على "CTRL+C" ومن ثم تنفيذ الأمر flask run مجدّدًا، أو في حال رغبتك بتشغيل كلا التطبيقين في نفس الوقت، فمن الممكن تمرير رقم منفذٍ مختلف باستخدام الوسيط p-. سنستخدم الأمر التالي لتشغيل تطبيقٍ آخر يستخدم المنفذ 5001 مثالًا حول هذه الطريقة:

(env)user@localhost:$ flask run -p 5001

وبذلك أصبح لديك تطبيق ويب صغير باستخدام فلاسك، وبعد أن شغّلت وعرضت معلومات في متصفح الويب، سنستخدم فيما يلي ملفات HTML في هذا التطبيق.

الخطوة الثالثة – استخدام قوالب HTML

لا يعرض تطبيقك حاليًا سوى رسالةٍ صغيرة دون أي استخدامٍ للغة HTML، إذ تستخدم تطبيقات الويب عادةً HTML بصورةٍ رئيسية لعرض المعلومات لزوار الموقع أو التطبيق، لذا سنعمل فيما يلي على دمج ملفات HTML في التطبيق، والتي يمكن عرضها في متصفح الويب.

يقدّم فلاسك تابعًا مساعدًا يُسمّى ()render_template، يمكنّنا من استخدام [محرّك القوالب جينجا jinja، وهذا يجعل إدارة HTML أسهل عن طريق كتابة شيفرة HTML إلى جانب الشيفرة الوظيفية المعبّرة عن غرض التطبيق وذلك في ملفات ذات امتداد "html."، وستُستخدم ملفات HTML هذه (أي القوالب) لبناء كل صفحات التطبيق، مثل الصفحة الرئيسية التي تستعرض التدوينات الحالية في المدونة، والصفحة الخاصة بكل تدوينة، والصفحة التي يتمكن المستخدم من خلالها إضافة تدوينة جديدة، وغيرها.

في هذه الخطوة، سننشئ تطبيق فلاسك رئيسي في ملفٍ جديد. بدايةً استخدم محرّر النصوص نانو nano أو أي محرِّر تفضله لإنشاء الملف app.py والتعديل عليه ضمن مجلد "flask_blog"، إذ سيحتوي هذا الملف على كل الشيفرة المُستخدمة لإنشاء تطبيق التدوينات:

(env)user@localhost:$ nano app.py

سنستورد في هذا الملف الجديد كائن فلاسك لإنشاء نسخة تطبيق فلاسك كما فعلنا سابقًا، كما سنستورد التابع المساعد ()render_template، الذي سيمكننّا من إخراج ملفات قوالب HTML الموجودة في المجلد templates الذي سننشئه بعد قليل. سيحتوي هذا الملف الجديد على تابعٍ وحيدٍ عامل في فلاسك مسؤول عن التعامل مع الطلبات الموجهة إلى الرابط /. الآن، أضِف مايلي إلى الملف الجديد app.py:

from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
 return render_template('index.html')

سيعيد التابع ()index العامل في فلاسك نتيجة استدعاء التابع ()render_template عن طريق تمرير الوسيط index.html، وهذا يخبر التابع ()render_template بالبحث عن ملف باسم index.html في مجلد القوالب المُسمّى templates.

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

الآن، احفظ الملف وأغلقه، ثمّ أوقف خادم التطوير في الطرفية الأخرى التي تشغِّل التطبيق hello عن طريق الضغط على "CTRL+C"؛ ولكن قبل تشغيل التطبيق تأكد من تحديد قيمة متغير البيئة FLASK_APP على نحوٍ صحيح، نظرًا لكونك لم تعد تستخدم تطبيق hello، وذلك على النحو التالي:

(env)user@localhost:$ export FLASK_APP=app
(env)user@localhost:$ flask run

سيؤدي فتح العنوان "http://127.0.0.1:5000" في المتصفح إلى ظهور صفحة تنقيح الأخطاء التي ستُعلمك بعدم العثور على القالب "index.html"، ويُميَّز سطر الشيفرة الرئيسي المسؤول عن ظهور هذا الخطأ بلونٍ واضح، وهو في هذه الحالة السطر:

return render_template('index.html')

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

first_img_step3a.png

كي نصحّح هذا الخطأ، سننشئ مجلدًا باسم templates ضمن المجلد flask_blog، ومن ثم سنفتح ملف index.html بداخله للتحرير كما يلي:

(env)user@localhost:$ mkdir templates
(env)user@localhost:$ nano templates/index.html

ثم سنضيف شيفرة HTML التالية في الملف index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>FlaskBlog</title>
</head>
<body>
   <h1>Welcome to FlaskBlog</h1>
</body>
</html>

احفظ الملف ثم استخدم المتصفح للانتقال إلى الرابط "/http://127.0.0.1:5000" مجددًّا، أو حدِّث الصفحة، وفي هذه المرة سيعرض المتصفح النص "Welcome to FlaskBlog" وذلك في صيغة وسم <h1>.

تمتلك تطبيقات فلاسك عادةً مجلدًا للملفات الثابتة يسمّى static، إضافةً إلى مجلد القوالب templates، إذ يستضيف هذا المجلد ملفات، مثل CSS و JavaScript والصور التي يستخدمها التطبيق.

يمكنك إنشاء ملف تنسيقات style.css لإضافة CSS إلى التطبيق من خلال البدء بإنشاء مجلدٍ باسم static داخل مجلد flask_blog الرئيسي كما يلي:

(env)user@localhost:$ mkdir static

ثم أضِف مجلدًا آخر باسم css داخل المجلد static لتضع به الملفات ذات اللاحقة "css."، إذ أن الهدف من هذا الإجراء هو تنظيم الملفات الثابتة في مجلدات مخصصّة حسب نوعها؛ فمثلًا نضع ملفات JavaScript في مجلد باسم js، والصور في مجلد باسم images أو img، وهكذا. ستنشئ التعليمة التالية مجلدًا باسم css داخل المجلد static:

(env)user@localhost:$ mkdir static/css

اِفتح الآن الملف style.css الموجود داخل المجلد css بهدف تحريره:

(env)user@localhost:$ nano static/css/style.css

اكتب قاعدة CSS التالية ضمن الملف style.css:

h1 {
    border: 2px #eee solid;
    color: brown;
    text-align: center;
    padding: 10px;
}

شيفرة CSS هذه مسؤولةٌ عن إضافة حدود لكافة الوسوم من نوع <h1>، كما أنّها تغيِّر لون الخط فيها إلى البني، وتجعل محاذاة النص إلى الوسط، وتضيف هوامشًا داخليةً ضيقةً إليها.

احفظ الآن الملف وأغلقه، ثم افتح ملف القالب index.html بهدف تحريره:

(env)user@localhost:$ nano templates/index.html

سنضيف الآن رابطًا في قسم الترويسة <head> من القالب index.html للوصول إلى الملف style.css على النحو التالي:

. . .
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}">
    <title>FlaskBlog</title>
</head>
. . .

استخدمنا التابع المساعد url_for()‎ لإنشاء المسار المناسب للملف، إذ يحدد الوسيط الأول أنّنا ننشئ ارتباطًا إلى ملفٍ ساكن، والوسيط الثاني هو مسار الملف هذا داخل مجلد الملفات الساكنة.

اِحفظ الملف وأغلقه، وحالما تحدّث الصفحة index في التطبيق، ستلاحظ أن النص "Welcome to FlaskBlog" أصبح بلونٍ بني، ومحاذاته إلى الوسط، ومحاطًا بحدود.

يمكنك استخدام لغة CSS لتنسيق مظهر التطبيق وجعله أكثر جاذبية بصريًا باستخدام التصميم الذي تريد؛ ولكن إن لم تكن مصمم ويب، أو لم يكن لديك الخبرة الكافية في التعامل مع CSS، فبإمكانك استخدام حزمة بوتستراب Bootstrap، التي تزودك بمكونات سهلة الاستخدام لتنسيق مظهر التطبيق، إذ سنستخدم في تطبيقنا هذا بوتستراب.

وفي حال أردت إنشاء قوالب HTML أُخرى، فلن تضطر لإعادة كتابة الشيفرة التي كتبتها سابقًا في القالب index.html، فمن الممكن تفادي التكرار غير الضروري بالاستفادة من ملف القالب الرئيسي base template، والتي سترث كافّة ملفات HTML الأُخرى شيفرته.

ولكي ننشئ قالب رئيسي، سننشئ بدايةً ملفًا باسم base.html في مجلد القوالب templates:

(env)user@localhost:$ nano templates/base.html

ثم سنكتب الشيفرة التالية في القالب base.html:

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>{% block title %} {% endblock %}</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-light bg-light">
        <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
            <li class="nav-item active">
                <a class="nav-link" href="#">About</a>
            </li>
            </ul>
        </div>
    </nav>
    <div class="container">
        {% block content %} {% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>

اِحفظ الملف وأغلقه حالما تنتهي من تحريره.

إنّ الجزء الأكبر من الشيفرة السابقة هو تعليمات HTML معيارية وشيفرات لازمة لعمل بوتستراب، إذ تزوّد الوسوم <meta> متصفح الويب بالمعلومات، في حين ينشئ الوسم <link> ارتباطًا إلى ملفات CSS الخاصة ببوتستراب، ويضمِّن الوسم <script> شيفرة جافا سكربت مسؤولة عن بعض ميزات بوتستراب الإضافية.

أمّا الأجزاء من الشيفرة الموضحّة فيما يلي فهي خاصةٌ بمحرك القوالب جينجا:

  • {% block title %} {% endblock %}: كتلة برمجية تحجز مكانًا لعنوان الصفحة، والذي سنستخدمه في تحديد العنوان الخاص بكل صفحة في التطبيق دون الحاجة إلى إعادة كتابة قسم الترويسة <head> كاملًا في كل مرةٍ من أجل كل صفحة.
  • {{ url_for('index')‎ }}: استدعاءٌ لدالة تعيد عنوان URL إلى دالة عرض index()‎، والتي نمرر لها وسيطًا واحدًا فقط وهو اسم تابع فلاسك، لتنشئ ارتباطًا مع وجهة تابع فلاسك بدلًا من ملف ساكن كما كان الحال لدى استدعاء التابع ()url_for السابق الذي استخدمناه لإنشاء ارتباط مع ملف CSS الساكن.
  • {% block content %} {% endblock %}: كتلة برمجية أخرى ستُستبدل لاحقًا بالمحتوى الفعلي بالاعتماد على القالب الابن، أي القالب الذي يرث شيفرات القالب الرئيسي base.html.

والآن بعد أن أصبح لديك قالبٌ رئيسي، يمكنك الاستفادة من مميزاته باستخدام فكرة الوراثة، لذلك افتح الملف index.html:

nano templates/index.html

ثمّ استبدل محتوياته بما يلي:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
{% endblock %}

استخدمنا في هذه النسخة الجديدة من القالب template.html الوسم {% extends %} لترث الشيفرات من القالب الرئيسي base.html، وذلك عن طريق استبدال الشيفرة السابقة بكتلة المحتوى content في القالب الرئيسي.

تحوي كتلة المحتوى block content على وسم <h1> وبداخله كتلة عنوان title تحتوي العبارة "Welcome to FlaskBlog"، وبذلك نتفادى تكرار نفس النص مرتين، ذلك لأنّ هذا النص سيظهر في عنوان الصفحة ونص ضمن وسم <h1> أسفل شريط التصفح الموروث من القالب الرئيسي.

تمكنّك وراثة القوالب من إعادة استخدام شيفرة HTML الموجودة في القوالب الأخرى (القالب الرئيسي base.html في حالتنا) دون الحاجة لتكراره في كل مرة.

اِحفظ الملف وأغلقه، ثم حدِّث الصفحة الرئيسية index في المتصفح، فستظهر الصفحة بشريط تصفح وعنوانٍ منسقٍ كما يلي:

Second_img_step3b.png

وبذلك نكون قد استخدمنا قوالب HTML والملفات الساكنة في فلاسك، وكذلك استخدمنا بوتستراب لتنسيق مظهر الصفحة، واستفدنا من القالب الرئيسي لتفادي تكرار الشيفرات البرمجية. سنُعدُّ في الخطوة التالية قاعدة بيانات لتخزين بيانات التطبيق.

الخطوة الرابعة – إعداد قاعدة البيانات

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

سنستخدم ملف قاعدة بيانات من نوع SQLite لتخزين البيانات، لأن وحدة sqlite3 المُستخدمة للتعاطي مع قاعدة البيانات هذه موجودة وجاهزة افتراضيًا في مكتبة لغة بايثون.

بدايةً، وبما أنّ تخزين البيانات في SQLite يكون ضمن جداول وأعمدة، وكون بيانات تطبيقنا هي تدوينات، سننشئ جدولًا باسم posts يحتوي على كافّة الأعمدة اللازمة، إذ سننشئ ملفًا بلاحقة sql. يحتوي على أوامر SQL اللازمة لإنشاء جدول posts وأعمدته اللازمة، ثم سنستخدم هذا الملف لإنشاء قاعدة البيانات.

افتح الملف المُسمى schema.sql الموجود في المجلد flask_blog:

(env)user@localhost:$ nano schema.sql

اكتب تعليمات SQL التالية داخل هذا الملف:

DROP TABLE IF EXISTS posts;

CREATE TABLE posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    title TEXT NOT NULL,
    content TEXT NOT NULL
);

ثم اِحفظ الملف وأغلقه.

يعمل أوّل أمر من أوامر SQL وهو:

DROP TABLE IF EXISTS posts;

على حذف أي جداول موجودةٍ مسبقًا باسم posts لتجنُّب أي تضاربٍ، أو نتائج عشوائية ناتجةٍ عن تشابه أسماء الجداول، ومن الجدير بالملاحظة أنّ هذا الأمر سيحذِف كل المحتويات في قاعدة البيانات في كل مرة تشغِّل فيها أوامر SQL هذه، لذلك لا تُدخل أي بيانات مهمّة في التطبيق حتى تنتهي من كافّة الخطوات التعليميّة في المقال وتجربة نتائجه النهائية.

بينما ينشئ الأمر الثاني من أوامر SQL وهو:

CREATE TABLE posts

جدولًا باسم posts له الأعمدة التالية:

  • "id": ويحتوي على بياناتٍ من نوع رقم صحيح ويمثّل مفتاحًا أساسيًا يحتوي على قيمةٍ فريدة في قاعدة البيانات من أجل كل سجل (والسجل هو التدوينة في حالتنا).
  • "created": يحتوي على تاريخ ووقت إنشاء التدوينة، وتشير NOT NULL إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة، أما القيمة الافتراضية فهي CURRENT_TIMESTAMP والتي تمثِّل تاريخ ووقت إضافة التدوينة إلى قاعدة البيانات، وكما هو الحال في عمود id، لا يتوجب عليك تحديد قيم لهذا العمود، إذ أنها تُملأ تلقائيًا.
  • "title": عنوان التدوينة.
  • "content": محتوى التدوينة.

الآن، وبعد أن أصبح لدينا تخطيط قاعدة البيانات SQL المطلوب في ملف schema.sql، سنستخدمه لإنشاء قاعدة البيانات باستخدام ملف بايثون لإنشاء ملف قاعدة بيانات SQLite بلاحقة db..

افتح الملف المُسمّى init_db.py الموجود داخل المجلد flask_blog باستخدام محرِّر النصوص المفضل لديك:

(env)user@localhost:$ nano init_db.py

ثم اكتب ضمنه الشيفرة التالية:

import sqlite3

connection = sqlite3.connect('database.db')


with open('schema.sql') as f:
    connection.executescript(f.read())

cur = connection.cursor()

cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
            ('First Post', 'Content for the first post')
            )

cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)",
            ('Second Post', 'Content for the second post')
            )

connection.commit()
connection.close()

استوردنا في الشيفرة السابقة وحدة sqlite3، ثم أنشأنا اتصالًا مع ملف قاعدة بيانات باسم database.db، والذي يُنشأ تلقائيًا فور تشغيل ملف بايثون هذا، ومن ثم استخدمنا الدالة ()open لفتح الملف schema.sql، ثم نفّذنا باستخدام التابع ()executescript محتويات هذا الملف، الذي ينفِّذ عدة عبارات SQL معًا، وهكذا يُنشأ الجدول posts.

استخدمنا في الشيفرة السابقة كائن المؤشر Cursor، الذي يمكِّننا من استخدام تابعه ()execute لتنفيذ تعليمتي إدخال INSERT في SQL لإضافة تدوينتين معًا إلى جدول التدوينات posts. وفي النهاية أُرسلت هذه الأوامر إلى قاعدة البيانات وأُغلِق الاتصال المفتوح معها.

اِحفظ الملف وأغلقه، ثم شغِّله من موجّه الأوامر باستخدام أمر بايثون التالي:

(env)user@localhost:$ python init_db.py

وحالما ينتهي التنفيذ، سيكون لديك ملفٌ جديدٌ باسم database.db ضمن مجلد flask_blog، وهذا يدل على نجاح عملية إعداد قاعدة البيانات. سنعمل في الخطوة التالية على قراءة التدوينات المُضافة إلى قاعدة البيانات وعرضها في الصفحة الرئيسية للتطبيق.

الخطوة 5 – عرض كل التدوينات

الآن وبعد أن أعددنا قاعدة البيانات، يمكننا تعديل دالة عرض فلاسك index()‎ لعرض كل التدوينات الموجودة في قاعدة البيانات. لذلك، افتح الملف app.py لتنفيذ التعديلات التالية:

(env)user@localhost:$ nano app.py

سيكون التعديل الأوّل هو استيراد وحدة sqlite3 في بداية الملف على النحو التالي:

import sqlite3
from flask import Flask, render_template

. . .

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

. . .
from flask import Flask, render_template

def get_db_connection():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn
. . .

تفتح الدالة get_db_connection()‎ اتصالًا مع ملف قاعدة البيانات database.db، وتحدّد بعد ذلك قيمة السمة row_factory لتكون sqlite3.Row لنتمكّن من الوصول إلى الأعمدة باستخدام أسمائها، ما يعني أنّ اتصال قاعدة البيانات سيعيد سجلات يمكننا التعامل معها كما هو الحال مع قواميس بايثون الاعتيادية (وحدات تخزين القيم المنظمّة في بايثون)، ونهايةً يعيد التابع كائن الاتصال conn، الذي سنستخدمه للوصول إلى قاعدة البيانات.

بعد أن عرّفنا الدالة get_db_connection()‎، سنعدّل دالة index()‎ لتصبح كما يلي:

. . .
@app.route('/')
def index():
    conn = get_db_connection()
    posts = conn.execute('SELECT * FROM posts').fetchall()
    conn.close()
    return render_template('index.html', posts=posts)

بعد هذا التعديل الجديد على الدالة index()‎، فإنّنا نفتح اتصالًا مع قاعدة البيانات باستخدام الدالة get_db_connection()‎ التي عرفناها للتو. سننفذ بعد ذلك استعلام SQL للحصول على كل المدخلات الموجودة في الجدول posts، إذ أنّنا نستدعي التابع fetchall()‎ لجلب كل الأسطر الناتجة عن الاستعلام، وهذا سيعيد بالنتيجة قائمةً بالتدوينات المُدخلة إلى قاعدة البيانات في الخطوة السابقة.

يمكنك إغلاق الاتصال بقاعدة البيانات باستخدام التابع close()‎ وإعادة نتيجة عرض القالب index.html، كما يمكنك تمرر الكائن posts وسيطًا، فهو الكائن الحاوي على النتائج المُستخلصة من قاعدة البيانات، ويساعدنا هذا التمرير على الوصول إلى التدوينات برمجيًا داخل قالب index.html.

وبعد تنفيذ كل هذه التعديلات، احفظ الملف app.py وأغلقه.

الآن وبعد أن مرّرنا كل التدوينات المُستخلصة من قاعدة البيانات إلى القالب index.html، سنستخدم حلقة for التكرارية لعرض معلومات عن كل تدوينة داخل صفحة index.

افتح ملف index.html:

(env)user@localhost:$ nano templates/index.html

ثم عدِّله كما يلي:

{% extends 'base.html' %}
{% block content %}
    <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
    {% for post in posts %}
        <a href="#">
            <h2>{{ post['title'] }}</h2>
        </a>
        <span class="badge badge-primary">{{ post['created'] }}</span>
        <hr>
    {% endfor %}
{% endblock %}

تقابل هنا الصيغة البرمجية {% for post in posts %} حلقة for في محرّك جينجا jinja، والتي تشبه بناء حلقة for في لغة بايثون ما عدا ضرورة إغلاقها باستخدام الصيغة البرمجية {% endfor %}. الهدف من استخدام هذه الحلقة حاليًا هو المرور على كل عنصر في القائمة posts المُمررة إلى الدالة index()‎ من خلال السطر البرمجي التالي:

 return render_template('index.html', posts=posts)

وضمن هذه الحلقة نعرض عنوان التدوينة في وسمٍ عنوان <h2> داخل وسم الرابط <a> (لأنّنا سنستخدم هذا الوسم لاحقًا لإنشاء رابط مخصّص لعرض معلومات خاصة عن كل تدوينة).

يُعرض عنوان التدوينة باستخدام محدّد المتغير المُجرد Literal variable delimiter وهو {{ ... }}. تذكّر أنّ كل عنصر post في القائمة سيكون مشابهًا لما هو عليه في قاموس بايثون، وبالتالي يمكنك الوصول إلى عنوان التدوينة من خلال post['title']‎؛ كما من الممكن عرض تاريخ إنشاء التدوينة بنفس الآلية.

وحالما تنتهي من تعديل الملف، احفظه وأغلقه، ثم انتقل في المتصفح إلى صفحة index، وعندها ستظهر لك المشاركتان posts المُضافتان سابقًا إلى قاعدة البيانات.

third_img_step5.png

والآن، وبعد تعديل دالة index()‎ المسؤولة عن عرض كل التدوينات الموجودة في قاعدة البيانات داخل الصفحة الرئيسية للتطبيق، سننتقل إلى خطوة عرض كل تدوينة في صفحة خاصة بها وذلك من خلال تمكين المستخدمين من الانتقال إلى أي تدوينة -كلٌ على حدى-.

الخطوة السادسة – عرض تدوينة بحد ذاتها

تهدف هذه الخطوة إلى عرض تدوينة واحدة بناءً على معرّفها ID، سننشئ وجهة route فلاسك ودالة عرض جديدة، إضافةً لقالب HTML جديد.

في نهاية هذه الخطوة، سيكون الرابط "http://127.0.0.1:5000/1" مثلًا هو الصفحة التي تعرض التدوينة الأولى، لأن معرف التدوينة الأولى هو الرقم 1؛ أي سيعرض الرابط "http://127.0.0.1:5000/ID" التدوينة ذات المعرّف ID في حال وجودها في قاعدة البيانات.

الآن افتح الملف app.py لتحريره:

(env)user@localhost:$ nano app.py

وطالما أنّنا سنجلب التدوينات من قاعدة البيانات بناءً على معرّفها ID من مسارات متعدّدة لاحقًا عند استخدام التطبيق، سننشئ دالةً منفردة باسم get_post()‎ تعيد معلومات التدوينة ذات المعرّف ID هذا بمجرد تمرير القيمة ID وسيطًا لها، أما في حال عدم وجود تدوينة تحمل هذا المعرّف في قاعدة البيانات، ستكون الاستجابة برسالة "غير موجود 404 Not Found"؛ ولنتمكَّن من الاستجابة برسالة الخطأ رقم 404، لا بدّ من تضمين الدالة abort()‎ من مكتبة فلاسك Werkzeug التي يجري تنزيلها أثناء تثبيت فلاسك.

ننجز الاستدعاء السابق أعلى الملف على النحو التالي:

import sqlite3
from flask import Flask, render_template
from werkzeug.exceptions import abort
. . .

ثم نضيف الدالة get_post()‎ تمامًا بعد الدالة get_db_connection()‎ المُعرّفة في الخطوة السابقة.

. . .
def get_db_connection():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn
def get_post(post_id):
    conn = get_db_connection()
    post = conn.execute('SELECT * FROM posts WHERE id = ?',
                        (post_id,)).fetchone()
    conn.close()
    if post is None:
        abort(404)
    return post
. . .

تتضمّن الدالة الجديدة الوسيط post_id، الذي يحدّد التدوينة المُعادة نتيجةً لاستدعاء هذه الدالة.

تُستخدم خلال تنفيذ هذه الدالة دالةٌ أخرى، هي get_db_connection()‎ لفتح اتصال مع قاعدة البيانات وتنفيذ استعلام SQL لجلب التدوينة الموافقة لقيمة post_id الممرّرة، كما يُستخدم التابع fetchone()‎ لجلب نتيجة الاستعلام وتخزينها في المتغير post، ومن ثمّ يُغلق الاتصال مع قاعدة البيانات؛ فإذا كانت قيمة المتغير post فارغة None، فهذا يدل على عدم العثور على نتائج في قاعدة البيانات، وعندها نستخدم الدالة abort()‎ التي ضمّناها توًّا للرد برقم الخطأ 404 وبالتالي التوقُّف عن متابعة تنفيذ شيفرة الدالة؛ أما في حال العثور على التدوينة، تُعاد قيمة المتغير post.

الآن، أضف دالة عرض فلاسك التالية في نهاية الملف app.py:

. . .
@app.route('/<int:post_id>')
def post(post_id):
    post = get_post(post_id)
    return render_template('post.html', post=post)

سنضيف في دالة عرض فلاسك الجديدة قاعدة متغيرة Variable Rule هي <int:post_id> لنحدّد أنّ الجزء بعد العلامة (/) هو رقمٌ صحيحٌ موجب، وذلك بتحويل قيمة هذا الجزء إلى النوع int، وهو ما نحتاجه للوصول إلى دالة العرض. يتعرف فلاسك على هذا الشرط ويمرّر القيمة إلى الوسيط post_id في دالة فلاسك post()‎، وبعد ذلك يمكننا استخدام الدالة ()get_post للحصول على التدوينة المرتبطة بالمعرّف ID المحدّد، ومن ثمّ تخزين النتيجة في المتغير post، الذي سيُمرّر في النهاية إلى القالب post.html، الذي سننشئه فيما يلي.

الآن احفظ الملف app.py وافتح ملف قالب جديد post.html لتحريره:

(env)user@localhost:$ nano templates/post.html

اكتب الشيفرة التالية في الملف الجديد post.html، إذ تتشابه هذه الشيفرة مع تلك المُستخدمة في ملف index.html، إلّا أنّنا سنعرض معلومات عن تدوينةٍ واحدةٍ فقط، إضافةً إلى عرض محتويات هذه التدوينة:

{% extends 'base.html' %}
{% block content %}
    <h2>{% block title %} {{ post['title'] }} {% endblock %}</h2>
    <span class="badge badge-primary">{{ post['created'] }}</span>
    <p>{{ post['content'] }}</p>
{% endblock %}

اضفنا كتلة العنوان title المُعرفة سابقًا في القالب base.html لجعل عنوان التدوينة معروضًا باستخدام وسم من نوع عنوان <h2>، وجعله أيضًا عنوانًا للصفحة.

اِحفظ الملف وأغلقه.

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

http://127.0.0.1:5000/1
http://127.0.0.1:5000/2
http://127.0.0.1:5000/3

سنعود إلى الصفحة الرئيسية لإنشاء رابط لكل عنوان تدوينة لينقلنا إلى صفحة التدوينة الخاصة به باستخدام الدالة url_for()‎، لذا افتح قالب index.html أولًا لتحريره:

(env)user@localhost:$ nano templates/index.html

ثم عدّل قيمة السمة href لتصبح {{ url_for('post', post_id=post['id'])‎ }} بدلًا من #، بحيث تبدو حلقة for على النحو التالي:

{% for post in posts %}
    <a href="{{ url_for('post', post_id=post['id']) }}">
        <h2>{{ post['title'] }}</h2>
    </a>
    <span class="badge badge-primary">{{ post['created'] }}</span>
    <hr>
{% endfor %}

تُمرَّر القيمة post  إلى الدالة url_for()‎ التي تحتاج رقم معرّف التدوينة وسيطًا لها، وبما أنها اسم دالة العرض ()post فتُستدعى على النحو التاليpost['id' ]‎ لتُستخدم وسيطًا للدالةurl_for()‎ التي ستعيد بالنتيجة الرابط المناسب لكل تدوينة بناءً على معرّفها ID.

اِحفظ الملف وأغلقه.

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

الخطوة 7 – التعديل على جدول التدوينات

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

إنشاء تدوينة جديدة

لدينا حتى الآن تطبيقٌ يعرض التدوينات من قاعدة البيانات، ولكنه لا يوفر أي طريقةٍ لإضافة تدوينات جديدة، إلّا عن طريق إضافة تدوينة يدويًا عبر الاتصال بقاعدة البيانات SQLite. فيما يلي سننشئ صفحةً تمكنّنا من إضافة تدوينة جديدة عن طريق إدخال عنوانها ومحتواها مباشرةً.

افتح ملف app.py لتحريره:

(env)user@localhost:$ nano app.py

لا بُد أولًا من استيراد التالي من إطار العمل فلاسك:

  • الكائن request العام المسؤول عن الوصول إلى بيانات الطلب والتي ستُرسل من خلال نموذج HTML.
  • الدالة url_for()‎ لتوليد عناوين الروابط.
  • الدالة flash()‎ لعرض رسالةٍ وامضة عند انتهاء معالجة الطلب.
  • الدالة redirect()‎ لإعادة توجيه المستخدم إلى موقعٍ آخر في المتصفح.

أضِف هذه الاستيرادات إلى الملف كما يلي:

import sqlite3
from flask import Flask, render_template, request, url_for, flash, redirect
from werkzeug.exceptions import abort
. . .

تخزّن الدالة flash()‎ الرسائل في جلسة المتصفح لدى المستخدم، وهذا ما يتطلّب إعداد مفتاح أمان Secret Key، إذ سيُستخدم هذا المفتاح لجعل الجلسات آمنة، وبذلك يتمكّن فلاسك من الحفاظ على المعلومات عند الانتقال من طلبٍ إلى آخر، مثل حالة الانتقال من صفحة إنشاء تدوينة جديدة إلى صفحة عرض كل التدوينات، مع ملاحظة أن المستخدم يستطيع الوصول إلى المعلومات المُخزّنة في الجلسة، ولكنه لا يستطيع تعديلها إلّا إذا كان لديه مفتاح الأمان، وبالتالي لا يجب أن تسمح لأي أحدٍ بالوصول إلى مفتاح الأمان الخاص بك.

ولإعداد مفتاح أمان، سنضيف ضبط SECRET_KEY إلى التطبيق من خلال الكائن app.config، الذي سنضيفه مباشرةً بعد تعريف الكائن app وقبل دالة عرض فلاسك index()‎، على النحو التالي:

. . .
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your secret key'


@app.route('/')
def index():
    conn = get_db_connection()
    posts = conn.execute('SELECT * FROM posts').fetchall()
    conn.close()
    return render_template('index.html', posts=posts)

. . .

تذكّر أنّ مفتاح الأمان يجب أن يكون سلسلةً نصيةً عشوائية بطولٍ مناسب.

الآن وبعد إعداد مفتاح الأمان، سننشئ دالة عرض فلاسك مسؤولة عن إخراج قالب يحتوي على نموذج إدخال يمكن للمستخدم ملؤه لإنشاء تدوينة جديدة. أضِف دالة فلاسك هذه في نهاية الملف على النحو التالي:

. . .
@app.route('/create', methods=('GET', 'POST'))
def create():
    return render_template('create.html')

ستنشئ هذه الشيفرة وجهة route مهمتها قبول الطلبات سواءً بطريقة GET، أو POST، إذ أن طريقة GET هي الطريقة الافتراضية؛ ولتمكين قبول طريقة POST أيضًا المُستخدمة من قبل المتصفح عند إرسال نماذج الإدخال، سنمرر متغيرًا من نوع tuple يحتوي على طرق الطلبات المقبولة إلى وسيط الطرائق methods في المزخرف ‎@app.route()‎.

اِحفظ الملف وأغلقه، ثمّ لإنشاء القالب، افتح ملفًا باسم create.html داخل مجلد القوالب templates على النحو التالي:

(env)user@localhost:$ nano templates/create.html

أضف الشيفرة التالية داخل هذا الملف الجديد:

{% extends 'base.html' %}
{% block content %}
<h1>{% block title %} Create a New Post {% endblock %}</h1>
<form method="post">
    <div class="form-group">
        <label for="title">Title</label>
        <input type="text" name="title"
               placeholder="Post title" class="form-control"
               value="{{ request.form['title'] }}"></input>
    </div>
    <div class="form-group">
        <label for="content">Content</label>
        <textarea name="content" placeholder="Post content"
                  class="form-control">{{ request.form['content'] }}</textarea>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
{% endblock %}

الجزء الأكبر من هذه الشيفرة ما هو إلّا تعليمات HTML معيارية لإظهار صندوق إدخال لاستقبال عنوان التدوينة، وصندوق كتابة لتحرير محتواها، وزرٌ لتأكيد إرسال النموذج.

القيمة داخل أداة عنوان التدوينة هي {{ request.form['title']‎ }} والقيمة داخل أداة صندوق الكتابة هي {{ request.form['content']‎ }}، وقد فعلنا هذا للحفاظ على البيانات المدخلة في كل من الأداتين في حال حدوث خطأ ما؛ فمثلًا إذا كتب المستخدم محتوًى كبير للتدوينة وأرسل النموذج دون إدخال عنوان، فستظهر رسالةٌ تعلمه أنّ العنوان مطلوب دون فقدان المحتوى الذي كتبه في حقل المحتوى، لأنّ المعلومات المدخلة سيُحتفظ بها في الكائن العام request.

الآن وفي حين خادم التطوير يعمل، استخدم المتصفح للانتقال إلى الوجهة ‎/create:

http://127.0.0.1:5000/create

فستظهر لك صفحة إنشاء تدوينة جديدة مع أدوات لإدخال كلٍ من العنوان والمحتوى.

fourth_img_step7a.png

يرسل نموذج الإدخال هذا الطلب بطريقة POST إلى دالة عرض فلاسك create()‎، ولكن حتى هذه اللحظة لا يوجد شيفرة مسؤولة عن معالجة هذا الطلب في الدالة، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله.

لذا فيما يلي ستعالج الدالة create()‎ الطلبات الواردة بطريقة POST عند إرسال محتويات نموذج الإدخال وذلك بعد التحقق من قيمة تابع الطلب request.method؛ فإذا كانت قيمته 'POST'، تستمر بمتابعة قراءة البيانات المرسلة والتحقق منها وإدخالها في قاعدة البيانات.

الآن، افتح الملف app.py لتحريره:

(env)user@localhost:$ nano app.py

عدِّل شيفرة دالة عرض فلاسك create()‎ لتصبح كما يلي:

. . .

@app.route('/create', methods=('GET', 'POST'))
def create():
    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']

        if not title:
            flash('Title is required!')
        else:
            conn = get_db_connection()
            conn.execute('INSERT INTO posts (title, content) VALUES (?, ?)',
                         (title, content))
            conn.commit()
            conn.close()
            return redirect(url_for('index'))

    return render_template('create.html')

تَمكَّنا باستخدام العبارة الشرطية التي توازن قيمة request.method مع القيمة POST من التحقُّق بأنّ التعليمات التالية لها لن تُنفّذ إلّا إذا كان الطلب الحالي هو فعلًا بطريقة POST، ومن ثمّ قرأنا قيم العنوان والمحتوى المرسلين من الكائن request.form الذي يمكِّننا من الوصول إلى بيانات نموذج الإدخال المُضمّنة في الطلب؛ ففي حال عدم إدخال قيمةٍ للعنوان، فسيتحقق الشرط if not title وبالتالي ستظهر رسالة للمستخدم نعلمه من خلالها بأن العنوان مطلوب؛ أمّا في حال وجود العنوان سيُفتَح الاتصال مع قاعدة البيانات باستخدام الدالة get_db_connection()‎ لإدخال العنوان والمحتوى المرسلين إلى الجدول posts فيها.

بعد تنفيذ هذه التعديلات في قاعدة البيانات وإضافة التدوينة الجديدة إليها، يُغلق الاتصال معها، ومن ثمّ نعيد توجيه الطلب إلى الصفحة الرئيسية باستخدام الدالة redirect()‎ بتمرير الرابط الناتج عن التابع url_for()‎ الذي قد مررنا له القيمة index وسيطًا.

اِحفظ الملف وأغلقه، ثم انتقل في المتصفح إلى الوجهة ‎/create:

http://127.0.0.1:5000/create

املأ النموذج بالعنوان والمحتوى الذي تريد، وسترى التدوينة الجديدة في الصفحة الرئيسية حال إرسال النموذج.

الآن سنعمل على إظهار الرسائل للمستخدم وإضافة رابط في شريط التصفح في القالب base.html لتسهيل الوصول إلى هذه الصفحة الجديدة، من خلال فتح ملف القالب:

(env)user@localhost:$ nano templates/base.html

الآن، عدِّل الملف من خلال إضافة وسم القائمة <li> بعد الرابط About داخل وسم شريط التصفُّح <nav>، ثمّ أضف حلقة تكرارية أعلى كتلة المحتوى مباشرةً لعرض الرسائل أسفل شريط التصفح، إذ يوفّر فلاسك هذه الرسائل من خلال دالة فلاسك الخاصة get_flashed_messages()‎:

<nav class="navbar navbar-expand-md navbar-light bg-light">
    <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav">
        <li class="nav-item">
            <a class="nav-link" href="#">About</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="{{url_for('create')}}">New Post</a>
        </li>
        </ul>
    </div>
</nav>
<div class="container">
    {% for message in get_flashed_messages() %}
        <div class="alert alert-danger">{{ message }}</div>
    {% endfor %}
    {% block content %} {% endblock %}
</div>

اِحفظ الملف وأغلقه. سيتضمن الآن شريط التصفح عنصرًا جديدًا باسم "New Post"، الذي ينقلنا إلى الوجهة ‎/create.

تعديل تدوينة منشورة

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

بدايةً، أضِف وجهة route جديدة إلى الملف app.py، إذ ستستقبل دالة هذه الوجهة رقم التدوينة ID المُراد تعديلها على هيئة وسيط، بحيث يكون الرابط بالشّكل ‎/post_id/edit إذ يمثّل المتغير post_id رقم التدوينة. ولإنجاز ذلك، ابدأ بفتح الملف app.py في وضع التحرير على النحو التالي:

(env)user@localhost:$ nano app.py

ومن ثمّ أضف في نهاية الملف دالة edit()‎ المشابهة للدالة create()‎ التي أنشأناها سابقًا، لأنّ تعديل تدوينة منشورة أصلًا أمرٌ مشابهٌ من حيث المبدأ البرمجي لإنشاء تدوينة جديدة:

. . .

@app.route('/<int:id>/edit', methods=('GET', 'POST'))
def edit(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        content = request.form['content']

        if not title:
            flash('Title is required!')
        else:
            conn = get_db_connection()
            conn.execute('UPDATE posts SET title = ?, content = ?'
                         ' WHERE id = ?',
                         (title, content, id))
            conn.commit()
            conn.close()
            return redirect(url_for('index'))

    return render_template('edit.html', post=post)

نلاحظ من بناء الدالة السابقة تحديد التدوينة المراد تعديلها من خلال معرفة الرابط الخاص بها URL، وسيمرّر فلاسك رقم ID الخاص بها إلى دالة ()edit وسيطًا، ومن ثم نضيف هذه القيمة إلى دالة استدعاء التدوينات get_post()‎ لاستدعاء التدوينة ذات رقم ID المحدّد من قاعدة البيانات، ومن ثم تُضاف البيانات الجديدة إليها على هيئة طلب تدوينة جديدة POST وذلك ضمن الجملة الشرطية `if request.method == 'POST.

وعلى نحوٍ مشابه لحالة إنشاء تدوينة جديدة، سنبدأ باستخلاص البيانات من الكائن request.form، بحيث تُعرض رسالة تنبيهية في حال كانت قيمة العنوان فارغة، وإلّا سنؤسس الاتصال مع قاعدة البيانات، ثم نحدَّث جدول التدوينات posts بالعنوان والمحتويات الجديدة للتدوينة ذات الرقم ID الموافق للرقم الذي حصلنا عليه من رابط التدوينة المُراد تعديلها.

أمّا بالنسبة للطلب GET، فسيحدث إخراجٌ لقالب edit.html المُمرر إلى المتغير post، الذي يحتوي القيمة المُعادة من الدالة get_post()‎، والهدف من هذا الطلب استعراض محتوى وعنوان التدوينة الحالية ضمن صفحة التعديل.

احفظ الملف وأغلقه، ثمّ أنشئ قالب edit.html جديد على النحو التالي:

(env)user@localhost:$ nano templates/edit.html

اكتب الشيفرة البرمجية التالية في الملف الجديد:

{% extends 'base.html' %}

{% block content %}
<h1>{% block title %} Edit "{{ post['title'] }}" {% endblock %}</h1>

<form method="post">
    <div class="form-group">
        <label for="title">Title</label>
        <input type="text" name="title" placeholder="Post title"
               class="form-control"
               value="{{ request.form['title'] or post['title'] }}">
        </input>
    </div>

    <div class="form-group">
        <label for="content">Content</label>
        <textarea name="content" placeholder="Post content"
                  class="form-control">{{ request.form['content'] or post['content'] }}</textarea>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
<hr>
{% endblock %}

اِحفظ الملف وأغلقه.

تتبِّع هذه الشيفرة البرمجية نفس النمط السابق ما عدا التعليمتان:

{{ request.form['title'] or post['title']‎ }}

{{ request.form['content'] or post['content']‎ }}

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

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

http://127.0.0.1:5000/1/edit

فستظهر لك صفحة تعديل "التدوينة الأولى First Post" على النحو التالي:

fifth_img_step7b.png

عدّل التدوينة ثمّ أكّد النموذج وأرسله، وتأكّد من تحديث بيانات التدوينة.

الآن، يجب إضافة رابط يشير إلى صفحة التعديل لكل تدوينة وذلك في صفحة المؤشر (الفهرس)، لذلك افتح ملف القالب index.html:

(env)user@localhost:$ nano templates/index.html

عدّل الملف ليصبح على النحو التالي:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
    {% for post in posts %}
        <a href="{{ url_for('post', post_id=post['id']) }}">
            <h2>{{ post['title'] }}</h2>
        </a>
        <span class="badge badge-primary">{{ post['created'] }}</span>
        <a href="{{ url_for('edit', id=post['id']) }}">
            <span class="badge badge-warning">Edit</span>
        </a>
        <hr>
    {% endfor %}
{% endblock %}

ويمكنك إضافة وسم <a> للربط مع الدالة ()edit، مرورًا بالقيمة post['id']‎ لربط صفحة التعديل لكلِّ تدوينة مع الرابط Edit.

حذف تدوينة

أحيانًا، يصبح بقاء بعض التدوينات منشورةً للعموم أمرًا غير مطلوب، ومن هنا تأتي أهمية إمكانية حذف التدوينات المنشورة أصلًا، وفيما يلي سنتعلّم كيفيّة إضافة آلية لحذف هذه التدوينات من التطبيق.

بدايةً، سنضيف وجهة جديدة للحذف، هو ‎/ID/delete يتعامل مع الطلبات من النوع POST بما يشبه الدالة edit()‎ الذي أنشأناها سابقًا، إذ ستستقبل دالة الحذف delete()‎ رقم معرّف التدوينة ID لحذفها من الرابط URL.

الآن، افتح الملف app.py:

(env)user@localhost:$ nano app.py

أضِف الدالة التالية إلى نهاية الملف:

# ....

@app.route('/<int:id>/delete', methods=('POST',))
def delete(id):
    post = get_post(id)
    conn = get_db_connection()
    conn.execute('DELETE FROM posts WHERE id = ?', (id,))
    conn.commit()
    conn.close()
    flash('"{}" was successfully deleted!'.format(post['title']))
    return redirect(url_for('index'))

تتعامل هذه الدالة فقط مع الطلبات الواردة بطريقة POST؛ وهذا يعني أنك إذا انتقلت إلى الوجهة ‎/ID/delete في المتصفح، ستحصل على خطأ، لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات؛ ولكن يمكنك الوصول إلى هذه الوجهة من خلال نموذج الإدخال الذي يرسل طلبًا بطريقة POST يتضمن قيمة معرّف التدوينة المُراد حذفها، لتستقبل دالة فلاسك هذه قيمة المعرّف ID وتستخدمها لجلب التدوينة من قاعدة البيانات باستخدام دالة get_post()‎.

افتح بعد ذلك اتصالًا مع قاعدة البيانات ونفِّذ تعليمة SQL التالية لحذف التدوينة:

DELETE FROM

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

لاحظ أننا هنا لا نخرج render ملف قالب، وإنمّا نضيف فقط زر أوامر "Delete" إلى صفحة تعديل التدوينة.

الآن افتح ملف القالب edit.html:

(env)user@localhost:$ nano templates/edit.html

ثم أضف وسم النموذج <form> بعد وسم إظهار الفاصل الأفقي <hr> تماماً بعد السطر البرمجي {% endblock %} على النحو التالي:

<hr>
<form action="{{ url_for('delete', id=post['id']) }}" method="POST">
    <input type="submit" value="Delete Post"
            class="btn btn-danger btn-sm"
            onclick="return confirm('Are you sure you want to delete this post?')">
</form>
{% endblock %}

إذ استخدمنا التابع confirm()‎ لعرض رسالة تأكيد قبل إرسال الطلب.

الآن انتقل إلى صفحة تعديل تدوينة في المتصفح مجددًا وجرّب حذفها:

http://127.0.0.1:5000/1/edit

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

الخاتمة

قدّمنا في هذا المقال المفاهيم الأساسية لإطار عمل فلاسك في بايثون، وتعلمّت كيفيّة إنشاء تطبيق ويب بسيط وتشغيله في خادم تطوير، وآلية تمكين المستخدم من التواصل مع الخادم وتزويده ببياناتٍ مخصصة باستخدام محدِّدات الرابط URL ونماذج الادخال؛ كما اسُتخدم محرك قوالب جينجا jinja لتوفير ملفات HTML ذات المحتوى القابل لإعادة الاستخدام لكتابة منطق التطبيق فيها. ومع نهاية هذا المقال سيكون لديك مدونة ويب بكامل المميزات، والتي تتعامل مع قاعدة بيانات SQLite لإنشاء وعرض أو تعديل أو حذف التدوينات باستخدام لغة بايثون واستعلامات SQL.

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

يوجد العديد من الإضافات المطوَّرة من قبل المبرمجين لفلاسك والتي يمكنك استخدامها لتسهيل عملية التطوير، ومنها:

  • Flask-Login: لإدارة جلسة المستخدم ومعالجة عمليات تسجيل الدخول وتسجيل الخروج وتذكّر معلومات المستخدمين الذين سجلوا الدخول سابقًا.
  • Flask-SQLAlchemy: لتسهيل استخدام فلاسك مع مكتبة SQLAlchemy، والتي هي حزمة SQL في بايثون للتعامل مع قواعد بيانات SQL.
  • Flask-Mail: لتنفيذ مهام إرسال رسائل البريد الإلكتروني من خلال تطبيق فلاسك.

ترجمة -وبتصرف- للمقال How To Make a Web Application Using Flask in Python 3 لصاحبه Abdelhadi Dyouri.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...