MettaProgramming

Thoughts on Software and Technology

Archive for the ‘Ruby’ Category

Polymorphic :has_many, :through in Rails modules

leave a comment

[Update: Stephan Wehner actually showed me how to get this working (see his branch). Something else was wrong in the program I was actually using, which went away when I built this simplified version- I just never really tested with the simplified version. Thus, this post is now pointless. Thanks Stephan.]

Recently, I had need to create a plugin that held a polymorphic :has_many, :through relationship. I spent a day or so trying to get this to work, only to realize that it is essentially impossible to do inside a pluggable Rails module, but possible if you templatize the classes.

PolymorphicPlugin Example Project

There’s a StackOverflow thread that describes this problem and the solution a bit; however, Based on the difficulty, and the fact that I couldn’t find any documentation specific to this problem, I figured I’d write it up as an example, and create a PolymorphicPlugin gem to illustrate it better.

The PolymorphicPlugin inside-module branch is an example of the way you’d distribute a pluggable Rails module with all classes contained in the module. The generator only creates a migration in the Rails project.

The PolymorphicPlugin outside-module branch is an example of the way you’d distribute a pluggable Rails module in such a way that the generator creates a migration in and also copies model files to the Rails project.

Problem Description

The idea is that there’s an “acts_as” like plugin that I want to add to my Rails models:

module PolymorphicPlugin
  module PolymorphicHolder

    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def holds_things
        has_many :thing_groupings, :as => :thingable
        has_many :things, :through => :thing_groupings
        include PolymorphicPlugin::PolymorphicHolder::InstanceMethods
      end
    end

    module InstanceMethods

      def has_things?
        true
      end

    end
  end
end

You can see that anything that calls holds_things should have a :has_many relationship with Thing through the ThingGrouping join table. This is a standard paradigm for a polymorphic :has_many, :through relationship.

Basically, I want to do this:

class User < ActiveRecord::Base
  holds_things
end

class Admin < ActiveRecord::Base
  holds_things
end

user = User.new
user.has_things? -> true

admin = Admin.new
admin.has_things? -> true

inside-module plugin layout

The failing case

Seems pretty simple, right?

Turns out it’s not. Take a look at the directory layout that I tried to use. This seems like a standard for a gemmable plugin. The model files are contained in the gem library, and accessed as part of the module. When you use gem 'polymorphic_plugin', the only thing that gets copied to your Rails project is the migration file. Nice and clean.

Unfortunately, that doesn’t work. If you clone this branch and run rake test you’ll get exceptions. Specifically, a NameError: uninitialized constant User::ThingGrouping because our User model, by extending the PolymorphicHolder module, doesn’t have access to this unknown model called ThingGrouping.

And no, using things like :class_name and :source were no help. I tried everything, or seemingly everything. Nothing would make this work.

outside-module plugin layout

The passing case

That is until I pulled the models outside of the plugin module.

Take a look at the directory layout now and you’ll notice that the Thing and ThingGrouping models are no longer in the plugin’s lib directory. Moving them to the templates directory is the unfortunate answer.

I say “unfortunate” because this means that the models need to be distributed by copying them to the Rails project. In other words, there’s a line in my PolymorphicPluginGenerator that reads: template "thing.rb", File.join('app/models', "thing.rb"). In other words: Copy the Thing model into this project so it can be accidentally futzed with by users and maybe, if we’re lucky, cause some interesting name collisions.

Thing and ThingGroup are no longer contained inside my PolymorphicPlugin module. They are top-level classes equivalent to anything generated with rails generate model. Not a solution that I’m fond of, but one that passes my tests. We can at least have a polymorphic :has_many, :through relationship now.

Outcome

Polymorphic :has_many, :through relationships are available in pluggable Rails modules, but it’s a bit of a kludge. You’ll need to make sure you name your polymorphic classes smartly to avoid name collisions with the calling Rails project. You’d hate to create a plugin with a model like Book only to find that Book already exists in someone’s Rails project!

I’d love to be proven wrong on this, by the way. Go ahead and grab the Github repository and play with the inside-module branch. If you can make it work, I’ll buy you a beer!

Written by john

November 15th, 2011 at 12:28 pm

Posted in Ruby

Rails 3.1 Gemmable Plugins with RSpec

leave a comment

I’ve had a bitch of a time lately getting the new Rails 3.1 plugin architecture to play nicely with RSpec. I actually gave up and coded an entire plugin with Test::Unit before finally figuring out a decent way to do it.

Currently, the new Rails 3.1 mountable plugin architecture will not work with RSpec. There’s a gem called Enginex that will work with RSpec, but it won’t work with Rails 3.1. So it looks like you either get RSpec or Rails 3.1. What’s the deal?

Playing around, I figured out that combining the two will work:

$ gem install enginex
…
$ enginex my_plugin --test-framework=rspec
…
$ rails -v
Rails 3.1.1
$ rails plugin new stock_plugin --full
…
$ rm -rf my_plugin/spec/dummy
$ cp -r stock_plugin/test/dummy my_plugin/spec

After that, it’s just a matter of modifying my_plugin/spec/dummy/config/application.rb to require your gem, and clean up the spec_helper.rb file. I also changed the Gem files so that my_plugin.gemspec held the dependencies, and Gemfile just called that. This is the way the stock Rails 3.1 generator works. Enginex uses Gemfile as the canonical source, and I like the new way better.

Using this I’ve currently got a Rails 3.1 gemmable plugin with RSpec, Cucumber, Capybara, Spork, Guard, and Simplecov working just fine.

Written by john

November 14th, 2011 at 12:41 pm

Posted in Ruby

Wrestling ActiveResource Associations

one comment

I need to state up front that I don’t have a solution to this problem. Hopefully, I can iterate toward a solution- using this interim solution (discussed at the end).

The basic problem that I’ve found is that ActiveResource is a great Rails model to inherit from, except when it isn’t. And it seems that “when it isn’t” basically means “anytime you are using associations.” To be fair, associations don’t always break, it just turns out that my architecture illustrates exactly when they do break.

To lay out the situation, I have a backend server running Rails with three models, User, Service, and Groups. I have a Rails frontend server without a database hitting this (as well as other) backend servers.

Backend

class User  :members
  has_many :managed_groups, :class_name =&gt; "Group"
  accepts_nested_attributes_for :managed_groups, :services, :groups
end

class Group  :members
  belongs_to :manager, :class_name =&gt; "User", :foreign_key =&gt; :user_id
  accepts_nested_attributes_for :users  
end

class Service &lt; ActiveRecord::Base
  belongs_to :user
end

class Member &lt; ActiveRecord::Base
  belongs_to :group
  belongs_to :user
end

You can see it’s really basic setup. Users can have many Services, but Service can only have one User. By contrast, users can Manage many groups, and can belong to many Groups, while Groups can have many users who belong to it.

Users -> Services is a basic :has_many relationship and Users -> Groups is a :has_many, :through relationship. There is nothing complicated about this setup at all…

Frontend

…until you get to an ActiveResource frontend

class User &lt; ActiveResource::Base
  self.site = &quot;http://localhost:8801&quot;
end

class Group &lt; ActiveResource::Base
  self.site = &quot;http://localhost:8801&quot;
end

class Service &lt; ActiveResource::Base
  self.site = &quot;http://localhost:8801&quot;
end

The frontend server sees all of these models as ActiveResources, which works fine if you’re accessing User.name, but which doesn’t work so well if you’re trying to access User.services because User.services doesn’t exist. ActiveResource simply doesn’t know anything about associations, so you have to work around a big problem.

:include => :associated_model

One easy way to make User.services available to ActiveResource is by including it in the UsersController. Here’s a sample UsersController#Show method on the backend:

def show
@user = User.find(params[:id])
  respond_to do |format|
    format.xml  { render :x ml =&gt; @user.to_xml(:include =&gt; [:managed_groups, :services, :groups]) }
    format.json  { render :json =&gt; @user.to_json(:include =&gt; [:managed_groups, :services, :groups]) }
  end
end

We just include a list of whatever associated objects we want to include in the returned user package. Easy enough. Starting up the Rails console on the frontend now allows you to access User.services. However, what if you tried the following:

service = Service.create(:name =&gt; "Test Service")
user = User.first
user.services &lt;&lt; service

Turns out that we can’t do that. We get an exception that the User model expects a Service class, but was supplied with an IndifferentHash.

With a :has_many/:belongs_to relationship, this is easy to work around:

service = Service.create(:name =&gt; "Test Service", :user_id =&gt; User.first.id)

Of course, user_id is a “primitive” type, a direct attribute of Server. So we can force it by going to the other side of the relationship and set that attribute and not have a problem.

:has_many, :through => :needles_in_your_eye

Working around the :belongs_to issue was easy, working around :has_many, :through is as fun as… well, you get the idea.

The problem is that there’s no backdoor to setting the association. You simply can’t access an array of objects as an association through ActiveResource. Look at what we see when the frontend accesses the backend server with a PUT request at /users/1.xml:

{"user"=&gt;
  {"created_at"=&gt;2011-06-23 02:58:15 UTC,
   "id"=&gt;1,
   "name"=&gt;"John Metta",
   "updated_at"=&gt;2011-06-23 02:58:15 UTC,
   "managed_groups"=&gt;[],
   "services"=&gt;[
     {"created_at"=&gt;2011-06-23 02:58:15 UTC,
      "id"=&gt;1,
      "updated_at"=&gt;2011-06-23 02:58:15 UTC,
      "user_id"=&gt;1}
   ],
   "groups"=&gt;[
     {"created_at"=&gt;2011-06-23 02:56:37 UTC,
      "id"=&gt;1,
      "updated_at"=&gt;2011-06-23 02:56:37 UTC,
      "user_id"=&gt;nil,
      "users"=&gt;[
        {"created_at"=&gt;2011-06-23 03:36:28 UTC,
         "id"=&gt;6,
         "name"=&gt;"Jefferey",
         "updated_at"=&gt;2011-06-23 03:36:28 UTC},
        {"created_at"=&gt;2011-06-23 02:59:36 UTC,
         "id"=&gt;2,
         "name"=&gt;"George",
         "updated_at"=&gt;2011-06-23 03:05:13 UTC}
       ]
     },
     {"created_at"=&gt;2011-06-23 02:56:37 UTC,
      "id"=&gt;1,
      "name"=&gt;"Site Admin",
      "updated_at"=&gt;2011-06-23 02:56:37 UTC,
      "user_id"=&gt;nil,
      "users"=&gt;[
        {"created_at"=&gt;2011-06-23 03:36:28 UTC,
         "id"=&gt;6,
         "name"=&gt;"Jefferey",
         "updated_at"=&gt;2011-06-23 03:36:28 UTC},
        {"created_at"=&gt;2011-06-23 02:59:36 UTC,
         "id"=&gt;2,
         "name"=&gt;"George",
         "updated_at"=&gt;2011-06-23 03:05:13 UTC},
        {"created_at"=&gt;2011-06-23 02:58:15 UTC,
         "id"=&gt;1,
         "name"=&gt;"John Metta",
         "updated_at"=&gt;2011-06-23 02:58:15 UTC}
       ]
     }
   ]
  },
  "id"=&gt;"1"}

Cats talking to dogs, here. They’d both be happy eating a squirrel, sure, but they just speak different languages.

Basically, the backend tries to find the “groups” and “services” properties on the User model. ActiveRecord expects groups to be, well Group objects- but it’s a Hash, not a Group. In order for this to work, those should be named “groups_attributes” and “services_attributes.”

This is basically telling me that ActiveRecord and ActiveResource basically don’t know how to talk to one another. What the hell was the Rails core team thinking?

ActiveResource should be patched to format it’s JSON string to use MODEL_attributes, right? Well, that would break a lot of stuff. ActiveResource is supposed to talk fine to my Scala backend server too, which it wouldn’t.

ActiveRecord should automatically treat MODEL: {ASSOCIATED_MODEL: …}} as something that can be converted to an ActiveRecord, right? I’m not sure of this one- it seems too possible to break things down the line that we didn’t expect.

So, you can do it from either direction without possibly horribly breaking something down the line.

Ahh, apparently the Rails core team was thinking “You know, we shouldn’t do this because we might horribly break something down the line.”

Wrestling ActiveResource to the ground

In order to fix this, I’ve added a simple hack to my UsersController on the backend. I say it’s simple because it’s pretty dumb. I say it’s a hack because I think it’s really ugly and inelegant– something I don’t expect from Rails.

Here’s the full UsersController#Update method on my backend server:

  def update
    @user = User.find(params[:id])

    respond_to do |format|
      begin
        groups = []
        params[:user][:groups].each do |g|
          groups &lt;&lt; Group.find(g[:id])
        end
        params[:user][:groups] = groups
      rescue
      end

      begin
        services = []
        params[:user][:services].each do |s|
          services &lt; @user.errors, :status =&gt; :unprocessable_entity }
        format.json  { render :json =&gt; @user.errors, :status =&gt; :unprocessable_entity }
      end
    end
  end

As you can see, we grab that rogue IndifferentHash groups list and, basically, walk through it and find the local groups that correspond to the Group that’s in the list. This works- it allows ActiveResource to barely communicate with ActiveRecord. It’s no longer cats and dogs talking, it’s more like dogs and wolfs.

Yes, I know, wolves bite.

One Model, One Resource

This hack works, but it’s just that- A hack. If something seems this inelegant to me, then I have to question whether it is just that inelegant.

Today a friend mentioned that he’s never used accepts_nested_attributes_for. Never. He has never used nested resources like this because his strong philosophy is “One model, one resource.” I’m going to explore this for a few reasons, not the least of which is that I don’t think I want to pass the groups around inside the user if the user is a member of 3000 groups that we don’t care about. That’s just stupid.

Trying this method would mean that the User model on the frontend would require a groups method that would search the join table based on the user, and an additional add_group method which would go through the Group model to create a new group and somehow add that group to the users list of groups.

There’s a hole in my bucket, dear Liza, dear Liza, a hole in my bucket, dear Liza, a hole…

Really not sure how that would work– at least without some inclusion of my hack above. It seems like there’s just no way to get around this problem elegantly. I hope I’m wrong about that.

Someone please tell me I’m wrong.

Written by john

June 24th, 2011 at 6:13 pm

Posted in Ruby

Ruby, Chef, and Tilde- Home directory shortcut #fail

3 comments

This should have been obvious to me, but wasn’t, so I want to make sure that it’s in the Googleverse in case someone steps into the same pit of idiocy that I did.

The problem is that Ruby and Ruby-based tools like Chef are becoming so natural that they feel like they’re just part of the BASH shell. It took me a while to figure out the following error:

% knife cookbook create somename

/Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/knife/cookbook_create.rb:90:in `initialize': No such file or directory -
 ~/Dev/Chef/chef-repo/cookbooks/sti-rewrites/recipes/default.rb (Errno::ENOENT)
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/knife/cookbook_create.rb:90:in `open'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/knife/cookbook_create.rb:90:in `create_cookbook'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/knife/cookbook_create.rb:74:in `run'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/knife.rb:127:in `run'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/lib/chef/application/knife.rb:121:in `run'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/gems/chef-0.9.12/bin/knife:25
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/bin/knife:19:in `load'
	from /Users/john/.rvm/gems/ruby-1.8.7-p302/bin/knife:19

Because, no matter how much I confirmed that the directory existed, it still failed. Why?

Because Ruby, as anyone really thinking about it would naturally realize, doesn’t expand Bash shortcuts.

To wit: The following entry in my ~/.chef/knife.rb file:

...
cookbook_path [ '~/Dev/Chef/chef-repo/cookbooks', '~/Dev/Chef/chef-repo/cookbooks' ]
...

Which I generated by running a command at the bash shell:

knife configure

As anyone would expect, changing that line to:

...
cookbook_path [ '/Users/john/Dev/Chef/chef-repo/cookbooks', '/Users/john/Dev/Chef/chef-repo/cookbooks' ]
...

fixed the problem. D’oh!

As I use more Ruby-based server configuration tools, I think I need to be a bit more conscious of when I’m with the purview of the Bash shell, and when I’m not.

Written by john

January 24th, 2011 at 1:17 pm

Posted in Ruby

Tagged with , , , ,

Creating a Robotic Secretary with Hazel, Automator, & Ruby

one comment

If you’re on a Mac, and have to do a task more than once, then you need to use Hazel and Automator, full stop.

I’m a consultant, and that means I work on multiple projects, and have to bill those projects. Recently, I began working on a contract that required weekly billing, using an Excel spreadsheet that can’t be modified, and a signature from a project manager in another city. Talk about tedious! My workflow for just filling out a weekly timesheet is this:

  1. Use Freshbooks daily to track time
  2. On Friday, open Freshbooks and pull the hours on that project
  3. Open Excel (in VMWare) to update the read-only Master Timesheet file by changing the date, and adding my hours into the day slots
    1. I can’t use Open Office or anything on the Mac since the formatting changes when I save it as an Excel file- I’ve tried a bunch of ways to do this, but as I usually have VMWare open to work in Visual Studio for this project anyway, I just use MS Excel on Windows
  4. Save the timesheet under another name
  5. Drag the timesheet into another folder in my Dropbox (so that I have a record)
  6. Open my email client and create an email to my project manager that says little more than “Timesheet, Cheers, -J”
  7. Drag the timesheet into the email (I Google Mail’s ability to drag a file to attach!) and hit send
  8. When he sends it back, it’s an attachment as an image- save the image to my project folder locally
  9. Print out the image
  10. Sign it (in blue pen, mind you)
  11. Scan the signed copy
  12. Move the scanned copy file to my local computer
  13. Open a web browser to connect to their email system, which I’m supposed to use, but which is proxied, so I can’t use my normal email client.
  14. Create another email to the timesheet recipient
  15. Attach the file using the dialog boxes (Drag is not possible) and send it
  16. Grab the timesheet original off my scanner and put in an envelope to mail to New York.

Now, if you think about doing all of that, it’s not like it takes half the day. Still, it takes enough time that a good bit of my morning is lost to it. There’s not only the time spent “actively doing everything” but the lost time where I could have been concentrating on something like our object model or database abstraction layer, but which I’m distracted by simultaneously dealing with the timesheet issue. Anyone working in a “mental sphere” such as programming knows that a few seconds of distraction with something will pull you out of the mindset for 20 minutes or more.

Plus, there’s the added issue of this just being boring. It’s something a robot should be doing- or at least a secretary. More importantly, it’s something that could happen automatically! We programmers tend to get annoyed when things “should happen automatically” and they don’t. It is, in fact, why many of us became programmers.

Enter Hazel

For a while, I’ve been simplifying things a wee bit using Hazel. For those who don’t know, Hazel is a Mac program that watches a folder and performs actions based on the folder contents. I first heard about it on the Mac Power Users podcast and bought the software pretty much immediately following the podcast.

Hazel rules screen

Hazel is one amazing piece of time-saving software. It has a super simple graphical dialog box to let you set things like which folder, what attributes (“if files are older than” and “if file is a:” (e.g. movie, soundfile, pdf, etc) and “if the name contains:”) and what actions (Open, move, delete, archive, change name, import into iTunes, etc.) to perform on files in that folder.

One of the ways that I use it is on my desktop. If there are any pictures or movies on my desktop that are older than 1 day, Hazel takes them off my desktop and puts them on my remote hard drive, in appropriate folders, and simultaneously imports them into iTunes (I don’t have iTunes copy files to the local drive, so they’re watched from the remote drive).

When dealing with my timesheet, I have Hazel set up so that anything that goes into the “Timesheets” folder and is an Excel file gets automatically moved to a subfolder called “archive” and gets renamed with the pattern “John_Metta_<date added>.xls.” That way, I have an easily recognizable location and file to drag into an email to my project manager.

Using Hazel to automatically move, delete and organize your files can save you a lot of time. It’s tiny little chunks of time, sure, but it’s time. In reality, it’s saving you more the headache of having to do the same thing over and over again. Hazel is powerful, check it out.

Add A Little Ruby

Because I love Ruby, I decided to automate the creation of the Excel spreadsheet a bit. The Windows version of Ruby has the win32ole module, which can open and handle spreadsheets really easily. So I created a small script that opened a read-only version of the master timesheet spreadsheet and changed the date and the hours/day based on the arguments.

Here’s the whole script:

require 'win32ole'

xl = WIN32OLE.new('Excel.Application')
xls = xl.Workbooks.open("c:\timesheets\Timesheet")
sheet = xls.Worksheets(1)

sheet.Cells(4,11).Value = ARGV[0]

#Time cell index
t = 6
ARGV.each_with_index do |a, i|
  sheet.Cells(10,t+i).Value = a
end

xls.SaveAs("Z:\john\Dropbox\Consulting Stuff\Nova\timesheets\timesheet")
xls.Close
xl.Quit

With this, the command:

 C:timesheets$ ruby timesheet.rb "10/16" 0 0 8 7 6 7 8

will create the correct timesheet for the week beginning 10/16, with the correct dates all lined up for the hours, and will save that timesheet to my Dropbox/timesheets directory- afterwhich Hazel will do the rest.1

More with Automator

Automator: More "secretary" than I'd thought!

Earlier this week, I was still frustrated at the workflow of timesheets, however, because I still had a bunch of steps. Hazel was good at moving files and renaming them, but I still had to email them, print them, etc. So I decided to get my programming game on… then I decided that I didn’t even need to. Like all Macs, my Mac has Automator- a program that will create “workflows” and “applications” which basically amount to my own personal robotic secretary. Hazel can run both applications and Automator workflows on files, so I decided to give it a try.

Mailing to my PM

The first thing I do when I have my timesheet is move it to the timesheets folder where it gets renamed and archived so that I can send it to my PM. It would only be a single step to mail the renamed file. So I opened my Automator script and used the “create mail message,” “add attachment to open messages,” and “send mail” actions to create a script. Now, whenever a folder goes into Timesheets, it gets moved, renamed, and automatically mailed to my PM in one step!2

Printing and Signing

When my PM signs and mails it back it’s an image. So, I created another automator script to take any images with the name pattern “John_Metta_*” and the extension JPG in my “Downloads” directory and automatically move them to the “timesheets/images” directory and send them to the printer. That means all I have to do is open the email and click “download” and I’ll soon have a printed timesheet awaiting my signature. Sounds pretty secretary-like to me.

Mailing the final

My Canon all-in-one will scan automatically save files to a specific directory, so I could just set up Hazel to monitor that directory. The problem with this is that ANYTHING I scan will get processed. Rather than that, I’ve decided to have Hazel monitor that directory for a specific file name. Thus, when I scan any document, I see a dialog box on my screen prompting me to add a name. I can add a name in the rare occasions I want to scan something else. If I just hit <enter>, I’ve set Hazel up with another Automator script: Take the file, rename it with the pattern “John_Metta_Timesheet_<date addedd>”, mail it to the company as an attachment, and then throw the local copy in the trash (because Google Apps mail will store it for me).

A Robotic Secretary

The function of a secretary is to take care of everything that doesn’t absolutely need your action. A secretary should take care of everything in a “Do everything and let me just sign the paper.” That’s almost what I have now.

With this workflow, I’ve gone from 16 tedious steps to

  1. Track time daily in Freshbooks
  2. Friday morning, while working, run the ruby command with my hours3
  3. Work until lunch/break, before stopping, check my email (have an email from my PM waiting)
  4. Click “download”4
  5. Sign, scan, then hit enter

That’s it!5

Coda

I highly suggest looking into combining Hazel, Automator, and any other talents you may bring to creating robotic secretaries for your automated tasks. This is just an example of a way you can take a morning’s worth of frustration and productivity vacuum and turn it into a few steps. Best of all, it took me about as long as two of these frustrating mornings to set everything up– meaning that it pays itself off in about half a month, on a year-long contract.6

  1. There’s actually an API library for Freshbooks in Ruby that is written and maintained by Ben Curren at Outright.com, so you could pull the hours straight from the project in a Ruby script- I decided against this step []
  2. In full disclosure, the Automator script actually uses “Save to a variable” “create new mail message” “get from variable” “add attachments” and “send mail”– in that order. For some reason, the “add attachments” phase was throwing an error otherwise. []
  3. This automatically does everything through emailing my timesheet to my PM []
  4. This automatically files and prints the timesheet, it’s just awaiting my signature []
  5. Well, I still have to mail it to New York, but that final step is a lot less annoying when it’s not preceded by so much tedium! []
  6. I’m sure this’ll only get easier as more companies have APIs available. As I mentioned, I could probably have an automated task in Windows start the Ruby  script and use output from a Freshbooks API query to automate step 2 above, meaning that I’d do nothing until I got an email from my PM. However, I wanted the additional steps of adding my hours manually- more as a double check than anything. []

Written by john

October 18th, 2010 at 4:08 pm

Posted in Miscellany,Ruby

Tagged with , , ,