Skip to content

Commit

Permalink
Daily Nerd (#90)
Browse files Browse the repository at this point in the history
* initial commit for Daily Nerd, fixes #88

* working db changes, form, policy

* lint

* add validation

* update daily nerd counter

* make message field required

* correct fixture, and method name

* add specs

* add comment

* update comment

* update readme

* create feedback on demand

* Update app/policies/daily_nerd_message_policy.rb

Co-authored-by: Maximilian Langenbeck <[email protected]>

* address feedback

* back to locals

* Add a little UX

---------

Co-authored-by: Maximilian Langenbeck <[email protected]>
Co-authored-by: Daniel Diekmeier <[email protected]>
  • Loading branch information
3 people authored Apr 11, 2024
1 parent 2567ebd commit 22d96e2
Show file tree
Hide file tree
Showing 28 changed files with 287 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ GEM
railties (>= 3.2)
timeout (0.4.0)
translate_client (0.0.1)
turbo-rails (1.3.3)
turbo-rails (2.0.5)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Make sure the nerdgeschoss development is running correctly (https://github.com/

To test notifications join the `test-channel` on the NG workspace in slack. That is where messages arrive in development.
To test notifications to private channels (e.g. HR channel ), create a new private channel, add the Nerdgeschoss App to the channel as an integration and insert the channel ID into the credentials YAML file in Rails.

To test notifications via the legacy webhook join the `flink_testing` channel. It is not possible to create new custom webhooks. They will be deprecated eventually.
10 changes: 10 additions & 0 deletions app/assets/stylesheets/application/components/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,15 @@

&__input {
outline: none;

&--daily-nerd {
min-height: 100px;
margin-top: 0.5rem;
padding: 0.66rem;
border-top: 1px solid var(--color-border);
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
font-size: 16px;
}
}
}
36 changes: 36 additions & 0 deletions app/controllers/daily_nerd_messages_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

class DailyNerdMessagesController < ApplicationController
before_action :authenticate_user!
before_action :assign_daily_nerd_message, only: [:update]

def create
@daily_nerd_message = authorize DailyNerdMessage.new(daily_nerd_message_attributes.merge(sprint_feedback:))
if @daily_nerd_message.save
SlackPostDailyNerdJob.perform_later(daily_nerd_message: @daily_nerd_message)
sprint_feedback.add_daily_nerd_entry(@daily_nerd_message.created_at)
redirect_to sprints_path
else
render "new", status: :unprocessable_entity
end
end

def update
@daily_nerd_message.update!(daily_nerd_message_attributes)
redirect_to sprints_path
end

private

def daily_nerd_message_attributes
params.require(:daily_nerd_message).permit(:message)
end

def assign_daily_nerd_message
@daily_nerd_message = authorize DailyNerdMessage.find(params[:id])
end

def sprint_feedback
@sprint_feedback ||= current_user.sprint_feedbacks.find_by(sprint: Sprint.current.take)
end
end
6 changes: 5 additions & 1 deletion app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ class PagesController < ApplicationController
def home
@payslips = current_user.payslips.reverse_chronologic.page(0).per(6)
@sprint = Sprint.current.take
@upcoming_leaves = current_user.leaves.future.chronologic
@upcoming_leaves = current_user.leaves.future.not_rejected.chronologic
if @sprint
sprint_feedback = current_user.sprint_feedbacks.find_or_create_by(sprint: @sprint)
@daily_nerd_message = authorize DailyNerdMessage.find_by(created_at: Time.zone.today.all_day, sprint_feedback:) || sprint_feedback.daily_nerd_messages.build
end
end

def offline
Expand Down
10 changes: 10 additions & 0 deletions app/jobs/slack_post_daily_nerd_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class SlackPostDailyNerdJob < ApplicationJob
queue_as :notification
sidekiq_options retry: 0

def perform(daily_nerd_message:)
daily_nerd_message.post_to_slack
end
end
4 changes: 2 additions & 2 deletions app/models/awesome_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

class AwesomeForm < ActionView::Helpers::FormBuilder
def input(method, as: nil, placeholder: nil, required: nil, collection: nil, id_method: nil, name_method: nil,
min: nil, step: nil)
min: nil, step: nil, additional_class: "")
as ||= guess_type(method)
options = {class: "input__input"}
options = {class: "input__input #{additional_class}"}
collection_based = !collection.nil? || as == :select
collection ||= guess_collection(method) if collection_based
name_method ||= guess_name_method(method) if collection_based
Expand Down
21 changes: 21 additions & 0 deletions app/models/daily_nerd_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: daily_nerd_messages
#
# id :uuid not null, primary key
# sprint_feedback_id :uuid not null
# message :string
# created_at :datetime not null
# updated_at :datetime not null
#
class DailyNerdMessage < ApplicationRecord
belongs_to :sprint_feedback

validates :message, presence: true

def post_to_slack
User::SlackNotification.new(sprint_feedback.user).post_daily_nerd_message(message)
end
end
1 change: 1 addition & 0 deletions app/models/leave.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Leave < ApplicationRecord
scope :future, -> { where("UPPER(leaves.leave_during) > NOW()") }
scope :with_status, ->(status) { (status == :all) ? all : where(status:) }
scope :starts_today, -> { where("LOWER(leaves.leave_during) = ?", Time.zone.today) }
scope :not_rejected, -> { where.not(status: :rejected) }

enum type: [:paid, :unpaid, :sick, :non_working].index_with(&:to_s)
enum status: [:pending_approval, :approved, :rejected].index_with(&:to_s)
Expand Down
31 changes: 29 additions & 2 deletions app/models/slack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,23 @@ def notify(channel:, text:)
request http_method: :post, slack_method: "chat.postMessage", body: {channel:, text:}.to_json
end

def post_personalized_message_to_daily_nerd_channel(user:, message:)
# This is the old way of posting to slack on behalf of a user using a webhook.
# https://api.slack.com/legacy/custom-integrations/messaging/webhooks
# It might be necessary to change this in the future to use the new Slack API.
request_hook url: Config.slack_webhook_url!, body: personalized_webhook_body(user:, message:).to_json
end

def retrieve_users_slack_id_by_email(email)
response = request http_method: :get, slack_method: "users.lookupByEmail", query: {email:}
response.dig("user", "id")
end

def retrieve_users_profile_image_url_by_email(email)
response = request http_method: :get, slack_method: "users.lookupByEmail", query: {email:}
response.dig("user", "profile", "image_72")
end

def set_status(slack_id:, emoji:, text:, until_time:)
return @last_slack_status_update = Status.new(slack_id:, text:, until_time:) if debug

Expand All @@ -31,10 +43,25 @@ def set_status(slack_id:, emoji:, text:, until_time:)
def request(http_method:, slack_method:, query: nil, body: nil, token_type: :bot)
token = Config.public_send("slack_#{token_type}_token!")
headers = {"Content-Type": "application/json", authorization: "Bearer #{token}"}
response = HTTParty.public_send(http_method, "https://slack.com/api/#{slack_method}", headers:,
query:, body:)
response = HTTParty.public_send(http_method, "https://slack.com/api/#{slack_method}", headers:, query:, body:)

raise NetworkError, response["error"].humanize unless response.ok?

response
end

def request_hook(url:, body:)
response = HTTParty.post(url, headers: {"Content-Type": "application/json"}, body:)
raise NetworkError, response unless response.ok?

response
end

def personalized_webhook_body(user:, message:)
{
username: user.display_name,
icon_url: user.slack_profile.image_url,
text: message
}
end
end
2 changes: 2 additions & 0 deletions app/models/sprint_feedback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class SprintFeedback < ApplicationRecord
belongs_to :sprint
belongs_to :user

has_many :daily_nerd_messages, dependent: :destroy

scope :ordered, -> { joins(:user).order("users.email ASC") }

before_validation do
Expand Down
4 changes: 4 additions & 0 deletions app/models/user/slack_notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def send_message(message)
Slack.instance.notify(channel: slack_id, text: message)
end

def post_daily_nerd_message(message)
Slack.instance.post_personalized_message_to_daily_nerd_channel(user:, message:)
end

private

def slack_id
Expand Down
4 changes: 4 additions & 0 deletions app/models/user/slack_profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ def ensure_slack_id!
user.update!(slack_id: id)
id
end

def image_url
Slack.instance.retrieve_users_profile_image_url_by_email(user.email)
end
end
end
19 changes: 19 additions & 0 deletions app/policies/daily_nerd_message_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class DailyNerdMessagePolicy < ApplicationPolicy
def home?
hr? || users_own_message?
end

def create?
hr? || users_own_message?
end

def update?
hr? || users_own_message?
end

def users_own_message?
user.id == record.sprint_feedback.user_id
end
end
3 changes: 3 additions & 0 deletions app/views/daily_nerd_messages/_form.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
= form_with model: daily_nerd_message, class: :stack, builder: AwesomeForm do |f|
= f.input :message, as: :text, placeholder: t(".message_placeholder"), required: true, additional_class: "input__input--daily-nerd"
span = f.submit class: :button, data: { turbo_submits_with: t('.submits_with') }
1 change: 1 addition & 0 deletions app/views/daily_nerd_messages/edit.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= render partial: "form", locals: {daily_nerd_message: @daily_nerd_message}
1 change: 1 addition & 0 deletions app/views/daily_nerd_messages/new.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
= render partial: "form", locals: {daily_nerd_message: @daily_nerd_message}
8 changes: 8 additions & 0 deletions app/views/pages/home.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
h1.headline = t ".hello", name: current_user.display_name
- if @sprint
= render @sprint
- if @daily_nerd_message
.card
.card__header
.card__icon 📝
.card__header-content
.card__title = t ".daily_nerd"
.stack
= render partial: "daily_nerd_messages/form", locals: {daily_nerd_message: @daily_nerd_message}
.columns
- if @upcoming_leaves.any?
.card
Expand Down
2 changes: 1 addition & 1 deletion config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8g0slLgy60K8tQLudFHNQKKy1hHtKlHA9ltjc45wVSN7qrB+qufXavh6szdM3NMUPC8C9V8jKnC8+fYG8YBGiNGIAUubccFtaITGUGxCQQ6hnrxZMVe+fIaXBoY3zxnGbpML/O+EEoD9ULnHJU+o/3Gzpd/QkEFEwK8f3bJGBJTkMkVo6NzEP6WF0e3p7G3NYaRxVTClkz2DZ/2DLuv/wXJ+8oTrGy7+0+nzYRkZoPQ/RNnSTn25uHlX1ssz0UNtqUQ4u0QhlMPb4yph1GXJSo4gFWfaxNGlDOctP5CDuswF+rdQXfop7a98uFV9uxz/vP87LGs39550R5+I6bPtPpKQedEBZQLOnE82C/TAiKxJJgrGCmmijkSK7p3Ie0WauYdAFonbzv+Jt4ckvmUCqYu3S59Vd+T9UKQ8Q3VKn7bMN2WAFDln/lOT+UFgZ/AfOwkzLGQ8vikC7syzEXsZlnngmlMzbOWznYN6z085ZulbSnxZhTYu+pKCP+VsvuHi/2kMxv+5J4CMezmFQqLTLCfJ24TJIlcKUPe3LEAAilHLm73jMmy6t86RZgq6BUkaBjxOKp/HNMq4ySScMNwyMe9bdcTDJSZro8smM717cCxOZfn/PomuAw0YZk1vfYi2jRBI/saVtBrVfUpuOkOtfw0VxxzGARWMEDK/g0a9X7TSkjvzaQhvpv1XiWRM0p8o85EUC8ThNxo3Ln5iw090vD4oR2lbA8UA/EYNhpKEZsQimsfh8vjq/ZB62PyPhyEijJo8VRlekcMHx3k9hxsKTIdp+lK0aa9eiwuWqw==--Xnf31zJIFmazdXrX--oq9Vfnn3TAvZrmEyVzxBNw==
O90/wmo9p0C0K+QFvjTre29qNQDOYRjqjnwgGcmsJc9f+AitdqOxkfPJ4mD603DUnUHptVIaSgR7KQ8TxAY0EvRv0EdSfYl99GRSAVW3lHfVzX2f+bH1yEGpXUuQ1g9dJ/FGff/QO9SqyI42bMoLG0tG8JUkeFSixvgS8QlQJPpBSgbgv3XcWIepQcns4EV68ZabLbYd6qAHCC1f/aX6yb7zROz6RA71H87Y5K8WpqRNsC8c0ZEGIPTYaPuApqbb36gGdNgio+ffBpeGi6/rBdx3sG8hMOXVRFrAdzWDNLKJhyhw6MR0klTzCsbTUlMBYtJOx+ioqzLuUc1NQ/ov2OSnkCg+BaRARub6U/yKtbn3vxE2xchHY+A8GzwyfN5+i9u97GItd/U8S2HJiTDSbLKqWlLwIf4MrEBYUp1Xk9BWjzh/ANXejixksAZmu/tlgBaJgetCqFQUfYIRIXGDbvMADbshUBRczgKrWu34z9iYFqZchgifoLWnlQGBPA2GoHRW1ZLsUxRLjMcW3gjM2zP5zrZCUI+sfcxRLQB2k4Fqyi/bvZPraGpnAsYd385gE6I+XUwQrpDNRQJxNPWGSD5Svv58OSPYOhIuhTCVtPoU2sH7qlBKgtSW2ZjsLM6M1GH0Kkcp9Nx3Pet8kfv775QVVoKwzQgY6gzjjnT2ihbVulPMq/Dya55GnkYLIOJ6jlN28bFCiRzY70L9MQREixn5Wkg6VNxc7Dkj39XodIO+/9OuMSElnzzR+irzXCn95NvD0TkfgF1+Az1GXwmN9Cw1xDgyveM986UQzUfPW3WBP2AdjZzUQgNXuDFd5RxZ2J0PliobhNCcLgYQS3RZZlxZC4eqG+wGMCuvbjLq9aAaU342EV6xtpoaiR06+rBkVYDR24FTitW7WckIhB+9BVHifIGctoXkN3+3QtDW1w==--mqXfOgQK7lZRAjp6--Ds5DfdaBanAHn9tXqPmoiQ==
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
---
en:
daily_nerd_messages:
form:
message_placeholder: How was your day? What did you learn?
submits_with: Saving …
date:
formats:
month_long: "%B"
Expand Down Expand Up @@ -45,6 +49,7 @@ en:
pages:
home:
archive: Archive
daily_nerd: Daily Nerd
hello: Hello %{name}
last_payments: Last payments
remaining_holidays: Remaining holidays
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
namespace :feed do
resources :leaves, only: :index
end
resources :daily_nerd_messages, only: [:create, :update]
root "pages#home"
end

Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20240404112633_create_daily_nerd_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class CreateDailyNerdMessage < ActiveRecord::Migration[7.0]
def change
create_table :daily_nerd_messages, id: :uuid do |t|
t.references :sprint_feedback, null: false, foreign_key: true, type: :uuid
t.string :message, null: false

t.timestamps
end
end
end
11 changes: 10 additions & 1 deletion db/schema.rb

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"license": "MIT",
"dependencies": {
"@hotwired/stimulus": "^3.0.1",
"@hotwired/turbo-rails": "^7.1.0",
"@hotwired/turbo-rails": "^8.0.4",
"@nerdgeschoss/shimmer": "^0.0.10",
"chart.js": "^3.7.0",
"chartkick": "^4.1.1",
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/sprint_feedbacks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
sprint_feedback_1:
sprint: empty
user: john
daily_nerd_count: 5
daily_nerd_count: 3
tracked_hours: 30
billable_hours: 20
review_notes: 'Great progress!'
Expand Down
19 changes: 19 additions & 0 deletions spec/models/daily_nerd_message_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe DailyNerdMessage do
fixtures :all
let(:user) { users(:john) }
let(:daily_nerd_message) { user.sprint_feedbacks.take.daily_nerd_messages.create(message: "I'm a daily nerd") }

describe "#post_to_slack" do
it "posts the daily nerd message to Slack" do
slack_notification = instance_double("User::SlackNotification")
allow(User::SlackNotification).to receive(:new).with(user).and_return(slack_notification)
expect(slack_notification).to receive(:post_daily_nerd_message).with(daily_nerd_message.message)

daily_nerd_message.post_to_slack
end
end
end
Loading

0 comments on commit 22d96e2

Please sign in to comment.