Activity
Chris, a great episode again! So I compared your solution to mine and my structure ended up being a bit more complex since I needed to provide users to accept/decline joining the product. So I created a separated AR class called product_invitation
, which I think the correct solution to tackle the problem. My controller though is screwed up. The accept
action is pretty simple, if the user accept the invitation then the product_user
object gets created and some notifications get sent out. But my create
action is terrible. You mentioned in the video there are some good refactoring patterns and I really like to see a new episode on that. In the meantime could you recommend me some of those patterns to discover and tell me how to get started with refactoring this?
So my create
action:
A 3 way conditional. I have to check if the user is already invited or already a team member. With these I can prevent users from joining more times than one to the team. If none of these then I send the invitations.
A conditional again. Is the user already using the app? If he is then just send him a product invitation. If don't then send him devise invitation as well.
If product invitation doesn't get saved send some error message. In this case only the email field can be empty so it's not a huge problem that I gotta call redirect with "Type an email address". So I use the redirect since for some reason I can't call render properly. If I call
render :new
it throws me on thecreate page
(YES, create page and it loads it) and if I hit reload on that page then it tells me that route doesn't exist of course. I'm not sure what can cause these kind of issues (maybe devise or attr_accessor).
product_invitation.rb
attr_accessor :email
belongs_to :product
belongs_to :recipient, class_name: "User"
belongs_to :sender, class_name: "User"
product_invitation_controller.rb
def create
authorize @product, :create_product_invitations?
@recipient = User.find_by(email: params[:product_invitation][:email])
if @recipient && @recipient.product_invitations.where("product_id = ?", @product.id).any?
redirect_to :back, alert: "User already invited!"
elsif @recipient && @product.users.where("user_id = ?", @recipient.id).any?
redirect_to :back, alert: "User already a team member!"
else
@product_invitation = @product.product_invitations.new(product_invitations_params)
unless @recipient
@recipient = User.invite!({ email: params[:product_invitation][:email] }, @product.owner)
end
@product_invitation.sender = @product.owner
@product_invitation.recipient = @recipient
if @product_invitation.save
Notification.create(recipient_id: @product_invitation.recipient_id,
sender_id: @product_invitation.sender_id, notifiable: @product, action: "invited")
ProductInvitationJob.perform_later(@product_invitation)
redirect_to :back, notice: "Invitation sent!"
else
flash[:error] = "Type an email address!"
redirect_to :back
end
end
end
def accept
if @product_invitation.accepted == false
@product_invitation.update_attribute(:accepted, true)
ProductUser.create(product_id: @product.id, user_id: @product_invitation.recipient_id, role: "member")
Notification.create(recipient_id: @product_invitation.sender_id,
sender_id: @product_invitation.recipient_id, notifiable: @product, action: "accepted" )
redirect_to :back, notice: "Invitation accepted!"
else
redirect_to :back, notice: "Invitation is alredy accepted!"
end
end
Chris, I just cut through this a few hours ago :D:D:D. I go and compare your solution to mine right away. Thanks for the episode!
Thanks Chris! For the guys who might will use this later:
For simple has_many
associations use class_name
:
has_many :owned_products, ->{ where(product_users: {role: :owner}) },
through: :product_users, class_name: "Product"
For has_many through
like this one use source
instead, since it's not working with class_name
:
has_many :owned_products, ->{ where(product_users: {role: :owner}) },
through: :product_users, source: :product
Chris, could you answer my previous question? I've got some problems here. I can't figure out how to get the products you are the owner of. As you see there is has_many :products
and has_many :products, through: :product_users
. So I can't just query user.products
to get back the owner.
I thought about a solution where I'm getting rid of has_many :products
and using the following instance method in user class:
def owned_products
Product.where("user_id = ?", self.id)
end
However I'm not sure if this is the rails convention and don't wanna run into some issues later on cuz my schema is screwed up.
Thanks!
Chris, awesome episode! I'm trying to implement something similar, but before I dive deep into it I'd like to make sure I go down the right path. So I will do the exactly same, except comments are gonna have replies. My guess is if I have the right polymorphic setup for the comments, then I can just setup a simple `has_many + belongs_to` relationship between the comment model and the reply model, so from the reply's perspective it doesn't matter if the comment is polymorphic or not since every comment will have its unique id. Is this right?
Hey Chris, thanks for the answer!
I'm using devise fortunately just didn't know devise invitable had this feature. It sounds straightforward!
As for the data model, let me sum it up just to make sure I get it:
user table:
has_many :product_users
has_many :products, through: :product_users
has_many :products #I guess this one is needed for the creator.
product table:
has_many :product_users
has_many :users, through: :product_users
belongs_to :user
product_users:
belongs_to :user
belongs_to :product
I think with this setup I will be able to call product.user
to get the creator, and product.users
to get everybody else.
So, is this what you were talking about? Could you also tell me why this approach is better, than let's say having a boolean field on ProductUsers
to decide who the owner is? I guess in the boolean case it's easier to change this setup later on if I'd like to add more "owner" to the product.
Hey!
At the moment I have a user and a product table and products are created by users. So the user can create his own product he is working on. User has_many :products
and product belongs_to :user
. Now I'd like to provide the user to be able to add more users to the product (who worked on product with him/her) even if those are not registered yet in the app.
So I will change the product and user table connection to has_many :users, through: :ProductUsers
and has_many :products, through: :ProductUsers
. That's easy.
When the user creates a product and adds the extra name
and the email
of the other users in the form the app will send out emails to those people to register (only if they are not registered). I can manage this far.
So here is my problem: Let's say the product is already created with the added names and emails and email invitation is sent out.
- How can I make sure that after registration the new user entity will be assigned to the product his email/name is already assigned to by the guy who created the product?
- How can I make sure the data will be consistent?
- What kinda table should contain the email/name when product is created?
- Is the approach for the table the good one or I should differentiate the creator and other users somehow for some authorization reasons?
I hope my question is clear :)
A great episode again Chris! I wish you had released this one a week earlier though :). It would have made my life way easier. Could you tell me if there is any advantage of using meta tag like
<meta content="<%= current_user.id % >">
over just adding the data attr to the body like
<body data-currentuserid="<%= current_user.id %>"> ?
Thanks Chris, I guess I'm just doing what you are talking about. I load all the links and if the current_user is the post author (what I check with data-attrs
) then I remove the hidden class.
Can't wait to see that episode! Could you also include some complex cache keys in that episode? I mean I struggled a bit when I had to include other models in the cache key like this:
On the tasks index page I display the task.executor.profile.name
and the task.assigner.profile.name
. The other day I realized when a profile gets updated the profile.name on the tasks index page doesn't change. I ended up using the following, but still not sure if this is the preferred way to do it.
<% cache ['tasks-index', @tasks.map(&:id), @tasks.map(&:updated_at).max, @tasks.map{|task| task.assigner.profile.updated_at}.max, @tasks.map{|task| task.executor.profile.updated_at}.max] do %>
<%= render @tasks %>
<% end %>
Nice tutorial Chris! I've got 2 questions and would be nice to know your opinion.
What do you think about using
<% cache ['lists-index', @lists.map(&:id), @lists.map(&:updated_at).max] do %>
instead of
<% cache ['lists-index', "page-#{params[:page] || 1}", @lists.count, @lists.map(&:updated_at).max] do %>
?
Could you also take a look at this question from the recent questions thread? In many situations there are some parts of the page that depend on the user's role (current_user, etc.). I would like to know what approaches can be used in those cases.
Thanks!
After watching this video https://www.youtube.com/watch?v=ktZLpjCanvg with DHH I ended up using JS that loads the authorized parts like this:
$(document).on("page:change", function() {
if ($('.post-container').length > 0) {
collectionPostEditDropdown();
};
});
function collectionPostEditDropdown() {
$('.edit-post-dropdown-button').each(function(index) {
if ($(this).data('postauthorid') == $('#bodycurrentuser').data('currentuserid')) {
$(this).removeClass('hidden');
};
});
};
I have a facebook/disqus like news feed in my app. Every action handled via AJAX on the posts index page. I have hard time figuring out how to do the russian-doll-catching when there are authorized parts in the partials. By authorized parts I mean editing/deleting link should only be visible for the post/post_comment/post_comment creator.
At the moment with the following code after creation the edit/delete links also get cached and don't change based on the current_user. What is the good approach here to handle this issue?
Post has many :post_comments, touch: true
, post_comment has_many :post_comment_replies, touch: true
.
posts index.html.erb
<% cache ["posts-index", @posts.map(&:id), @posts.map(&:updated_at).max, @posts.map {|post| post.user.profile.updated_at}.max] do %>
<%= render @posts %>
<% end %>
_post.html.erb
<% cache ['post', post, post.user.profile] do %>
<%= post.body %>
<% if policy(post).edit? && policy(post).delete? %> #this is the part that should only be visible to the creator
<%= link_to "Edit Post", edit_post_path(post), remote: true............... %>
<% end %>
<%= render partial: 'posts/post_comments/post_comment', collection: post.post_comments.ordered.includes(:user, :user_profile), as: :post_comment, locals: {post: post} %>
<% end %>
_post_comment.htmle.erb
<% cache ['post-comment', post_comment, post_comment.user.profile] do %>
<%= post_comment.body %>
<% if policy(post_comment).edit? && policy(post_comment).delete? %> #this is the part for comments that should only be visible to the creator
<%= link_to "Edit Post Comment", edit_post_post_comment_path(@post, post_comment), remote: true............... %>
<% end %>
<%= render partial: 'posts/post_comment_replies/post_comment_reply', collection: post_comment.post_comment_replies.ordered.includes(:user, :user_profile), as: :post_comment_reply, locals: { post_comment: post_comment } %>
<% end %>
_post_comment_reply.html.erb
<% cache ['post-comment-reply', post_comment_reply, post_comment_reply.user.profile] do %>
<%= post_comment_reply.body %>
<% if policy(post_comment_reply).edit? && policy(post_comment_reply).delete? %> #here the same again
<%= link_to "Edit Reply", edit_post_comment_post_comment_reply_path(@post_comment, post_comment_reply), remote: true............... %>
<% end %>
<% end %>
Posted in best way to validate URL
Enrique, thanks! I found all of these on my own, that's why I wrote this question :). There are 4 different solutions and none of them looks that good. I thought there was a "best" solution all the experienced guys are using for production apps.
I realized my way of doing it is broken thanks to rspec. I just wrote a new question on stackoverflow: http://stackoverflow.com/questions/36566056/rspec-with-website-format-validation-fails. Could you take a look at it? I was using this version (but I think there must be a better approach) since I think users should be able to type 'example.com' and 'www.example.com' on the top of the URI versions.
Posted in best way to validate URL
Hi Chris!
I've been looking for a good URL validator for a while, but all the solutions I found are kinda weird, so I'm not even sure if I wanna use any.
Here is my usecase: A user can create a product where one of the fields (attrs) is the product website_url. On the product page the website_url will be displayed. I don't exactly know if I should just let them save the string regardless the format or I should use some validation via regex or URI.
What would you do? Leave it as it is or validate it somehow? If the latter what is your way doing this?
Thanks,
Szilard
Ken, this looks amazing. I hope you keep it open source, a lot of us can learn a ton from it. How long have you been using rails?
Posted in File Uploads with Refile Discussion
I've checked it out already, but it's a bit high level, so I don't understand it. https://github.com/refile/r..., https://github.com/refile/r... I just thought you have also run into this problem since as far as I know you are using refile gem. Chris after writing this comment I found an answer on stackoverflow which might be useful. If so I will circle back with the solution. I guess it's important for everybody here.
Posted in File Uploads with Refile Discussion
Chris, I started using refile with more models (product model for avatars and message model for file sending) and realized all the things go to the same folder on S3. I also wanna upload files for the product model but it would be messy if everything was in the same folder (product avatar, product files and message files). I guess I will change the product avatar uploader to use carrierwave, but still the product files and message files will go to the same S3 folder which is not the best I guess. As I see in the docs/issues on github there is no way to put them into different folders. Did I miss something and I can put them into different buckets somehow? If I can't how I can make sure everything will be fine down the road?
Posted in integrating analytics
Hey Cesar. What I mentioned above I think are crucial for every production app. If you go thru these for the first time (like me at the moment) you will have hard time figuring out because of the absence of good resources. But as you see most of them are basically just configuration which is pretty much the same in all of the apps. If some experienced coder shares with you his/her best practices in these kinda topics that can help a ton and spare you countless hours. So I hope Chris will make some awesome screencasts to cover these topics.
Posted in integrating analytics
Hi Chris!
I see GoRails is using segment.io for analytics. Could you recommend me a good resource on integrating segment with GA and mixpanel? There are too many approaches like javascript version http://railsapps.github.io/rails-google-analytics.html) OR ruby version https://github.com/segmentio/analytics-ruby OR with some patterns like facade https://robots.thoughtbot.com/segment-io-and-ruby. On the top of these I find the docs incomplete and confusing.
It would be awesome too see a screencast on this topic. I guess most of the guys here are super interested in making sure their app is production ready and this topic would be an important part of it like deploying to production on heroku with puma and deploying sidekiq to heroku episodes. Those episodes are invaluable and personally making me sleep well, since I know my app is configured properly.
The other day I found this http://www.akitaonrails.com/2016/03/22/is-your-rails-app-ready-for-production article. It was scary that I hadn't heard about rack-attack and rack-protection before. These gems have millions of downloads but hard to discover them if you are not an experienced dev. I don't know what are all the crucial steps to make sure your app is production ready, but I'd like to see episodes on it. At the moment as I see (maybe I'm not right) a bunch of guys would benefit from the following, since they have to go thru all of these regardless what kind of app they are tinkering:
- deploying to production on heroku with puma (already done)
- deploying sidekiq to heroku (already done)
- heroku full integration (this is less important since there are good resources out there including the docs)
- analytics integration (segment + mixpanel + GA; pretty hard to find good resource)
- intercom.io integration
- basic security settings/explanation (rack-attack, rack-protection, etc.; pretty hard to find good resource)
- SSL + DNS config (rack-rewrite, customized domain in case of heroku, etc)
- basic app performance (loading js libraries, proper SQL queries, indexing, etc.)
Cheers,
Szilard
Posted in GoRails speed
+1