وب با Flask
یک وبسایت مینویسیم: آدرسها، قالبهای HTML، فرمها، ورود کاربر، یک وبسرویس JSON، و یک پایگاه داده پشت همه این موارد. هر چه در این دوره خواندهاید اینجا یکجا برمیگردد: تابع، دیکشنری، کلاس، دکوراتور، SQLAlchemy و JSON.
۱. وب چطور کار میکند
- مرورگر یک درخواست میفرستد: یک روش (
GET،POSTو مانند آن) و یک آدرس. - برنامه شما تابع نمایی را انتخاب میکند که مسیرش با آن آدرس جور است.
- تابع یک پاسخ برمیگرداند: مقداری HTML یا JSON، بهعلاوه یک کد وضعیت مثل ۲۰۰ یا ۴۰۴ یا ۵۰۰.
- مرورگر آن را میکشد.
- و بعد همه چیز را فراموش میکند. HTTP حافظه ندارد، و نشست و کوکی دقیقاً برای همین وجود دارند.
python -m venv .venv .venv\Scripts\activate # Windows pip install -r requirements.txt
۲. اولین سایت
from flask import Flask
# `__name__` tells Flask where this file is, so it can find `templates/`
# and `static/` next to it.
app = Flask(__name__)
@app.route('/')
def home() -> str:
'''The home page. The name of the function is up to you.'''
return '<h1>Hello!</h1><p>This is my first Flask program.</p>'
@app.route('/about')
def about() -> str:
return ('<h1>Kadoos Institute</h1>'
'<p>Python course, Rasht.</p>')
# One function can answer several addresses.
@app.route('/contact')
@app.route('/contact-us')
def contact() -> str:
return '<h1>Contact</h1><p>Come and see us in Rasht.</p>'
if __name__ == '__main__':
# host='127.0.0.1' = only this computer can open it.
# host='0.0.0.0' would let the whole classroom network in.
app.run(debug=True)
کوچکترین سایت دنیا: نمونه 01_hello_flask/app.py
python app.py # then open http://127.0.0.1:5000
شرح مثال
- دکوراتور
@app.route('/about')آدرس را به تابع وصل میکند. این همان دکوراتور فصل بیست و ششم است که حالا کاربرد واقعی گرفته. - نام تابع با خودتان است، ولی مهم است:
url_forبعداً با همین نام کار میکند. - یک تابع میتواند به چند آدرس جواب بدهد؛ کافی است دو دکوراتور روی هم بگذارید.
حالت اشکالزدایی دو چیز عالی به شما میدهد: سرور با هر بار ذخیره فایل خودش را دوباره راه میاندازد، و خطا به جای پیام خشک Internal Server Error با ردیابی کامل نشان داده میشود. ولی همان صفحه، به هر بازدیدکنندهای یک کنسول پایتون داخل برنامه شما میدهد. روی سرور واقعی، هرگز.
۳. آدرس متغیر، url_for و ۴۰۴
@app.route('/user/<username>')
def profile(username: str) -> str:
'''A missing user is a 404, not a crash.'''
if username not in USERS:
abort(404)
return f'<h1>{USERS[username]}</h1><p>username: {username}</p>'
@app.route('/post/<int:post_id>')
def post(post_id: int) -> str:
'''`<int:...>`: post_id really is an int here.'''
title = POSTS.get(post_id)
if title is None:
abort(404)
# post_id is a real int, so arithmetic works without any conversion.
return (f'<h1>{title}</h1>'
f'<p>post {post_id}; the next one would be {post_id + 1}</p>')
@app.route('/old-home')
def old_home():
'''A page that moved: send the browser to the new address.'''
return redirect(url_for('home'))
@app.errorhandler(404)
def not_found(error):
'''Our own 404 page. The second value is the HTTP STATUS CODE.'''
return ('<h1>404 - not found</h1>'
f'<p><a href="{url_for("home")}">back home</a></p>'), 404
مسیرهای متغیر: نمونه 02_routes_and_variables/app.py
| مبدل | میپذیرد |
|---|---|
string (پیشفرض) | هر متنی بدون اسلش |
int | عدد صحیح، و واقعاً int تحویل میدهد |
float | عدد اعشاری |
path | متن همراه با اسلش |
uuid | شناسه یکتای جهانی |
شرح مثال
<int:post_id>ارزش زیادی دارد: آدرس/post/abcیک ۴۰۴ تمیز میگیرد و اصلاً به کد شما نمیرسد.url_for('نام تابع', آرگومانها)آدرس را از روی نام تابع میسازد. هرگز لینک را دستی ننویسید: مسیر را یک بار عوض کنید و همه لینکها دنبالش میآیند.- نسخه اصلی این درس مستقیم
usernames[username]مینوشت. کاربر ناشناس یعنیKeyErrorو بازدیدکننده یک ۵۰۰ میگرفت، که یعنی «برنامهنویس اشتباه کرده». کاربری که وجود ندارد اشتباه نیست؛ پاسخ صادقانهاش ۴۰۴ است. کارabort(404)همین است. @app.errorhandler(404)صفحه خطای خودتان را جایگزین میکند. عدد دوم درreturnهمان کد وضعیت است.
۴. قالبهای Jinja2
برگرداندن HTML از داخل رشته پایتون، بعد از سه خط از دست میرود. Flask
از Jinja2 استفاده میکند: یک فایل HTML که در آن
سوراخهایی هست. قالبها در پوشهای به نام templates/
کنار app.py مینشینند و این نام ثابت است.
@app.route('/students')
def students():
return render_template('students.html', students=STUDENTS,
passing_mark=10)
@app.route('/empty')
def empty():
'''The same template with an empty list: see the {% else %} of the loop.'''
return render_template('students.html', students=[], passing_mark=10)
تابع نما: نمونه 03_templates_jinja/app.py
<table border="1" cellpadding="6">
<tr><th>#</th><th>Name</th><th>City</th><th>Score</th><th></th></tr>
{# `loop.index` counts from 1; `students` is the list we passed in #}
{% for student in students %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ student.name | title }}</td>
<td>{{ student.city }}</td>
<td>{{ student.score }}</td>
<td>
{% if student.score >= passing_mark %}
passed
{% else %}
failed
{% endif %}
</td>
</tr>
{% else %}
{# runs when the list is EMPTY -- no `if` needed around the loop #}
<tr><td colspan="5">Nobody has registered yet.</td></tr>
{% endfor %}
</table>
قالب: templates/students.html
| نحو Jinja | وظیفه آن |
|---|---|
{{ value }} | چاپ یک مقدار |
{% if %} ... {% endif %} | شرط |
{% for x in items %} | حلقه، با {% else %} برای حالت خالی |
{{ value | upper }} | فیلتر |
{# ... #} | توضیح Jinja، که به HTML نمیرسد |
loop.index | شماره تکرار، از یک |
فیلترهایی که زود به کارتان میآیند: length،
upper، title، round،
default('-')، join(', ') و
tojson.
Jinja به طور خودکار محتوای متغیرها را بیخطر میکند: یک script که کاربر تایپ کرده، به متن ساده تبدیل میشود و اجرا نمیشود. اگر این محافظ را با فیلتر safe روی دادهای که کاربر فرستاده خاموش کنید، در همین مرحله یک حفره XSS ساختهاید. قاعده ساده است: هرگز safe را روی دادهای که از کاربر آمده نگذارید.
۵. ارثبری قالب و فایل ایستا
نوشتن سرصفحه و منو و پاصفحه در ده فایل یعنی ده جا برای تغییر دادن. راه درست، یک قالب پایه است که بقیه از آن ارث میبرند.
<head>
<meta charset="UTF-8">
{# A block with a DEFAULT value: a child page may override the title. #}
<title>{% block title %}{{ site_name }}{% endblock %}</title>
{# url_for('static', ...) -- never write the path by hand. #}
<link rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<h1>{{ site_name }}</h1>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('courses') }}">Courses</a>
</nav>
</header>
<main>
{# The hole every child page fills. #}
{% block content %}{% endblock %}
</main>
</body>
قالب پایه: نمونه 04_inheritance_and_static/templates/base.html
شرح مثال
- هر صفحه فرزند با
{% extends 'base.html' %}شروع میشود و فقط بلوکها را پر میکند. {% include %}یک تکه قالب را در جای دیگر میگذارد؛ برای کارتها و اجزای تکراری.- فایلهای ایستا (CSS، تصویر، جاوااسکریپت) در پوشه
static/میروند و همیشه باurl_for('static', filename=...)صدا زده میشوند. @app.context_processorمتغیری مثلsite_nameرا در همه قالبها در دسترس میگذارد، بدون اینکه در هرrender_templateپاسش بدهید.
۶. فرمها و قاعده PRG
def validate(form) -> tuple[dict, list[str]]:
'''Clean the fields and collect every problem, not just the first one.'''
data = {
'name': form.get('name', '').strip(),
'email': form.get('email', '').strip(),
'course': form.get('course', ''),
'sessions': form.get('sessions', '').strip(),
'newsletter': 'newsletter' in form, # a checkbox is present or not
}
errors = []
if len(data['name']) < 3:
errors.append('The name must be at least 3 characters long.')
if not EMAIL_PATTERN.match(data['email']):
errors.append('That does not look like an e-mail address.')
# Never trust a <select>: the browser is not the only thing that can POST.
if data['course'] not in COURSES:
errors.append('Please choose a course from the list.')
if not data['sessions'].isdigit() or not 1 <= int(data['sessions']) <= 20:
errors.append('The number of sessions must be between 1 and 20.')
return data, errors
@app.route('/', methods=['GET', 'POST'])
def register():
'''One address, two methods: show the form, then receive it.'''
if request.method == 'POST':
data, errors = validate(request.form)
if errors:
for message in errors:
flash(message, 'error')
# Give the form back WITH what the visitor already typed.
return render_template('register.html', courses=COURSES,
data=data)
data['sessions'] = int(data['sessions'])
REGISTRATIONS.append(data)
flash(f'Thank you {data["name"]}, you are registered.', 'success')
# POST / Redirect / GET: F5 now reloads a harmless page.
return redirect(url_for('registrations'))
# GET: an empty form.
return render_template('register.html', courses=COURSES, data={})
فرم: نمونه 05_forms/app.py
| روش | معنایش | کجا |
|---|---|---|
GET | چیزی به من بده | جستوجو و نمایش. مقدارها در آدرساند |
POST | این را بگیر و نگه دار | هر چیزی که داده را تغییر میدهد |
شرح مثال
request.form.get('x', '')امنتر ازrequest.form['x']است: به جای خطا، مقدار پیشفرض میدهد.- همه خطاها با هم جمع میشوند، نه فقط اولی. کاربری که فرم را چهار بار پشت سر هم پر میکند، خیلی زود سایت شما را میبندد.
- به
selectاعتماد نکنید. مرورگر تنها چیزی نیست که میتواندPOSTبفرستد. - وقتی خطا هست، فرم را با همان چیزی که کاربر تایپ کرده برگردانید. پاک کردن فرم، بیاحترامی به وقت اوست.
بعد از یک POST موفق، هرگز صفحه را رندر نکنید؛
هدایت کنید. وگرنه مرورگر هنوز همان POST را در دست
دارد و زدن F5 فرم را دوباره میفرستد: ثبتنام
دوم، پرداخت دوم. هدایت کردن، آخرین کار را به یک GET بیخطر تبدیل
میکند. پیام flash() هم برای همین است: پیام را
نگه میدارد تا صفحه بعدی نشانش دهد.
۷. نشست و کوکی
HTTP حافظه ندارد. هر درخواست تنها میآید و سرور نمیداند کسی که
/profile را میخواهد همان کسی است که یک ثانیه پیش فرم
ورود را پر کرد. پاسخ، کوکی است.
session['username'] = 'sara' # write
session.get('username') # read
session.pop('username', None) # forget
session.clear() # forget everything
نشست، یک دیکشنری درون کوکی
آنچه باید درباره نشست بدانید
- کوکی با
app.secret_keyامضا میشود، پس کسی نمیتواند بدون لو رفتن تغییرش دهد. ولی فقط base64 است، پس هر کسی میتواند بخواندش. هرگز رمز عبور یا شماره کارت یا هر چیز خصوصی در نشست نگذارید. - اگر کلید مخفی عوض شود، همه نشستها همان لحظه باطل میشوند.
- همهاش در مرورگر بازدیدکننده زندگی میکند، پس کوچک نگهش دارید.
- کلید مخفی، مثل توکن ربات فصل سی و هفتم، نباید در کد یک سایت واقعی نوشته شود. از متغیر محیطی بخوانیدش.
۸. رمز عبور و login_required
def login_required(view):
'''A decorator that sends anonymous visitors to the login page.'''
@functools.wraps(view) # keep the name of the wrapped function
def wrapper(*args, **kwargs):
if 'username' not in session:
flash('Please log in first.', 'error')
# `next=` remembers where the visitor wanted to go.
return redirect(url_for('login', next=request.path))
return view(*args, **kwargs)
return wrapper
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username', '').strip().lower()
password = request.form.get('password', '')
user = USERS.get(username)
# ONE message for both cases: saying "no such user" would tell an
# attacker which names exist.
if user is None or not check_password_hash(user['password_hash'],
password):
flash('Wrong user name or password.', 'error')
return render_template('login.html', username=username)
# A new session id at login: it closes the "session fixation" hole.
session.clear()
session['username'] = username
# Never redirect to any address the visitor sends us: an absolute URL
# here would be an "open redirect" straight to a phishing page.
target = request.args.get('next', '')
if not target.startswith('/') or target.startswith('//'):
target = url_for('profile')
return redirect(target)
return render_template('login.html', username='')
@app.route('/profile')
@login_required
def profile():
...
ورود کاربر: نمونه 06_sessions_and_login/app.py
درهم رمز را ذخیره کنید: generate_password_hash('1234') چیزی مثل scrypt:32768:8:1$xYz... میسازد، و check_password_hash(hash, typed) درست یا نادرست میدهد. درهم را نمیشود به رمز برگرداند، پس پایگاه داده دزدیدهشده، رمز همه کاربران را به مهاجم نمیدهد. کتابخانه werkzeug همراه Flask میآید، پس در همین مرحله داریدش.
سه نکته امنیتی در همین چند خط
- برای «کاربر وجود ندارد» و «رمز غلط است» یک پیام بدهید. پیام جداگانه به مهاجم میگوید کدام نام کاربری واقعی است.
session.clear()هنگام ورود، حفره تثبیت نشست را میبندد.- هرگز به هر آدرسی که بازدیدکننده میفرستد هدایت نکنید. یک آدرس مطلق در
nextیعنی هدایت باز مستقیم به یک صفحه فیشینگ. @functools.wrapsدر دکوراتور فراموش نشود: Flask توابع نما را با نامشان میشناسد و بدون آن، دو نما هر دوwrapperنام میگیرند و برنامه بالا نمیآید.
۹. وبسرویس JSON
در فصل بیست و هشتم یک REST API را صدا زدیم. حالا خودمان یکی مینویسیم. تفاوتش با صفحههای بالا این است که به جای HTML، JSON برمیگردانیم و کد وضعیت معنا را حمل میکند.
| کد | معنایش |
|---|---|
| ۲۰۰ | بفرمایید، این هم جوابتان |
| ۲۰۱ | چیزی که فرستادی را ساختم |
| ۲۰۴ | انجام شد و چیزی برای گفتن نیست |
| ۴۰۰ | پیام تو غلط بود |
| ۴۰۱ و ۴۰۳ | وارد نشدهای / اجازه نداری |
| ۴۰۴ | چنین چیزی وجود ندارد |
| ۴۰۵ | روش اشتباه برای این آدرس |
| ۵۰۰ | من اشتباه کردم |
def read_payload() -> tuple[dict | None, tuple | None]:
'''Return (data, error_response). Exactly one of the two is None.'''
# silent=True -> None instead of an exception when the body is not JSON.
payload = request.get_json(silent=True)
if not isinstance(payload, dict):
return None, (jsonify(error='the body must be a JSON object'), 400)
return payload, None
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id: int):
if book_id not in BOOKS:
return jsonify(error=f'no book with id {book_id}'), 404
return jsonify(with_id(book_id))
@app.route('/api/books', methods=['POST'])
def create_book():
global next_id
payload, error = read_payload()
if error:
return error
title = str(payload.get('title', '')).strip()
if not title:
return jsonify(error='"title" is required'), 400
book_id = next_id
next_id += 1
BOOKS[book_id] = {'title': title, ...}
# 201 Created, and the address of the new thing in the Location header.
return jsonify(with_id(book_id)), 201, {
'Location': f'/api/books/{book_id}'}
@app.errorhandler(404)
def api_not_found(error):
'''An API must answer JSON even when it fails -- never an HTML page.'''
return jsonify(error='not found'), 404
وبسرویس: نمونه 07_json_api/app.py
| آدرس | روش | وظیفه آن |
|---|---|---|
/api/books | GET | فهرست |
/api/books | POST | ساختن یکی |
/api/books/3 | GET | گرفتن یکی |
/api/books/3 | PUT | جایگزینی کامل |
/api/books/3 | PATCH | تغییر بخشی از آن |
/api/books/3 | DELETE | حذف |
شرح مثال
- برگرداندن ۲۰۰ با یک
errorداخل بدنه (کاری که تازهکارها میکنند) هر مشتری را مجبور میکند بدنه را بخواند تا بفهمد کار انجام شده یا نه. از کدها استفاده کنید. - نسخه اصلی این درس مستقیم
payload['title']مینوشت. اگر مشتری فیلدی را جا بیندازد یا اصلاً JSON نفرستد، نتیجهاشKeyErrorاست و یک صفحه ۵۰۰. ولی فیلد جاافتاده اشتباه مشتری است: ۴۰۰. get_json(silent=True)به جای استثنا،Noneمیدهد؛ دقیقاً همان چیزی که برای بررسی لازم داریم.- بستهبندی فهرست در یک شیء (
countوbooks) جا برای اضافه کردن «صفحه بعدی» باقی میگذارد، بدون شکستن همه مشتریها. - یک API حتی هنگام شکست هم باید JSON بدهد، نه صفحه HTML. برای همین
errorhandlerهمjsonifyبرمیگرداند.
۱۰. مصرفکننده API
BASE_URL = 'http://127.0.0.1:5000'
TIMEOUT = 5
def main() -> None:
session = requests.Session() # reuses the connection
try:
show('GET the list', session.get(f'{BASE_URL}/api/books',
timeout=TIMEOUT))
response = session.post(f'{BASE_URL}/api/books',
json={'title': 'Automate the Boring Stuff',
'author': 'Al Sweigart',
'year': 2019},
timeout=TIMEOUT)
show('POST a new book', response)
book_id = response.json()['id']
print(' Location header:', response.headers.get('Location'))
مشتری: نمونه 08_api_client/client.py
سه کاری که این مشتری میکند و نسخه اصلی نمیکرد: روی
هر درخواست timeout میگذارد (بدون آن یک
درخواست میتواند تا ابد معلق بماند)؛ کورکورانه
raise_for_status() صدا نمیزند بلکه به کد نگاه میکند تا
بتواند «کتاب وجود ندارد» را از «سرور خراب است» تشخیص دهد؛ و وقتی سرور
اصلاً بالا نیست، صریح به کاربر میگوید.
۱۱. Flask-SQLAlchemy
همان SQLAlchemy فصل سی و دوم، این بار داخل یک سایت.
Flask-SQLAlchemy لایه نازکی روی آن است که
db.session و db.Model را از قبل به برنامه
گره میزند.
class Base(DeclarativeBase):
'''The base class every model inherits from (SQLAlchemy 2 style).'''
db = SQLAlchemy(model_class=Base)
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///library.db'
db.init_app(app)
class Author(db.Model):
__tablename__ = 'author'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
# One author has many books. `back_populates` connects the two sides;
# `cascade` deletes the books of an author we delete.
books: Mapped[list['Book']] = relationship(
back_populates='author', cascade='all, delete-orphan')
# -- commands, not routes --------------------------------------------------
@app.cli.command('init-db')
def init_db_command() -> None:
'''Create the tables and a little sample data.
Run it with: flask --app app init-db
'''
db.create_all()
مدلها و فرمان: نمونه 09_flask_sqlalchemy/app.py
| دستور | وظیفه آن |
|---|---|
db.session.scalars(db.select(Book)).all() | همه سطرها |
db.select(Book).where(...) | با شرط |
db.session.get(Book, 3) | با کلید اصلی |
db.get_or_404(Book, 3) | یا شیء، یا یک ۴۰۴ تمیز |
db.session.add(x); db.session.commit() | ذخیره |
db.session.delete(x) | حذف |
نسخه اصلی این درس یک مسیر GET /api/init-db
داشت که جدولها را میساخت. یعنی هر کسی که آن آدرس را پیدا کند
میتواند به پایگاه داده شما دست بزند؛ و اصولاً یک GET
نباید هیچ چیزی را عوض کند. راه درست یک فرمان است:
flask --app app init-db.
۱۲. پروژه: دفترچه یادداشت
پروژه پایانی این فصل، یک برنامه یادداشت با دستهبندی، جستوجو، چهار عمل اصلی و یک API است. مهمترین درسش دو الگوست که هر پروژه Flask بعد از سی مسیر به آنها نیاز پیدا میکند: کارخانه برنامه و بلوپرینت.
def create_app(database_uri: str | None = None) -> Flask:
'''Build and return a ready application.'''
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY',
'dev-key-not-for-production')
app.config['SQLALCHEMY_DATABASE_URI'] = (database_uri
or 'sqlite:///notes.db')
db.init_app(app)
app.register_blueprint(notes_bp)
register_error_handlers(app)
register_commands(app)
return app
کارخانه برنامه: نمونه 10_notes_project/app.py
چرا این تابع اضافه ارزشش را دارد
- پیکربندی یک آرگومان میشود، پس همان کد میتواند با پایگاه داده دیگری اجرا شود: یک پایگاه داده واقعی، یک موقتی، یکی برای هر برنامهنویس.
- صرفاً به خاطر
importشدن ماژول، هیچ اتفاقی نمیافتد. - گره وارد کردن دایرهای باز میشود:
modelsوroutesهرگزappرا وارد نمیکنند.
ساختار پروژه سه فایل پایتون دارد که هر کدام یک کار میکنند:
models.py فقط جدولها، routes.py فقط مسیرها
در قالب یک بلوپرینت، و app.py که همه را به هم وصل
میکند. همان لایهبندی فصل سی و پنجم، این بار در وب.
class Note(db.Model):
__tablename__ = 'note'
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(120), nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False, default='')
# A note without a category is allowed, so the column may be NULL.
category_id: Mapped[int | None] = mapped_column(
ForeignKey('category.id'), nullable=True)
category: Mapped[Category | None] = relationship(back_populates='notes')
# server_default=func.now(): the DATABASE fills it in, so the value is
# right even for a row inserted by hand in another program.
created_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now())
def to_dict(self) -> dict:
'''The shape the JSON API sends out.'''
return {'id': self.id,
'title': self.title,
'body': self.body,
'category': self.category.name if self.category else None,
'created_at': self.created_at.isoformat()}
مدل یادداشت: نمونه 10_notes_project/models.py
پوشه S38-flask-web-development شامل ده پوشه است. در
هر پوشه python app.py بزنید و
http://127.0.0.1:5000 را باز کنید؛ با
Ctrl-C متوقفش کنید. برای نمونه هشتم، اول سرور نمونه هفتم را در
ترمینال دوم راه بیندازید. برای نمونههای نهم و
دهم، اول flask --app app init-db بزنید.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
Flask(__name__) | ساختن برنامه |
@app.route('/x') | وصل کردن آدرس به تابع |
<int:post_id> | بخش متغیر آدرس، با تبدیل خودکار |
url_for('name', ...) | ساخت لینک. هرگز دستی ننویسید |
abort(404) | پاسخ صادقانه به چیزی که وجود ندارد |
@app.errorhandler(404) | صفحه خطای خودتان |
redirect(...) | فرستادن مرورگر به آدرس دیگر |
render_template(...) | رندر یک قالب Jinja2 |
| گریز خودکار | محافظ XSS. safe روی داده کاربر ممنوع |
extends و block | ارثبری قالب |
url_for('static', filename=...) | فایلهای ایستا |
request.form و request.args | داده فرم و داده آدرس |
| POST / Redirect / GET | تا F5 فرم را دوباره نفرستد |
flash() | پیام برای صفحه بعدی |
session و secret_key | کوکی امضاشده. خواندنی است، پس چیز خصوصی در آن نگذارید |
generate_password_hash | هرگز رمز را خام ذخیره نکنید |
@login_required | دکوراتور محافظ صفحه |
jsonify(...), 201 | پاسخ JSON با کد وضعیت درست |
get_json(silent=True) | بدنه غیر JSON یعنی ۴۰۰، نه ۵۰۰ |
db.get_or_404 | شیء یا ۴۰۴ تمیز |
@app.cli.command | فرمان، به جای مسیری که پایگاه داده را دست میزند |
| کارخانه برنامه و بلوپرینت | تا پروژه بعد از سی مسیر خوانا بماند |
تمرینهای فصل سی و هشت
- سایتی با سه صفحه بسازید: خانه، درباره ما و تماس، با یک منوی مشترک در قالب پایه.
- مسیری بسازید که عددی بگیرد و جدول ضرب آن عدد را در یک جدول HTML نشان دهد.
- صفحه ۴۰۴ و ۵۰۰ اختصاصی برای سایتتان بنویسید.
- فهرست دانشجویان را از یک فایل JSON بخوانید و با
forوifدر قالب نمایش دهید. - یک فرم نظرسنجی بسازید که همه فیلدها را اعتبارسنجی کند، خطاها را با
flashنشان دهد، و بعد از ثبت موفق هدایت کند. - جستوجوی
?q=روی همان فهرست پیاده کنید. - ورود کاربر با رمز درهمشده و دکوراتور
@login_requiredبسازید و صفحه پروفایل را محافظت کنید. - به سایتتان صفحه خروج اضافه کنید و مطمئن شوید نشست کاملاً پاک میشود.
- یک REST API برای «کارهای امروز» بنویسید با هر شش عمل و کدهای وضعیت درست.
- برای همان API یک مشتری با
requestsبنویسید کهtimeoutدارد و ۴۰۴ را از ۵۰۰ تشخیص میدهد. - پروژه یادداشت را با یک مدل تازه گسترش دهید: برچسبها، با رابطه چندبهچند.
- به پروژه یادداشت، صفحهبندی اضافه کنید: ده یادداشت در هر صفحه.
- توضیح دهید چرا
debug=Trueروی سرور واقعی خطرناک است و دقیقاً چه چیزی را در اختیار مهاجم میگذارد. - توضیح دهید چرا فیلتر
safeروی دادهای که کاربر نوشته، یک حفره امنیتی است. یک نمونه ورودی بنویسید که حمله را نشان دهد.