RailsアプリにはUser、Hikingtrail、Photoの3つのモデルがあります。user_id
ユーザーが写真を作成するときに、新しい写真の外部キーを設定したいと思います。
- 各ユーザー
has_many :hikingtrails
&has_many :photos
- 各ハイキングコース
has_many :photos
&belongs_to :user
- 各写真
belongs_to :hikingtrail
&belongs_to :user
Hikingtrail を作成するためのフォームは非常に長いため、2 つの部分に分割しました。最初の部分 ( new.html.erb
) では、ユーザーが Hikingtrail を送信/作成した後、検証が必要なデータ (つまり、名前と住所) を入力するだけです。アプリedit.html.erb
は、ユーザーがハイキングトレイルを詳細情報で更新し、写真をアップロードできる場所にそれらをルーティングします。
ユーザーがログインすると、Hikingtrail を作成できます。これを行うとuser_id
、Hikingtrail モデルに外部キーが設定されます。@hikingtrail.user = current_user
Hikingtrails Controller の create アクションを追加することで、これを実現できました。
user_id
また、ユーザーが Hikingtrail を更新しようとしたときに、新しい写真に外部キーを設定したいと思います。すべての写真属性が Hikingtrail 内にネストされているため、写真コントローラーはありません。以下のコードを Hikingtrails Controller の更新アクションに追加することで、上記と同様のアプローチを試みました。
@photo = Photo.new(params[:photo])
@photo.user_id = current_user.id
&
@photo = Hikingtrail.photo.new(params[:photo])
@photo.user_id = current_user.id
しかし、データベースに user_id 外部キーを設定せずに写真を追加するか、undefined method 'photo'
エラーが発生します。
- - - 編集 - - -
私も追加してみました
@photo = @hikingtrail.photos
@photo.user_id = current_user.id
ハイキングトレイルコントローラーに送信しますが、これによりundefined method 'user_id='
エラーが発生します。
-- 編集終了 --
これが私のコードの残りの部分です:
ユーザーモデル
class User < ActiveRecord::Base
attr_accessible :user_name, :email, :password, :password_confirmation, :photos_attributes, :hikingtrails_attributes
validates :email, :presence => true, :uniqueness => true, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } #, :message => "Invalid email address format"
validates :user_name, :presence => true
# has_secure_password automatically validates presence of password fields
# validates :password, :presence => true
# validates :password_confirmation, :presence => true
has_secure_password
has_many :photos
accepts_nested_attributes_for :photos, :allow_destroy => :true, :reject_if => lambda { |a| a[:image].blank? }
has_many :hikingtrails
accepts_nested_attributes_for :hikingtrails, :allow_destroy => :true, :reject_if => :all_blank
before_create { generate_token(:auth_token) } # calls the generate_token method below to store unique token in :auth_token field
def send_password_reset
generate_token(:password_reset_token) # uses method below to generate a unique token
self.password_reset_sent_at = Time.zone.now # sets the time token was generated
save! # save to database
UserMailer.password_reset(self).deliver # calls user mailer to send email with instructions
end
# generates a unique token which is unguessable for each user
def generate_token(column) # take a column argument so that we can have multiple tokens
begin
self[column] = SecureRandom.urlsafe_base64 # ActiveSupport’s SecureRandom class generates a random string
end while User.exists?(column => self[column]) # checks that no other user exists with this token and repeatedly generates another random token while this is true
end
end
ハイキングトレイルモデル
class Hikingtrail < ActiveRecord::Base
attr_accessible :description,
:duration_hours,
:duration_mins,
:name,
:looped,
:addr_1,
:addr_2,
:addr_3,
:country,
:latitude,
:longitude,
:photos_attributes,
:trails_attributes,
:directions_attributes,
:user_id
validates :name, :presence => {message:"Name can't be blank."}, :length => { :minimum => 3, message:"Name is too short (minimum is 3 characters)" }
validates :addr_2, :presence => {message:"Address field can't be blank."}
validates :addr_3, :presence => {message:"Address field can't be blank."}
validates :country, :presence => {message:"Country field can't be blank."}
validates :description, :length => { :minimum => 50, message:"Description is too short (minimum is 50 characters)" }, :allow_blank => true
validates :latitude, :presence => {message: "Not a valid location on Google Maps, please check name address & country fields" }
has_many :photos
has_many :trails
has_many :directions
belongs_to :user
accepts_nested_attributes_for :photos, :allow_destroy => :true,
:reject_if => lambda { |a| a[:image].blank? }
accepts_nested_attributes_for :trails, :allow_destroy => :true,
:reject_if => lambda { |a| a[:step].blank? }
accepts_nested_attributes_for :directions, :allow_destroy => :true,
:reject_if => :all_blank
geocoded_by :address
before_validation :geocode, :if => :address_changed?
def address
[name, addr_1, addr_2, addr_3, country].compact.join(' ')
end
def address_changed?
attrs = %w(name addr_1 addr_2 addr_3 country)
attrs.any?{|a| send "#{a}_changed?"}
end
# Full text search method utilizing PostgreSQL search functionality for stemming & ommitting stop words
# http://www.postgresql.org/docs/9.1/static/textsearch.html
def self.text_search(query)
if query.present?
# sets a rank variable to the value of the sum of the ranks for the name and description fields. So that the results are ordered by that rank in descending order.
rank = <<-RANK
ts_rank(to_tsvector(name), plainto_tsquery(#{sanitize(query)})) +
ts_rank(to_tsvector(description), plainto_tsquery(#{sanitize(query)}))
RANK
where("name @@ :q or description @@ :q", q: query).order("#{rank} desc") # uses the PG @@ operater as opposed to ilike operator
else
scoped
end
end
end
写真モデル
class Photo < ActiveRecord::Base
belongs_to :hikingtrail
belongs_to :user
attr_accessible :image, :user_id
mount_uploader :image, ImageUploader
end
ハイキングトレイルコントローラー
class HikingtrailsController < ApplicationController
before_filter :authorize, only: [:new, :destroy]
# GET /hikingtrails
# GET /hikingtrails.json
def index
if params[:search_nearby].present?
@hikingtrails = Hikingtrail.near(params[:search_nearby], 50, :order => :distance)
@hikingtrails = @hikingtrails.page(params[:page]).per_page(2)
elsif params[:query].present?
@hikingtrails = Hikingtrail.text_search(params[:query]).page(params[:page]).per_page(3)
else
@hikingtrails = Hikingtrail.order("created_at DESC").page(params[:page]).per_page(4)
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: @hikingtrails }
end
end
# GET /hikingtrails/1
# GET /hikingtrails/1.json
def show
@hikingtrail = Hikingtrail.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: @hikingtrail }
end
end
# GET /hikingtrails/new
# GET /hikingtrails/new.json
def new
@hikingtrail = Hikingtrail.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: @hikingtrail }
end
end
# GET /hikingtrails/1/edit
def edit
@hikingtrail = Hikingtrail.find(params[:id])
end
# POST /hikingtrails
# POST /hikingtrails.json
def create
@hikingtrail = Hikingtrail.new(params[:hikingtrail])
@hikingtrail.user = current_user
respond_to do |format|
if @hikingtrail.save
#format.html { redirect_to @hikingtrail, notice: 'Hikingtrail was successfully created.' }
format.html { redirect_to edit_hikingtrail_path(@hikingtrail), notice: 'Hikingtrail was successfully created.' }
format.json { render json: @hikingtrail, status: :created, location: @hikingtrail }
else
format.html { render action: "new" }
format.json { render json: @hikingtrail.errors, status: :unprocessable_entity }
end
end
end
# PUT /hikingtrails/1
# PUT /hikingtrails/1.json
def update
@hikingtrail = Hikingtrail.find(params[:id])
@photo = Photo.new(params[:photo])
@photo.user_id = current_user.id
respond_to do |format|
if @hikingtrail.update_attributes(params[:hikingtrail])
format.html { redirect_to @hikingtrail, notice: 'Hikingtrail was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @hikingtrail.errors, status: :unprocessable_entity }
end
end
end
# DELETE /hikingtrails/1
# DELETE /hikingtrails/1.json
def destroy
@hikingtrail = Hikingtrail.find(params[:id])
@hikingtrail.destroy
respond_to do |format|
format.html { redirect_to hikingtrails_url }
format.json { head :no_content }
end
end
end
ハイキングトレイル/_form.html.erb
<% @hikingtrail.photos.build %>
<% @hikingtrail.trails.build %>
<% @hikingtrail.directions.build %>
<%= simple_form_for @hikingtrail, :html => { :class => 'form-horizontal' } do |f| %>
<% if @hikingtrail.errors.any? %>
<div class="alert">
<a class="close" data-dismiss="alert">×</a> Please correct the
<%= pluralize(@hikingtrail.errors.count, "error") %> below.
<ol>
<% @hikingtrail.errors.each do |attribute, errors_array| %>
<li><%= errors_array %></li>
<% end %>
</ol>
</div>
<% end %>
<div class="accordion" id="accordion2">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseOne">
Name & Location
</a>
</div>
<div id="collapseOne" class="accordion-body collapse">
<div class="accordion-inner">
<%= render :partial => 'new_form' %>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseTwo">
Description
</a>
</div>
<div id="collapseTwo" class="accordion-body collapse in">
<div class="accordion-inner">
<%= f.input :description, :input_html => { :cols => 10, :rows => 3 } %> <br/>
<%= f.input :looped, :as => :boolean %>
<%= f.input :duration_hours, :label => 'Duration', :collection => 0..12, :include_blank => false, :hint => "hours" %>
<%= f.input :duration_mins, collection: [ 0, 15, 30, 45 ], :include_blank => false, label: false, :hint => "mins" %>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseThree">
Images
</a>
</div>
<div id="collapseThree" class="accordion-body collapse">
<div class="accordion-inner">
<% if current_user %>
<%= f.simple_fields_for :photos do |builder| %>
<%= render 'photo_fields', f: builder %>
<% end %>
<div style=clear:both;> </div>
<%= link_to_add_fields "Add More", f, :photos %>
<% else %>
You must <%= link_to "Log In", login_path %> before you can upload photos.
<% end %>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseFour">
Hiking Trail Route
</a>
</div>
<div id="collapseFour" class="accordion-body collapse">
<div class="accordion-inner">
<%= f.simple_fields_for :trails do |builder| %>
<%= render 'trail_fields', f: builder %>
<% end %>
<%= link_to_add_fields "Add Step", f, :trails %>
</div>
</div>
</div>
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseFive">
Directions to The Hike
</a>
</div>
<div id="collapseFive" class="accordion-body collapse">
<div class="accordion-inner">
<%= f.simple_fields_for :directions do |builder| %>
<%= render 'direction_fields', f: builder %>
<% end %>
<%= link_to_add_fields "Add More Directions", f, :directions %>
</div>
</div>
</div>
</div>
<div class="form-actions">
<%= f.submit nil, :class => 'btn btn-primary' %>
<%= link_to t('.cancel', :default => t("helpers.links.cancel")),
hikingtrails_path, :class => 'btn' %>
</div>
<% end %>