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

استخدام علاقة one-to-many مع إطار العمل فلاسك Flask ومحرك قواعد البيانات SQLite


محمد الخضور

فلاسك هو إطار عمل يستخدم لغة بايثون Python لبناء تطبيقات الويب؛ أما SQLite فهو محرّك قواعد بيانات يُستخدم لتخزين بيانات تطبيق الويب.

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

تتمثّل الفائدة العلمية والعملية من هذا المقال بتعلمّك كيفيّة استخدام SQLite مع فلاسك، بالإضافة لتعلّم كيفيّة إنشاء علاقات قاعدة البيانات من النوع واحد-إلى-متعدد.

تُعرَّف علاقة قاعدة البيانات من النوع واحد-إلى-متعدد one-to-many بكونها علاقةٌ بين جدولين من قاعدة البيانات، بحيث يمكن لسجلٍ موجودٍ في أحد الجدولين الإشارة إلى عدّة سجلات من الجدول الثاني، ويشكّل تطبيق المدونة خير مثال على العلاقات في قواعد البيانات من النوع واحد-إلى-متعدد، نظرًا لارتباط الجدول الخاص بتخزين التدوينات posts مع الجدول الخاص بتخزين التعليقات comments بعلاقة من نوع واحد-إلى-متعدد؛ إذ يمكن أن يكون لكل تدوينة عدّة تعليقاتٍ مرتبطةٍ بها، وبالمقابل يرتبط كل تعليق بتدوينة واحدةٍ فقط، وبذلك فإنّ للتدوينة الواحدة علاقةٌ بالعديد من التعليقات. وفي هذه الحالة يُعد الجدول الخاص بالتدوينات "الجدول الأب parent"، في حين يُعد الجدول الخاص بالتعليقات "الجدول الابن child"؛ إذ يمكن للسجل الموجود في الجدول الأب الإشارة إلى عدّة سجلات موجودة في الجدول الابن، وهو أمرٌ أساسي لإتاحة إمكانية الوصول إلى البيانات المطلوبة في كل جدول.

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

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

توجد مجموعةٌ من المتطلبات الواجب توفرّها وإعدادها قبل البدء في اتباع الخطوات التفصيلية المشروحة تاليًا ضمن هذا المقال، وهذه المتطلبات هي:

  • توفُّر بيئة برمجة بايثون 3 محلية، وفي مقالنا سنسمّي مجلد المشروع بالاسم flask_todo.
  • فهم مختلف مفاهيم فلاسك الأساسية، مثل إنشاء الوجهات وإخراج قوالب HTML والاتصال بقاعدة بيانات SQLite.

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

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

إذا لم تنشّط البيئة البرمجية الخاصة بك بعد، فاستخدم الأمر التالي لتنشيطها بعد التأكّد من كون موجه الأوامر يشير إلى مسار مجلد المشروع flask_todo:

$ source env/bin/activate

وبمجرّد تنشيط البيئة البرمجية، ثبّت فلاسك باستخدام الأمر التالي:

(env)user@localhost:$ pip install flask

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

الآن سننشئ ملفًا باسم schema.sql ضمن المجلد flask_todo:

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

وسنكتب أوامر SQL التالية داخل هذا الملف:

DROP TABLE IF EXISTS lists;
DROP TABLE IF EXISTS items;

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

CREATE TABLE items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    list_id INTEGER NOT NULL,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    content TEXT NOT NULL,
    FOREIGN KEY (list_id) REFERENCES lists (id)
);

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

أوّل سطرين من شيفرة SQL السابقة مسؤولان عن حذف أي جداول موجودة سابقًا ضمن قاعدة البيانات باسم lists أو items، وهما:

DROP TABLE IF EXISTS lists;
DROP TABLE IF EXISTS items;

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

بعد ذلك، يمكنك استخدام الأمر CREATE TABLE lists لإنشاء جدول القوائم lists، الذي سيخزن قوائم المهام المطلوبة، مثل قائمة الدراسة وقائمة العمل والقائمة الرئيسية، وما إلى ذلك، باستخدام الأعمدة التالية:

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

وبعد إنشاء جدول القوائم lists، سننشئ الآن جدول العناصر items لتخزين عناصر المهام، إذ يحتوي هذا الجدول على معرّف "ID"، وعمود "list_id" يحوي أعدادًا صحيحة تحدّد معرّف القائمة التي ينتمي إليها العنصر، إضافةً لعمود تاريخ إنشاء العنصر، وعمود محتوى المهمّة.

يمكنك استخدام مفتاح أجنبي لربط عنصرٍ ما بقائمةٍ معيّنة في قاعدة البيانات عن طريق كتابة الشيفرة التالية:

 FOREIGN KEY (list_id) REFERENCES lists (id)

يمثّل جدول القوائم lists الجدول الأب المربوط بالجدول الآخر باستخدام المفتاح الأجنبي، وهذا يبيّن امكانية احتواء القائمة الواحدة على عدّة عناصر؛ في حين يمثّل جدول العناصر items الجدول الحاوي على المفتاح الأجنبي، وهذا يعني أن كل عنصر ينتمي إلى قائمة واحدةٍ فقط. يشير العمود list_id من جدول العناصر items الابن إلى عمود المعرِّف id من جدول القوائم lists الأب.

وبذلك نجد أنّه قد حققنا فعليًا علاقة واحد-إلى-متعدّد بين جدولي lists و items، نظرًا لأنّ القائمة يمكن أن تحتوي على عدّة عناصر، وأنّ العنصر الواحد سينتمي إلى قائمةٍ واحدةٍ فقط.

بعد الانتهاء من إنشاء الجداول، سنستخدم ملف schema.sql لإنشاء قاعدة البيانات. لذلك، أنشئ ملفًا باسم init_db.py ضمن المجلد flask_todo على النحو التالي:

(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 lists (title) VALUES (?)", ('Work',))
cur.execute("INSERT INTO lists (title) VALUES (?)", ('Home',))
cur.execute("INSERT INTO lists (title) VALUES (?)", ('Study',))

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (1, 'Morning meeting')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (2, 'Buy fruit')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (2, 'Cook dinner')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (3, 'Learn Flask')
            )

cur.execute("INSERT INTO items (list_id, content) VALUES (?, ?)",
            (3, 'Learn SQLite')
            )

connection.commit()
connection.close()

اِحفظ الملف وأغلقه، وبمجرّد تنفيذ الشيفرة السابقة سيُنشأ ملفٌ باسم database.db ويُحقَّق الاتصال به، وبعدها يمكننا فتح ملف schema.sql وتشغيله باستخدام التابع ()executescript الذي ينفِّذ العديد من تعليمات SQL البرمجية في وقتٍ واحد.

سيؤدي تشغيل الملف schema.sql لإنشاء جداول القوائم والعناصر، ومن ثمّ سننفّذ عددًا من تعليمات الإدخال في SQL لإنشاء ثلاث قوائم وخمسة عناصر مهام باستخدام كائن المؤشر Cursor.

سنستخدم العمود "list_id" لربط كلّ عنصرٍ بقائمة عن طريق قيمة معرّف القائمة. فعلى سبيل المثال، إذا كانت القائمة المسمّاة "العمل Work" هي الإدخال الأوّل في قاعدة البيانات، فسيكون معرّفها ذو القيمة 1، وسنتمكّن باستخدامه من ربط عنصر المهام المسمّى "الاجتماع الصباحي Morning meeting" مثلًا بقائمة مهام العمل، وينطبق المبدأ نفسه على كافّة القوائم والعناصر الأُخرى.

ومن ثمّ سنحفظ التغييرات ونغلق الاتصال، وسنشغّل البرنامج كما يلي:

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

وبعد تنفيذ هذا البرنامج سيظهر ملفٌ جديدٌ ضمن المجلد flask_todo باسم database.db، ومع انتهاء هذه الخطوة نكون قد فعّلنا البيئة البرمجية وثبتنا فلاسك، وأنشأنا قاعدة بيانات SQLite، وبذلك سنتمكن من استرجاع كل ما أُدخِل في قاعدة البيانات من قوائم وعناصر لعرضها في الصفحة الرئيسية للتطبيق الذي نحن في صدد إنشائه.

الخطوة الثانية - عرض عناصر المهام

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

أولاً، سننشئ ملف التطبيق، لذا أنشئ ملفًا باسم app.py في المجلد flask_todo:

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

ومن ثمّ سنضيف الشيفرة التالية إلى الملف:

from itertools import groupby
import sqlite3
from flask import Flask, render_template, request, flash, redirect, url_for


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


app = Flask(__name__)
app.config['SECRET_KEY'] = 'this should be a secret random string'


@app.route('/')
def index():
    conn = get_db_connection()
    todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
                          ON i.list_id = l.id ORDER BY l.title;').fetchall()

    lists = {}

    for k, g in groupby(todos, key=lambda t: t['title']):
        lists[k] = list(g)

    conn.close()
    return render_template('index.html', lists=lists)

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

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

الآن سنفتح اتصالًا مع قاعدة البيانات باستخدام دالة عرض فلاسك ()index، وسننفّذ استعلام SQL التالي:

SELECT i.content, l.title FROM items i JOIN lists l ON i.list_id = l.id ORDER BY l.title;

وسنتمكن باستخدام الدالة ()fetchall من الحصول على نتائج الاستعلام وحفظها ضمن متغيّر todos.

استخدمنا في الاستعلام السابق الأمر SELECT لجلب محتوى العنصر مع عنوان القائمة المنتمي إليها، عبر ربط كلٍ من جدولي القوائم والعناصر (إذ يشير الرمز i للعناصر، والرمز l للقوائم)، إذ سنحصل بالنتيجة على كافّة سجلات جدول العناصر ذات القيمة list_id الموافقة لقيمة المعرّف id من جدول القوائم باستخدام شرط التجميع i.list_id = l.id المكتوب بعد البادئة البرمجية ON، ومن ثمّ استخدمنا أمر الترتيب ORDER BY لفرز النتائج وفقًا لعناوين القوائم.

الآن ولفهم هذا الاستعلام بوضوح، سنفتح موجه أوامر بايثون التفاعلي Python REPL في مجلد المشروع flask_todo:

(env)user@localhost:$ python

ومن ثمّ سنختبر محتويات المتغير todos الحاوي على نتائج الاستعلام السابق عبر تشغيل الشيفرة التالية:

from app import get_db_connection
conn = get_db_connection()
todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
ON i.list_id = l.id ORDER BY l.title;').fetchall()
for todo in todos:
    print(todo['title'], ':', todo['content'])

استوردنا في الشيفرة السابقة قاعدة البيانات getdbconnection من الملف app.py، ومن ثمّ فُتِح الاتصال مع قاعدة البيانات ونُفِّذ الاستعلام (وهو استعلام SQL ذاته الموجود في الملف app.py)، وباستخدام حلقة for التكرارية طُبِع عنوان كل قائمة ومحتويات كل عنصر مهام مرتبطٍ بها، وبذلك سيظهر الخرج في حالتنا على النحو التالي:

Home : Buy fruit
Home : Cook dinner
Study : Learn Flask
Study : Learn SQLite
Work : Morning meeting

أغلق موجه أوامر بايثون REPL باستخدام الاختصار "CTRL+D".

الآن، وبعدما فهمت آلية عمل ونتائج الاستعلامات المُجمّعة في SQL، سنعود إلى دالة فلاسك ()index الموجودة في الملف app.py، إذ سنصرّح بدايةً عن المتغير todos ومن ثمّ سنستخدم الشيفرة التالية لتجميع النتائج:

lists = {}
for k, g in groupby(todos, key=lambda t: t['title']):
    lists[k] = list(g)

تبدأ الشيفرة المبينة أعلاه بالتصريح عن قاموس dictionary فارغ باسم lists، ثم تستخدم حلقة for التكرارية لتجميع النتائج الموجودة في المتغير todos وترتيبها حسب عنوان قائمة المهام باستخدام دالة الترتيب ()groupby المستوردة من المكتبة المعيارية itertools، إذ تولّد هذه الدالة مجموعةً من النتائج من أجل كل تكرار في الحلقة عبر مروره على كل عنصرٍ مخزّنٍ في المتغير todos.

يمثّل متغيّر الحلقة k عناوين قوائم المهام (المنزل والدراسة والعمل في حالتنا) المُستخرجة من قاعدة البيانات عبر تمرير قيمة التابع ['lambda t: t['title وسيطًا لدالة التجميع ()groupby، التي تُعيد عنوان القائمة التي يتبع لها هذا العنصر (بنفس مبدأ الدالة ['todo['title المُستخدم في حلقة for السابقة)؛ بينما يمثّل المتغير g المجموعة الحاوية على عناصر المهام التابعة لكل عنوان قائمة. فمن أجل أول دورة في الحلقة مثلًا، سيمثّل المتغير k القائمة Home، في حين سيتكرر المتغير g ليحتوي بالنتيجة عناصر القائمة الموجودة في k وهي في حالتنا "Buy fruit" و "Cook".

وهذا ما يمثّل علاقةً من نوع واحد-إلى-متعدّد بين القوائم والعناصر، بحيث يضم كل عنوان قائمة مجموعةً من عناصر المهام. وبذلك، ستظهر القوائم عند تشغيل الملف app.py وبعد انتهاء تنفيذ حلقة for على النحو التالي:

{'Home': [<sqlite3.Row object at 0x7f9f58460950>,
          <sqlite3.Row object at 0x7f9f58460c30>],
 'Study': [<sqlite3.Row object at 0x7f9f58460b70>,
           <sqlite3.Row object at 0x7f9f58460b50>],
 'Work': [<sqlite3.Row object at 0x7f9f58460890>]}

وسيحتوي كل كائن sqlite3.Row على البيانات المُسترجعة باستخدام استعلام SQL في الدالة ()index من جدول العناصر items، ولعرض هذه البيانات بوضوح أكثر، سنكتب برنامجًا يمر على محتويات الحاوية lists مستعرضًا كل قائمة وعناصرها.

لكتابة هذا البرنامج، سنفتح ملفًا باسم list_example.py في المجلد flask_todo:

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

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

from itertools import groupby
from app import get_db_connection

conn = get_db_connection()
todos = conn.execute('SELECT i.content, l.title FROM items i JOIN lists l \
                        ON i.list_id = l.id ORDER BY l.title;').fetchall()

lists = {}

for k, g in groupby(todos, key=lambda t: t['title']):
    lists[k] = list(g)

for list_, items in lists.items():
    print(list_)
    for item in items:
        print('    ', item['content'])

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

الشيفرة السابقة مشابهةٌ إلى حد كبير لتلك الموجودة في الدالة ()index، وتبيّن حلقة for التكرارية فيها كيفيّة هيكلة حاوية القوائم، إذ تبدأ بالمرور على عناصر الحاوية، التي تمثّل القوائم لطباعة عناوين هذه القوائم والموجودة ضمن المتغير list_variable، ومن ثمّ المرور على كافّة عناصر المهام التابعة لكل قائمة وطباعة محتويات كل منها.

وبتشغيل البرنامج list_example.py:

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

نحصل على الخرج التالي:

Home
     Buy fruit
     Cook dinner
Study
     Learn Flask
     Learn SQLite
Work
     Morning meeting

الآن وبعد فهم كل جزئية في الدالة ()index، سننشئ قالبًا رئيسي وسنعمل على إخراج الملف index.html باستخدام الأمر التالي:

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

لذا، سننشئ مجلدًا للقوالب باسم templates ضمن المجلد flask_todo وسنفتح ضمنه ملفًا باسم base.html:

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

والآن سنكتب شيفرة بوتستراب Bootstap التالية ضمن الملف base.html، وإن لم تكن قوالب HTML في فلاسك مألوفةً بالنسبة لك، يمكنك قراءة الخطوة الثالثة من المقال كيفية إنشاء تطبيق ويب باستخدام إطار فلاسك في لغة بايثون 3.

<!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')}}">FlaskTodo</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> متصفح الويب بالمعلومات، في حين ينشئ الوسم

ارتباطًا إلى ملفات CSS الخاصة ببوتستراب، أما الوسم <script> فيضمِّن شيفرة جافا سكربت المسؤولة عن بعض ميزات بوتستراب الإضافية.

الآن سننشئ الملف index.html الذي سيوسِّع extend شيفرة القالب الرئيسي base.html:

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

وسنكتب الشيفرة التالية في الملف index.html:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Welcome to FlaskTodo {% endblock %}</h1>
    {% for list, items in lists.items() %}
        <div class="card" style="width: 18rem; margin-bottom: 50px;">
            <div class="card-header">
                <h3>{{ list }}</h3>
            </div>
            <ul class="list-group list-group-flush">
                {% for item in items %}
                    <li class="list-group-item">{{ item['content'] }}</li>
                {% endfor %}
            </ul>
        </div>
    {% endfor %}
{% endblock %}

استخدمنا في الشيفرة السابقة، وباتبّاع نفس القواعد المشروحة في برنامج الملف list_example.py، حلقة for تكرارية للمرور على كل عنصر ضمن الحاوية lists، لعرض عنوان كل قائمة ضمن وسم من النوع <h3>، ومن ثمّ عرض كافّة عناصر المهام التابعة لكل قائمة ضمن وسم قوائم <li>.

أمّا الآن فسنعيّن قيم متغيرات البيئة التي يحتاجها فلاسك، وسنشغّل البرنامج باستخدام الأوامر التالية:

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

وبمجرّد تشغيل خادم التطوير، يمكنك الذهاب إلى الرابط "/http://127.0.0.1:5000" باستخدام المتصفّح، فستظهر صفحة ويب تتضمّن العبارة "Welcome to FlaskTodo" إضافةً إلى عناصر قائمتك كما هو مبين في الصورة التالية:

first_image_welcome.png

يمكنك في هذه المرحلة إيقاف خادم التطوير باستخدام الاختصار "CTRL+C".

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

الخطوة الثالثة - إضافة عناصر مهام جديدة

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

بدايةً، سنفتح الملف app.py:

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

وفي نهايته سننشئ وجهة جديدة ليعمل مثل دالة عرض فلاسك باسم ()create:

...
@app.route('/create/', methods=('GET', 'POST'))
def create():
    conn = get_db_connection()
    lists = conn.execute('SELECT title FROM lists;').fetchall()

    conn.close()
    return render_template('create.html', lists=lists)

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

وبما أنّنا سنستخدم هذه الوجهة لإدخال بيانات جديدة في قاعدة البيانات باستخدام نموذج ويب، فلا بدّ من تمكين كلا طريقتي الطلبات "GET" و "POST" باستخدام التعليمة التالية في المزخرف ()app.route:

 methods=('GET', 'POST')

يمكنك الآن فتح اتصال ضمن دالة فلاسك ()create مع قاعدة البيانات وجلب كافّة عناوين القوائم المتوفّرة فيها، ليُغلق بعدها هذا الاتصال ويُخرَج قالب create.html يحتوي عناوين القوائم المُمرّرة.

افتح إذًا ملف قالب جديد باسم create.html:

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

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

{% extends 'base.html' %}

{% block content %}
<h1>{% block title %} Create a New Item {% endblock %}</h1>

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

    <div class="form-group">
        <label for="list">List</label>
        <select class="form-control" name="list">
            {% for list in lists %}
                {% if list['title'] == request.form['list'] %}
                    <option value="{{ request.form['list'] }}" selected>
                        {{ request.form['list'] }}
                    </option>
                {% else %}
                    <option value="{{ list['title'] }}">
                        {{ list['title'] }}
                    </option>
                {% endif %}
            {% endfor %}
        </select>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>
{% endblock %}

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

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

وباستخدام العنصر <select>، يحدث التنقّل ما بين القوائم المُسترجعة من قاعدة البيانات باستخدام الدالة ()create، وصولًا إلى عنوان القائمة المطابق للعنوان المُخزّن في الكائن request.form؛ فإذا كان عنوان القائمة مساويًا لما هو مُخزَّن في الكائن request.form، فسيكون الخيار المحدد هو عنوان القائمة؛ وإلّا سيُعرض عنوان القائمة على نحوٍ اعتيادي ضمن وسم <option> دون أن يكون الخيار مُحدّدًا.

الآن، سنشغّل تطبيق فلاسك من الطرفية terminal كما يلي:

(env)user@localhost:$ flask run

وبالذهاب إلى الرابط http://127.0.0.1:5000/create في المتصفح، سيظهر نموذج إنشاء عنصر مهام جديد، ولكن حتى هذه اللحظة لن يعمل هذا النموذج لعدم وجود أي شيفرة للتعامل مع الطلبات من النوع POST المُرسلة من المتصفح عند إرسال وتأكيد النموذج. لذا، سنوقف خادم التطوير حاليًا باستخدام الاختصار "CTRL+C"، ثمّ سنضيف للدالة ()create الشيفرة اللازمة لعمل النموذج بصورةٍ صحيحة عبر التعامل مع الطلبات من نوع POST.

افتح الملف app.py:

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

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

...
@app.route('/create/', methods=('GET', 'POST'))
def create():
    conn = get_db_connection()

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

        if not content:
            flash('Content is required!')
            return redirect(url_for('index'))

        list_id = conn.execute('SELECT id FROM lists WHERE title = (?);',
                                 (list_title,)).fetchone()['id']
        conn.execute('INSERT INTO items (content, list_id) VALUES (?, ?)',
                     (content, list_id))
        conn.commit()
        conn.close()
        return redirect(url_for('index'))

    lists = conn.execute('SELECT title FROM lists;').fetchall()

    conn.close()
    return render_template('create.html', lists=lists)

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

يتيح الشرط 'request.method == 'POST في الشيفرة السابقة إمكانية الحصول على كلٍ من محتوى عنصر المهام وعنوان قائمة المهام من البيانات المُدخلة في النموذج؛ ففي حال ترك حقل المحتوى فارغًا، سترسِل رسالةً للمستخدم باستخدام الدالة ()flash ثمّ تُعيد التوجيه إلى الصفحة الرئيسية index؛ أما في حال كون حقل المحتوى غير فارغ، فستنفَّذ العبارة SELECT للحصول على معرّف القائمة الموافق لعنوان القائمة المُدخل في النموذج وحفظه في المتغير المُسمّى list_id، ثمّ يُنفّذ الأمر INSERT INTO لإدراج عنصر المهام الجديد في جدول العناصر items، إذ تربط قيمة المتغير list_id العنصر بالقائمة التي ينتمي إليها. وفي النهاية لا بُد من الالتزام بتأكيد العملية وإغلاق الاتصال مع قاعدة البيانات والعودة بالمستخدم إلى الصفحة الرئيسية index.

سنضيف في الخطوة الأخيرة رابط إنشاء عنصر مهام جديد create/ في شريط التصفح، وعرض الرسائل أدناه، لذا سنفتح الملف base.html:

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

الآن سنعدّل الملف السابق بإضافة عنصر تنقّل <li> مرتبط بدالة فلاسك ()create، كما سنضيف حلقة for تكرارية أعلى كتلة المحتوى البرمجية لعرض الرسائل من دالة فلاسك ()get_flashed_messages على النحو التالي:

<nav class="navbar navbar-expand-md navbar-light bg-light">
    <a class="navbar-brand" href="{{ url_for('index')}}">FlaskTodo</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="{{ url_for('create') }}">New</a>
        </li>

        <li class="nav-item active">
            <a class="nav-link" href="#">About</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>

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

الآن سيظهر الرابط create/ في شريط التصفح لدى تشغيل تطبيق فلاسك:

(env)user@localhost:$ flask run

وسننتقل بالنقر عليه إلى صفحة إنشاء عنصر مهام جديد، فإذا أُرسِل النموذج دون محتوى، ستُعرض رسالةٌ مفادها بأنّ المحتوى مطلوب !Content is required، أمّا في حال ملء النموذج، فستظهر المهمّة الجديدة المُضافة في الصفحة الرئيسية index، وبذلك نكون قد أضفنا إمكانية إنشاء عناصر مهام جديدة وحفظها في قاعدة البيانات.

يمكنك الحصول على الشيفرة الكاملة للتطبيق.

الخاتمة

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

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

كما تعرفّت على الدالة ()groupby المسؤولة عن فرز البيانات المستخلصة من قاعدة البيانات، وتعلمّت طريقة ربط سجلات قاعدة البيانات بالجداول التابعة لها.

ترجمة -وبتصرف- للمقال How To Use One-to-Many Database Relationships with Flask and SQLite لصاحبه 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.


×
×
  • أضف...