Skip to content

Commit

Permalink
Feature/add rich text reaction model (#451)
Browse files Browse the repository at this point in the history
* feat: add RichTextReaction model

* feat: rollback and remigrate RichTextReaction creation with emoji and user_id columns

* chore: add missing association

* feat: allow RichTextReaction to interface with a limited set of emojis and captions

* feat: add routes and actions for rich text reaction create and destroy

* refactor: Emoji class isn't needed

* feat: add rich text reaction view component

* feat: have standup update rendered with emoji selector

* chore: comment for future improvement

* refacto: improve test

* refactor: organize in order to more easily add new rich text reaction components

* rubocop corrections
- Removed test from meeting_update_component_spec.rb to add in a new branch.

* refactor: using the emoji text as the key is easier to use with better code readability

* chore: remove temporary components that were used for testing

* feat: gather reaction info for all rich text of a class

* chore: EmojiSelectorComponent has been deleted

* refactor: use a more fitting column name

* clean up

* rubocop corrections

* refactor: store emojis and their captions for global usage

* refactor: `Reactionable` is not a fitting way of DRYing up as of now

* chore: use `require` for the one lib class

* refactor: emojis and their captions defnitions were moved

* refactor: change Emoji to a reusable class that provides API for frontend code

* fix: rescuing the error doesn't stop ctlr action flow
- Moved rescue into #destroy since it's the only action that requires the logic.

* chore: clean up tests

* chore: rubocop corrections

* chore: add helpful comment

* feat: update Emoji for easier API for frontend to use

* feat: scope for finding all reactions belonging to given rich text(s)

* rubocop

* refactor: eliminate view component query and do some clean up

* clean up

* chore: revert changes that should go in another branch

* chore: remove scopes that are too trivial and aren't even needed yet

* feat: add factory for ActionText::RichText, skip lint

* chore: add test for code coverage
  • Loading branch information
wwrk22 authored Jan 10, 2024
1 parent 2fc41b0 commit 1544afe
Show file tree
Hide file tree
Showing 16 changed files with 296 additions and 1 deletion.
34 changes: 34 additions & 0 deletions app/controllers/rich_text_reactions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class RichTextReactionsController < ApplicationController
before_action :build_rich_text_reaction, only: %i[create]

# POST /rich_text_reactions.turbo_stream
def create
if @rich_text_reaction.save
render :create, status: :created
else
flash.now[:alert] = 'Reaction could not be created.'
end
end

# DELETE /rich_text_reactions/:id.turbo_stream
def destroy
@rich_text_reaction = RichTextReaction.find(params[:id])

msg = 'Reaction could not be deleted.'
flash.now[:alert] = msg unless @rich_text_reaction.destroy
rescue ActiveRecord::RecordNotFound
flash.now[:alert] = "RichTextReaction with id #{params[:id]} could not be found."
end

private

def build_rich_text_reaction
@rich_text_reaction = current_user.rich_text_reactions.build(
rich_text_reaction_params
)
end

def rich_text_reaction_params
params.require(:rich_text_reaction).permit(:emoji_caption, :rich_text_id)
end
end
33 changes: 33 additions & 0 deletions app/models/rich_text_reaction.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# == Schema Information
#
# Table name: rich_text_reactions
#
# id :bigint not null, primary key
# emoji_caption :string not null
# created_at :datetime not null
# updated_at :datetime not null
# rich_text_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_rich_text_reactions_on_rich_text_id (rich_text_id)
# index_rich_text_reactions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (rich_text_id => action_text_rich_texts.id)
# fk_rails_... (user_id => users.id)
#
require 'emoji'

class RichTextReaction < ApplicationRecord
belongs_to :user
belongs_to :rich_text, class_name: 'ActionText::RichText'

validates :emoji_caption,
inclusion: {
in: Emoji.captions,
message: 'must be present in permissible set'
}
end
2 changes: 2 additions & 0 deletions app/models/standup_meeting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class StandupMeeting < ApplicationRecord
belongs_to :standup_meeting_group, inverse_of: :standup_meetings
belongs_to :user

# Under the hood, Rails calls a `has_one :rich_text_yesterday_work_description`,
# so we can do `StandupMeeting.includes(:rich_text_yesterday_work_description).
has_rich_text :yesterday_work_description
has_rich_text :today_work_description
has_rich_text :blockers_description
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class User < ApplicationRecord

has_many :mentee_applications, class_name: 'UserMenteeApplication', dependent: :destroy

has_many :rich_text_reactions, dependent: :destroy

# rubocop:disable Rails/InverseOf
has_one :current_resume, -> { where(current: true) }, class_name: 'Resume', dependent: nil
# rubocop:enable Rails/InverseOf
Expand Down
1 change: 1 addition & 0 deletions app/views/rich_text_reactions/create.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= "PLACEHOLDER" %>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,6 @@
scope controller: :static do
get :faq
end

resources :rich_text_reactions, only: %i[create destroy]
end
11 changes: 11 additions & 0 deletions db/migrate/20231207143236_create_rich_text_reactions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateRichTextReactions < ActiveRecord::Migration[7.0]
def change
create_table :rich_text_reactions do |t|
t.references :user, null: false, foreign_key: true
t.references :rich_text, null: false, foreign_key: { to_table: :action_text_rich_texts }
t.string :emoji_caption, null: false

t.timestamps
end
end
end
14 changes: 13 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified erd.pdf
Binary file not shown.
32 changes: 32 additions & 0 deletions lib/emoji.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

class Emoji
DICTIONARY = {
'THUMBS_UP' => '👍',
'THUMBS_DOWN' => '👎',
'THINKING' => '🤔',
'HOORAY' => '🎉',
'SHRUG' => '🤷',
'EYES' => '👀'
}.freeze

attr_reader :caption

def initialize(caption = DICTIONARY.keys.sample)
@caption = caption
end

def emoji
DICTIONARY[@caption]
end

class << self
def captions
DICTIONARY.keys
end

def emojis
DICTIONARY.values
end
end
end
19 changes: 19 additions & 0 deletions spec/factories/action_text_rich_texts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FactoryBot.define do
factory :action_text_rich_text, class: 'ActionText::RichText' do
# +rich_text_owner+ must be given by the user of this factory, and it may
# be any persisted ActiveRecord model record.
transient do
rich_text_owner { nil }
end

name { Faker::Lorem.word }
body { ActionText::Content.new "<div>#{Faker::Lorem.sentence}</div>" }

after(:build) do |action_text_rich_text, evaluator|
if evaluator.rich_text_owner
action_text_rich_text.record_type = evaluator.rich_text_owner.class.name
action_text_rich_text.record_id = evaluator.rich_text_owner.id
end
end
end
end
35 changes: 35 additions & 0 deletions spec/factories/rich_text_reactions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# == Schema Information
#
# Table name: rich_text_reactions
#
# id :bigint not null, primary key
# emoji_caption :string not null
# created_at :datetime not null
# updated_at :datetime not null
# rich_text_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_rich_text_reactions_on_rich_text_id (rich_text_id)
# index_rich_text_reactions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (rich_text_id => action_text_rich_texts.id)
# fk_rails_... (user_id => users.id)
#
FactoryBot.define do
factory :rich_text_reaction do
# +rich_text_owner+ can be given by the user of this factory, and it may
# be any persisted ActiveRecord model record. By default, a User record is
# used.
transient do
rich_text_owner { user }
end

user
emoji_caption { Emoji.captions.sample }
rich_text { association(:action_text_rich_text, rich_text_owner:) }
end
end
2 changes: 2 additions & 0 deletions spec/factories_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

RSpec.describe 'FactoryBot' do
FactoryBot.factories.each do |factory|
next if factory.name == :action_text_rich_text

# rubocop:disable RSpec/NoExpectationExample
it "#{factory.name} should pass lint" do
FactoryBot::Linter.new([factory]).lint!
Expand Down
26 changes: 26 additions & 0 deletions spec/lib/emoji_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'rails_helper'
require 'emoji'

RSpec.describe Emoji do
let(:caption) { described_class.captions.sample }

describe '#emoji' do
subject { described_class.new(caption) }

it 'returns the emoji in string format' do
expect(subject.emoji).to eq(Emoji::DICTIONARY[caption])
end
end

describe '.captions' do
it 'returns the set of allowed emoji captions' do
expect(described_class.captions).to eq(described_class::DICTIONARY.keys)
end
end

describe '.emojis' do
it 'returns the set of allowed emojis' do
expect(described_class.emojis).to eq(described_class::DICTIONARY.values)
end
end
end
17 changes: 17 additions & 0 deletions spec/models/rich_text_reactions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'rails_helper'

RSpec.describe RichTextReaction do
it 'is valid with an emoji in the permissible set' do
emoji_caption = Emoji.captions.sample
rich_text_reaction = described_class.new(emoji_caption:)

expect(rich_text_reaction.errors[:emoji_caption]).to be_empty
end

it 'is invalid with an emoji not in the permissible set' do
rich_text_reaction = described_class.new(emoji_caption: 'disguised_face')
rich_text_reaction.valid?

expect(rich_text_reaction.errors[:emoji_caption]).to be_present
end
end
67 changes: 67 additions & 0 deletions spec/requests/rich_text_reactions_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'rails_helper'
require 'emoji'

RSpec.describe 'RichTextReaction requests' do
let!(:user) { create(:user) }

before do
sign_in user
end

describe 'POST /rich_text_reactions' do
it 'creates a rich text reaction' do
count = RichTextReaction.count
params = {
rich_text_reaction: {
emoji_caption: Emoji.captions.sample,
rich_text_id: create(:standup_meeting).yesterday_work_description.id
},
format: :turbo_stream
}

post(rich_text_reactions_url, params:)

expect(RichTextReaction.count).to eq(count + 1)
expect(response).to have_http_status :created
end

it 'sets a flash message to indicate failure to create' do
post rich_text_reactions_url,
params: {
rich_text_reaction: {
emoji_caption: Emoji.captions.sample,
rich_text_id: 2
},
format: :turbo_stream
}

expect(flash.now[:alert]).to be_present
end
end

describe 'DELETE /rich_text_reactions/:id' do
it 'deletes a rich text reaction' do
id = create(:rich_text_reaction).id

delete(rich_text_reaction_url(id), params: { format: :turbo_stream })

expect(RichTextReaction.count).to be_zero
expect(response).to have_http_status :no_content
end

it 'sets a flash message to indicate failure to delete' do
fake_id = '1'
rich_text_reaction = instance_double(RichTextReaction, destroy: false)
allow(RichTextReaction).to receive(:find).with(fake_id).and_return(rich_text_reaction)

delete rich_text_reaction_url(fake_id),
params: {
format: :turbo_stream
}

expect(flash.now[:alert]).to be_present

expect(response).to have_http_status :no_content
end
end
end

0 comments on commit 1544afe

Please sign in to comment.