flask 101 التعامل مع قواعد البيانات SQLite في تطبيقات Flask


عبدالهادي الديوري

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

flask-sqlite.png

ما هي قاعدة البيانات

قاعدة البيانات ببساطة مخزَن للبيانات المُختلفة كأسماء المستخدمين، كلمات المرور، وباقي القيم التي يُمكن أن تحصل عليها ممن يستخدم تطبيقك، ويُمكن كذلك جلب، تعديل وحذف البيانات منها بسهولة.

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

سنستخدم في هذا الدّرس نظام SQL لقواعد البيانات، وهو نظام يعتمد على الجداول، وسنستخدم في هذا الدّرس جدولا لتخزين المقالات كالتّالي:

رقم المُعرّف عنوان المقال مُحتوى المقال
1 عنوان المقال الأول مُحتوى المقال الأول
2 عنوان المقال الثّاني مُحتوى المقال الثّاني

بنية تطبيق "مدونتي"

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

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

وهذه صور للتّطبيق النّهائي:

الصفحة الرئيسية 

img1.png

صفحة المقال 

img2.png

إنشاء قاعدة البيانات وإنشاء جدول المقالات

سنستعمل في الدّرس قواعد البيانات Sqlite لإدارة قاعدة البيانات، وذلك لسهولة التّعامل معها وسهولة نقل ملفّات قاعدة البيانات إلى أجهزة أخرى، كما أنّها لا تعمل على خادوم كما هو الحال مع MySQL أو Postgresql. 

تنويه: من المُفضّل عدم استخدام Sqlite في التّطبيقات التي ستنشرها على الأنترنت أو المشاريع الرّسميّة، ومن المُفضّل استخدام Postgresql أو MySQL في هذه الحالة.

سننشئ قاعدة بيانات باسم database

في قاعدة البيانات هذه سنضيف جدولا للمقالات باسم posts.

سيتكون جدول المقالات من ثلاثة أعمدة:

  1. رقم المقال/المعرّف (ID)
  2. عنوان المقال (Title)
  3. مُحتوى المقال (Content)

لإنشاء قاعدة البيانات وجدول المقالات يُمكنك تنفيذ الشيفرة التّالية، ضعها داخل ملفّ باسم create_db.py وقم بتنفيذه:

# -*- coding: utf-8 -*- 

import sqlite3 

# الاتّصال بقاعدة البيانات 
db = sqlite3.connect('database.db') 

# إنشاء مُؤشّر في قاعدة البيانات لنتمكّن من تنفيذ استعلامات 
SQL cursor = db.cursor() 

# إنشاء الجدول 
cursor.execute(""" CREATE TABLE posts( id INTEGER PRIMARY KEY, title CHAR(200), content TEXT )""") 

# إدخال القيم إلى الجدول 
cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (u'عنوان المقال الأول', u'محتوى المقال الأول'))
cursor.execute('''INSERT INTO posts(title, content) VALUES (?,?)''', (u'عنوان المقال الثّاني', u'مُحتوى المقال الثّاني'))

# تطبيق التغييرات
db.commit()

لاحظ بأنّنا نستدعي الوحدة sqite3 في البداية، وذلك لتنفيذ شيفرة لغة SQL، والشيفرة الممرّرة كمُعاملات للدّالة execute هي شيفرة SQL خاصّة بقاعدة البيانات Sqlite. بعد تنفيذ الشيفرة سنحصل على ملف database.db وهو الذي سيكون قاعدة بيانات التّطبيق، يوجد داخل قاعدة البيانات جدول مقالات باسم posts يحتوي بدوره على 3 أعمدة (رقم مُعرّف المقال، عنوان المقال ومحتواه)، مُعرّف المقال سيزيد بواحد تلقائيّا في كلّ مرّة نُضيف فيها عنوانا ومحتوى جديدين وهذا لأنّه من النّوع PRIMARY KEY، ما يعني بأنّنا نستطيع توفير قيمتين فقط دون الاهتمام بخانة رقم المعرّف. 

نضيف بعد ذلك مقالين: 

  1. المقال الأول: عنوانه "عنوان المقال الأول"، مُحتواه "محتوى المقال الأول"
  2. المقال الثاني: عنوانه "عنوان المقال الثاني"، مُحتواه "محتوى المقال الثّاني"

بعد الانتهاء من إضافة القيم، نقوم باستدعاء الدّالة commit لحفظ التّغييرات إلى قاعدة البيانات.

الحصول على المقالات

للحصول على رقم مُعرّف وعنوان ومحتوى المقالات يُمكننا تنفيذ الاستعلام التّالي:

SELECT * FROM posts;

النّجمة عبارة تعني all أو الكل. يُمكننا كذلك الحصول على قيم عمود واحد فقط:

SELECT title FROM posts;

ويُمكن الحصول على أكثر قيم أكثر من عمود:

SELECT title, content FROM posts;

لوضع القيم في مُتغيّر وإرجاعه في دالة في بايثون يُمكنك كتابة دالة كالتّالي:

import sqlite3 

BASE_DIR = path.dirname(path.realpath(__file__)) 
DB_PATH = path.join(BASE_DIR, 'database.db') 

def get_posts(): 
    db = sqlite3.connect(DB_PATH) 
    cursor = db.cursor() 
    query = cursor.execute('''SELECT * FROM posts''') 
    posts = query.fetchall() 
    return posts

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

أما الدالة فتقوم أولا بالاتصال بقاعدة البيانات بالدّالة connect ومعامل DB_PATH الذي يُمثّل مسار ملف قاعدة البيانات database.db، بعدها نُنشئ مؤشّرا بالدّالة cursor، ثمّ ننفّذ الاستعلام كمُعامل مُمرّر للدّالة execute، بعدها نُطبّق الدالّة fetchall على نتيجة الاستعلام للحصول على القيم في قائمة من المجموعات، بحيث تحتوي القائمة على مجموعة بثلاثة عناصر العنصر الأول هو رقم المعرّف والعنصر الثّاني يمثّل عنوان المقال والعنصر الثّالث يمثّل محتوى المقال.

وبالتّالي فإنّنا سنتمكن من الوصول إلى محتويات المقال كعناصر في مجموعة داخل قائمة، والقائمة تحتوي على العديد من المجموعات. 

قائمة المقالات ستكون كالتّالي:

posts = [(1, u'عنوان المقال الأول', u'محتوى المقال الأول'), (2, u'عنوان المقال الثّاني', u'محتوى المقال الثّاني') ]

ما يعني بأنّنا نستطيع الوصول إلى مُعرّف كل مقال، عنوانه ومحتواه بحلقة For بسيطة:

posts = get_posts() 

for post in posts: 
    post[0] # رقم المعرّف 
    post[1] # عنوان المقال 
    post[2] # محتوى المقال

احفظ الدّالة get_posts في ملفّ باسم manage_db.py لنستعملها لاحقا كوحدة مع تطبيقنا (انظر درس الوحدات والحزم في لغة بايثون).

الحصول على مقال حسب معرفه/رقمه

للحصول على مقال حسب رقم مُعرّفه يكفي أن نُضيف جملة WHERE إلى استعلام SQL:

SELECT title, content FROM posts WHERE id=1

ستُمكّننا الجملة أعلاه من الحصول على عنوان ومحتوى المقال الذي يمتلك رقم المُعرّف 1. 

لاستغلال الأمر في لغة بايثون بمُساعدة وحدة sqlite يُمكننا أن نكتب دالة باسم get_post_by_id لنحصل على مقال حسب رقم مُعرّفه، وبالطّبع سيكون للدّالة مُعامل واحد باسم post_id ليحمل قيمة رقم المُعرّف.

def get_post_by_id(post_id): 
    db = sqlite3.connect(DB_PATH) 
    cursor = db.cursor() 
    post_id = int(post_id) 
    query = cursor.execute('''SELECT title, content FROM posts WHERE id=?''',(post_id,)) 
    post = query.fetchone() 
    return post

بعد الاتّصال بقاعدة البيانات وإنشاء مؤشّر، نقوم أولا بتحويل قيمة رقم المُعرّف إلى عدد صحيح لأن الدّالة رقم المعرّف في قاعدة البيانات عبارة عن عدد صحيح. 

بعدها نُنفّذ الاستعلام الذي سبق وأن ذكرناه، لكن هذه المرّة قُمنا بتمرير مجموعة من عنصر واحد، وهذا العنصر هو مُعامل الدّالة، بعدها عرّفنا مُتغيّرا باسم post ليحمل بيانات المقال التي حصلنا عليها بتنفيذ الدّالة fetchone على الاستعلام، بعدها نُرجع المُتغيّر post

إذا استدعيت الدّالة مع تمرير قيمة بالعدد 1 فسيكون المُخرج كالتّالي:

(u'عنوان المقال الأول', u'محتوى المقال الأول')

أضف الدالة get_post_by_id إلى ملفّ manage_db.py واحفظه.

حذف مقال حسب رقم المقال

طريقة حذف المقال مُشابهة لطريقة الحصول عليه، فقط استبدل SELECT بالأمر DELETE.

DELETE FROM posts WHERE id=?

ما يعني بأنّنا نستطيع كتابة دالة في لغة بايثون لحذف مقال حسب رقم مُعرّفه:

def delete(post_id): 
    db = sqlite3.connect(DB_PATH) 
    cursor = db.cursor() 
    cursor.execute('''DELETE FROM posts WHERE id=?''', (post_id,)) 
    db.commit()

الاختلاف هنا هو أنّنا سنحتاج إلى تنفيذ الدّالة commit لتأكيد العمليّة. 

وكما العادة، أضف دالة الحذف إلى ملفّ manage_db.py.

إضافة مقال

تعرّفنا في بداية هذا الدّرس على طريقة إضافة مقال إلى قاعدة البيانات.

INSERT INTO posts(title, content) VALUES('Title 1','Content 1')

يُمكننا في بايثون إدخال قيم المُتغيّرات إلى قاعدة البيانات بالطّريقة التّالية:

import sqlite3 

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

title_variable = 'Title 3' 
content_variable = 'Content 3' 

cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (title_variable, content_variable)) 
db.commit()

لا تنس أن تقوم باستدعاء الدّالة commit لتأكيد العمليّة. إذا قُمت بتنفيذ الشّيفرة أعلاه، وقُمت بعدها بتنفيذ الدّالة get_posts التي أنشأناها سابقا، ستتمكّن من رؤية القيمتين Title 3 و Content 3 كعنصرين من قائمة المقالات.

لنضع هذه الشّيفرة في دالة باسم create لإضافتها إلى الوحدة manage_db، ستقبل الدّالة مُعاملين، مُعامل للعنوان، ومُعامل آخر لمُحتوى المقال.

def create(title, content): 
    db = sqlite3.connect('DB_PATH') 
    cursor = db.cursor() 
    cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (title, content)) 
    db.commit()

الحصول على القيم وتمريرها إلى القالب

بعد أن أنشأنا وحدة تحتوي على أربعة دوال تؤدّي أربعة أوامر أساسيّة:

  • get_posts: الحصول على المقالات على شكل قائمة من المجموعات يُمكن الدّوران حولها
  • get_post_by_id: الحصول على عنوان ومُحتوى مقال حسب رقم مُعرّفه
  • delete: حذف مقال
  • create: إنشاء مقال جديد 

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

مبدأ التطبيق

سيحتوي التّطبيق على 4 موجّهات:

  • موجّه الصّفحة الرّئيسية /
  • موجّه إضافة المقال  create/
  • موجّه صفحة المقال الواحد <post/<post_id/
  • موجّه حذف المقال <delete/<post_id/

موجها إضافة المقال وحذفه لن يقدّما صفحة HTML بل سيُنفّذان دالّة وبعدها سيعيدان التّوجيه إلى الصّفحة الرّئيسيّة مباشرة.

الصفحة الرئيسية

ستحتوي الصّفحة الرّئيسية على عناوين ومحتويات المقالات لذا سنستخدم الدّالة get_posts من الوحدة manage_db في المُوجّه الرّئيسي ما يعني بأنّنا يجب علينا استدعاء الوحدة، كما سنُقدّم المقالات في ملفّ HTML باسم index.html

في ملّف app.py ضع ما يلي:

# -*- coding:utf8 -*- 
from flask import Flask, render_template import manage_db 

app = Flask(__name__) 

# Home Page 
@app.route("/") 
def home(): 
    posts = manage_db.get_posts() 
    return render_template('index.html', posts = posts) 


if __name__ == "__main__": 
  app.run(debug=True)

لاحظ بأنّنا استدعينا الدّالة get_posts وأسندنا قيمتها إلى المُتغيّر posts وبعدها نُقدّم الملفّ index.html مع تمرير المُتغيّر posts

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

الجزء المسؤول عن عرض المقالات في ملفّ index.html:

{% for post in posts %} 
<a href="post/{{ post[0] }}">
  <h2> {{ post[1] }} </h2>
</a> 
<a href="delete/{{ post[0] }}"><span class="delete">حذف</span></a>
<p> {{ post[2] }} </p> 
{% endfor %}

الشّيفرة أعلاه هي الجزء المسؤول عن عرض المقالات فقط، وقد تجاهلت العناصر الأخرى التي لا تهمّنا مثل الشّعار والتّنسيق وغير ذلك. يُمكنك الحصول على ملفّ index.html كاملا من على Github

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

post/{{ post[0] }} => http://127.0.0.1:5000/post/1

ويُمكن حذفه بالرّابط التّالي:

delete/{{ post[0] }} => http://127.0.0.1:5000/delete/1

الرّوابط لن تعمل حاليّا لأنّنا لم ننشئ المُوجّهات بعد.

سيُعرض عنوان المقال داخل وسم h2 بالسّطر التّالي:

<h2> {{ post[1] }} </h2>

سيُعرض مُحتوى المقال داخل وسم p بالسّطر التّالي:

<p> {{ post[2] }} </p>

صفحة عرض المقال

لعرض المقال الواحد، سنستخدم ملفّ HTML آخر وسنسمّيه post.html، أمّا الموجّه المسؤول عن تقديم هذا المقال فسيكون كالتّالي:

موجّه post في ملفّ app.py:

# Single Post Page 
@app.route("/post/<post_id>") 
def post(post_id): 
  post = manage_db.get_post_by_id(post_id) 
  return render_template('post.html', post = post)

الشّيفرة أعلاه عبارة عن مُوجّه باسم post يقبل مُعاملا post_id لنتمكّن من تمريره كمُعرّف للمقال للدّالة get_post_by_id من الوحدة manage_db

بعدها نقوم باستدعاء الدّالة للحصول على بيانات المقال على شكل مجموعة يُمكننا أن نصل إلى عناصرها كالتّالي:

post[0] # عنوان المقال post[1] # المحتوى

صفحة post.html:

<div class="main"> 
  <h2> {{ post[0] }} </h2> 
  <p> {{ post[1] }} </p> 
</div> 
<a href="{{ url_for('home') }}" class="back_to_home">عُد إلى الصّفحة الرّئيسيّة</a>

في الشّيفرة أعلاه، نقوم بعرض عنوان المقال داخل وسم h2 ونقوم بعرض المُحتوى داخل وسم p

السّطر الأخير عبارة عن رابط لتمكين الزّائر من العودة إلى الصّفحة الرّئيسيّة و home اسم الدّالة المسؤولة عن تقديم الصّفحة الرّئيسية (الموجودة مُباشرة بعد الموجّه /).

# Home Page 
@app.route("/") 
def home(): 
	...

ملحوظة: نستطيع استخدام الدّالة url_for لتوليد روابط الموجّهات، وذلك بوضع اسم الدّالة كمعامل. 

مثال: لنفرض بأنّ لدينا مُوجّها باسم hello ودالة باسم hello_page، سنتمكّن من إنشاء رابط إلى الموجّه hello كالتّالي:

<a href="{{ url_for('hello_page') }}">Link to Hello Page</a>

يُمكن كذلك وضع عنوان المقال كعنوان للصّفحة داخل وسم title:

<title>{{ post[0] }}</title>

حذف مقال

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

لاستخدام الدّالة redirect سيتوجّب علينا استيرادها من وحدة Flask في بداية الملفّ app.py

سنحتاج كذلك إلى الدّالة url_for للتوجيه إلى الرّابط الصّحيح.

from flask import Flask, render_template, redirect, url_for

موجّه delete سيقبل معاملا باسم post_id لتمريره إلى الدّالة delete من الوحدة manage_db لحذف المقال الذي يحمل رقم المعرّف المُمرّر.

# Delete Post 
@app.route("/delete/<post_id>") 
def delete(post_id): 
	manage_db.delete(post_id) 
  	return redirect(url_for('home'))

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

إنشاء مقال جديد

تعرّفنا مُسبقا على طريقة الحصول على القيم من المستخدم بطريقة طلبات GET من عنوان URL كالآتي:

/create?title=post1&content=content1

يُمكننا استخدام request للحصول على القيم كالتّالي:

title = request.args.get('title') content = request.args.get('content')

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

لكي نحصل على العنوان والمُحتوى بطريقة أفضل، سنستخدم نماذج HTML أو HTML Forms، وسنستخدم طريقة POST عوضا عن GET. سنضع النّماذج في ملفّ index.html مُباشرة تحت الجزء المسؤول عن عرض المقالات.

<h4>أضف مقالا</h4> 

<form action="{{ url_for('create') }}" method="POST"> 
  <input class="input" type="text" name="title" placeholder="عنوان المقال"/> <br> 
  <textarea name="content" class="input" rows="10" placeholder="مُحتوى المقال"></textarea> <br> 
  <input type="submit" value="أضف" /> 
</form>

في الوسم form نضع رابط الموجّه create داخل الصّفة action لنُخبر المُتصفّح بأنّ البيانات التّي سيُرسلها المُستخدم يجب أن تذهب إلى موجّه إضافة مقال جديد create. بعدها نُخصّص طريقة إرسال البيانات بوضع كلمة POST داخل الصّفة method. 

بعد ذلك ننشئ حقلا لعنوان المقال باسم title وحقل نصّ باسم content وبعدها نضيف زرّا لتأكيد الإرسال.

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

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

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

@app.route("/create", methods=['GET', 'POST'])

بعدها سنتمكّن من الحصول على البيانات وإدخالها إلى قاعدة البيانات كالتّالي:

if request.method == 'POST': 
    title = request.form['title'] # الحصول على عنوان المقال 
    content = request.form['content'] # الحصول على مُحتوى المقال 

    manage_db.create(title, content) # إدخال القيم إلى قاعدة البيانات

لاحظ بأنّنا نضع شرطا للتأكّد من أن الطلب الذي يرسله المُتصفح من نوع POST. 

بعدها نحصل على القيم التي أدخلها المُستخدم في النّموذج الموجود بملفّ index.html عبر القاموس form المُتواجد داخل الوحدة request

وكما فعلنا مع الموجّه delete سنقوم بإعادة توجيه المُستخدم إلى الصّفحة الرّئيسية.

return redirect(url_for('home'))

الموجّه create كاملا:

# Create Post Page 
@app.route("/create", methods=['GET', 'POST']) 
def create(): 
    if request.method == 'POST': 
        title = request.form['title'] 
        content = request.form['content'] 
        manage_db.create(title, content) 
        return redirect(url_for('home'))

أصبح التّطبيق كاملا الآن ويُمكنك مُشاركته مع العالم.

يُمكنك إضافة تنسيق css خاصّ بك أو تحميل الملفّات الكاملة للتّطبيق من Github وإضافة ملفّ style.css إلى التّطبيق الذي أنشأته (يُمكنك كذلك تعديله).

إذا كان لديك سؤال حول هذا الدّرس، يُمكنك وضعه في قسم الأسئلة والأجوبة.

ختاما

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





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن