طرح بلاگ

شما از همان تکنیک هایی که هنگام نوشتن طرح احراز هویت یاد گرفتید برای نوشتن طرح اولیه وبلاگ استفاده خواهید کرد. وبلاگ باید همه پست ها را فهرست کند، به کاربرانی که وارد سیستم شده اند اجازه ایجاد پست ها را بدهد و به نویسنده یک پست اجازه دهد آن را ویرایش یا حذف کند.

همانطور که هر نما را پیاده سازی می کنید، سرور توسعه را در حال اجرا نگه دارید. همانطور که تغییرات خود را ذخیره می کنید، سعی کنید به URL موجود در مرورگر خود بروید و آنها را آزمایش کنید.

طرح

طرح اولیه را تعریف کرده و در کارخانه اپلیکیشن ثبت کنید.

flaskr/blog.py
from flask import (
    Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort

from flaskr.auth import login_required
from flaskr.db import get_db

bp = Blueprint('blog', __name__)

با استفاده از app.register_blueprint() طرح اولیه را از کارخانه وارد و ثبت کنید. قبل از بازگرداندن برنامه، کد جدید را در انتهای عملکرد کارخانه قرار دهید.

flaskr/__init__.py
def create_app():
    app = ...
    # existing code omitted

    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')

    return app

بر خلاف طرح auth، طرح اولیه وبلاگ url_prefix ندارد. بنابراین نمای index در / ، نمای create در /create و سایر موارد با نام خودشان خواهد بود. وبلاگ ویژگی اصلی Flaskr است، بنابراین منطقی است که نمایه وبلاگ شاخص اصلی باشد.

با این حال، نقطه پایانی برای نمای index که در زیر تعریف شده است، blog.index خواهد بود. برخی از نماهای احراز هویت به یک نقطه پایانی ساده index اشاره می‌کنند. app.add_url_rule() نام نقطه پایانی 'index' را با نشانی اینترنتی / مرتبط می کند به طوری که url_for('index') یا url_for ('blog.index') هر دو کار خواهند کرد و در هر صورت یک URL / ایجاد می شود.

در برنامه دیگری، می‌توانید به وبلاگ یک url_prefix بدهید و یک نمای index جداگانه در کارخانه برنامه، شبیه به نمای hello تعریف کنید. سپس نقاط پایانی و URL های index و blog.index متفاوت خواهند بود.

فهرست(Index)

فهرست همه پست‌ها به ترتیب تاریخ را نشان می‌دهد. از یک``JOIN`` استفاده می شود تا اطلاعات نویسنده از جدول user در نتیجه موجود باشد.

flaskr/blog.py
@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html', posts=posts)
flaskr/templates/blog/index.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Posts{% endblock %}</h1>
  {% if g.user %}
    <a class="action" href="{{ url_for('blog.create') }}">New</a>
  {% endif %}
{% endblock %}

{% block content %}
  {% for post in posts %}
    <article class="post">
      <header>
        <div>
          <h1>{{ post['title'] }}</h1>
          <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
          <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
        {% endif %}
      </header>
      <p class="body">{{ post['body'] }}</p>
    </article>
    {% if not loop.last %}
      <hr>
    {% endif %}
  {% endfor %}
{% endblock %}

هنگامی که یک کاربر وارد سیستم می شود، بلوک header پیوندی را به نمای create اضافه می کند. وقتی کاربر نویسنده یک پست باشد، پیوند «ویرایش» به نمای «به روز رسانی» برای آن پست را می بیند. loop.last یک متغیر ویژه است که در Jinja for loops موجود است. برای نمایش یک خط بعد از هر پست به جز آخرین پست، برای جداسازی بصری آنها استفاده می شود.

ایجاد(Create)

نمای create مانند نمای تأیید اعتبار register عمل می کند. یا فرم نمایش داده می شود یا داده های ارسال شده اعتبارسنجی می شود و پست به پایگاه داده اضافه می شود یا خطا نشان داده می شود.

دکوراتور login_required که قبلا نوشتید در نماهای وبلاگ استفاده می شود. یک کاربر برای بازدید از این نماها باید وارد سیستم شود، در غیر این صورت به صفحه ورود هدایت می شوند.

flaskr/blog.py
@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?, ?, ?)',
                (title, body, g.user['id'])
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/create.html')
flaskr/templates/blog/create.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
{% endblock %}

بروزرسانی

هر دو نمای update و delete باید post را توسط id واکشی و بررسی کنند که آیا نویسنده با کاربر وارد شده مطابقت دارد یا خیر. برای جلوگیری از تکرار کد، می‌توانید تابعی بنویسید تا post را دریافت کند و آن را از هر نما فراخوانی کند.

flaskr/blog.py
def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, f"Post id {id} doesn't exist.")

    if check_author and post['author_id'] != g.user['id']:
        abort(403)

    return post

تابع abort() یک استثنای خاص ایجاد می کند که یک کد وضعیت HTTP را برمی گرداند. یک پیام اختیاری طول می کشد تا با خطا نشان داده شود، در غیر این صورت یک پیام پیش فرض استفاده می شود. ۴۰۴ به معنای «یافت نشد» و ۴۰۳ به معنای «ممنوع» است. (۴۰۱ به معنای «غیرمجاز» است، اما شما به جای بازگرداندن آن وضعیت، به صفحه ورود هدایت می شوید.)

آرگومان check_author به گونه ای تعریف شده است که می توان از این تابع برای دریافت post بدون بررسی نویسنده استفاده کرد. اگر یک نما بنویسید تا یک پست را در یک صفحه نشان دهید، جایی که کاربر اهمیتی ندارد زیرا پست را تغییر نمی دهد.

flaskr/blog.py
@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title = ?, body = ?'
                ' WHERE id = ?',
                (title, body, id)
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/update.html', post=post)

برخلاف نماهایی که تاکنون نوشته‌اید، تابع update آرگومان , id را می‌گیرد. که با <int:id> در مسیر مطابقت دارد. یک نشانی وب واقعی شبیه /1/update خواهد بود. فلاسک 1 را می گیرد، مطمئن می شود که یک int است و آن را به عنوان آرگومان id ارسال می کند. اگر int: را مشخص نکنید و به جای آن <id> را انجام دهید، یک رشته خواهد بود. برای ایجاد یک URL به صفحه به‌روزرسانی، url_for() باید از id عبور داده شود تا بداند چه چیزی را باید پر کند url_for('blog.update', id=post['id']) . این نیز در فایل index.html در بالا موجود است.

نمای create و update بسیار شبیه به هم هستند. تفاوت اصلی این است که نمای update از یک شی post و یک کوئری UPDATE به جای INSERT استفاده می کند. با مقداری refactoring هوشمندانه، می توانید از یک نما و الگو برای هر دو عملکرد استفاده کنید، اما برای آموزش، جدا نگه داشتن آنها واضح تر است.

flaskr/templates/blog/update.html
{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title"
      value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
  <hr>
  <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
  </form>
{% endblock %}

این قالب دو فرم دارد. اولین مورد داده های ویرایش شده را به صفحه فعلی پست می کند (/<id>/update). فرم دیگر فقط حاوی یک دکمه است و یک ویژگی عملی را مشخص می کند که به جای آن در نمای حذف پست می شود. این دکمه از مقداری جاوا اسکریپت برای نشان دادن گفتگوی تأیید قبل از ارسال استفاده می کند.

الگوی {{ request.form['title'] or post['title'] }} برای انتخاب داده‌هایی که در فرم نشان داده می‌شوند استفاده می‌شود. هنگامی که فرم ارسال نشده است، داده‌های post اصلی ظاهر می‌شود، اما اگر داده‌های فرم نامعتبر پست شده باشد، می‌خواهید آن را نمایش دهید تا کاربر بتواند خطا را برطرف کند، بنابراین request.form به جای آن استفاده می‌شود. request متغیر دیگری است که به صورت خودکار در قالب ها موجود است.

حذف

نمای حذف الگوی خود را ندارد، دکمه حذف بخشی از update.html است و به نشانی اینترنتی /<id>/delete پست می‌شود. از آنجایی که هیچ الگوی وجود ندارد، فقط روش POST را مدیریت می‌کند و سپس به نمای «نمایه» هدایت می‌شود.

flaskr/blog.py
@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))

تبریک می گویم، شما اکنون نوشتن درخواست خود را به پایان رسانده اید! کمی وقت بگذارید و همه چیز را در مرورگر امتحان کنید. با این حال، هنوز کارهای بیشتری قبل از تکمیل پروژه وجود دارد.

با پروژه را قابل نصب کنید ادامه دهید.