Friday, July 11, 2014

Angular JS in ActiveAdmin

We use an angular app to display exams at BoardVitals.com. We found out that the Explanations being displayed for answers on the exams were differently displayed than how they are displayed in our ActiveAdmin edit pages.

This makes sense - AA uses simple textareas, whereas we render the field as HTML on the front end. What we wanted was a Live Preview feature, where admin users could see EXACTLY how explanations were being displayed to users, within active admin. I thought, why not inject Angular and render the exact same directive we use?

Let's get into it.

To be specific, here is the template for the Angular tag I used for explanations:

<p>{{response.question.combined_letter_choices}}</p>
<p ng-bind-html="visibleExplanation"></p>
<p id="reference" ng-bind-html="reference"></p>

Here is the directive for that tag:

angular.module("exam.directives")
.directive('explanationText', ['$sce', function($sce) {
  return {
    restrict: "E",
    templateUrl: 'exams/explanation_text.html',
    link: function($scope) {
      $scope.sanitizeAndSet = function(text) {
        $scope.visibleExplanation = $sce.trustAsHtml(text);
        // Used in ActiveAdmin to set explanation with jquery from outside.
      }
    }
  }
}])


I wanted to render this tag inside ActiveAdmin, beneath the explanation name field. To inject it, first I required my angular manifest inside active_admin.js.coffee, the manifest used by AA:

//= require active_admin/base
//= require angular_dash.js
//= require lodash

(I used coffeescript before I switched to angular...forgive me, haven't swapped it out yet.)

Angular_dash.js is pretty simple:

//= require angular
//= require angular-route
//= require angular-rails-templates
//= require_tree ../templates
//= require lodash
//= require dashboard/exam/exam.js
//= require_tree ./dashboard/exam

This would load the angular library in active admin. Next I had to place it in the page. But how to do that? AA uses a DSL from the FormTastic library, it won't let you edit HTML directly. You could inject it with a $.append call after the fact, but then you have to compile the Angular app and bootstrap it yourself. That sucks.

The solution is a custom input field. FormTastic lets you create these custom input classes with custom to_html methods and then you can use them in the forms. Create a folder called app/inputs, and add this file to it, called angular_input.rb:

For ActiveAdmin before Rails 4.2:

class AngularInput
  include Formtastic::Inputs::Base
  include Formtastic::Inputs::Base::Placeholder

  def to_html
    "<label class='label'><b>Live Preview</b></label>" +
    "<div ng-app='exam' class='admin_explanation' id='explanation'>" +
    "<explanation-text></explanation-text></div>"
  end
end

For ActiveAdmin Rails 4.2+:

class AngularInput
  include Formtastic::Inputs::Base
  def to_html
   html =  "<label class='label'><b>Live Preview</b></label>" +
    "<div ng-app='exam' class='admin_explanation' id='explanation'>" +
    "<explanation-text></explanation-text></div>"
    input_wrapping do
      builder.template.content_tag(:preview, html, { :id => "live_preview" }, false)
    end
  end
end


Then you can add this angular input to your form by doing:

    f.inputs :name => "Explanation", :for => [:explanation, f.object.explanation || Explanation.new] do |explanation_form|
      explanation_form.input :name
      explanation_form.input :preview, :as => :angular
....

As you can see, I just render it like any other input, and FormTastic injects my HTML and my angular app for me. It gets bootstrapped normally. 

Finally, I want to tie the explanation name field to the angular rendered tag. I do this in active_admin.js.coffee:

    # Automatically sets the field inside the explanation element in angular with the changes from the field.
    # Uses a clone of the gsub logic on production-side to mimic the changes that happen there.
    if($("#question_explanation_attributes_name"))
      adjustAngular = (e) ->
        visibleExplanation = $("#question_explanation_attributes_name").val().replace(/[\r\n]/g,"<br/>\n")
        angular.element('#explanation explanation-text').scope().$apply((scope) ->
           scope.sanitizeAndSet(visibleExplanation)
        )
      $("#question_explanation_attributes_name").on('keypress', adjustAngular);
      adjustAngular();

Notice my use of the sanitizeAndSet method I defined in the directive. This is because you can't access the $sce service from $apply, but you can from the linking function.

The admin_explanation class I use in the video is this:

ol div.admin_explanation {
  width: 76%;
  margin-left: 20.5%;
  margin-bottom: 20px;
  background-color: white;
  box-shadow: 0 1px 1px;
  border-color:#dddddd;
  border: 1px solid transparent;
  border-radius: 4px;
}

if you're interested.

I made a screencast view of the result:

http://screencast.com/t/YPUenkiFjf (You'll have to copy paste this link)


Wednesday, August 14, 2013

Transparently merge duplicate objects on save of new duplicate using ActiveRecord

I had to deal with this issue at work. I was trying to create customers, and I didn't want duplicates. I wanted it so that when I did Customer.create, I would get have some validation or hook that would instead of saving this dupe, turn it into the old one, ie merge and save the values between the two duplicates, and then set the ID on this new customer to that of the old. It would be completely transparent.

The way to do this is to put a before_create hook that tests whether or not there is a duplicate. If there is, return false at the end of before_create. This halts the transaction from creating. You would think, oh, in before_create, I can look up the duplicate, merge the values between this guy and the old one, and then save the database entry - but you can't. This is inside a transaction, it'll be lost on the Rollback.

Next, make an after_rollback hook, this is outside the transaction of save. In it, find the duplicate again, and now do your merging. I tend to take preference on newer values - if the new guy has an email and the old one does, the newer email is kept, but if the old one has a field set the new one doesn't, I merge it. Then I set self.id = old.id, and here is the key part:

@new_record = false

This is the field active record uses to figure out if it should do insert into or update in the database. If you set the id to some number but don't change this to false, it will still try insert into, and blow up.

So you do:

self.id = old.id
@new_record = false
self.save

Now you overwrite the database entry and your object looks like the old one. Your code thinks you created a new one but really it just found the old one and merged it transparently. Beautiful, right?

Sunday, January 27, 2013

5 Reasons Not to Barhop Ever Again

One of my favorite activities in college was barhopping. My buddies and I would go out every night,  paying outrageous covers and standing in poorly lit rooms filled with loud bone shaking noise trying to meet girls, failing almost every time. In an attempt to save the rest of you from wasting your time in this manner, I decided to list why bars suck. Here we go:

1. Everybody's a Wallflower

I know in the movies, when they depict bars and barhopping, it's always a rip-roaring adventure where everybody is partying with everybody else, girls are half naked and throwing themselves at drunken slobs of college boys and condoms are a figment of everyone's collective imagination. This is complete crap. Every time I've been to a bar, it's filled with tiny cliques of people who know one another, often a mix of girls and guys, or just a few lonely looking saps around the edges, never deviating from their patterns or talking to anybody new. Why would I pay a 5-10$ cover to go sit with people I already know? It's bull. Goto a house party with your friends, you're much more likely to strike up a conversation over beer pong, or a card-based drinking game.

2. One Night Stands are a Lie

Be honest with me - how many times have you met a girl at a bar, taken her back to your place, and didn't hit the walk of shame til 6am? Can you count it on even two hands? Yeah, I didn't think so. I've gone out to bars hundreds of times in my life, always with that singular goal: To meet and take home hundreds of women. It's only ever happened a few times, and given the time investment, it's just not worth it.

If the entire reason for going out with your buddies is to meet and bed random strangers, why would you goto a place where you are the least likely to do it? Girls aren't stupid, they know why you're slogging up to their stools and slovenly offering to buy them a drink! At least at a house party, a coffee house, a club meeting or some kind of extracurricular activity(LivingSocial volleyball, anyone?) you're meeting people and doing something interesting together you can talk about. That's the way to meet women.

3. It's Loud

Why in God's name would you EVER go and meet friends at a place to hang out when you can't even hear yourself think? I'm not even going to elaborate on this one, it's obvious. The only real plus to it being unbearable in a bar is that you get to reuse the same conversation pieces with the opposite sex over and over again as you both keep asking each other to repeat yourselves...

4. It Makes You Fat

You goto a bar and you drink 5 beers. Given that a beer is 150 calories, you're drinking 750 calories without even thinking about it. For most of you, that's a little less than half your daily intake of calories before you start storing fat. Ever wonder how girls gain that freshman 15? It's all those drinks you're buying them.

5. It's 40$ today, but what is it in ten years?

If you goto a bar and spend 40$ on drinks, that's not too bad. What's 40 dollars for a good night out with your buddies when you can't hear them speak, don't get laid, and gain a quarter of a pound? Well, let's do some math. Say you invested it and expected a reasonable rate of return of 7-10%. That's about 70-100$ ten years from now. That's ridiculous: Would you really pay 100$ of your hard earned cash for this shitty experience? I think not.

I'd rather save that money, and go out to a Morton's and get an amazing steak, or go skydiving(only 200$), or see 5 movies. A bar? Screw that. It's a waste of time.

Friday, January 25, 2013

On my conversion to Mustachianism...

Growing up, I lived in an upper middle to lower upper class household, depending on whether we're talking before or after 2008(Sometimes, I miss the Bush economy, 5% unemployment is boss!). Our answer to a laptop filled with malware was a brand new laptop that will get filled with brand new malware!

Yeah, I was lucky. My parents sent me to college with no loans to speak of, a car, and a monthly stipend of 1500$ cash. I lived large, and my father paid for it. I never had to learn how to cut back, because it had never been required.

Then, one day, I graduated. I spent two or three years dicking around at the U of I, working on startups and taking pre-med classes on my father's dime in case I wanted to goto medical school, aimlessly wandering the intellectual field, before fate finally came a'calling. I got a call one day from the CEO of a certain Chicago real estate company, who had happened to invest in one of my startups a year or so back. That start up failed, but he wanted me to come code for him anyway. Apparently, drive is more important than being right when you're young.

At the time, I was dating this wonderful girl named Jasmine, and she was moving back up from Champaign, IL to Chicago too, and I was worried what might become of our relationship with such distance. I thought it a sign from God, and took the job. As soon as I did, my step mother, never one for my spending, cut me off from the family teat.

It seemed fine, at first. With 2000$ or so in my bank account, I lavished my new girlfriend with dinners on Michigan Avenue, Cirque du Soleil shows, and joined an expensive gym. I thought this new job would give me the life I always wanted, and just blindly expected the paychecks coming in would fill the gap.

Then, during the first month of my financial "freedom" in Chicago, I spent so much money I forgot to think about how much rent was going to be. It wasn't included in my "mental budget," and I ended up facing a 1500$ rent bill with 345$ in my bank account to pay for it.

So much for living large! I grudgingly went to ask my boss for a 1000$ advance, but he slapped me on the back, told me not to do it again, and gave me 1000$ outright. It was the cheapest lesson I've ever learned: privileged, spoiled spending like mine needs this thing called a "budget" in the real world.

I learned how to use Excel, segmented my spending into discrete units, and then continued to spend it just as carelessly, only now always hitting exactly 90% of my income every month. You would think after a scare like that, I'd change, but I really didn't - I just became a little smarter about being stupid.

Some months later, I was perusing hacker news and came upon the post The Shockingly Simple Math Behind Early Retirement, by Mr. Money Mustache. He advocated saving more than half of your after-tax income for retirement, in a comic pseudo-religion known as "Mustachianism."

Now, I've always been a man of numbers, starting with my Physics B.S. from University of Virginia. My girlfriend, her mother, and my step mother, among others, have always advocated saving a large percentage of one's income, but I couldn't understand how saving even 20% of your income(this seemed large to me at the time) could ever allow you to retire. I had just never run the numbers, because it seemed faintly ludicrous to me.

Mr. Money Mustache laid me out like a punch straight to my fiscal nose. Then I visited Firecalc, and saw the math projected into the future across some 300 parallel economic universes. It was like science fiction brought to life. Retirement was possible - and it was possible to in ten years or less, on a programmer's salary like mine. What the hell, I thought. Why did nobody teach this stuff in schools?

I was converted. I took to my excel spreadsheet, and added a new column: "Percent of income saved." I calculated it based on after-tax contribution equivalent to my 401k(5% before tax is like 8% after tax), and money left over outside the budget. I found that the hardest part about my tectonic life shift was not finding the cuts, it was admitting cuts need to be made in the first place. I got a raise about 3 months ago, and I never updated my budget to reflect the raise(maybe some murmurs of Mustachianism already echoing around inside my skull?), and that brought me up to about 25%. But I needed more!

I then cancelled my cable, my 130$/month gym(I have weights in the basement of my building, I don't need a fancy gym, just some creativity), and I cut down on expensive haircuts. None of these things effected my lifestyle very much, but brought me up to 38% of my income being saved. I was ecstatic.

Now, all I need is a couple more raises, and a cheaper apartment, and I'll easily be breaking 75%.

I hereby declare on this blog to the internet, by the end of 2013 I will break 50% income savings, and by the end of 2014, 75%. I will retire by the time I am 35, and it's all thanks to Mr. Money Mustache.

Wednesday, January 2, 2013

Fix for 1 error(s) on assignment of multiparameter attributes rails

I have a form that submits dates like so in the params hash:

{
"dob(1i)" => "01",
"dob(2i)" => "02",
"dob(3i)" => "2011"
}

and I started getting

1 error(s) on assignment of multiparameter attributes rails.

 What I did to fix this, was in the controller action, I took the params object:

 dobs = params.delete("dob").split(/[\D]/)

  params["dob"] = Time.new(dobs[2], dobs.first, dobs[1])

When update is called on these params, it will work just fine. It's a hacky work around, but a work around it is. It splits the date on a non-numeric delimiter(/, -, what have you) and then makes a new Time object.

You're welcome.




Saturday, December 8, 2012

How to get Artist from a Youtube Video in Ruby

I've not seen a good answer for this so far, but I discovered one, and I think you'll agree it's quite clever! I considered making this into a gem, but it's so simple, one is not required.

First, use whatever youtube gem you want to grab the title of the youtube video.

Second, get this gem: https://github.com/wiseleyb/google_custom_search_api

Follow instructions there on how to setup your Google Custom Search account and a Custom Search Engine(CSE) that searches all websites. Put the gem into your app as per their instructions, get it working.

Then use this code:

query = video_title
first_entry = "no artist found"

results = GoogleCustomSearchApi.search(query)
results.items.each do |item|
  if item["htmlSnippet"] and m = item["htmlSnippet"].match(/by.*<b>([\w\s]*)<\/b>/)
    first_entry = m[1]
   end
end

first_entry has your artist after you run this. What it does is it scans every Google result snippet for the html "by ...some more useless words... <b>BandName</b>. There is almost ALWAYS a page on the search results that has this pattern, and we isolate the band name by using the fact these pages ALWAYS turn band names into links going to pages about them.

I've tried these videos and gotten it to work:

Shakira - Give it up to me
Linkin park - in the end
Imagine Dragons - it's time

etc. It relies on the fact too that most youtube videos include the band name in the title. The problem is for us coders that it's not often very standard, sometimes it will be linkin park in the end, or in the end - linkin park, or linkin park "in the end", etc. Instead of trying to puzzle it out, let Google do it!

Hope this helped you, certainly helped me.



Tuesday, December 4, 2012

Delete route not showing up in rails 3

I wanted to add a destroy action to my rails controller. My routes file looked like so:
match "applications/:id" => 'applications#show'
delete "applications/:id" => "applications#destroy"

But no matter what I did, the destroy action never got called. This is because match is above delete, overriding it. You must switch the order in which you call them in routes or else delete will get superseded.

delete "applications/:id" => "applications#destroy"
 match "applications/:id" => 'applications#show'

Works.