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

استخدام قاعدة بيانات SQLite في تطبيق فلاسك


محمد الخضور

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

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

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

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

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

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

الخطوة الأولى - إعداد قاعدة البيانات

سنعمل في هذه الخطوة على إعداد قاعدة بيانات من النوع SQLite والتي سنخزّن فيها بيانات التطبيق (وهي التدوينات في حالة تطبيقنا)، ثم سنملؤها ببعض المُدخلات التجريبية، وسنستخدم الوحدة sqlite3 المتوفرة بسهولة في مكتبة بايثون المعيارية بغية التخاطب والتفاعل مع قاعدة البيانات.

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

لذا، افتح ملف تخطيط قاعدة البيانات المُسمى "schema.sql" الموجود في المجلد "flask_app":

$ 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 -في ملف تخطيط قاعدة البيانات السابق- على حذف أي جداول موجودةٍ مسبقًا باسم "posts" لتجنُّب أي تضارب، أو نتائج عشوائية ناتجةٍ عن تشابه أسماء الجداول (مثل حالة وجود جدولين بنفس الاسم وبأعمدة مُختلفة في قاعدة البيانات)، ولكن وفي حالتنا الآن وكوننا لم ننشئ أي جداول بعد، فلن يُنفَّذ هذا السطر في الوقت الراهن، ومن الجدير بالملاحظة أنّ هذا الأمر سيحذِف كل المحتويات في قاعدة البيانات في كل مرة تُشغِّل فيها أوامر SQL هذه، ولكن في حالة مثالنا سننفذ هذا الملف لمرّة واحدة فقط، إلّا أنّك قد ترغب مُستقبلًا بتنفيذه مُجدّدًا بغية تفريغ قاعدة البيانات من كافّة محتوياتها والبدء من جديد بقاعدة بيانات فارغة.

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

CREATE TABLE posts

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

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

والآن سنستخدم ملف تخطيط قاعدة البيانات "schema.sql" لإنشاء قاعدة البيانات، لذا سنُنشئ ملف بايثون سيبني بدوره ملف قاعدة بيانات SQLite بلاحقة db. اعتمادًا على الملف schema.sql.

افتح الملف "init_db.py" ضمن المجلد "flask_app" لتحريره (باستخدام محرِّر النصوص المفضل لديك، وهو محرر نانو nano هنا):

$ 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"، وفي النهاية أُرسلت هذه الأوامر إلى قاعدة البيانات وأُغلِق الاتصال المفتوح معها.

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

$ python init_db.py

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

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

سنعمل في هذه الخطوة على إنشاء تطبيق فلاسك ذو صفحة رئيسية مسؤولة عن عرض التدوينات الموجودة في قاعدة البيانات. بعد تفعيل البيئة البرمجية وتثبيت فلاسك، سنفتح ملفًا باسم "app.py" ضمن المجلد "flask_app" لتحريره:

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

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

import sqlite3
from flask import Flask, render_template

app = Flask(__name__)

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


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

نحفظ الملف ونغلقه.

استوردنا بدايةً في الشيفرة السابقة وحدة sqlite3 لاستخدامها في إنشاء الاتصال مع قاعدة البيانات، ومن ثمّ استوردنا كل من الصنف Flask ودالة تصيير القوالب ()render_template من حزمة فلاسك، كما أنشأنا نسخةً فعليةً من التطبيق باسم app، ومن ثمّ عرفنا دالةً باسم ()get_db_connection لإنشاء اتصال مع ملف قاعدة البيانات "database.db" المُنشأ مُسبقًا، وتحدّد بعد ذلك قيمة السمة row_factory لتكون sqlite3.Row لنتمكّن من الوصول إلى الأعمدة باستخدام أسمائها، ما يعني أنّ اتصال قاعدة البيانات سيعيد سجلات يمكننا التعامل معها كما هو الحال مع قواميس بايثون النمطية، ونهايةً تعيد الدالة كائن الاتصال conn، الذي سنستخدمه للوصول إلى قاعدة البيانات.

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

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

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

لذا سننشئ مجلدًا للقوالب باسم "templates"، وسننشئ ضمنه ملف قالب باسم "base.html"، والذي سيمثّل القالب الأساسي لبقية القوالب، كما يلي:

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

وسنكتب فيه الشيفرة التالية:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock %}- FlaskApp</title>
    <style>
        .post {
            padding: 10px;
            margin: 5px;
            background-color: #f3f3f3
        }

        nav a {
            color: #d64161;
            font-size: 3em;
            margin-left: 50px;
            text-decoration: none;
        }
    </style>
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">FlaskApp</a>
        <a href="#">About</a>
    </nav>
    <hr>
    <div class="content">
        {% block content %} {% endblock %}
    </div>
</body>
</html>

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

يتضمّن القالب الأساسي كافّة الشيفرات المتداولة التي سنحتاجها في القوالب الأُخرى.

ستُستبدل لاحقًا كتلة العنوان title بعنوان كل صفحة وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن رابطين، الأوّل ينقل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for لتحقيق الربط مع دالة العرض ()index، والآخر لصفحة المعلومات حول التطبيق في حال قررت تضمينها في تطبيقك.

الآن، سننشئ ملف قالب باسم "index.html" وهو الاسم الذي حددناه في الملف "app.py":

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

ونضيف ضمنه الشيفرة التالية:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Posts {% endblock %}</h1>
    {% for post in posts %}
        <div class='post'>
            <p>{{ post['created'] }}</p>
            <h2>{{ post['title'] }}</h2>
            <p>{{ post['content'] }}</p>
        </div>
    {% endfor %}
{% endblock %}

نحفظ الملف ونغلقه.

اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب "base.html" من خلال تعليمة extends، واستبدلنا محتوى كتلة المحتوى content مُستخدمين تنسيق عنوان من المستوى الأوّل <h1> الذي يفي أيضًا بالغرض ويكون عنوانًا للصفحة.

استخدمنا في السطر البرمجي {% for post in posts %} حلقة for من تعليمات محرّك القوالب جينجا jinja، والهدف من استخدام هذه الحلقة هو المرور على كل عنصر في القائمة posts، وبما أنّ كل عنصر (تدوينة post) في القائمة سيكون مشابهًا لما هو عليه في قاموس بايثون، فمن الممكن الوصول إلى تاريخ إنشاء التدوينة باستخدام الأمر {{ post['created']‎ }}، وإلى عنوانها من خلال post['title'] }}‎ }}، وإلى محتواها من خلال {{ post['content']‎ }}.

الآن ومع وجودنا ضمن المجلد "flask_app" ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف app.py) باستخدام متغير البيئة FLASK_APP على النحو التالي:

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

نضبط متغير البيئة FLASK_ENV على القيمة development لتشغيل التطبيق في وضع التطوير مع تشغيل مُنقّح الأخطاء، ولمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية (مع ملاحظة أنّنا نستخدم الأمر set في بيئة ويندوز عوضًا عن الأمر export?

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

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

(env)user@localhost:$ flask run

وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح:

http://127.0.0.1:5000/

وعندها ستظهر لك التدوينتان المُضافتان سابقًا إلى قاعدة البيانات بالشّكل التالي:

ظهور التدوينات المضافة إلى قاعدة البيانات

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

الخطوة 3 - إنشاء تدوينات

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

لذلك، سنفتح نافذة أسطر أوامر جديدة مع بقاء خادم التطوير قيد التشغيل، ثمّ سنفتح الملف "app.py":

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

للتعامل مع نموذج الويب، لا بُد أولًا من استيراد التالي من حزمة فلاسك:

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

أضِف هذه الاستيرادات إلى السطر الأوّل من ملف "app.py" كما يلي:

from flask import Flask, render_template, request, url_for, flash, redirect

# ...

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

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

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

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

بعد ذلك، سنضيف الوجهة التالية إلى نهاية الملف app.py :

# ...

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

نحفظ الملف ونغلقه.

نمرّر في هذه الوجهة صفًا tuple يحتوي على القيم ('GET', 'POST') إلى المعامل methods بغية السماح بكلا نوعي طلبيات HTTP وهما GET و POST؛ إذ تتخصّص الطلبيات من النوع GET بجلب البيانات من الخادم؛ أمّا الطلبيات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبيات من النوع GET هي الوحيدة المسموحة افتراضيًا، وحالما يطلب المستخدم الوجهة create/ باستخدام طلبية من النوع GET، سيُصيّر ملف قالب باسم "create.html".

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

والآن، افتح ملف "create.html" الجديد داخل مجلد القوالب "templates" على النحو التالي:

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

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

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Add a New Post {% endblock %}</h1>
    <form method="post">
        <label for="title">Title</label>
        <br>
        <input type="text" name="title"
               placeholder="Post title"
               value="{{ request.form['title'] }}"></input>
        <br>

        <label for="content">Post Content</label>
        <br>
        <textarea name="content"
                  placeholder="Post content"
                  rows="15"
                  cols="60"
                  >{{ request.form['content'] }}</textarea>
        <br>
        <button type="submit">Submit</button>
    </form>
{% endblock %}

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

وبذلك نكون قد ورثنا خصائص القالب "base.html"، واستخدمنا الوسم <form> وفيه ضبطنا سمة نوع طلبية HTTP لتكون من النوع POST ما يعني أنّ نموذج الويب هذا سيرسل طلب من النوع POST، كما أضفنا صندوق نصي باسم title لنستخدمه في الوصول إلى بيانات عنوان التدوينة في الوجهة create/، وضبطنا القيمة داخل الصندوق النصي هذا الخاص بعنوان التدوينة إلى {{ request.form['title']‎ }} والتي قد تكون فارغةً، أو نسخةً محفوظةً مؤقتًا من العنوان في حال كون النموذج المرسل خاطئًا، وهذا ما يحافظ على البيانات المدخلة في الأداة عند حدوث خطأ ما.

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

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

http://127.0.0.1:5000/create

فستظهر لك صفحة إنشاء تدوينة جديدة مع صندوق نصي لإدخال العنوان، وحقل نصي مُتعدّد الأسطر لإدخال المحتوى، وزر لتأكيد إرسال النموذج Submit، كما في الشّكل التالي:

صفحة إنشاء تدوينة جديدة

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

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

الآن، افتح الملف "app.py" بهدف تعديله ليتعامل مع الطلبات من النوع POST المُرسلة من قبل المستخدم:

(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!')
        elif not content:
            flash('Content 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')

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

تَمكَّنا باستخدام العبارة الشرطية if request.method == 'POST' التي تقارن قيمة request.method مع القيمة POST من التحقُّق بأنّ التعليمات التالية لها لن تُنفّذ إلّا إذا كان الطلب الحالي هو فعلًا بطريقة POST، ومن ثمّ قرأنا قيم العنوان والمحتوى المرسلين من الكائن request.form، الذي يمكِّننا من الوصول إلى بيانات نموذج الإدخال المُضمّنة في الطلب.

في حال عدم إدخال قيمةٍ للعنوان ستظهر رسالة خاطفة للمستخدم نعلمه من خلالها باستخدام الدالة ()flash بأن العنوان مطلوب !Title is required، وسيحدث الأمر ذاته في حال عدم ملء حقل المحتوى؛ أمّا في حال وجود كل من العنوان والمحتوى، فسيُفتَح اتصال مع قاعدة البيانات باستخدام الدالة get_db_connection()‎، وسيُنفّذ الأمر INSERT INTO من أوامر SQL باستخدام التابع ()execute وذلك لإضافة التدوينة الجديدة إلى جدول التدوينات "posts" في قاعدة البيانات وفق العنوان والمحتوى المُدخلان أصلًا من قِبل المُستخدم في النموذج.

استخدمنا الموضع المؤقت ? بما يضمن إدخال البيانات في الجدول بأمان، ثمّ حفظنا التغييرات باستخدام الدالة ()connection.commit، وأُغلق الاتصال مع قاعدة البيانات باستخدام الدالة ()connection.close، ونهايةً أعدنا توجيه المًستخدم إلى الصفحة الرئيسية من التطبيق ليرى تدوينته الجديدة أسفل التدوينات السابقة الموجودة أصلًا.

تنبيه: لا تستخدم عمليات بايثون المحرفية بغية إنشاء سلسلة تعليمات SQL ديناميكيًا، بل استخدم دائمًا الموضع المؤقت ? في تعليمات SQL لتعويض القيم ديناميكيًا، فمن الممكن تمرير صف tuple من القيم مثل وسيط ثانٍ ضمن التابع ()execute لربط هذه القيم مع تعليمة SQL، وهذا ما يمنع هجمات حقن استعلامات SQL (إحدى أكثر طرق الاختراق خطرًا على كل من المواقع والأنظمة، وتتضمن هذه الطريقة إدخال استعلامات SQL في حقول الإدخال).

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

http://127.0.0.1:5000/create

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

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

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

ونعدل الملف ليصبح كما يلي:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %} {% endblock %} - FlaskApp</title>
    <style>
        .post {
            padding: 10px;
            margin: 5px;
            background-color: #f3f3f3
        }

        nav a {
            color: #d64161;
            font-size: 3em;
            margin-left: 50px;
            text-decoration: none;
        }

        .alert {
            padding: 20px;
            margin: 5px;
            color: #970020;
            background-color: #ffd5de;
        }
    </style>
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">FlaskApp</a>
        <a href="{{ url_for('create') }}">Create</a>
        <a href="#">About</a>
    </nav>
    <hr>
    <div class="content">
        {% for message in get_flashed_messages() %}
            <div class="alert">{{ message }}</div>
        {% endfor %}

        {% block content %} {% endblock %}
    </div>
</body>
</html>

نحفظ الملف ونغلقه.

أضفنا في الشيفرة السابقة وسم رابط <a> جديد إلى شريط التصفُّح والذي يشير إلى صفحة إنشاء تدوينة جديدة.

استخدمنا حلقة for التكرارية من تعليمات جينجا للمرور على الرسائل الخاطفة، إذ يوفّر فلاسك هذه الرسائل من خلال دالة فلاسك الخاصة get_flashed_messages()‎ لتُعرض ضمن وسم <div> ذو صنف CSS يدعى alert، ونسقنا مظهر الوسم <div> من خلال الوسم <style> المتوضّع في قسم الترويسة <head>.

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

ولو أرسلنا بعد ذلك نموذجًا بعنوان ما وحقل محتوى فارغ، فستظهر رسالة خاطفة لتعلمنا بكون حقل المحتوى مطلوب "!Content is required"، وعندها تختفي الرسالة "!Title is required" السابقة، لأنها رسالة خاطفة وليست دائمة.

ومع نهاية هذه الخطوة أصبح لدينا آلية لإضافة التدوينات الجديدة، وسنعمل في الخطوة التالية على إضافة وجهة جديدة لتعديل التدوينات الموجودة أصلًا.

الخطوة 4 - تعديل تدوينات

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

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

لذا، افتح الملف "app.py":

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

ستعيد الدالة الجديدة التي سنستخدمها في جلب التدوينات المطلوبة من قاعدة البيانات خطأ HTTP من النوع "404" أي "404 Not Found" في حال كون المعرّف المطلوب لا يوافق أي من معرفات التدوينات الموجودة في قاعدة البيانات، ولإنجاز ذلك سنستعين بالدالة ()abort التي تُلغي الطلب الخاطئ مُستجيبةً برسالة خطأ.

لذا سنضيف الدالة ()abort إلى مجموعة الاستيرادات في بداية الملف:

from flask import Flask, render_template, request, url_for, flash, redirect, abort

سنضيف بعد الدالة ()get_db_connection دالة جديدة باسم ()get_post، كما يلي:

# ...

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، والذي نحدّد من خلاله معّرف ID التدوينة المراد جلبها مثل قيمة معادة من قاعدة البيانات، لذا تعمل الدالة ()get_post على فتح اتصال مع قاعدة البيانات باستخدام الدالة ()get_db_connection لتنفّذ فيها استعلامًا للوصول إلى التدوينة الموافقة لقيمة المُعرّف المُمرّر أصلًا إلى الوسيط post_id، إذ تُجلب التدوينة باستخدام التابع ()fetchone لتُخزّن في المتغير post، ومن ثمّ يُغلق الاتصال مع قاعدة البيانات.

إذا كانت قيمة المتغير post فارغة أي تساوي "None"، فهذا يعني أنّه لم يوجد أي نتيجة موافقة في قاعدة البيانات، وعندها تُستخدم الدالة ()abort التي استوردناها سابقًا للاستجابة برسالة خطأ HTTP من النوع "404"، ثمّ إيقاف التنفيذ؛ أمّا في حال إيجاد معرّف التدوينة المطلوبة ضمن قاعدة البيانات فتُعاد قيمة المتغير post.

والآن سنضيف وجهة جديدة مُخصّصة لتعديل التدوينات إلى نهاية الملف "app.py"، كما يلي:

# ...

@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!')

        elif not content:
            flash('Content 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)

ثمّ نحفظ الملف ونغلقه.

سنستخدم الوجهة الجديدة بالشكل /int:id>/edit>/، إذ يمثّل :int محوّلًا لنمط البيانات، ويقبل قيمًا عددية صحيحة موجبة فقط، أما id فيمثّل الجزء المتغير من الرابط المطلوب، والذي يدل على التدوينة المراد تعديلها، فمثلًا استخدام الوجهة (ضمن الرابط) بالشكل /‎2/edit/ يعني أنّنا نود تعديل التدوينة ذات معرّف ID المساوي "2"، إذ يُمرّر المعرّف هذا من الرابط إلى دالة العرض ()edit، لتُمرر فيها قيمة الوسيط id إلى الدالة ()get_post لجلب التدوينة الموافقة لهذا المعرّف من قاعدة البيانات، مع ملاحظة أنّه في حال عدم وجود تدوينة موافقة للمعرّف المطلوب، ستكون الاستجابة برسالة خطأ "‎404 Not Found".

أمّا السطر الأخير من الشيفرة السابقة فيصيّر ملف قالب باسم edit.html مُمرّرًا إليه المتغير post المُتضمّن بيانات التدوينة المراد تعديلها، وهذا القالب مسؤول عن عرض عنوان ومحتوى التدوينة الحاليين (قبل التعديل) ضمن صفحة التعديل.

وعلى نحوٍ مشابه لحالة إنشاء تدوينة جديدة فإن الجملة الشرطية 'if request.method == 'POST مسؤولة عن التعامل مع البيانات الجديدة التي يرسلها المُستخدم، فبعد تحقّق الشرط نستخلص من النموذج العنوان والمحتوى الجديدين مع إظهار رسالة خاطفة في حال كون أحدهما فارغ.

في حال كون النموذج المرسل صحيحًا، نفتح اتصالًا مع قاعدة البيانات ونحدّث الجدول "posts" بالعنوان والمحتوى الجديدين للتدوينة ذات المعرّف المساوي لذلك المحدَّد في الرابط المطلوب باستخدام التعليمة UPDATE من تعليمات SQL، ومن ثمّ نؤكّد التغييرات ونغلق الاتصال مع قاعدة البيانات ونعيد توجيه المستخدم إلى الصفحة الرئيسية للتطبيق.

أمّا الآن فعلينا إنشاء الصفحة التي سيعدّل المستخدمون التدوينات ضمنها، لذا سنفتح ملف قالب جديد باسم "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">
        <label for="title">Title</label>
        <br>
        <input type="text" name="title"
               placeholder="Post title"
               value="{{ request.form['title'] or post['title'] }}"></input>
        <br>

        <label for="content">Post Content</label>
        <br>
        <textarea name="content"
                  placeholder="Post content"
                  rows="15"
                  cols="60"
                  >{{ request.form['content'] or post['content'] }}</textarea>
        <br>
        <button type="submit">Submit</button>
    </form>
{% endblock %}

نحفظ الملف ونغلقه.

الشيفرة السابقة مُشابهة لتلك في القالب "create.html" ما عدا أنها تعمل على إظهار عنوان التدوينة مثل عنوان للصفحة من خلال التعليمة:

{% block title %} Edit "{{ post['title'] }}" {% endblock %}

وتجعل القيمة داخل الصندوق النصي بالشّكل:

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

والقيمة داخل الحقل النصي مُتعدّد الأسطر على النحو التالي:

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

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

الآن وفي حين خادم التطوير يعمل، استخدم المتصفح للانتقال إلى الرابط التالي بغية تعديل التدوينة الأولى`:

http://127.0.0.1:5000/1/edit

فتظهر صفحة بالشّكل:

كيفية تعديل التدوينات في فلاسك flask

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

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

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

ونعدله ليصبح تمامًا على النحو التالي:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Posts {% endblock %}</h1>
    {% for post in posts %}
        <div class='post'>
            <p>{{ post['created'] }}</p>
            <h2>{{ post['title'] }}</h2>
            <p>{{ post['content'] }}</p>
            <a href="{{ url_for('edit', id=post['id']) }}">Edit</a>
        </div>
    {% endfor %}
{% endblock %}

نحفظ الملف ونغلقه.

أضفنا الوسم <a> للربط مع دالة العرض ()edit، مُمرّرين معّرف التدوينة ID الموجود في post['id'])‎ إلى الدالة ()url_for لتُنشئ رابط تعديل التدوينة الموافقة، وهذا ما سيضيف بالنتيجة رابطًا إلى صفحة التعديل من أجل كل تدوينة.

الآن، وبعد تحديث الصفحة الرئيسية للتطبيق يصبح من الممكن تعديل أي تدوينة بالنقر على رابط التعديل Edit الخاص بها.

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

الخطوة 5 - حذف تدوينات

سنعمل في هذه الخطوة على إضافة زر حذف إلى صفحة التعديل بحيث يسمح للمستخدمين بحذف تدوينة ما.

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

الآن، افتح الملف "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 وفق ما جرى تحديده في المعامل methods المسؤول عن تحديد أنواع طلبيات HTTP المسموحة؛ وهذا يعني أنك إذا انتقلت إلى الوجهة ‎/ID/delete في المتصفح، ستحصل على خطأ "‎405 Method Not Allowed"، لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات؛ لذا أضفنا زرًا لحذف التدوينات كونه يُرسل إلى الوجهة طلبًا من النوع POST.

تستقبل الدالة قيمة المعرّف ID للتدوينة المراد حذفها وتستخدمها لجلب التدوينة من قاعدة البيانات باستخدام دالة get_post()‎، لتستجيب بخطأ من النوع 404 بالرسالة "‎404 Not Found" في حال عدم وجود تدوينة موافقة، وإلّا يُفتح اتصال مع قاعدة البيانات وتُنفّذ تعليمة DELETE FROM من تعليمات SQL لحذف التدوينة، إذ استخدمنا التعبير WHERE id = ? للدلالة على التدوينة المراد حذفها.

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

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

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

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

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

<button type="submit">Submit</button>
    </form>


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

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

أنشأنا في الملف السابق نموذج ويب يُرسل طلبًا من النوع POST إلى دالة العرض ()delete ممررين القيمة ['post['id لتحديد التدوينة المراد حذفها، كما استخدمنا التابع confirm()‎ المتوفّر في متصفحات الويب لعرض رسالة تأكيد قبل إرسال الطلب.

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

http://127.0.0.1:5000/1/edit

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

الخاتمة

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

ترجمة -وبتصرف- للمقال How To Use an SQLite Database in a Flask Application لصاحبه 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.


×
×
  • أضف...