Save 36% for Black Friday! Learn more

Ask A Question

Notifications

You’re not receiving notifications from this thread.

I'm lost and can't find the way out

Andrea Fomera asked in Rails

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

Reply

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.

Reply

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

Reply

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

Reply

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

Reply

Chris beat me to the punch, foiled again :)

Reply

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
Reply

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

Reply

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.

Reply

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
Reply

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. :)

Reply

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 %>
Reply

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

Reply

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.

Reply

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 ;)

Reply

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

Reply

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
Reply

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

Reply

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
Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 87,110+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.