Using onKeyDown to check for form submission is bad, mmk?

•March 4, 2009 • Leave a Comment

Virgin America ScreencapI’m a big fan of AJAX submission of forms. Recently I’ve gotten lazy and omitted the FORM tag for single (and dual) input interfaces, instead checking for enter-keypress form submission using the onKeyDown event on my form elements.

This lazyness is now biting me in the butt when using Virgin America, Washington Mutual, and many other commercial sites. When the user is entering text into a text input element, many browsers present a pull-down menu of previous inputs for similar fields.

Using latest FireFox on OS X, the enter event on the browser’s drop down menu is propagated to the HTML input element. Although I pressed enter in the context of a browser interface component, the event is propagated to the page element, where it triggers form submission.

Lesson learned: Always use onsubmit on your surrounding form element. Don’t have a form element? You might be unpleasantly surprised that IE6 will create one for you, using all input elements on the page as form elements and the page’s URL as the POST destination. Remove ambiguity, remove fail.

New Challenge: ID3v2.4 autotagging w/ cover art in Python

•March 4, 2009 • Leave a Comment

I’ve been ripping my CDs into directories for years, mostly keeping an Artist / Album structure. This has worked for almost a decade, as I’d invariably use a folder-based browser to choose music to listen to. I’ve recently migrated to using Apple products, including iTunes, and am missing out on a good portion of my archived music collection. I could go through the albums by hand, but would prefer a scalable solution.

Existing tools:

Mutagen: http://code.google.com/p/quodlibet/wiki/Mutagen
Reads and writes ID3v2 and parses ID3v2.4 tags. Well-documented, clean code. I have not seen any code samples for writing the frame that holds the image, but this seems to be the best library out. If it doesn’t support it out of the box, it’d provide a great starting point for the feature.

FreeDB: http://www.freedb.org/
FreeDB is the public-domain offshoot of CDDB, a database of CD-media information (metadata). People add CDs into the system by scanning the CD and entering the Artist/Album/Tracks/etc by hand. CDDB then matches the starting/ending times of the individual tracks to the information provided by the user, and the next person who scans their CD gets that information for free. Although CDDB was bought by Gracenote, the user-contributed information continues to be available through FreeDB.

Pymad and MAD: http://spacepants.org/src/pymad/ and http://www.underbit.com/products/mad/
Mpeg Audio Decoder and Python interface. Although I’m not terribly interested in playing MP3 files at the moment, this lets me measure the start and end times of each track. Since I ripped the tracks in full, I retrieve the album metadata using FreeDB.

My own directory parsing code: http://github.com/philippp/albumparser/tree/master
Parses a directory tree for groups of audio files and tries to infer Artist – Album information from the directory names.

Strategy:

Use my directory parser and mutagen to determine which albums need tagging
Use pymad and freedb to retrieve neccessary metadata
Use Amazon to retrieve cover art where available
Use mutagen to write new metadata

Day 7: User-side walkthrough

•June 22, 2007 • Leave a Comment

The home view of the application presents two lists: People that are employed by Basecamp-account-owner company are shown on the left; Projects that exist in basecamp are listed on the right. Both lists have a special column that presents the link “View Time” for every row.

step-1_cropped.png
The “View Time” link triggers a view showing detailed time information for the selected row. If the user clicks on “View Time” for a Person, the total hours logged on each date, against each project is presented as a line graph. Each line (data series) represents one Project on which the person worked, and dates increase along the x-axis. The sum of hours logged is shown along the Y-axis.

A list of all timeentries for this Person AND this Project is shown below the graph. The Person and Project columns are hidden automatically by ActiveScaffold (they are filter criteria and therefore all the same).

step2_cropped.png

Lastly, clicking on “View Time” for a Project shows the total number of hours logged by each Person that worked on the Project, per date. As in the previous graph, the date increases along then x-axis and the y-axis shows the total number of hours worked. In this view, the lines (data series) correspond to individuals who worked on the Project.

step3_cropped.png

That’s it! There are a few things that need to be done before this project is “presentable,” but IMHO these are eventualities and do not invalidate my proof of concept:

  • Functional testing
    • Improve abstraction of controller functions
    • Subclassing the graphing controller from Person and Project
    • Automated Functional test scripts
  • Graph Readability
    • Resize, rotate, or filter the dates so they are readable
    • Resize the series legend so it fits on screen
  • Basecamp object support
    • Expand beyond current set
      • Milestone
      • Todo-list, Todo-item
    • Dynamic background (not at-once) caching
    • Logging/Study of bandwidth usage

…and many more. It’s not about that though –

I learned Ruby and Rails this week and am now ready to start a “real” project with it. Luckily I’ll be traveling for the next 3 weeks without a laptop and will have plenty of time to think of new challenges.

Day 7: The fix is in

•June 21, 2007 • Leave a Comment

Got back from Sumeet’s at 11:15 PM, determined to resolve the last issue barring me from displaying proper basecamp data. Discovered that “breakpointer” is un-officially broken in Ruby 1.8.5 — The official answer is that Ruby 1.8.5 fixes a bug that allowed breakpointer to work.

Using script/console I was able to produce the following, which could explain my problem:

>> date_hash = {}
=> {}
>> (0..30).step(1) do |day_offset|
?> date_hash[(Date.today-day_offset).to_s] = 0
>> end
=> 0..30
>> date_hash
=> {"2007-05-22"=>0, "2007-06-01"=>0, "2007-06-12"=>0, "2007-05-23"=>0, "2007-06-02"=>0, "2007-06-13"=>0, "2007-05-24"=>0, "2007-06-03"=>0, "2007-06-14"=>0, "2007-05-25"=>0, "2007-06-04"=>0, "2007-06-15"=>0, "2007-05-26"=>0, "2007-06-05"=>0, "2007-06-16"=>0, "2007-05-27"=>0, "2007-06-06"=>0, "2007-06-17"=>0, "2007-05-28"=>0, "2007-06-07"=>0, "2007-06-18"=>0, "2007-05-29"=>0, "2007-05-30"=>0, "2007-06-08"=>0, "2007-06-19"=>0, "2007-06-20"=>0, "2007-05-31"=>0, "2007-06-09"=>0, "2007-06-10"=>0, "2007-06-21"=>0, "2007-06-11"=>0}
>>

The date strings are not stored in the order that they are set (or sorted), but that is fixed easily enough with the Hash sort method.

This is the total time logged by a gentleman who works 8 hours a day and took a few days off — the different color lines are different projects he logged time to.

correct.png

The next graph shows the time logged to a single project by everyone who logged time against it:

project_correct.png

Day 7: One last issue

•June 21, 2007 • Leave a Comment

I’ve written the controller logic to generate the graph. The controller filters the model data and configures the graph and tabulated time data for the view. One issue remains before I can call this a success: 0 values are generated for certain dates in the graph, although time data does not exhibit this pattern.

extra0.png

I’ll figure this out tonight with breakpoint (about time I learned that tool, anyway ;] ); right now I need to whip up some Dolmades for a dinner party and refresh my mind with good company. =)

Day 6: ActiveScaffold Snag: config.actions.exclude :edit vs. :update

•June 21, 2007 • Leave a Comment

This is kind of annoying: To hide the “delete” button, exclude :delete; to hide the “search” button, disable :search; to hide the “show” button, exclude :show; to hide the “edit” button, disable :update. Come on guys!

Day 6: Custom action_links in ActiveScaffold

•June 20, 2007 • Leave a Comment

The Basecamp visualization application will provide time views for each Person and Project. The user selects the Person or Project from a table (rendered by ActiveScaffold) and indicates that statistics are to be shown by clicking a link in the cell.

To link this view change to the ActiveScaffold row, we will use the action_link property of our configuration. The API is regrettably sparse, but a little bit of digging brought up this ActiveScaffold wiki entry and an example at the end of the newbie guide.

To keep things simple, I am currently presenting the data as a seperate barebones view. My Person controller code is:

app/controllers/people_controller.rb

class PeopleController < ApplicationControllerlayout "activescaffold"
  active_scaffold(:person) do |config|
    config.label = "People"
    config.columns = [:first_name, :last_name, :title, :im_handle]
    config.columns[:im_handle].label = "IM Handle"
    config.actions.exclude :create, :delete, :edit, :show, :search
    config.action_links.add 'time_per_person', :label => "Time", :type => :record, :page => true
    list.columns.exclude :timeentries
    list.sorting = {:last_name => 'ASC'}
  end

  def time_per_person
    @buns = "foo"
  end

end

config.action_links.add breaks down like this:
Parameter 0: String of the controller method to invoke (Ex: ‘time_per_person’)
:label : Displayed label for the link (Ex: “Time”)
:type : Is this link shown once per :record (row) or once per :table?
:page : Load the result as a new view?
time_per_person currently sets a test variable that is shown in the view:

app/views/people/time_per_person.rhtml

<%= @buns %>

This variable will hold my generated image’s filename and will be presented as an embedded image.

Day 5: Viewlayer, first view

•June 20, 2007 • 1 Comment

The first composite view (seen below) incorporates three controllers to generate three embedded ActiveScaffold tables.
view_layer.png

The final step is plotting the hour data for each entity and presenting this to the user. This will require a new controller to drive the graphing engine and probably some AJAX mechanism to update the image on the page. In the spirit of keeping things simple, and to leave time for documentation (and packing for Uganda/Rwanda) I MAY just push the png to the user as a pop-up for now.

Day 5: Fixtures and Testing

•June 19, 2007 • Leave a Comment

I’ve been developing this system using a live Basecamp connection, but testing and debugging is made a lot simpler when one works with a static data set. All rails model unit tests include a fixture reference — this defines the static data set against which the model’s unit tests are written.

I wrote up some YAML to define sample people, companies and projects by hand. Timeentries are too numerous to write out manually, so I opted to generate them. Due to my time constraint, I am randomly distributing time entries to projects, people and dates. Any model or controller logic that interprets this data must reflect this random distribution in order for my test to pass.

This test is not reliable: there is a finite chance that the test will fail — even with a large number of time entries. This ham-handed approach is suitable only for first-inspection testing, and should be replaced by a documented model if this interface ever finds serious use.

For the code-curious, here is my timeentry YAML fixture definition:

<% for i in 1..1000 %>

<% entry_date = DateTime.now()-rand(30)
y = entry_date.year.to_s
m = entry_date.month.to_s
d = entry_date.day.to_s
if (m.length == 1) m = "0"+m
if (d.length == 1) d = "0"+d
date_str = y+m+d %>fix_<%= i %>:
  id: <%= i %>
  person_id: <%= rand(3)+1 %>
  project_id: <%= rand(5)+1 %>
  hours: <%= rand(0)*8 %>
  date: <%= date_str %>
  api_login: philippp
<% end %>

3 and 5 should be defined as external constants and used to generate people and projects: 3 is the highest person_id of employees (in my dataset, employees come first — only employees have time entries) and there are 5 mock projects.

Day 5: Controller and View

•June 19, 2007 • Leave a Comment

The model layer completes the (imho) most challenging part of this project — creating a cached, object-oriented ruby model containing relevant basecamp entities. I expect the rest of this project to proceed in strides, but just burned an hour on a “gotcha.”

I’m leveraging ActiveScaffold to display the tabulated project / person / timeentry / company data. ActiveScaffold is the bee’s kees: it provides asynchronous record editing, nested forms, and you can trick it out like a 64 Impala.

Frustratingly, it just didn’t work for me — I kept getting the error message:

ArgumentError (wrong number of arguments (0 for 1)):
    .//vendor/plugins/active_scaffold/lib/active_scaffold.rb:54:in `initialize'
    .//vendor/plugins/active_scaffold/lib/active_scaffold.rb:54:in `new'
    .//vendor/plugins/active_scaffold/lib/active_scaffold.rb:54:in `active_scaffold'

Looking at like 54 of active_scaffold.rb reveals:

      # defines the attribute read methods on the model, so record.send()
      #doesn't find protected/private methods instead
      # NOTE define_read_methods is an *instance* method even though
      #it adds methods to the *class*.
      klass = self.active_scaffold_config.model
      klass.new.send(:define_read_methods) if klass.read_methods.empty? && klass.generate_read_methods

So they pull the Class of my model, create an empty instance and then call “send” with the direction to “define_read_methods” on my blank object, creating accessors in every class instance. This is a little bit weird, but perfectly legal.

My error originated from my Model class. As posted yesterday, the constructor accepts a Recordwrapper instance and expects at least one input. Changing the constructor from def initialize( project_info ) to def initialize( project_info = nil ) fixed this right up, and ActiveScaffold now works like a charm.