I'm lost and can't find the way out
So I'm in the middle of this project and I thought I could figure it out on my own, but it appears I am in over my head here. So any advice/help you can give me would be great.
Basically I'm trying to do the following:
- User goes to /link/minecraft on my website and if they haven't done it before shows them a form to enter their Minecraft username.
- On the backend once that field is submitted then it checks against their api and returns a UUID which is then stored in the Users table (from devise) in a minecraft_uuid field.
I can handle the api lookup already (it's working via CLI). My problem appears to be my lack of understanding the get/post and forms in rails. I'm not sure what my route is supposed to be for this. I tried doing
get 'link/minecraft' => "link#minecraft"
but I don't know in my controller how my code would work inside of the minecraft action/method?
Any advice or help on how to handle this? I need to be able to pull the value they submit and work with it in the backend before saving the result to the database...
Thanks <3
Hey,
Forms normally send a POST request and since the user is saving data, you definitely want a POST.
You'll need two routes for this, one to show the form and one to save the data after the form is submitted. I would recommend changing your controller to Links plural so you can stay with the standard naming.
get 'links/minecraft' => "links#minecraft"
post "links" => "links#create"
Then you can make your controller:
class LinksController < ApplicationController
def minecraft
# This is empty so the view can render the form
end
def create
if current_user.update(user_params)
redirect_to action: :minecraft, notice: "Successfully saved your Minecraft ID"
else
redirect_to action: :minecraft, alert: "Please input a valid ID"
end
end
private
def user_params
params.require(:user).permit(:minecraft_uuid)
end
end
And your form would be a normal form_for current_user
using the minecraft_uuid
as the field to render out.
Since you've got some code to look up the Minecraft UUID, you can add that to the model as a validation to look up and verify it. Add an error to the record if it is invalid and that will cause the update
call to return false.
Thanks for this, I took a bit of a break from the Rails sides of things to go pick up more ruby knowledge and I think I'm going to attempt implementing this stuff soon. Will just need to verify the API calls return successfully with the UUID (after the user submits a username via my site)... and build in some error handling for that
Hey Chris, I've been working on this for a bit now and I want to say thanks for all the help so far.
I was able to do a little finagling and get it to actually store the stuff that's submitted via the form to the minecraft_uuid field in the db but I cannot figure out how to handle the validations / conversion from the input to the actual UUID that's a response from the API.
The gem I'm using to handle the API stuff via the command line currently is https://rubygems.org/gems/mojang_api
Basically I can get it to work in the IRB by doing
require 'mojang_api'
then I can make my call via IRB
MojangApi.get_profile_from_name('king601').uuid
where king601 should be whatever the UUID was entered as via the form. Any advice or help here? I need to save the response via the api but I'm not entirely sure how to take it from IRB into the rails app. I tried googling how to do validations in this fashion but I wasn't googling the right thing.
P.S: I had to change the form_for from current_user to
<%= form_for @user, url: links_path, action: 'create', method: :post do |f| %>
hopefully that's okay still?
edit: also in case you're wondering i'm using Devise for my User model.
the User.rb currently looks like
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates :username, presence: true, uniqueness: { case_sensitive: false }
has_many :revisions
has_many :casts
end
I do need to also add a validation so Users can't have spaces in their username on my site, but that's not high on my to-do list
Check this out and see if it helps!
If you've got a field on the User model called minecraft_uuid, when you query for a user, you can pass that attribute into the API.
@user = User.first # query for a single user
profile = MojangApi.get_profile_from_name(@user.username) # this should return the profile
You can then take the profile attributes and save them to the User or something. If you only want this to happen one time, you can make it a before_create
on the User model.
class User < ActiveRecord::Base
validates :username, presence: true
before_create :load_profile
def load_profile
profile = MojangApi.get_profile_from_name(username) # Only use username here because we are inside a User
assign_attributes(uuid: profile.uuid, other_attribute: profile.other_attribute) # Save those attributes to the user
end
end
You can validate no spaces using the format validator. Some examples here: http://stackoverflow.com/questions/18281198/rails-validate-no-white-space-in-user-name
Also, there's a gem that can help with whitespaces in a a model's field. I've used it before with good success. Attribute_Normalizer. This won't will only handle leading/trailing whitespace with the strip or squish option but it works really well to sanitize your data. I use it in nearly every app I've written where I need to make sure there's no goofy data entry. After installation here's a quick snippet on how I use it in a model:
normalize_attribute :name, address, :with => :squish
Thanks guys once again.. I got much closer and was able to get the UUID and save it in the DB.
Now my problem is if a user enters a username but it's not found in the system, aka they made a typo or just don't have an account.
I've been trying to work out how to I ensure that it's valid and not nil, because if the user doesn't exist it currently just saves it as nil
I'd also like to be able to show the user an error message when that happens but was unable to get it to work (I can tell when it's null, but trying to do a flash notice gave me an error)
By just doing under in the load_profile
if profile.uuid.nil?
raise "an error occured"
end
I was able to get it to show in my dev environment but I feel like I should put it in the controller somehow and I don't know where/how?
Once again thanks for the help, I'm so close to 'done' for this feature I can smell it, just need to get this error thing figured out
Ah, yes. What you really want is a before_validation
rather than before_create
to look up the UUID and then attach it to the model. Then your validation can check to make sure that the UUID is not nil
.
class User
validates :uuid, presence: true
before_validation :set_uuid, unless: :uuid? # Skip the validation if the uuid is already set
def set_uuid
self.uuid = MojangApi.get_profile_from_name(username).uuid
rescue Exception => e# You will need the MojangApi exception thrown when not found
# This can be empty because we just need to make sure it doesn't break
end
end
So this basically would be what you want to only look up the UUID the first time and if it is already set, you can leave it.
Optionally, the only other thing you might want is a hidden field for the UUID in the form if validations fail so that you can submit it the second time without having to do a second lookup.
So the way I was able to get it to work previously was to use before_update instead of before_create. My understanding was this would allow users to be able to update it as well.
Am I trying to put the error handling in the wrong place? Should it go in the controller? When I enter in a failed name I don''t even see the flash for it not updating, it just don't show up. Here's the relevant controller:
class LinksController < ApplicationController
def minecraft
@user = current_user
end
def create
if current_user.update(user_params)
redirect_to links_minecraft_path, notice: "Successfully saved your Minecraft UUID"
else
redirect_to links_minecraft_path, error: "An error occurred and we could not save your Minecraft UUID"
end
end
private
def user_params
params.require(:user).permit(:minecraft_uuid)
end
end
I think Chris touched on this with the hidden field of uuid
in your form. That will allow Rails to perform validations on the field and params sent which if there is an error, it will throw it up in the view, which is what you are looking for. If you can output your current version of the form I can show you how to get the errors to show up if you are still having problems. :)
Okay I'm on my phone but was able to pull it via my gitlab repo. Hopefully this looks ok
<% page_title "Linking Minecraft Account" %>
<div class="well">
<div class="page-header">
<h1>Link Minecraft Account</h1>
</div>
<div class="row">
<div class="col-md-12">
<p>
It is easy to link your Minecraft account to your AthensMC account! We don't require any passwords, and you'll just need to enter your <strong>current Minecraft username</strong>
below and we will handle the rest.
</p>
<%= form_for @user, url: links_path, action: 'create', method: :post do |f| %>
<% if @user.errors.any? %>
<div id="error_explanation">
<h2><%= @user.errors.count %> Error(s) prohibited this from being saved:</h2>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= f.label :minecraft_uuid, "Minecraft Username"%><br />
<%= f.text_field :minecraft_uuid, autofocus: false, class: "form-control" %>
</div>
<div class="form-group">
<%= f.submit "Link your account!", class: "btn btn-primary btn-lg" %>
</div>
<% end %>
Now that I'm home... I realize that the @users.error thing wasn't even functioning but it was left in from a previous attempt
So I think what you'd want to do is add a hidden form field for :uuid
like so.
<%= f.hidden_field :uuid %>
That way validations can run and errors will be raised.
So I attempted it that way with the hidden field, but could not get it to render the error messages. Some more poorly typed google questions later and I decided to go back to the controller and play around with it until I was able to come up with a solution...
Here's my new controller method
def create
if current_user.update(user_params)
redirect_to root_path, notice: "Successfully saved your Minecraft account!"
else
if !current_user.update(user_params)
redirect_to links_minecraft_path, alert: "An error occurred while looking up your Minecraft UUID, please try again! Make sure you double check your spelling."
end
end
end
aka, I found out after the else statement when I rendered the user_params to plain text it prints out false whenever there's a bad response. So I decided... if it's false it just redirects and displays a generic error message... may not be the 'best' way to solve it and heck I'll probably find something that breaks it after deployment... but it works... sort of :) Thanks guys!
Now to build the next half of the feature... where they click a button and an application gets created ;)
Just kidding... I thought it was done, but I appear to hit another bug ;) Just as I was ready to deploy I found out existing users / any users can't actually update their profile because the UUID is required as a validation. But because it's something I don't want to be updatable by users after the first time I run into an issue here.
I had it on the create action, but because I don't ask for it at registration that failed, so I moved it to :update and it worked on the links/minecraft page
So I removed the presence required and figured it was ok, was able to edit my user's profile... then found out it's still broken because when I save the profile I have
before_validation :set_uuid, on: :update
which because the minecraft_uuid form doesn't exist completely breaks it (makes it vanish).
So I fixed it mostly by wrapping it in an if statement
if :minecraft_uuid.nil?
before_validation :set_uuid, on: :update
end
def set_uuid
begin
self.minecraft_uuid = MojangApi.get_profile_from_name(minecraft_uuid).uuid
rescue Exception => e
end
end
But now folks can if they go to the page and submit the links/ form again clear out their UUID... Would the best solution to this be to just not show them the field/button to submit the form again if the UUID is not null?
I realize this is really app specific here, but I feel like my user model is an absolute trainwreck now.
Or is there a way to do validations (that the minecraft_uuid field is filled out) only on my links controller?
edit: okay I need to slow down here. I'm asking questions before thinking things through. Sorry... I actually stopped and thought about what the if statement does and realized that then if the UUID was somehow updated it wouldn't actually set it... so I removed the if statement. I think my plan now is to just disable the form if there's a UUID set and have a button for them to contact support to fix it.
Sorry <3
That's not a bad solution for now to handle it manually.
In general, you simply want to lookup and validate the UUID only when the username changes. There are a bunch of different ways you could do that, but I forgot that you could use ActiveModel::Dirty to check if the username field had changed. This works because when you set the field the first time it technically "changed" from nil to one the user submitted.
before_validation set_uuid, if: :username_changed?
validates :minecraft_uuid, presence: true
def set_uuid
begin
self.minecraft_uuid = MojangApi.get_profile_from_name(minecraft_uuid).uuid
rescue Exception => e
end
end
Is there a way to make the uuid validation NOT happen on the devise edit registration? Any time I enter the field it clears it or when I wrap the setting in an if it doesn't actually the api lookup
You could do
def set_uuid
return if persisted? # Don't run if this record has already been saved
begin
self.minecraft_uuid = MojangApi.get_profile_from_name(minecraft_uuid).uuid
rescue Exception => e
end
end