الهدف: تحويل حفظ المشتركين من ملف Excel / قائمة مؤقتة إلى قاعدة بيانات SQLite باستخدام Flask-SQLAlchemy
، مع صفحة لعرض المشتركين وإمكانية حذف سجل.
1) المتطلبات والتهيئة
أنشئ بيئة افتراضية (مستحسن) ثم ثبّت الحزم:
python -m venv venv
venv\Scripts\activate # ويندوز
# أو: source venv/bin/activate # لينكس / ماك
pip install flask flask-sqlalchemy
(إن أردت إرسال بريد لاحقًا، ثبّت flask-mail أيضاً، لكن هذا الدرس يركز على قواعد البيانات.)
أضف ملف requirements.txt
(اختياري):
Flask
Flask-SQLAlchemy
2) هيكل المشروع المقترح
flask_project/
├── app.py
├── templates/
│ ├── base.html
│ ├── subscribe.html
│ └── subscribers.html
└── static/
└── style.css
3) كود التطبيق (app.py)
انسخ هذا الملف كما هو — يشتمل على نموذج الاشتراك، النموذج يحفظ في قاعدة SQLite، وصفحة عرض وحذف المشتركين:
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os
app = Flask(__name__)
app.secret_key = "change_this_to_a_secret_key" # استخدم قيمة آمنة في الإنتاج
# إعداد قاعدة البيانات (ملف SQLite في نفس المجلد)
basedir = os.path.abspath(os.path.dirname(__file__))
db_path = os.path.join(basedir, "subscribers.db")
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{db_path}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# نموذج البيانات (الـ Model)
class Subscriber(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
email = db.Column(db.String(200), unique=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<Subscriber {self.email}>"
# إنشاء قاعدة البيانات والجداول إن لم تكن موجودة
with app.app_context():
db.create_all()
# الصفحة الرئيسية (رابط للاشتراك وعرض المشتركين)
@app.route('/')
def home():
return render_template('base.html')
# صفحة الاشتراك: GET لعرض النموذج، POST لحفظ البيانات
@app.route('/subscribe', methods=['GET', 'POST'])
def subscribe():
if request.method == 'POST':
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip().lower()
if not name or not email:
flash("الرجاء ملء جميع الحقول.", "warning")
return redirect(url_for('subscribe'))
# التحقق من وجود البريد مسبقاً
existing = Subscriber.query.filter_by(email=email).first()
if existing:
flash("هذا البريد مسجل بالفعل.", "danger")
return redirect(url_for('subscribe'))
# إضافة المشترك إلى DB
new_sub = Subscriber(name=name, email=email)
db.session.add(new_sub)
db.session.commit()
flash("شكراً للاشتراك! تم حفظ بياناتك.", "success")
return redirect(url_for('subscribers_list'))
return render_template('subscribe.html')
# صفحة عرض المشتركين
@app.route('/subscribers')
def subscribers_list():
subs = Subscriber.query.order_by(Subscriber.created_at.desc()).all()
return render_template('subscribers.html', subscribers=subs)
# حذف مشترك (بواسطة id)
@app.route('/subscribers/delete/<int:sub_id>', methods=['POST'])
def delete_subscriber(sub_id):
sub = Subscriber.query.get_or_404(sub_id)
db.session.delete(sub)
db.session.commit()
flash("تم حذف المشترك بنجاح.", "info")
return redirect(url_for('subscribers_list'))
if __name__ == '__main__':
app.run(debug=True)
ملاحظات سريعة:
العمودemail
محدد كـ unique=True
لمنع التكرار.flash()
تستخدم رسائل مؤقتة يمكن عرضها في القالب.db.create_all()
ينشئ الملف والجداول تلقائياً عند أول تشغيل.
4) قوالب HTML (templates)
templates/base.html
قالب بسيط مع navbar وروابط:
<!DOCTYPE html>
<html lang="ar">
<head>
<meta charset="UTF-8">
<title>Flask - إدارة المشتركين</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('home') }}">الرئيسية</a> |
<a href="{{ url_for('subscribe') }}">الاشتراك</a> |
<a href="{{ url_for('subscribers_list') }}">عرض المشتركين</a>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, msg in messages %}
<div class="flash {{ category }}">{{ msg }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}
<h1 style="text-align:center;margin-top:40px">مرحبًا في تطبيق إدارة المشتركين</h1>
{% endblock %}
</div>
</body>
</html>
templates/subscribe.html
نموذج الاشتراك:
{% extends "base.html" %}
{% block content %}
<h2>الاشتراك في النشرة البريدية</h2>
<form method="POST" action="{{ url_for('subscribe') }}" class="form">
<label>الاسم:</label><br>
<input type="text" name="name" required><br><br>
<label>البريد الإلكتروني:</label><br>
<input type="email" name="email" required><br><br>
<button type="submit">اشترك الآن</button>
</form>
{% endblock %}
templates/subscribers.html
عرض المشتركين مع زر حذف لكل صف:
{% extends "base.html" %}
{% block content %}
<h2>قائمة المشتركين</h2>
{% if subscribers %}
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>الاسم</th>
<th>البريد</th>
<th>تاريخ الاشتراك</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
{% for s in subscribers %}
<tr>
<td>{{ s.id }}</td>
<td>{{ s.name }}</td>
<td>{{ s.email }}</td>
<td>{{ s.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<form method="POST" action="{{ url_for('delete_subscriber', sub_id=s.id) }}" onsubmit="return confirm('هل متأكد من الحذف؟');">
<button type="submit">حذف</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>لا توجد مشتركين بعد.</p>
{% endif %}
{% endblock %}
static/style.css
تنسيقات بسيطة لتحسين العرض:
body { font-family: "Cairo", sans-serif; direction: rtl; background: #f7f7f7; color: #333; }
nav { background: #007bff; color: #fff; padding: 10px; text-align: center; }
nav a { color: white; text-decoration: none; margin: 0 10px; }
.container { max-width: 900px; margin: 20px auto; background: white; padding: 20px; border-radius: 8px; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; text-align: center; }
.form input { width: 60%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 8px 12px; border: none; background: #28a745; color: white; border-radius: 6px; cursor: pointer; }
button:hover { opacity: 0.9; }
.flash { padding: 10px; margin-bottom: 12px; border-radius: 6px; }
.flash.success { background: #d4edda; color: #155724; }
.flash.warning { background: #fff3cd; color: #856404; }
.flash.danger { background: #f8d7da; color: #721c24; }
.flash.info { background: #d1ecf1; color: #0c5460; }
5) كيفية التشغيل والاختبار
1- شغّل التطبيق:- python app.py
http://127.0.0.1:5000/
3-اذهب إلى صفحة الاشتراك، سجِّل مشتركاً جديداً — ستحفظ السجلات في
subscribers.db
(ملف SQLite).4-اذهب إلى
/subscribers
لعرض السجلات، وحاول حذف أحدها.6) التمرين (مطلوب منك)
المطلوب:
- أضف ميزة تعديل بيانات المشترك (تعديل الاسم أو البريد).
- أضف صفحة بحث تمكنك من البحث عن مشترك بواسطة البريد أو الاسم (بحث نصي جزئي).
- (اختياري) أضف صفحة لتصدير المشتركين كـ CSV للتحميل.
7) الحل المقترح للتمرين (ملخص وقطع كود جاهزة)
أ) تعديل بيانات مشترك — فكرة سريعة
-
أضف رابط "تعديل" لكل صف يذهب إلى
/subscribers/edit/<id>
(GET يعرض نموذج ملؤه بقيم الحالية، POST يحفظ التعديل). - مثال مسار التعديل:
# داخل app.py
@app.route('/subscribers/edit/<int:sub_id>', methods=['GET','POST'])
def edit_subscriber(sub_id):
sub = Subscriber.query.get_or_404(sub_id)
if request.method == 'POST':
name = request.form.get('name','').strip()
email = request.form.get('email','').strip().lower()
if not name or not email:
flash("املأ الحقول.", "warning")
return redirect(url_for('edit_subscriber', sub_id=sub_id))
# التحقق من تكرار البريد مع آخرين
exists = Subscriber.query.filter(Subscriber.email==email, Subscriber.id!=sub_id).first()
if exists:
flash("هذا البريد مستخدم من قبل مشترك آخر.", "danger")
return redirect(url_for('edit_subscriber', sub_id=sub_id))
sub.name = name
sub.email = email
db.session.commit()
flash("تم التعديل.", "success")
return redirect(url_for('subscribers_list'))
# GET -> عرض النموذج
return render_template('edit_subscriber.html', subscriber=sub)
وتحتاج قالب edit_subscriber.html
بسيط يرث base.html
ويحتوي نموذجًا بلاحق POST
إلى نفس الرابط.
ب) بحث نصي (جزئي)
-
أضف نموذج بحث في
/subscribers
يرسلGET
مع باراميترq
:
@app.route('/subscribers')
def subscribers_list():
q = request.args.get('q','').strip()
if q:
subs = Subscriber.query.filter(
(Subscriber.name.contains(q)) | (Subscriber.email.contains(q))
).order_by(Subscriber.created_at.desc()).all()
else:
subs = Subscriber.query.order_by(Subscriber.created_at.desc()).all()
return render_template('subscribers.html', subscribers=subs, q=q)
وفي القالب أضف حقل بحث:
<form method="get" action="{{ url_for('subscribers_list') }}">
<input type="text" name="q" value="{{ q|default('') }}" placeholder="ابحث بالاسم أو البريد">
<button type="submit">بحث</button>
</form>
ج) تصدير CSV
-
مسار بسيط يولّد CSV من الاستعلام:
import csv
from flask import Response
@app.route('/subscribers/export')
def export_csv():
subs = Subscriber.query.order_by(Subscriber.created_at).all()
def generate():
yield "id,name,email,created_at\n"
for s in subs:
yield f"{s.id},{s.name},{s.email},{s.created_at.isoformat()}\n"
return Response(generate(), mimetype='text/csv',
headers={"Content-Disposition":"attachment;filename=subscribers.csv"})
8) ملاحظات أمنية ونصائح
-
لا تحفظ كلمات مرور في plain text (لو كان لديك مستخدمين). استخدم
werkzeug.security.generate_password_hash
وcheck_password_hash
. - في بيئة الإنتاج غير
debug=True
. - ضع
SECRET_KEY
آمنًا (لا تحتفظ به في الكود العام)، واستعمل متغيرات بيئة. - ضع حدود على إدخالات النص لمنع هجمات XSS، واستخدم Jinja الافتراضي الذي يفلتر النصوص.