-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Notifications system for Evans #163
base: master
Are you sure you want to change the base?
Conversation
class NotificationsController < ApplicationController | ||
|
||
def index | ||
# @notifications = Notification.unread_for_user current_user.id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✂️
@@ -0,0 +1,13 @@ | |||
module GeneratesNotifications |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be named better.
|
||
def self.up | ||
create_table(:notifications) do |t| | ||
t.string :title, :null => false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null: false
, E.g. new hash syntax.
app/models/topic.rb
Outdated
@@ -1,5 +1,7 @@ | |||
class Topic < Post | |||
has_many :replies, -> { order 'created_at ASC' } | |||
|
|||
has_many :users, :through => :replies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New style hashes (through: :replies
).
create_table(:notifications) do |t| | ||
t.string :title, null: false | ||
t.references :source, polymorphic: true | ||
t.boolean :read, default: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null: false
?
Ето още малко храна (за размисъл). Две неща са. За първото, как да е, но за второто се изненадвам, че аз трябваше да се сетя, понеже ми се струва, че @mitio и @gsamokovarov ползват evans много повече от мен. Първото касае защо тия callback-ци не ми харесват. Има две отделни неща, които кода в момента третира като едно:
В момента пращаме известието при (2), а всъщност, то трябва да бъде пратено при (1). Изглежда, че съвпадат, но само изглежда така. Може би защото има малко видове известия все още, може би защото са изкодени само лесните, а може би и защото изпускаме някой детайл. Представете си, че добавяме функционалност, позволяваща разговор от решението на задача да се превърне в тема. Админ решава, че дискусията е интересна за всички, и си казва "хайде да я преместим във форумите". За целта трябва да се изтрият коментарите по решението и да се направят Второ, известия за предизвикателства. Workflow-а ни не е такъв, какъвто предполагаш. Предизвикателството не е публикувано в момента на създаване. Обикновено го създаваме по-рано и го публикуваме чак покрай лекцията. Дори често имаме предизвикателства готови няколко дена по-рано – създадени но непубликувани. Което пак опира до: известията не се пращат в правилното време понеже третираме създаването на запис и събитието, породило известие като едно нещо, а те са две отделни. |
private | ||
|
||
def post_notification | ||
Notification.create_notifications_for topic, to: topic.participants, title: "Нов отговор в тема: #{topic_title}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Като го се замисля, това няма ли да създаде известие за човека, който е пуснал отговора?
Яко, че си се помъчил с Cucumber. Не съм му голям фен и действително спира някои мързеливи хора да пишат тестове (гледам към теб, @mitio), така че бонус точки, че си му се хвърлил :) Липсват ти няколко теста, обаче. Един, който проверява какви notification-и се създават за отговор на тема и друг, който проверява същото за предизвикателства. Знам, че краставицата ги покрива на end-to-end ниво, но трябва да има и unit тест :) |
@skanev и аз не се кефя на AR callbacks, това за което се боря са обекти (есплицитността), а не генеричната event система. Като главният аргумент ми е време: ще ни трябва interface, за което ще се бием доста време докато го догодим. Ще има technicalities, с които ще трябва да се оправяме. Ще има и проблеми, които не мога да превидя сега и т.н. И аз обичам да ми е забавно и да експериментирам с интерфейси и идеи, но защо да го правим на чужд гръб? Общо взето ще зе завъртим за нищо, IMO. Защо мислиш, че ще е по просто? Ние, предполагам, нямаме интерфейса, които ти имаш в главата си. Ако толкова държиш на тази система защо не го изкараш в др. PR, където scope-а му е само това, а не да сучем този? Тук може да решим проблема сега, там може да покажеш "вие сте тъпи, това беше мн. по-яко" и между временно да имаме feature-a готов. |
Ох. Ощипано. Ще отговоря в разбъркан ред:
Първо, искам да уточня две неща:
Ся, ще адресирам някои от коментарите ти, които, често казано, намирам за абсолютна загуба на времето си – най-вече, защото веме съм им отговорил. Очаквам повече.
Bullshit. Интерфейса, който имам в главата си е изразен в gist по-горе. Имплементацията е очевидна. Имате го и няма нищо за догаждане. "Времето" е аргумент, който ти предлагам и двамата да запазим за работодателите си. В момента сме създали добро learning opportunity по много начини. Ако ти нямаш времето да научиш нещо, защо въобще се занимаваш с преподаване? Ако @skovachev няма време да научи нещо, защо се занимава с pull request? Допълнително, "ще направим нещо тъпо, щото нямаме време да го направим хубаво" е обидно за хората, чиито труд ползваш. В случая, това е основно моя труд, hence – тона на този отговор.
Не експериментирам, особено на чужд гръб. Това би било нещо в моя полза. В момента правя обратното – опитвам се да науча няколко човека на нещо. Ще спестя имена и неща, но честно казано, от коментарите му тук и от посоките в които бутам проекта можеш да научиш доста. И не само ти. Ся, може да не си съгласен с методите, може да не си съгласен и с това, което се опитвам да ти покажа, а може и въобще да не искаш да бъдеш научен в тоя контекст. Това си е твое решение. На твое място бих пробвал (1) техническата идея и бих излязъл с аргументи за/против или (2) бих се отказал от този PR. Със bullshit аргументи нищо смислено няма да стане, освена да си загубим времето допълнително.
Обясних по-горе. Допускам, че не си го прочел. Мога да го copy/paste-на, мога да го изразя с други думи, а можеш и просто да идеш, да го прочетеш, да помислиш над него и да ми кажеш защо не e по-просто. Ще бъде полезно и за теб, и за мен.
Наглееш. Искаш да merge-неш лош код, в проект в който съм налял доста пот и сърце. Това е толкова неуважително, че губиш моралното право да се възмущаваш от тона ми в този PR. Just sayin'. Also, нахално. TL;DR: Може или всички да научим нещо техническо или да се правим на ощипани. Нямам интерес към второто, нямам интерес и към компромиси. |
def create_notifications_for(source, to: nil, title: nil) | ||
unless to.nil? | ||
to.find_each do |user| | ||
Notification.create do |notification| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notification.create!
може да помогнем, като изгърми мощно, ако променим схемата. create
просто ще ни върне невалиден notification, който не е записан в базата.
Ех, не искам да звучиа ущипано или пък да щипя. Сега, така си се изразявам, нали, не съм се и опитвал да обиждам. Времето не мисля, че bullshit, защото всички знаем колко може да се протакат нещата тук и това e силно демотивиращо. Мога да relate-на до някъде, страничен проект е, но това не помага на мотивацията. Ta, пак казвам, че не държа да го мържнем по този начин и моите пари са на малки обекчета като този: class TopicReplyCreator < Struct.new(:topic, :author, :attributes)
def self.call(*args)
new(*args).call
end
def call
topic.replies.build(attributes) do |reply|
reply.user = author
notifiy_participants if reply.save
end
end
private
def participants
topic.participants - [author]
end
def notifiy_participants
title = "Нов отговор на #{topic.title}"
Notification.create_notifications for: topic, to: participants, title: title
end
end Те могат да се използват така: class RepliesController < ApplicationController
before_action :require_user, only: :create
before_action :authorize, only: %w(edit update)
def create
@topic = Topic.find params[:topic_id]
@reply = TopicReplyCreator.call(@topic, current_user, params[:reply])
if @reply.valid?
redirect_to [@topic, @reply]
else
render :new
end
end
# ...
end Защо ми харесва това:
Защо не ми харесва това:
Защо не ми харесва event bus-a:
И накрая, трудно ми е да си сдържа думичките, щото е лесно да ги разхвърляш, а и те карат да се чувсташ добре, така, ама супер разочароващо. Оценявай труда на другите, които е напъно безвъзмезден към този проект. Дали си струваха тези думи, си прецени сам. |
Йей! Ще може да си говорим технически. Нека да наречем двете идеи "service layer"-а и "event bus"-а. И понеже вече съм стар и ме мързи да сменям layout във всяко изречение, нека ги наричаме още "слоя" и Тъй. Първо, да уточним нещо. В момента нямаме належащ проблем, който искаме да решим. Също, това не е PR, който имплементира цял развит feature. Той единствено поставя основи. Създава notification-и за две неща:
Ако сме сериозни за този feature, имаме нужда от известия за още:
И прочее. Както и да дизайнем сега, избираме какви основи полагаме. Няма нужда да правим супер мощната система за всичко, но е добре да съпоставим посоката в която движим дизайна, с посоката, в която вървем. Да минем на спицъсите. Защо слоя ти харесва:
Рейса също са прости руби обекти. Дори DSL-а за event handler-и който съм набутал е достатъчно прост. Виждам как едното може да е "по-просто" но не виждам как разликата между двете може да е съществена. Съответно, би трябвало и рейса да ти харесва, защото той има просто Ruby обекти.
Че защо рейса да се тества трудно? Контролер: describe TopicController do
describe 'POST create' do
context 'when successful' do
it 'triggers an event' do
EventBus.should_receive(:trigger).with(:post_created, new_post)
post :create
end
end
end
end Модел: describe NotificationSubscriber do
describe "(reply notifications)" do
def notifications_for(user, source)
Notification.where(:user_id: user.id, source_id: source.id, source_type: source.class.name)
end
it "notifies all participants in the topic for the new reply" do
topic = create :topic
previous_reply = create :reply, topic: topic
new_reply = create :reply, topic: topic
NotificationSubscriber.process(:post_created, reply)
notifications_for(previous_reply.user, new_reply).exist?.should be true
notifications_for(topic.user, new_reply).exist?.should be true
end
it "doesn't notify the reply's author" do
topic = create :topic
reply = create :reply, topic: reply
NotificationSubscriber.process(:post_created, reply)
notifications_for(reply.user, reply).exist?.should be false
end
end
end Понеже това се тества просто, също би трябвало да ти харесва.
Тук защо да не можеш? class NotificationsListener < EventBusSubscriber
def participants_to_be_notified(reply)
author = reply.user
reply.topic.participants - [author]
end
on :reply_created do |reply|
Notification.create_notifications for: topic,
to: participants_to_be_notified(reply),
title: "Нова отговор на #{topic.title}",
end
end Обърни внимание, че не съм съгласен, че това е достатъчно сложна логика, която има нужда да се изнесе в метод. Но имаш как да го направиш.
Това не е аргумент, защото няма да потрябва. Говорихме си, че трябва да става в background-а, remember? Иначе: class ChallengesController < ApplicationController
def create
@challenge = Challenge.new params[:challenge]
ActiveRecord::Base.transaction do
@challenge.save!
EventBus.transmit :challenge_created, @challenge
end
redirect_to @challenge, notice: 'Предизвикателството е създадено успешно'
rescue ActiveRecord::InvalidRecord
render :new
end
end Пак можеш да го направиш. Има недостатъци, но: Няма. Да. Потрябва. YAGNI. Защо не ти харесва рейса:
Вече уточнихме, че това не е вярно.
IDE-то е наклонена плоскост. Това е несериозен аргумент. Какво става в редактора не трябва да участва в това какви дизайн решения вземаш. Особено в редактор, в който можеш да направиш Сега, малко коментари от моя страна по твоята имплементация на слоя.
Ето ти алтернатива, която според мен е по-добър слой: class RepliesController < ApplicationController
def create
@topic = Topic.find params[:topic_id]
@reply = @topic.replies.build params[:reply]
@reply.user = current_user
if @reply.save
ReplyNotificationCreator.notify_about(@reply)
redirect_to [@topic, @reply]
else
render :new
end
end
end В крайна сметка, ако искаме малки обекти, нека да са малки обекти. Обаче, не мисля, че добрия слой е хубава идея. Мисля, че добрия рейс е хубава идея. Защо? Практическите причини вече ги изброих. Ето и философската: Ще вкараш нов pattern. Нямам нищо против новите pattern-и, но този: нито е широко приложим тук, нито решава проблема по-добре. Също, тегав е разбиране, тегав е и за репликиране. Моята идея за слой, твоята идея за слой и нечия друга идея за слой ще са три различни неща. Аз ще имам едни ценности, ти ще имаш други. За мен едно нещо ще е overkill, за теб ще е друго. Когато структурата на кода не показва кога един pattern е добра идея, този pattern е труден за обясняване. Също, почваш да губиш симетрия. Защо имаш Рейса няма тоя проблем. Или дори да го има, е в много по-малка степен. Изброих и други бъдещи известия, които също модулират тия аргументи. Помисли например, дали слой или рейс ще е по-подходящ за всички. Аз го виждам като рейс. Но не искам да задълбавам в твърде много детайли извън кода. Нека да завършим на нетехническа нотка:
Не ми пука за еднорозии и дъги. Аз се мотивирам от технологията. Чувствата и добрия тон са загуба на свободното ми време. Не ни се налага да правиш неща заедно – избираме да го правим. Твърдя, че в evans има много, от което може да се научи. В този pull request също има много, което да се научи. Ако мотивацията ти е да "научиш" нещо, си попаднал на добро място. Както виждаш, отговарям на всякакви технически въпроси, и съм склонен да бистря всякакви идеи в детайли. Полезно е и за мен. Ако мотивацията ти е, че ще се почувсташ оценен и ще ти стане готино от merge-нат PR, докато някой те залива с листа от рози докато същевременно ти отговаря културно, учтиво и подброно за всичко дребно, което не си си направил труда да помислиш – то, дошъл си на грешното място :)
Докосващо, но лицемерно. Да започнем от тук:
Ще припомня две неща:
Радвам се, ако се окаже полезен на някого, било то научавайки нещо от него или ползвайки го за свои цели. Затова е в GitHub и затова кода е отворен. Обаче, ако ми трябва нещо, ще си го направя сам. Тоя pull request не ми трябва. Защо въобще е тук? Може би му трябва на някой друг? Може би автора е искал да научи и да упражни нещо? Не трябва ли, аджеба, моя труд да се уважава, че съм прекарал немалко вечери да създам контекст, в който това може да се случи? Щото ако има нещо неуважително, то е да ми казваш "аре да го merge-нем, щото ни мързи да инвестираме повече време". Ако те мързи, не се занимавай. Или, нали, има fork бутон. Whatever. Просто не се дръж всякаш ти дължа нещо. Обратното няма да го коментирам.
Чак пък да ме карат да се чувствам добре. Всъщност, издразнен съм, понеже вместо да бистрим интересния въпрос, трябва да се занимавам с "мотивация" и да обяснявам тия неща. Сигурно е шоу отстрани, но само ми губи времето, честно казано. Или, нали, освен ако няма смисъл:
Разбира се. Показват кои къде стои. Показват какво мисля за "оценявай труда на другите, който идва безвъздмездно". Показвам и какво мисля за "какво влияе на мотивацията". От там нататък, всеки си решава дали да продължи да engage-ва или не. |
Харесва ми това, че изпращането на известието остава в контролера. Супер. При слоя на @gsamokovarov не ми харесва, че записването на обекта става в слоя. Колко подобни слоеве можем да имаме за дадена операция? Според мен е повече от един (примерно мейл слой). При слоя на @skanev този проблем го няма. Не ми харесва, че при слоя говорите за Ако се съгласите с горното остава да изговорим единствената разлика, която виждам между слоя и рейса - рейса разкача контролер/модел от изпълнител на заявката. Това всъщност е в природата на event-ите и носи своите плюсове и минуси. Относно рейса: Плюс:
Минус:
|
@s2gatev 👍 Малко уточнение:
Докато не съм несъгласен с теб, слоя страда от много подобен проблем. Вкарва още един pattern и то такъв, който не е много универсален и всеки го прави различно. Дори, бих казал, че рейса вкарва ясна абстракция, докато слоя вкарва неясна такава. Именно това поражда въпроси като "Ако направя |
Окей, това може да се проточи. Нека спрем със глупостите и да караме технически. Аз се издразних и скочих и да съм те обидил, не е била това целта, нали. Спирам да бутам за времето, дай да го направим така, че всички да сме щастливи накрая. ✌️
С това съм съгласен и е кофти ситуация, да. Имайки предвид, че кода е показен, може да се подлъжем и вкараме гаден тренд.
Сега, това е скеч имплементация. Друг минус с рейса, за мене е търсенето е имплицитността на subscriber-а. Когато тригърна нещо, на колко места трябва да погледна за да знам какво ще пусна? Да кажем, че това не е особен проблем за evans, тъй като не е мн. голям и ако търсим записали се само в EventBusSubscriber-и аз съм окей. @skanev Как го виждаш това API малко по-специфично?
Друг интерфейс има ли? Къде да оставим имплементацията и къде да живеят subscriber-ите?
Друг интерфейс има ли? |
Много сложно го мислиш. Няма нужда от storage: module EventBus
extend self
def subscribers
@subscribers ||= []
end
def register(subscriber)
subscribers << subscriber
end
def trigger(event_name, *args) do
subscribers.each do |subscriber|
subscriber.process event_name, *args
end
end
end
class EventSubscriber
class << self
def on(event_name, &block)
define_method :"on_#{event_name}", &block
end
end
def process(event_name, *args)
handler = :"on_#{event_name}"
send(handler, *args) if respond_to?(handler)
end
end Това е скеч след шест часа шофиране, така че сигурно има много грешки. Два коментара:
Така ми изглежда супер. Само не знам дали да е "subscriber", "listener" или нещо друго.
Бих казал "по-скоро не", но не съм сигурен защо. Това горе ми се струва по-просто. А и
Не съм сигурен за кои грешки говориш. Можеш да объркаш име на събитие, но тогава просто няма handler ;)
Бих казал "не". Ако ти трябва нещо с транзакция (пример би бил полезен), е хубаво транзакцията да е на едно място, а не разхвърляна из кода. Съответно, ако искам да направя X неща в транзакция, бих си направил обектче за тях, не бих ги разхвърлял из събития. Припомням, че не виждам случай, където транзакциите са фактор. Не ми се струва, че reply-а и notification-ите трябва да са в транзакция, още повече защото notification-ите ми се струват подходящи за job :) |
|
Не настоявам, мисля че е достатачно просто и без него.
Когато handler хвърли exception, но за сега не мисля че има смисъл да правим кавкото и да е. Нека гърми да се вижда. Супер тогава, имаме план :) |
Indeed. Няма асинхронност, няма транзакции – да ходи в bugsnag-а :) БТВ, щом имаме план, който се наеме да го имплементира, моля да разкара досегашните observer-и :) |
За мен цялата дискусия определено ми беше (и е) изключително интересна и
|
@skovachev @gsamokovarov bump |
Нещо против аз да довърша това? |
@s2gatev Аз съм ок. Имайки предвид кога е последния път, когато имах време да седна да го пипна, изглежда е добра идея. |
Adds support for simple notifications. This feature is implemented as part of the HackBulgaria Ruby on Rails course. In summary this pull request adds:
Full task description can be found here: Week 8, Evans task description