diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..be4e59be --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,23 @@ +AllCops: + Exclude: + - "vendor/**/*" + - "db/**/*" + - "bin/*" + - "node_modules/**/*" + - "config/initializers/devise.rb" + - "config/environments/*.rb" + - "Gemfile" + - "Rakefile" + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Style/Documentation: + Enabled: false + +Metrics/BlockLength: + Exclude: + - "config/routes.rb" diff --git a/Dockerfile b/Dockerfile index a498d272..0d189f00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ ARG UID=1000 ARG GID=1000 RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential curl libpq-dev \ + && apt-get install -y --no-install-recommends build-essential curl libpq-dev vim \ && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man \ && apt-get clean \ && groupadd -g "${GID}" ruby \ diff --git a/Gemfile b/Gemfile index 79a31661..2f006faf 100644 --- a/Gemfile +++ b/Gemfile @@ -49,8 +49,13 @@ group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mingw x64_mingw ] + gem "rubocop", require: false + gem "rubocop-performance", require: false + gem "rubocop-rails", require: false + # Reduces boot times through caching; required in config/boot.rb gem "bootsnap", require: false + end group :development do @@ -70,3 +75,5 @@ group :test do gem "selenium-webdriver" gem "webdrivers" end + +gem "devise" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 86dcc1a2..8a15d008 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,8 @@ GEM tzinfo (~> 2.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + bcrypt (3.1.18) bindex (0.8.1) bootsnap (1.15.0) msgpack (~> 1.2) @@ -90,6 +92,12 @@ GEM debug (1.6.3) irb (>= 1.3.6) reline (>= 0.3.1) + devise (4.9.2) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) erubi (1.11.0) globalid (1.0.0) activesupport (>= 5.0) @@ -103,6 +111,7 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.0.3) railties (>= 6.0.0) + json (2.6.3) loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -123,8 +132,15 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.13.9-aarch64-linux) + racc (~> 1.4) nokogiri (1.13.9-x86_64-linux) racc (~> 1.4) + orm_adapter (0.5.0) + parallel (1.23.0) + parser (3.2.2.3) + ast (~> 2.4.1) + racc pg (1.4.5) public_suffix (5.0.0) puma (6.0.0) @@ -161,6 +177,7 @@ GEM rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.0.6) redis (5.0.5) redis-client (>= 0.9.0) @@ -169,7 +186,30 @@ GEM regexp_parser (2.6.1) reline (0.3.1) io-console (~> 0.5) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) + rubocop (1.52.1) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.2.3) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-performance (1.18.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.20.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + ruby-progressbar (1.13.0) rubyzip (2.3.2) selenium-webdriver (4.6.1) childprocess (>= 0.5, < 5.0) @@ -198,6 +238,9 @@ GEM railties (>= 6.0.0) tzinfo (2.0.5) concurrent-ruby (~> 1.0) + unicode-display_width (2.4.2) + warden (1.2.9) + rack (>= 2.0.9) web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -216,6 +259,7 @@ GEM zeitwerk (2.6.6) PLATFORMS + aarch64-linux x86_64-linux DEPENDENCIES @@ -223,6 +267,7 @@ DEPENDENCIES capybara cssbundling-rails debug + devise jbuilder jsbundling-rails pg (~> 1.1) @@ -230,6 +275,9 @@ DEPENDENCIES rack-mini-profiler rails (~> 7.0.4) redis (~> 5.0) + rubocop + rubocop-performance + rubocop-rails selenium-webdriver sidekiq (~> 7.0) sprockets-rails diff --git a/app/controllers/expenses_controller.rb b/app/controllers/expenses_controller.rb new file mode 100644 index 00000000..5bb03065 --- /dev/null +++ b/app/controllers/expenses_controller.rb @@ -0,0 +1,40 @@ +class ExpensesController < ApplicationController + before_action :set_group_and_users, only: [:show, :edit, :update] + before_action :set_expense, only: [:edit, :update] + + def show + @difference = Expense.new.split_expenses(@users) + end + + def edit + end + + def update + @users.each do |user| + expense = user.expense + amount = params["group"]["group_user_#{user.id}_expenses"].to_i + if expense + expense.update(amount: amount) + else + Expense.create(user_id: user.id, group_id: @group.id, amount: amount) + end + end + + redirect_to group_expense_path(@group) + end + + private + + def set_group_and_users + @group = Group.find(params[:group_id]) + @users = @group.users + end + + def set_expense + @expense = @group.expenses.find_by(user_id: @users.pluck(:id)) + end + + def expense_params + params.require(:expense).permit(:amount, :group_id, :user_id) + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb new file mode 100644 index 00000000..6ad9fce2 --- /dev/null +++ b/app/controllers/groups_controller.rb @@ -0,0 +1,60 @@ +class GroupsController < ApplicationController + before_action :set_group, only: %i[show edit update] + + def index + @groups = Group.all + @user = User.find(current_user.id) + end + + def new + @group = Group.new + @user_list = @group.users.pluck(:id).join(',') + end + + def create + @group = Group.new(group_params) + if @group.save + user_list = user_params[:user_lists].delete(' ').split(',') + + @group.save_user(user_list) + redirect_to group_path(@group) + else + render :new + end + end + + def show + @group = Group.find(params[:id]) + @user = User.find(current_user.id) if current_user + end + + def edit + @group = Group.find(params[:id]) + @user_list = @group.users.pluck(:name).join(',') + end + + def update + @group = Group.find(params[:id]) + @user_list = user_params[:user_lists].delete(' ') + if @group.update(group_params) + @group.save_user(@user_list.split(',')) + redirect_to group_path(@group) + else + render :edit + end + end + + private + + def set_group + @group = Group.find(params[:id]) + end + + def group_params + params.require(:group).permit(:name) + end + + def user_params + params.require(:group).permit(:user_lists) + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 45f463e4..2f5a6000 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,4 +1,3 @@ class PagesController < ApplicationController - def home - end + def home; end end diff --git a/app/controllers/up_controller.rb b/app/controllers/up_controller.rb index 8416becf..f9f9e796 100644 --- a/app/controllers/up_controller.rb +++ b/app/controllers/up_controller.rb @@ -5,7 +5,7 @@ def index def databases RedisConn.current.ping - ActiveRecord::Base.connection.execute("SELECT 1") + ActiveRecord::Base.connection.execute('SELECT 1') head :ok end diff --git a/app/controllers/user/confirmations_controller.rb b/app/controllers/user/confirmations_controller.rb new file mode 100644 index 00000000..e37cc10c --- /dev/null +++ b/app/controllers/user/confirmations_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class User::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/app/controllers/user/omniauth_callbacks_controller.rb b/app/controllers/user/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..21aad21f --- /dev/null +++ b/app/controllers/user/omniauth_callbacks_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class User::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/heartcombo/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/app/controllers/user/passwords_controller.rb b/app/controllers/user/passwords_controller.rb new file mode 100644 index 00000000..5951f5b3 --- /dev/null +++ b/app/controllers/user/passwords_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class User::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/user/registrations_controller.rb b/app/controllers/user/registrations_controller.rb new file mode 100644 index 00000000..cc1670f4 --- /dev/null +++ b/app/controllers/user/registrations_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class User::RegistrationsController < Devise::RegistrationsController + before_action :configure_sign_up_params, only: [:create] + before_action :configure_account_update_params, only: [:update] + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + # def create + # super + # end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + def configure_sign_up_params + devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) + end + + # If you have extra params to permit, append them to the sanitizer. + def configure_account_update_params + devise_parameter_sanitizer.permit(:account_update, keys: [:name]) + end + + # The path used after sign up. + # def after_sign_up_path_for(resource) + # super(resource) + # end + + # The path used after sign up for inactive accounts. + # def after_inactive_sign_up_path_for(resource) + # super(resource) + # end +end diff --git a/app/controllers/user/sessions_controller.rb b/app/controllers/user/sessions_controller.rb new file mode 100644 index 00000000..be4b5692 --- /dev/null +++ b/app/controllers/user/sessions_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class User::SessionsController < Devise::SessionsController + # before_action :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) + # end +end diff --git a/app/controllers/user/unlocks_controller.rb b/app/controllers/user/unlocks_controller.rb new file mode 100644 index 00000000..05191c4e --- /dev/null +++ b/app/controllers/user/unlocks_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class User::UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c814..286b2239 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" + default from: 'from@example.com' + layout 'mailer' end diff --git a/app/models/expense.rb b/app/models/expense.rb new file mode 100644 index 00000000..3f5c65e3 --- /dev/null +++ b/app/models/expense.rb @@ -0,0 +1,18 @@ +class Expense < ApplicationRecord + belongs_to :user + belongs_to :group + + def split_expenses(users) + total_amount = users.sum { |user| user.expense&.amount.to_i } + num_users = users.size + per_person_amount = total_amount / num_users + + differences = users.map do |user| + paid_amount = user.expense&.amount.to_i + difference = per_person_amount - paid_amount + { user: user, difference: difference } + end + + return differences + end +end diff --git a/app/models/group.rb b/app/models/group.rb new file mode 100644 index 00000000..2e86e3d0 --- /dev/null +++ b/app/models/group.rb @@ -0,0 +1,26 @@ +class Group < ApplicationRecord + with_options presence: true do + validates :name + end + has_many :group_members, dependent: :destroy + has_many :users, through: :group_members + + has_many :expenses, dependent: :destroy + + def save_user(sent_users) + # 送られてきたユーザー名を元に、Userモデルからユーザーを検索し、存在するユーザーのみを抽出 + exist_new_users = sent_users.map do |sent_user| + User.find_by(name: sent_user) + end.compact + + current_users = users.pluck(:id) + old_users = current_users - exist_new_users.map(&:id) + new_users = exist_new_users - users + + # 古いユーザーを削除 + group_members.where(user_id: old_users).destroy_all + + # 新しいユーザーを追加 + users << new_users + end +end diff --git a/app/models/group_member.rb b/app/models/group_member.rb new file mode 100644 index 00000000..5bdf6dc0 --- /dev/null +++ b/app/models/group_member.rb @@ -0,0 +1,4 @@ +class GroupMember < ApplicationRecord + belongs_to :user + belongs_to :group +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..1dbeb2fe --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,12 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable + with_options presence: true do + validates :name + end + has_many :group_members, dependent: :destroy + has_many :groups, through: :group_members + has_one :expense, dependent: :destroy +end diff --git a/app/views/expenses/edit.html.erb b/app/views/expenses/edit.html.erb new file mode 100644 index 00000000..3bf2b518 --- /dev/null +++ b/app/views/expenses/edit.html.erb @@ -0,0 +1,31 @@ +
| Name | +Member | +CreatedAt | +UpdatedAt | ++ |
|---|---|---|---|---|
|
+ <%= link_to group_path(group) do %>
+
+
+ <% end %>
+
+
+ <%= group.name %>+ |
+ + + <% group.users.each do |user| %> + <%= user.name %> + <% end %> + + | ++ <%= l group.created_at.in_time_zone('Tokyo'), format: "%Y年%-m月%-d日 %H:%M" %> + | ++ <%= l group.updated_at.in_time_zone('Tokyo'), format: "%Y年%-m月%-d日 %H:%M" %> + | ++ <%= link_to edit_group_path(group), class: "font-semibold leading-tight text-xs text-slate-400" do %> + Edit + <% end %> + | +
- Rails <%= Rails::VERSION::STRING %> and Ruby <%= ENV["RUBY_VERSION"] %> -
+