diff --git a/Gemfile b/Gemfile index 8ad6655..a4694f1 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,8 @@ gem 'devise' gem 'omniauth-facebook' gem 'dotenv-rails' +gem 'cocoon' +gem 'counter_culture' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index c21ec20..f9e1db5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,6 +58,9 @@ GEM zeitwerk (~> 2.2) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) + after_commit_action (1.1.0) + activerecord (>= 3.0.0) + activesupport (>= 3.0.0) ast (2.4.0) bcrypt (3.1.13) bindex (0.8.1) @@ -74,7 +77,12 @@ GEM regexp_parser (~> 1.5) xpath (~> 3.2) childprocess (3.0.0) + cocoon (1.2.14) concurrent-ruby (1.1.6) + counter_culture (2.3.0) + activerecord (>= 4.2) + activesupport (>= 4.2) + after_commit_action (~> 1.0) crass (1.0.6) devise (4.7.1) bcrypt (~> 3.0) @@ -281,6 +289,8 @@ DEPENDENCIES bootsnap (>= 1.4.2) byebug capybara (>= 2.15) + cocoon + counter_culture devise dotenv-rails factory_bot_rails diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/concerns/ajax_helper.rb b/app/controllers/concerns/ajax_helper.rb new file mode 100644 index 0000000..3f291a8 --- /dev/null +++ b/app/controllers/concerns/ajax_helper.rb @@ -0,0 +1,5 @@ +module AjaxHelper + def ajax_redirect_to(redirect_uri) + { js: "window.location.replace('#{redirect_uri}')" } + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 0cb785d..7241b93 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,5 +3,6 @@ def index @user = current_user @meal_posts = @user&.meal_posts_feed&.includes(:user) @new_meal_post = @user&.meal_posts&.new + 3.times { @new_meal_post&.food_items&.build } end end diff --git a/app/controllers/meal_posts_controller.rb b/app/controllers/meal_posts_controller.rb index cf855b0..c179826 100644 --- a/app/controllers/meal_posts_controller.rb +++ b/app/controllers/meal_posts_controller.rb @@ -1,8 +1,13 @@ class MealPostsController < ApplicationController - before_action :authenticate_user!, only: %i[create destroy] + include AjaxHelper + before_action :authenticate_user!, only: %i[create destroy update] def create - @new_meal_post = current_user.meal_posts.build(meal_post_params) + @new_meal_post = current_user.meal_posts.build(new_meal_post_params) + + # meal_post object used for rendering partial form after successfully creating meal_post + @next_meal_post = current_user.meal_posts.new + 3.times { @next_meal_post&.food_items&.build } if @new_meal_post.save return respond_to do |format| @@ -17,7 +22,43 @@ def create # TODO: errorを伝搬するなりして、alertをもう少しdescriptiveにする format.html { redirect_to root, alert: 'You could not make a meal post.' } format.js do - render partial: 'meal_posts/create_failure', status: :bad_request + render 'meal_posts/create_failure', status: :bad_request + end + end + end + + def update + @meal_post = MealPost.find(params[:id]) + unless @meal_post.user_id == current_user.id + respond_to do |format| + format.html { redirect_to root_path, alert: 'You are not authorized to update the post' } + # (ajaxなど)js-acceptのリクエストでこれが叩かれることは基本ないだろうが、、、 + format.js { render ajax_redirect_to(root_path) } + end + end + + is_updated = @meal_post.update(update_meal_post_params) + # 以下の理由でreloadが必要である + # (1)counter_cultureで更新される@meal_post.total_caloriesの値をモデルに反映するため + # (2)@meal_post.food_itemsのそれぞれのmark_for_destructionをfalseに戻すため + # ここで設定しないと以下のようなバグが生じる + # [バグ]food_itemsを全て消して投稿した後に、画面描写後再び投稿ボタンを押すと「Food items should have at least 1 food item.」というエラーが出る + @meal_post.reload + + if is_updated + return respond_to do |format| + # TODO: friendly forwardingを実装 + format.html { redirect_to root, notice: 'You have successfully updated a meal post.' } + format.js + end + end + + respond_to do |format| + # TODO: friendly forwardingを実装 + # TODO: errorを伝搬するなりして、alertをもう少しdescriptiveにする + format.html { redirect_to root, alert: 'You could not update a meal post.' } + format.js do + render 'meal_posts/update_failure', status: :bad_request end end end @@ -36,6 +77,7 @@ def destroy def show @meal_post = MealPost.find(params[:id]) + @is_own_meal_post = @meal_post.id == current_user.id end def upvoted_index @@ -51,7 +93,13 @@ def downvoted_index private - def meal_post_params - params.require(:meal_post).permit(:content, :time) + def new_meal_post_params + params.require(:meal_post).permit(:content, :time, food_items_attributes: %i[name amount calory _destroy]) + end + + def update_meal_post_params + # patchメソッドだとRailsアプリケーションでrequest_bodyの配列をうまく解釈できない(hashになる)ため + params[:meal_post][:food_items_attributes] = params[:meal_post][:food_items_attributes].values + params.require(:meal_post).permit(:content, :time, food_items_attributes: %i[name amount calory _destroy id]) end end diff --git a/app/helpers/meal_posts_helper.rb b/app/helpers/meal_posts_helper.rb index d757a31..f6088c6 100644 --- a/app/helpers/meal_posts_helper.rb +++ b/app/helpers/meal_posts_helper.rb @@ -1,2 +1,12 @@ module MealPostsHelper + def total_calories_of(meal_post) + return '- kcal' if meal_post.food_items_with_calories_count.zero? + return "#{meal_post.total_calories}kcal+" if meal_post.food_items_count != meal_post.food_items_with_calories_count + + "#{meal_post.total_calories}kcal" + end + + def total_count_and_calories_of(meal_post) + "#{meal_post.food_items_count}品 #{total_calories_of(meal_post)}" + end end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index f60558d..31a7376 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -21,6 +21,7 @@ import "../stylesheets/application.scss"; import "flatpickr/dist/flatpickr.css"; import flatpickr from "flatpickr"; +import 'cocoon-js' require("../includes/vote.js") diff --git a/app/javascript/stylesheets/application.scss b/app/javascript/stylesheets/application.scss index d2bbafb..174d43e 100644 --- a/app/javascript/stylesheets/application.scss +++ b/app/javascript/stylesheets/application.scss @@ -1,7 +1,8 @@ @import '~bootstrap/scss/bootstrap'; @import '~@fortawesome/fontawesome-free/scss/fontawesome'; -@import './signup.scss'; -@import './destroy_confirmation.scss'; +@import './devise/signup.scss'; +@import './users/destroy_confirmation.scss'; +@import './meal_posts/_food_item_fields.scss'; .upvote, .downvote { diff --git a/app/javascript/stylesheets/signup.scss b/app/javascript/stylesheets/devise/signup.scss similarity index 100% rename from app/javascript/stylesheets/signup.scss rename to app/javascript/stylesheets/devise/signup.scss diff --git a/app/javascript/stylesheets/meal_posts/_food_item_fields.scss b/app/javascript/stylesheets/meal_posts/_food_item_fields.scss new file mode 100644 index 0000000..d10fd22 --- /dev/null +++ b/app/javascript/stylesheets/meal_posts/_food_item_fields.scss @@ -0,0 +1,5 @@ +.nested-fields { + .fi_field { + width: 30%; + } +} diff --git a/app/javascript/stylesheets/destroy_confirmation.scss b/app/javascript/stylesheets/users/destroy_confirmation.scss similarity index 100% rename from app/javascript/stylesheets/destroy_confirmation.scss rename to app/javascript/stylesheets/users/destroy_confirmation.scss diff --git a/app/models/food_item.rb b/app/models/food_item.rb new file mode 100644 index 0000000..e838b11 --- /dev/null +++ b/app/models/food_item.rb @@ -0,0 +1,19 @@ +class FoodItem < ApplicationRecord + belongs_to :meal_post + counter_culture :meal_post + counter_culture :meal_post, column_name: proc { |model| model.calory.nil? ? nil : 'food_items_with_calories_count' }, + column_names: { ['food_items.calory IS NOT NULL'] => 'food_items_with_calories_count' } + counter_culture :meal_post, column_name: 'total_calories', delta_column: 'calory' + + before_validation :strip_whitespaces + validates :name, presence: true, length: { maximum: 30 } + validates :amount, length: { maximum: 30 }, allow_blank: true + validates :calory, numericality: { only_integer: true, greater_than: 0 }, allow_blank: true + + private + + def strip_whitespaces + name&.strip! + amount&.strip! + end +end diff --git a/app/models/meal_post.rb b/app/models/meal_post.rb index 0988953..6ecb040 100644 --- a/app/models/meal_post.rb +++ b/app/models/meal_post.rb @@ -1,6 +1,8 @@ class MealPost < ApplicationRecord belongs_to :user has_many :votes, dependent: :destroy + has_many :food_items, dependent: :destroy + accepts_nested_attributes_for :food_items, reject_if: proc { |attr| attr['name'].blank? && attr['amount'].blank? && attr['calory'].blank? }, allow_destroy: true has_many :upvotes, -> { where(is_upvote: true) }, class_name: 'Vote' has_many :downvotes, -> { where(is_upvote: false) }, class_name: 'Vote' @@ -15,6 +17,7 @@ class MealPost < ApplicationRecord validates :user_id, presence: true validates :time, presence: true validates :content, presence: true, length: { maximum: 200 } + validates :food_items, length: { minimum: 1, message: 'should have at least 1 food item.' } def score # TODO: 負荷が高いのでバッチ処理化 or SQLの集計関数を使う diff --git a/app/models/user.rb b/app/models/user.rb index 7a3a69a..74d7078 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,7 +23,7 @@ class User < ApplicationRecord has_many :favorite_meal_posts, through: :upvotes, source: :meal_post has_many :unfavorite_meal_posts, through: :downvotes, source: :meal_post - before_validation :strip_whitespaces, only: %i[name account_id] + before_validation :strip_whitespaces validates :name, presence: true, length: { in: 1..20 } validates :account_id, presence: true, length: { in: 5..15 }, uniqueness: true, format: { with: /\A[A-Za-z0-9]+\z/, message: 'should be alphanumeric' } diff --git a/app/views/home/_signed_in_home.html.erb b/app/views/home/_signed_in_home.html.erb index 18ac9b2..03b1b6a 100644 --- a/app/views/home/_signed_in_home.html.erb +++ b/app/views/home/_signed_in_home.html.erb @@ -1,6 +1,6 @@
<%= link_to 'My Profile', user_path(@user) %>
<%= link_to 'Followings', followings_user_path(@user) %>
<%= link_to 'Followers', followers_user_path(@user) %>
@@ -8,11 +8,12 @@<%= link_to 'Your Downvotes', downvotes_user_path(@user) %>