Monday, September 21, 2009

Enhancing Forms

I want the forms to look nice... eventually...

http://www.jankoatwarpspeed.com/post/2008/07/27/Enhance-your-input-fields-with-simple-CSS-tricks.aspx

Wednesday, August 19, 2009

third-order associations using nested :include

The bottom line:

ward_members = Contact.find_all_by_ward(current_user.ward, :include => [{:calling => :calling_type}, :address_group, :photo ])

A little background for non-members:

I'm attending BYU, which is operated by The Church of Jesus Christ of Latter-day Saints.
In Provo, the Mormon Meca, each apartment complex belongs to one or more wards.
A ward (which is an old English word for neighborhood) is a unit of the church of about 300 members and may include members from various neighborhoods and apartment complexes (or a few entire cities in areas where the church is less widespread).
A member may have one or more callings (volunteer responsibility)
There are various types of callings (leadership, teachers, etc)

The Ward Menu (formally called Directory, but referred to as menu as it is often used for dating purposes) that I'm creating will have a few different sections.

Title - a picture and the name of the ward
Bishopric - the 3 primary leaders of the ward
Leadership - various members in positions of leadership, not including the bishopric. Sorted by calling type
Members - photo directory of all members including leadership, but excluding bishopric. Sorted by apartment building or neighborhood, door number, first name.
Index - list of all members as stated above, sorted by first name

I need to generate a query that gets all members, their calling, their calling's type, and their address' group.

@bishopric = Contact.find(:all, :conditions => ["callings.name LIKE ? AND ward_id = ?", 'Bishop%', current_user.contact.ward], :include => [:callings, :photo], :order => 'callings.name')

@leadership = Contact.find(:all, :conditions => ["callings.name IS NOT NULL AND callings.name NOT LIKE ? AND ward_id = ?", 'Bishop%', current_user.contact.ward], :include => [:callings])

@members = Contact.find(:all, :conditions => {:ward_id => current_user.contact.ward}, :include => [:photo], :order => ['address_line_1, address_line_2, first']) - @bishopric

One problem I ran into is that :include does a left join rather than a inner join, but since I'm not up to snaz on my :joins-fu and I don't need the calling info for the photo part of the directory, I decided to just subtract the bishopric from the array. Otherwise @members would only consist of members with callings.

Friday, August 7, 2009

Securing the Data

For ease-of-use I wanted to avoid using a self-signed certificate and so I was thinking to use the RSA javascript library (which I may still do, just for fun) to transmit the password.

However, after some chat with some people on the UUG mailing list I see that it would be much more gentlemanly to encrypt all of the data.

As it turns out, GoDaddy provides free SSL certs for OpenSource projects.

Thursday, August 6, 2009

Shaving response times and adding progress with spawn

One of the reasons that lds.org is such a hassle is that their server responds so slowly.

I want this application to be user-friendly. The problem is, how do I make an abstracted layer faster than the original?

I don't.

But I do fake it as best I can and I'll make up for it later with HIJAX. The idea is that I'll use spawn to fork blocks of code into the background (like the generator I created in the last post) so that the rendered page returns sooner and I can do a little ajax polling in the background on the client-side to update the client page as needed. In the meantime, I'll allow the user to take care of things which aren't on the 'critical path', like grouping and formatting options.

Installing spawn in rails
sudo apt-get git-core
cd ~/Code/TheWardMenu
./script/plugin install git://github.com/tra/spawn.git
#app/controller/directory.rb
...
spawn do
while record = @photo_directory_generator.next
member = Member.new(record)
member.ward = @ward
member.save end
end

Using links2 as a quick test for transfer speeds from lds.org here's a way that things could go

User initiates request
  • 4s - TheWardMenu.com responds
  • 5s - User enters credentials
  • 1s - twm receives request
  • 3s - Lds.org has responded
  • 1s - Update Profile page parsed
  • 0s - spawned the block of code that gets the directory
  • 1s - response sent
The user has spent the last 6 seconds choosing pdf options rather than dying of boredom
  • 4s - the text-only directory is now in the database
  • 0s - the photo downloading process is spawned
The user can now review the text version of the directory for mistakes and see some real-time sorting as well as see the % complete on the picture downloads via a polled feed which gets only pictures that are more recent than the last polling.
  • 2m - the photos are downloading
The user finishes adjusting the settings and hits 'Download PDF' and is given the approximate wait time needed to download the remaining photos (which are already being downloaded into a hidden div so that the browser caches them).

The user leaves the site happy or makes adjustments to a fully viewable picture directory and downloads the pdf again (and it only takes a few seconds this time around)

Generator (like an Iterator) in Ruby on Rails

I'm creating a rails-agnostic class in ruby for getting the member photo directory.

Downloading the entire photo directory is slow. It takes a few minutes. Users don't want to wait 3 minutes to find out what is happening (success or failure, etc), but I don't want to put my model code into a library which could otherwise easily be used for non-rails apps.

The main issue here is that once the class instance is gone, so is the authentication. So it doesn't work to just store the URLs to the photos. Each photo must be downloaded. However, if the URLs to the photos are temporarily stored in the class and then downloaded one-by-one in the class, the wait for the text part of the directory to the download is acceptable (for me).

Take a look
http://secure.lds.org/units/login
/Membership Directory/.click
/with photos/i.click

The solution? A generator.
Here's a brief work snippit that shows how my solution works generically:



class Contacts
def import_directory
@records = []
#... get names and photo urls
@records << [:name => 'abc']
@records << [:name => 'jkl']
@records << [:name => 'xyz']
return true
end

def next_contact
if not @gen
create_generator
end
return @gen.next
end

private
def create_generator
require 'generator'
@gen = Generator.new do |g|
for record in @records
g.yield record
end
g.yield nil
end
end
end

contacts = Contacts.new
if not contacts.import_directory
abort 'Invalid Import'
end

while record = contacts.next_contact
puts record
end


I know that generators can be done in python and after a little searching I found this, which inspired me to play around and create an example class solution.
http://anthonylewis.com/2007/11/09/ruby-generators/

Thursday, July 30, 2009

Bits and Pieces

When I run the same section of code from the alpha site it only gets some of the members.
I'll have to see if the original script still works (a minor change to page formatting on lds.org can break it).

Tuesday, July 28, 2009

Moving Forward

I'm still learning Ruby on Rails. I'm happy with the progress that I'm making.
My current goal is that the site will be functional by the start of fall semester.
Being that LDS.org is down for maintenance (or just plain down), I'll resume work in the morning.

Monday, July 20, 2009

Beginnings

TheWardMenu.com is a resource for all of the student and single LDS wards (especially at BYU) that create ward directories.

My goal is to port the ruby script I used to create the my ward's current directory to a ruby-on-rails site.
  • LDS.org will provide the data backend
  • This site will provide a frontend (initially just to grab data, not to edit it)
  • The deliverable will be ward directory as a PDF
Currently Available Resources

Wednesday, July 1, 2009

Domain & Server Setup


Documentation

I plan to document the process of creating this website and all of its features as a tutorial.

Buying the domain

  • google'd "google domain registration"
  • visited http://www.google.com/a/cpanel/domain/new
  • bought thewardmenu.com (planning to buy the .org as well)

Readying the Test Box

  • grabbed an older running commodity computer
  • installed ubuntu
  • apt-get install --yes apache2 apache2-mpm-worker libapache2-mod-passenger ruby-full rubygems rails vim-rails
  • OR to run rails alongside php: http://wiki.brightbox.co.uk/docs:phusion-passenger

Hello World on RAILS

Here's some really quick code just to see that rails is installed and up and running
  • mkdir -p ~/Code/
  • cd ~/Code
  • rails TheWardMenu
  • ruby scripts/generate controller hello
  • cat - > app/views/hello/index.html.erb <
    Hello World!
    EOF
  • ruby script/server
  • http://localhost:3000
  • http://localhost:3000/hello

Creating the Data Structure

Our local database will store user and ward names. It will not store passwords or any other personal information for the logged-in user.
However, it will cache the ward database for a few days. For this reason we will also log a timestamp for the user and the directory cache.
  • script/generate model Stake name:string
  • script/generate model Ward name:string stake:references
  • script/generate model User ldsaccount:string last_login:timestamp ward:references
  • script/generate model Member first:string last:string photo:binary 
  • edit Member Migration blurb:text ward:references

Logging Into LDS.org

LoginController:
  private
  def test_lds_account(username, password)
    require 'mechanize'
    #puts "Logging in..."
    # Start up our "web browser"
    agent = WWW::Mechanize.new
    agent.user_agent_alias = 'Mac Safari'


    # Start at the main LDS.org page
    begin
      page = agent.get('http://lds.org/')
    rescue Exception
      return false
      #abort "Can't connect to LDS.org. Try again later."
    end


    page = page.links.find {|l| l.text =~ /ward.*site/i}.click          # Go to the login page
    form = page.forms.find {|f| f.name =~ /log/i}                       # Find the login form
    form.fields.find {|f| f.name =~ /user/i}.value = username           # Enter the username
    form.fields.find {|f| f.name =~ /pass/i}.value = password           # Enter the password
    page = form.submit                                                  # Try to login


    # Check for a failed login
    if /login/i.match(page.title)
      #abort "Unable to login! Incorrect username and/or password?"
      return false
    end


    #puts "Navigating to the membership directory..."
    page = page.links.find {|l| l.text =~ /member.*directory/i}.click   # Go to the membership directory page
    link = page.links.find {|l| l.text =~ /photo/i}                     # Find the link to the version of the directory with the photos




    # The actual URL for the photo directory is embedded in javascript
    # in the HREF field. So we have to dig it out ourselves.
    # TODO Can we pull the base of the URI from the agent (or page)
    # instead of hard coding it?
    photo_directory_url = 'https://secure.lds.org' + /(\/.*\.html)/i.match(link.uri.to_s)[1]
    page = agent.get(photo_directory_url)




    #puts "Downloading the pictures (this can take several minutes)..."
    # Yes, this page actually has _two_ HTML title elements!
    ward_name = page.search('/html/head/title[1]').inner_text.strip
    ward_name.slice!(' Membership Directory')


    return ward_name
  end

Overriding the default authentication of Authlogic_Example
User.find_by_login(params[:user_session][:login])
ldsorg = Ldsorg(params[:user_session][:login], params[:user_session][:password])
if ldsorg.ldslogin
  UserSession.new(@user, true)
end

Updating the progress of the directory to the user requires using a generator.

Moving from the WEBrick test environment to a production apache server
http://www.modrails.com/documentation/Users%20guide.html#_deploying_a_ruby_on_rails_application

http://guides.rubyonrails.org/getting_started.html
cat config/database.yml
rake db:create

TODO.... sort out how to run passenger and php on the same system due to prefork / worker conflict