تحدّثنا في الجزء السابق من هذه السلسلة عن النماذج في إطار العمل Ruby on Rails وتعرّفنا على طريقة إنشائها والتعامل معها من خلال كتابة الشيفرة المسؤولة عن حفظ المقالة الجديدة في قاعدة البيانات.
في الجزء الثاني من هذا الموضوع سنتعلّم كيفية ربط نموذجين مع بعضهما البعض من خلال إنشاء نموذج جديد خاصّ بالتعليقات. ولكن قبل ذلك سنكمل ما بدأناه في الدروس السابقة من السلسلة في بناء عمليات “CRUD” حيث غطّينا سابقًا عمليتي الإنشاء Create و القراءة Read، وسنغطي اليوم العمليتين المتبقّيتين وهما التحديث Update والحذف Destroy.
تحديث المقالات
الخطوة الأولى في عملية تحديث المقالات هي إضافة حدث edit
إلى المتحكم ArticlesController
بين حدثي new
و create
وكما يلي:
def new @article = Article.new end def edit @article = Article.find(params[:id]) end def create @article = Article.new(article_params) if @article.save redirect_to @article else render 'new' end end
سيتضمن العرض استمارة مشابهة لتلك التي استخدمناها في إنشاء المقالات الجديدة. أنشئ ملفًّا باسم app/views/articles/edit.html.erb
وأضف إليه الشيفرة التالية:
<h1>Edit article</h1>
<%= form_for(@article) do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', articles_path %>
سنوجّه الاستمارة هذه المرة إلى حدث update
والذي لم نقم بتعريفه حتى الآن.
يؤدي تمرير كائن المقالة للتابع إلى إنشاء عنوان url
لإرسال استمارة المقالة التي تم تعديلها، ومن خلال هذا الخيار نخبر Rails بأنّنا نرغب في أن يتم إرسال هذا النموذج من خلال فعل HTTP PATCH
وهو أحد أفعال HTTP التي تستخدم في تحديث الموارد حسب بروتوكول REST.
يمكن أن يكون المعامل الأول لـ form_for
كائنًا، مثلًا @articl
، والذي سيؤدي بالدالة المساعدة إلى ملء الاستمارة بالحقول التابعة للكائن، ويؤدي تمرير الرمز (:article
) بنفس اسم المتغيّر من نوع instance
(@article
) إلى نفس النتيجة تلقائيًا.
والآن سنقوم بإنشاء الحدث update
في المتحكّم app/controllers/articles_controller.rb
وسنضيفه بين حدث create
والتابع ذي المحدّد الخاصّ private
:
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render 'new'
end
end
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render 'edit'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
يستخدم الحدث update
عندما ترغب في تحديث سجل موجود في قاعدة البيانات، ويستقبل هذا الحدث جدول تقطيع hash
يحتوي الخصائص التي ترغب في تحديثها. وكما سبق، في حال وجود خطأ في عملية التحديث سنعرض الاستمارة على المستخدم من جديد.
سنستخدم التابع article_params
الذي عرّفناه في وقت سابق للحدث create
.
لا حاجة لتمرير جميع الخصائص لغرض تحديثها، فعلى سبيل المثال، إن تم استدعاء @article.update(title: 'A new title'
) فسيقوم Rails بتحديث خاصية العنوان فقط، ويترك باقي الخصائص دون تعديل.
أخيرًا، نرغب في عرض رابط إلى الحدث edit
في الصفحة التي نعرض فيها قائمة المقالات، لذا توجّه إلى الملف app/views/articles/index.html.erb
وأضف الرابط ليظهر إلى جانب رابط “Show”:
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="2"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
</tr>
<% end %>
</table>
سنضيف كذلك رابطًا إلى قالب app/views/articles/show.html.erb
ليظهر رابط “Edit” في صفحة المقالة أيضًا:
...
<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>
هذا هو شكل تطبيقنا حتى هذه اللحظة:
استخدام الملفات الجزئية partials لإزالة التكرار من العروض
تبدو صفحة تحرير المقالة مشابهة تمامًا لصفحة إنشاء مقالة جديدة، وفي الواقع تستخدم الصفحتان الشيفرة ذاتها لعرض الاستمارة. سنقوم الآن بالتخلص من هذا التكرار باستخدام ملفات العرض الجزئية. تحمل هذه الملفات أسماء تبدأ بالمحرف (_).
أنشئ ملفًّا جديدًا باسم _form.html.erb
ضمن المسار app/views/articles/
وأضف إليه الشيفرة التالية:
<%= form_for @article do |f| %>
<% if @article.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(@article.errors.count, "error") %> prohibited
this article from being saved:
</h2>
<ul>
<% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
لاحظ أننا لم نغيّر شيئًا باستثناء الإعلان عن التابع form_for
وسبب استخدام هذه الأسلوب المختصر والبسيط في الإعلان عن التابع form_for
للتعبير عن الاستمارتين هو أن @article
عبارة عن مورد يرتبط بمجموعةٍ من مسارات RESTful، وبإمكان Rails أن يخمّن عنوان URI والتابع الذي يجب استخدامه.
والآن لنقم بتحديث العرض app/views/articles/new.html.erb
لاستخدام الملف الجزئي الذي أنشأناه وسنقوم بإعادة كتابة العرض من جديد وكما يلي:
<h1>New article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
ثم قم بالأمر عينه في ملف العرض app/views/articles/edit.html.erb
:
<h1>Edit article</h1>
<%= render 'form' %>
<%= link_to 'Back', articles_path %>
حذف المقالات
هذه هي العملية الأخيرة ضمن عمليات CRUD، وبحسب معايير REST فإن المسار الذي يؤدي إلى حذف المقالات وكما يظهر في مخرجات الأمر bin/rails routes
هو:
DELETE /articles/:id(.:format) articles#destroy
يجب استخدام الفعل DELETE
في المسار المسؤول عن حذف الموارد، أما في حال استخدام الفعل GET
فسيكون بالإمكان إنشاء رابط خبيث كهذا الرابط مثلًا:
<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
سنستخدم التابع delete
لحذف المصادر، وهذا المسار مرتبط بالحدث destroy
ضمن المتحكّم app/controllers/articles_controller.rb
والذي لم نقم بإنشائه بعد. عادة ما يكون التابع destroy
التابع الأخير ضمن المتحكّم، وكما هو الحال مع بقية التوابع العامّة public
يجب الإعلان عنه قبل أي توابع خاصّة أو محميّة protected
.
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path
end
الصورة النهائية للمتحكّم ArticleController
في الملف app/controllers/articles_controller.rb
هي:
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
def show
@article = Article.find(params[:id])
end
def new
@article = Article.new
end
def edit
@article = Article.find(params[:id])
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render 'new'
end
end
def update
@article = Article.find(params[:id])
if @article.update(article_params)
redirect_to @article
else
render 'edit'
end
end
def destroy
@article = Article.find(params[:id])
@article.destroy
redirect_to articles_path
end
private
def article_params
params.require(:article).permit(:title, :text)
end
end
يمكن استدعاء التابع destroy
في كائنات التسجيلة النشطة Active Record عندما ترغب في حذفها من قاعدة البيانات. لاحظ أنّنا لسنا بحاجة لإضافة عرض خاص بهذا الحدث لأنّنا نعيد توجيه المستخدم إلى الحدث index
.
أخيرًا أضف رابط ‘Destroy’ إلى القالب app/views/articles/index.html.erb
لنربط كل الصفحات مع بعضها البعض.
<h1>Listing Articles</h1>
<%= link_to 'New article', new_article_path %>
<table>
<tr>
<th>Title</th>
<th>Text</th>
<th colspan="3"></th>
</tr>
<% @articles.each do |article| %>
<tr>
<td><%= article.title %></td>
<td><%= article.text %></td>
<td><%= link_to 'Show', article_path(article) %></td>
<td><%= link_to 'Edit', edit_article_path(article) %></td>
<td><%= link_to 'Destroy', article_path(article),
method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
استخدمنا هنا التابع link_to
بطريقة مختلفة، حيث مررّنا اسم المسار كمعامل ثانٍ، ثمّ مرّرنا الخيارات الأخرى بعد ذلك. تستخدم الخيارات method: :delete
وdata: { confirm: 'Are you sure?' }
كخصائص HTML5 بحيث يؤدي الضغط على الرابط إلى عرض مربع حوار للتأكد من رغبة المستخدم في حذف المقالة، ثم إرسال الرابط باستخدام التابع delete
.
تتمّ تعملية التحقّق هذه بواسطة ملف JavaScript الذي يحمل الاسم rails-ujs
والموجود بصورة افتراضية في مخطط التطبيق (app/views/layouts/application.html.erb
)، وفي حال عدم وجود هذا الملف لن يظهر مربع الحوار التأكيدي للمستخدم.
تهانينا أصبح بإمكانك الآن إنشاء وعرض وسرد وتحديث وحذف المقالات في مدوّنتك.
Quoteملاحظة:
بصورة عامة يشجّع Rails على استخدام كائنات الموارد resources objects بدلًا من كتابة المسارات يدويًا.
إضافة النموذج الخاصّ بالتعليقات
سنقوم الآن بإنشاء نموذج جديد في تطبيقنا هذا ستكون وظيفته التعامل مع التعليقات.
إنشاء النموذج
لإنشاء النموذج الخاص بالتعليقات سنتبع الأسلوب السابق نفسه وذلك باستخدام أداة المولّد لإنشاء نموذج يحمل الاسم Comment ويمثّل مرجعًا إلى المقالة. اكتب الأمر التالي في سطر الأوامر:
$ bin/rails generate model Comment commenter:string body:text article:references
سينشئ هذا الأمر أربعة ملفات:
الملف Purpose
الملف/المجلد | الوظيفة |
db/migrate/20140120201010_create_comments.rb | ملف التهجير المسؤول عن إنشاء جدول التعليقات في قاعدة البيانات (سيحمل اسم الملف لديك ختمًا زمنيًا مختلفًا) |
app/models/comment.rb | النموذج الخاص بالتعليقات |
test/models/comment_test.rb | ملف الاختبارات الخاص بنموذج التعليقات |
test/fixtures/comments.yml | نماذج تعليقات تستخدم في إجراء الاختبارات |
لنلق نظرة في البداية على ملف app/models/comment.rb
:
class Comment < ApplicationRecord
belongs_to :article
end
كما تلاحظ فمحتوى هذا الملف مشابه لنموذج Article
الذي أنشأناه سابقًا، والفارق الوحيد هو السطر belongs_to :article
والذي ينشئ رابطًا بين النموذجين، وسنتحدّث عن الروابط بعد قليل.
أما الكلمة المفتاحية (:references
) ضمن الأمر الذي قمنا بتنفيذه في سطر الأوامر، فهي نوع خاص من البيانات بالنسبة للنماذج. تنشئ هذه الكلمة المفتاحية عمودًا في الجدول الموجود في قاعدة البيانات يحمل اسم النموذج الذي تمّ تمريره إلى هذه الكلمة مع إضافة _id
والذي يمثّل عددًا صحيحًا. ستتضح الأمور أكثر بالنسبة إليك إن تفحّصت ملف db/schema.rb
أدناه.
قام Rails - بالإضافة إلى إنشاء النموذج - بإنشاء تهجير وظيفته إنشاء الجدول المقابل للنموذج في قاعدة البيانات:
class CreateComments < ActiveRecord::Migration[5.0]
def change
create_table :comments do |t|
t.string :commenter
t.text :body
t.references :article, foreign_key: true
t.timestamps
end
end
end
يُنشئ السطر t.references
عمودًا من نوع integer
باسم article_id
إضافة إلى فهرس index خاص بهذا العمود وقيد مفتاح خارجي Foreign Key Constraint والذي يشير إلى عمود id
في جدول المقالات.
والآن نفذ التهجير باستخدام الأمر التالي:
$ bin/rails db:migrate
ينفّذ Rails التهجيرات غير المنفّذة فقط؛ لذا ستكون نتيجة الأمر التالي كما يلي:
== CreateComments: migrating =================================================
-- create_table(:comments)
-> 0.0115s
== CreateComments: migrated (0.0119s) ========================================
ربط النماذج مع بعضها البعض
تسهّل روابط التسجيلة النشطة تكوين العلاقات بين النماذج، وفي حالتنا هذه سنُنشئ علاقة بين جدولي التعليقات والمقالات، ولو فكّرنا في طبيعة العلاقة التي تربط بينهما فسنجد أنه:
- ينتمي كل تعليق إلى مقالة واحدة.
- تمتلك المقالة الواحدة العديد من التعليقات.
يستخدم Rails صياغة مشابهة للربط بين النماذج، وقد شاهدنا في نموذج Comment
في الملف app/models/comment.rb
الشيفرة المسؤولة عن ربط كل تعليق بمقالة واحدة:
class Comment < ApplicationRecord
belongs_to :article
end
سنحتاج الآن إلى تكوين الجانب الثاني من الرابطة، أي ربط المقالات بالتعليقات، لذا توجّه إلى الملف app/models/article.rb
وعدّله بالصورة التالية:
class Article < ApplicationRecord
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
end
والآن أصبح النموذجان مرتبطين مع بعضهما البعض تلقائيًا، فعلى سبيل المثال، في حال كان لدينا متغيّر @article
والذي يمثّل مقالة معيّنة، يمكن استدعاء جميع التعليقات المرتبطة بتلك المقالة على هيئة مصفوفة وذلك من خلال @article.comments
.
إضافة مسار خاص بالتعليقات
كما هو الحال مع متحكم welcome
سنحتاج إلى إضافة مسار نحدّد من خلاله العنوان الذي نرغب في استخدامه لمشاهدة التعليقات؛ لذا افتح ملف config/routes.rb
مرة أخرى، وعدّله كما يلي:
resources :articles do
resources :comments
end
بهذه الطريقة تصبح التعليقات بمثابة موارد مضمّنة في المقالات، وهذه الطريقة هي جزء من العلاقة الهرمية التي تنشأ بين المقالات والتعليقات.
إنشاء المتحكّم الخاصّ بالتعليقات
بعد أن انتهينا من إعداد النموذج، أصبح بإمكاننا الآن إنشاء المتحكّم الخاص بالتعليقات، وسنستخدم أداة المولّد كما فعلنا سابقًا:
$ bin/rails generate controller Comments
سينشئ هذا الأمر خمسة ملفات إضافة إلى مجلّد فارغ:
الملف/المجلد | الوظيفة |
app/controllers/comments_controller.rb | المتحكّم الخاص بالتعليقات |
/app/views/comments | يتم تخزين العروض الخاصّة بالتعليقات في هذا المجلد |
test/controllers/comments_controller_test.rb | ملف الاختبار الخاصّ بالمتحكّم |
app/helpers/comments | الملف الخاصّ بمساعد العرض |
app/assets/javascripts/comments.coffee | ملف CoffeScript الخاصّ بالمتحكّم |
app/assets/stylesheets/comments.scss | أوراق الأنماط المتتالية CSS الخاصّة بالمتحكّم |
كما هو الحال مع أي مدوّنة، فإن القرّاء سيكتبون تعليقاتهم بعد قراءة المقالة مباشرة، وبعد أن يرسلوا تعليقاتهم يتم توجيههم إلى صفحة عرض المقالة ليتمكّنوا من مشاهدة التعليقات. وستكون وظيفة المتحكّم CommentsController
هي توفير التوابع اللازمة لإنشاء التعليقات وحذف التعليقات المزعجة حال وصولها.
سنقوم أولًا بتعديل قالب عرض المقالات app/views/articles/show.html.erb
لنتمكن من إضافة تعليق جديد:
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>
ستضيف الشيفرة السابقة استمارة إلى صفحة عرض المقالات يمكن من خلالها إضافة تعليق جديد من خلال استدعاء الحدث create
ضمن المتحكّم CommentsController
. ويستخدم الاستدعاء form_for
مصفوفة ستعمل على إنشاء مسار متداخل nested route مثل: /articles/1/comments
.
لنجرِ الآن التعديلات اللازمة على الحدث create
في الملفّ app/controllers/comments_controller.rb
:
class CommentsController < ApplicationController
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
redirect_to article_path(@article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
ستتعقّد الأمور هنا قليلًا وذلك بسبب التداخل nesting الحاصل بين المسارات، إذ في كل مرة يتمّ فيها طلب تعليق معيّن يجب أن يتابع ذلك الطلب المقالة التي يرتبط بها هذا التعليق، وبالتالي استدعاء التابع find
في نموذج Article
والمسؤول عن اختيار المقالة المطلوبة حسب المعرّف المحدّد في المسار.
بالإضافة إلى ذلك، استفدنا من بعض التوابع التي تقدّمها عملية الربط بين النموذجين، فقد استخدمنا التابع create
على @article.comments
لإنشاء التعليق وحفظه، وسيؤدي هذا إلى ربط التعليق الجديد بالمقالة المحدّدة.
وبعد إنشاء التعليق الجديد نعيد توجيه المستخدم إلى المقالة الأصلية باستخدام الدالة المساعد article_path(@article)
. وكما شاهدنا تستدعي هذه الدالة الحدث show
ضمن المتحكّم ArticlesController
والذي يعمل بدوره على تصيير القالب show.html.erb
، وهو المكان الذي نرغب أن تظهر التعليقات فيه؛ لذا سنقوم بإجراء التعديلات اللازمة على الملف app/views/articles/show.html.erb
.
<p>
<strong>Title:</strong>
<%= @article.title %>
</p>
<p>
<strong>Text:</strong>
<%= @article.text %>
</p>
<h2>Comments</h2>
<% @article.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<% end %>
<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>
أصبح بإمكانك الآن إضافة المقالات والتعليقات إلى مدوّنتك وعرضها في الأماكن الصحيحة.
في الدرس القادم سنكمل العمل على التعليقات، حيث سنستخدم الملفات الجزئية لترتيب القوالب أوّلًا، ثم نضيف إمكانية حذف التعليقات من قاعدة البيانات، وفي الختام سنتطرّق إلى عملية الاستيثاق Authentication بصورة سريعة ومبسّطة.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.