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 @@

<%= "Welcome back, #{@user.name}!" %>

-
+

<%= 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) %>

-
+
<%= render template: 'meal_posts/index', locals: {title: 'Your Timeline', meal_posts: meal_posts} %>
-
- <%= render partial: 'meal_posts/new'%> +
+

食べたものを記録

+ <%= render partial: 'meal_posts/new', locals: {new_meal_post: @new_meal_post} %>
diff --git a/app/views/meal_posts/_create_failure.js.erb b/app/views/meal_posts/_create_failure.js.erb deleted file mode 100644 index fbcf813..0000000 --- a/app/views/meal_posts/_create_failure.js.erb +++ /dev/null @@ -1 +0,0 @@ -console.log('You could not create your meal post.'); diff --git a/app/views/meal_posts/_edit.html.erb b/app/views/meal_posts/_edit.html.erb new file mode 100644 index 0000000..934193e --- /dev/null +++ b/app/views/meal_posts/_edit.html.erb @@ -0,0 +1,32 @@ +
+ <% if meal_post.errors.any? %> +
+ <% meal_post.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +

    + <% end %> + + <%= form_for(meal_post, html: { method: :patch }, remote: true) do |f| %> +
    + <%= f.text_field :content, placeholder: 'メニュー名' %> +

    + +
    + <%= f.fields_for :food_items do |fi_field| %> + <%= render partial: 'meal_posts/food_item_fields', locals: {f: fi_field} %> + <% end %> + +

    + +
    + <%= f.text_field :time, class: 'datetimepicker', placeholder: '食べた日時を選択' %> +
    + + + + <%= f.submit "投稿する" %> + <% end %> +
    diff --git a/app/views/meal_posts/_food_item_fields.html.erb b/app/views/meal_posts/_food_item_fields.html.erb new file mode 100644 index 0000000..eed8534 --- /dev/null +++ b/app/views/meal_posts/_food_item_fields.html.erb @@ -0,0 +1,6 @@ +
    + <%= f.text_field :name, placeholder: '品目', class: 'fi_field' %> + <%= f.text_field :amount , placeholder: '分量', class: 'fi_field' %> + <%= f.number_field :calory, placeholder: 'カロリー', class: 'fi_field' %> + <%= link_to_remove_association '', f, class: "fas fa-times-circle" %> +
    diff --git a/app/views/meal_posts/_meal_post.html.erb b/app/views/meal_posts/_meal_post.html.erb index a00e617..6b713ed 100644 --- a/app/views/meal_posts/_meal_post.html.erb +++ b/app/views/meal_posts/_meal_post.html.erb @@ -3,22 +3,25 @@ <%= link_to meal_post.user.name, user_path(meal_post.user) %>
    -
    - +
    +
    <%= render partial: 'votes/vote', locals: { meal_post: meal_post } %>
    -
    <%= link_to meal_post.content, meal_post_path(meal_post) %>
    +
    + <%= link_to meal_post.content, meal_post_path(meal_post) %> + (<%= total_count_and_calories_of(meal_post) %>) +
    <%= meal_post.time %>
    - <%= link_to '評価してる人', meal_post_upvoters_path(meal_post) %> - <%= link_to '呆れてる人', meal_post_downvoters_path(meal_post) %> + <%= link_to '高評価した人', meal_post_upvoters_path(meal_post) %> + <%= link_to '低評価した人', meal_post_downvoters_path(meal_post) %>
    <% if current_user == meal_post.user %> - <%= link_to "Delete this post", meal_post_path(meal_post), + <%= link_to "投稿削除", meal_post_path(meal_post), method: "delete", remote: true %> <% end %>
    diff --git a/app/views/meal_posts/_new.html.erb b/app/views/meal_posts/_new.html.erb index 4c074c2..8323aed 100644 --- a/app/views/meal_posts/_new.html.erb +++ b/app/views/meal_posts/_new.html.erb @@ -1,17 +1,22 @@ - - -<%= form_for @new_meal_post, remote: true do |f| %> +<%= form_for new_meal_post, remote: true do |f| %>
    - <%= f.label :content %>
    - <%= f.text_field :content %> -
    + <%= f.text_field :content, placeholder: 'メニュー名' %> +

    + +
    + <%= f.fields_for :food_items do |fi_field| %> + <%= render partial: 'meal_posts/food_item_fields', locals: {f: fi_field} %> + <% end %> + +

    - <%= f.label :time %> - <%= f.text_field :time, class: 'datetimepicker' %> + <%= f.text_field :time, class: 'datetimepicker', placeholder: '食べた日時を選択' %>
    - <%= f.submit "Submit" %> + <%= f.submit "投稿する" %> <% end %> diff --git a/app/views/meal_posts/_total_calories.html.erb b/app/views/meal_posts/_total_calories.html.erb new file mode 100644 index 0000000..dd99445 --- /dev/null +++ b/app/views/meal_posts/_total_calories.html.erb @@ -0,0 +1,8 @@ +
    +

    + 総カロリー +

    +

    + <%= total_calories_of(meal_post) %> +

    +

    diff --git a/app/views/meal_posts/create.js.erb b/app/views/meal_posts/create.js.erb index 58734fe..74e05f1 100644 --- a/app/views/meal_posts/create.js.erb +++ b/app/views/meal_posts/create.js.erb @@ -1,3 +1,10 @@ console.log("post successfully created"); -// TODO: prependではなくtimeの順番にしたがってしかるべきところに挿入する +<%# TODO: prependではなくtimeの順番にしたがってしかるべきところに挿入する %> $(".meal-posts").find(".card").first().prepend("<%= escape_javascript(render(partial: 'meal_posts/meal_post', locals: {meal_post: @new_meal_post})) %>"); +$(".new_meal_post").replaceWith('<%= escape_javascript(render(partial: 'meal_posts/new', locals: {new_meal_post: @next_meal_post})) %>'); +flatpickr(".datetimepicker", { + altInput: true, + enableTime: true, + dateFormat: "Y-m-d H:i", + inline: true +}); diff --git a/app/views/meal_posts/create_failure.js.erb b/app/views/meal_posts/create_failure.js.erb new file mode 100644 index 0000000..0f45ea8 --- /dev/null +++ b/app/views/meal_posts/create_failure.js.erb @@ -0,0 +1,5 @@ +console.log('You could not create your meal post.'); +$(".meal_post_errors").remove(); +errorMessages = '
    <% @new_meal_post.errors.full_messages.each do |message| %>
  • <%= message %>
  • <% end %>

    '; +$(".new_meal_post").prepend(errorMessages); + diff --git a/app/views/meal_posts/index.html.erb b/app/views/meal_posts/index.html.erb index 47d1de4..ca83802 100644 --- a/app/views/meal_posts/index.html.erb +++ b/app/views/meal_posts/index.html.erb @@ -1,5 +1,5 @@ -

    <%= title %>

    +

    <%= title %>

    <% meal_posts&.each do |meal_post| %> <%= render partial: 'meal_posts/meal_post', locals: {meal_post: meal_post} %> diff --git a/app/views/meal_posts/show.html.erb b/app/views/meal_posts/show.html.erb index 2370b2a..2f43a2a 100644 --- a/app/views/meal_posts/show.html.erb +++ b/app/views/meal_posts/show.html.erb @@ -1,5 +1,22 @@

    このMealPostの詳細

    <%= render partial: 'meal_post', locals: {meal_post: @meal_post} %> +
    +
    - + +
    +
    + <%= render partial: 'total_calories', locals: {meal_post: @meal_post}%> +
    + + <% if @is_own_meal_post %> +
    + <%= render partial: 'meal_posts/new', locals: {new_meal_post: @meal_post} %> +
    + <% end %> + + + + +
    diff --git a/app/views/meal_posts/update.js.erb b/app/views/meal_posts/update.js.erb new file mode 100644 index 0000000..b33bd83 --- /dev/null +++ b/app/views/meal_posts/update.js.erb @@ -0,0 +1,10 @@ +console.log("post successfully updated"); +$(".total_calories").replaceWith('<%= escape_javascript(render partial: 'meal_posts/total_calories', locals: {meal_post: @meal_post}) %>'); +$(".edit_meal_post").replaceWith('<%= escape_javascript(render partial: 'meal_posts/edit', locals: {meal_post: @meal_post}) %>') +$("div.edit_meal_post").prepend('食事の投稿を更新できました

    '); +flatpickr(".datetimepicker", { + altInput: true, + enableTime: true, + dateFormat: "Y-m-d H:i", + inline: true +}); diff --git a/app/views/meal_posts/update_failure.js.erb b/app/views/meal_posts/update_failure.js.erb new file mode 100644 index 0000000..742cd7e --- /dev/null +++ b/app/views/meal_posts/update_failure.js.erb @@ -0,0 +1,8 @@ +console.log('You could not edit your meal post.'); +$(".edit_meal_post").replaceWith('<%= escape_javascript(render partial: 'meal_posts/edit', locals: {meal_post: @meal_post}) %>') +flatpickr(".datetimepicker", { + altInput: true, + enableTime: true, + dateFormat: "Y-m-d H:i", + inline: true +}); diff --git a/config/routes.rb b/config/routes.rb index bcbf4a4..42de653 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,7 +31,7 @@ end resources :relationships, only: %i[create destroy] - resources :meal_posts, only: %i[create destroy show] do + resources :meal_posts, only: %i[create destroy show update] do patch :upvote, to: 'votes#upvote' patch :downvote, to: 'votes#downvote' get :upvoters, to: 'users#upvoters_index' diff --git a/db/migrate/20200426062256_create_food_items.rb b/db/migrate/20200426062256_create_food_items.rb new file mode 100644 index 0000000..3650830 --- /dev/null +++ b/db/migrate/20200426062256_create_food_items.rb @@ -0,0 +1,10 @@ +class CreateFoodItems < ActiveRecord::Migration[6.0] + def change + create_table :food_items do |t| + t.string :name, null: false + t.string :amount + t.bigint :calory + t.references :meal_post, null: false, foreign_key: true + end + end +end diff --git a/db/migrate/20200504084917_add_columns_to_meal_posts.rb b/db/migrate/20200504084917_add_columns_to_meal_posts.rb new file mode 100644 index 0000000..c6d45c7 --- /dev/null +++ b/db/migrate/20200504084917_add_columns_to_meal_posts.rb @@ -0,0 +1,10 @@ +class AddColumnsToMealPosts < ActiveRecord::Migration[6.0] + def change + add_column :meal_posts, :total_calories, :integer, default: nil + add_column :meal_posts, :food_items_count, :integer, default: 0, null: false + add_column :meal_posts, :food_items_with_calories_count, :integer, default: 0, null: false + + # counter_cache for existing records + FoodItem.counter_culture_fix_counts + end +end diff --git a/db/schema.rb b/db/schema.rb index 6d1c4ef..0894693 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,17 +10,28 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_04_19_122131) do +ActiveRecord::Schema.define(version: 2020_05_04_084917) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "food_items", force: :cascade do |t| + t.string "name", null: false + t.string "amount" + t.bigint "calory" + t.bigint "meal_post_id", null: false + t.index ["meal_post_id"], name: "index_food_items_on_meal_post_id" + end + create_table "meal_posts", force: :cascade do |t| t.text "content" t.datetime "time" t.bigint "user_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.integer "total_calories" + t.integer "food_items_count", default: 0, null: false + t.integer "food_items_with_calories_count", default: 0, null: false t.index ["user_id"], name: "index_meal_posts_on_user_id" end @@ -66,6 +77,7 @@ t.index ["user_id"], name: "index_votes_on_user_id" end + add_foreign_key "food_items", "meal_posts" add_foreign_key "meal_posts", "users" add_foreign_key "votes", "meal_posts" add_foreign_key "votes", "users" diff --git a/package-lock.json b/package-lock.json index 0e68041..9f104fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2025,6 +2025,18 @@ "q": "^1.1.2" } }, + "cocoon": { + "version": "github:nathanvda/cocoon#c24ba53b40e688e7d8b7212824139ec7f352a242", + "from": "github:nathanvda/cocoon#c24ba53" + }, + "cocoon-js": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/cocoon-js/-/cocoon-js-0.0.5.tgz", + "integrity": "sha512-1h51/2LDnZdyQehtIIA4eqCMSef4yFvUwXQF1XWRp12KSVLCXrvuGOsnoEm7OXviM7wgEiTf1nAkjbyws9i1EQ==", + "requires": { + "jquery": "^3.2.1" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", diff --git a/package.json b/package.json index d5be6f4..4172303 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "@rails/ujs": "^6.0.0", "@rails/webpacker": "4.2.2", "bootstrap": "^4.4.1", + "cocoon": "github:nathanvda/cocoon#c24ba53", + "cocoon-js": "^0.0.5", "flatpickr": "^4.6.3", "jquery": "^3.5.0", "popper.js": "^1.16.1", diff --git a/spec/factories/food_item.rb b/spec/factories/food_item.rb new file mode 100644 index 0000000..bb2f632 --- /dev/null +++ b/spec/factories/food_item.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :food_item do + name { Faker::Food.dish } + amount { Faker::Lorem.word } + calory { Faker::Number.within(range: 1..1000) } + end +end diff --git a/spec/factories/meal_posts.rb b/spec/factories/meal_posts.rb index 127b810..bbb470d 100644 --- a/spec/factories/meal_posts.rb +++ b/spec/factories/meal_posts.rb @@ -3,5 +3,8 @@ content { Faker::Lorem.words } time { Faker::Time.between(from: DateTime.now - 10.years, to: DateTime.now) } user + after(:build) do |this_meal_post| + this_meal_post.food_items << build(:food_item, meal_post: this_meal_post) + end end end diff --git a/spec/models/food_item_spec.rb b/spec/models/food_item_spec.rb new file mode 100644 index 0000000..d602864 --- /dev/null +++ b/spec/models/food_item_spec.rb @@ -0,0 +1,87 @@ +require 'rails_helper' + +RSpec.describe FoodItem, type: :model do + let(:meal_post) { create(:meal_post) } + let(:food_item) { create(:food_item, meal_post: meal_post) } + + describe 'FactoryBot' do + it 'instantiates valid FoodItem model' do + expect(food_item).to be_valid + end + end + + describe 'Validation' do + it 'is invalid without name' do + food_item.name = nil + expect(food_item).not_to be_valid + expect(food_item.errors[:name].size).to eq 1 + expect(food_item.errors[:name]).to match_array ["can't be blank"] + end + + it 'requires name composed of 1-30 letters excluding surrounding blank spaces' do + fi = create(:food_item, meal_post: meal_post) + fi.name = ' ' + expect(fi).not_to be_valid + expect(fi.errors.size).to eq 1 + expect(fi.errors[:name]).to match_array ["can't be blank"] + + fi2 = create(:food_item, meal_post: meal_post) + fi2.name = ' a ' + fi2.save + # also confirm that it is stripped when saving + expect(fi2).to be_valid + expect(fi2.name).to eq 'a' + + fi3 = create(:food_item, meal_post: meal_post) + fi3.name = 'a' * 30 + ' ' + expect(fi3).to be_valid + + fi3 = create(:food_item, meal_post: meal_post) + fi3.name = 'a' * 31 + ' ' + expect(fi3).to be_invalid + end + + it 'is requires amount composed of 1-30 letters excluding surrounding blank spaces' do + fi = create(:food_item, meal_post: meal_post) + fi.amount = ' ' + expect(fi).to be_valid + + fi2 = create(:food_item, meal_post: meal_post) + fi2.amount = ' a ' + fi2.save + # also confirm that it is stripped when saving + expect(fi2).to be_valid + expect(fi2.amount).to eq 'a' + + fi3 = create(:food_item, meal_post: meal_post) + fi3.amount = 'a' * 30 + ' ' + expect(fi3).to be_valid + + fi3 = create(:food_item, meal_post: meal_post) + fi3.amount = 'a' * 31 + ' ' + expect(fi3).to be_invalid + end + + it 'is requires calory composed of positive integer' do + fi = create(:food_item, meal_post: meal_post) + fi.calory = 100 + expect(fi).to be_valid + + fi = create(:food_item, meal_post: meal_post) + fi.calory = nil + expect(fi).to be_valid + + fi2 = create(:food_item, meal_post: meal_post) + fi2.calory = 0 + expect(fi2).to be_invalid + expect(fi2.errors.size).to eq 1 + expect(fi2.errors[:calory]).to match_array ['must be greater than 0'] + + fi3 = create(:food_item, meal_post: meal_post) + fi3.calory = 1.5 + expect(fi3).to be_invalid + expect(fi3.errors.size).to eq 1 + expect(fi3.errors[:calory]).to match_array ['must be an integer'] + end + end +end diff --git a/spec/models/meal_post_spec.rb b/spec/models/meal_post_spec.rb index e04da60..823fdc1 100644 --- a/spec/models/meal_post_spec.rb +++ b/spec/models/meal_post_spec.rb @@ -43,6 +43,14 @@ m_post = build(:meal_post, user: user, content: 'a' * 200) expect(m_post).to be_valid end + + it 'is invalid without any meal_post' do + m_post = build(:meal_post, user: user) + m_post.food_items = [] + expect(m_post).not_to be_valid + expect(m_post.errors.size).to eq 1 + expect(m_post.errors[:food_items]).to match_array ['should have at least 1 food item.'] + end end describe 'List of meal_posts' do diff --git a/yarn.lock b/yarn.lock index 0f1fbbe..931c606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1785,6 +1785,17 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +cocoon-js@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/cocoon-js/-/cocoon-js-0.0.5.tgz#2d269db2e3375014dbc43e4dda3bdd8d161b238b" + integrity sha512-1h51/2LDnZdyQehtIIA4eqCMSef4yFvUwXQF1XWRp12KSVLCXrvuGOsnoEm7OXviM7wgEiTf1nAkjbyws9i1EQ== + dependencies: + jquery "^3.2.1" + +"cocoon@github:nathanvda/cocoon#c24ba53": + version "1.2.10" + resolved "https://codeload.github.com/nathanvda/cocoon/tar.gz/c24ba53b40e688e7d8b7212824139ec7f352a242" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -3839,6 +3850,11 @@ jest-worker@^25.1.0: merge-stream "^2.0.0" supports-color "^7.0.0" +jquery@^3.2.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" + integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== + jquery@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.0.tgz#9980b97d9e4194611c36530e7dc46a58d7340fc9"