require 'camping' require 'camping/db' Camping.goes :Jot module Jot def self.create Jot::Models.create_schema :assume => (Jot::Models::Project.table_exists? ? 1.0 : 0.0) end end module Jot::Models ONE_MINUTE = 60 ONE_HOUR = ONE_MINUTE * 60 ONE_DAY = ONE_HOUR * 24 ONE_YEAR = ONE_DAY * 365 class Task < Base belongs_to :project def is_stopped? completed_at != nil end def stop! self.completed_at = Time.now save! end def duration if is_stopped? _duration_of( (completed_at - created_at).to_i ) else _duration_of( (Time.now - created_at).to_i ) end end # FIXME: # This is not accurate, due to UNIX time durations not mapping directly # to reality. # See: http://en.wikipedia.org/wiki/Unix_time # A more accurate implementation would require passing in two timestamps # and performing a table lookup. # I do not feel like doing that right now. # In addition, this should probably be in the view, not the model. def _duration_of secs vars = { :years => secs / ONE_YEAR, :days => (secs % ONE_YEAR) / ONE_DAY, :hours => (secs % ONE_YEAR % ONE_DAY) / ONE_HOUR, :minutes => (secs % ONE_YEAR % ONE_DAY % ONE_HOUR) / ONE_MINUTE, :seconds => secs % ONE_YEAR % ONE_DAY % ONE_HOUR % ONE_MINUTE, } buffer = [] for k in [ :years, :days, :hours, :minutes, :seconds ] do if vars[k] > 0 then word = k.to_s if vars[k] == 1 then word = word.sub /s$/, '' end buffer.push("#{vars[k]} #{word}") end end if buffer.length > 1 buffer[0..-2].join(", ") + " and #{buffer[-1]}" else buffer[0] end end end class Project < Base validates_uniqueness_of :name end class CreateTheBasics < V 1.0 def self.up create_table :jot_projects do |t| t.column :name, :string, :null => false, :limit => 100 t.timestamps end create_table :jot_tasks do |t| t.column :project_id, :integer, :null => false t.column :name, :string, :null => false, :limit => 50 t.column :completed_at, :datetime, :null => true t.timestamps end end def self.down drop_table :jot_tasks drop_table :jot_projects end end end module Jot::Controllers class Index < R '/' def get @projects = Project.find :all render :index end end class AddProject < R '/project/add' def get render :add_project end def post project = Project.create :name => input.project_name redirect ViewProject, project.name end end class StartTask < R '/project/start_task/(\w+)' def get project_name @project = Project.find(:all, :conditions => [ 'name = ?', project_name ])[0] render :start_task end def post project_name @project = Project.find(:all, :conditions => [ 'name = ?', project_name ])[0] task = Task.create :project_id => @project.id, :name => input.task_name redirect ViewProject, @project.name end end # Eventually, it would be nice to use jquery or some other ajax # lib to directly send a POST request to this controller. class StopTask < R '/project/stop_task/(\d+)' def get task_id @task = Task.find task_id @project = @task.project render :stop_task_form end def post task_id @task = Task.find task_id @project = @task.project @task.stop! render :stop_task end end class ViewProject < R '/project/view/(\w+)' def get name @project = Project.find(:all, :conditions => [ 'name = ?', name ])[0] if @project @tasks = Task.find :all, :conditions => [ 'project_id = ?', @project.id ] render :view_project end end end class Style < R '/styles.css' def get @headers["Content-Type"] = "text/css; charset=utf-8" @body = %[ body { font-family: Utopia, Georga, serif; } h1.header { background-color: #fef; margin: 0; padding: 10px; } div.content { padding: 10px; } ] end end end module Jot::Views def layout html do head do title 'jot' link :rel => 'stylesheet', :type => 'text/css', :href => '/styles.css', :media => 'screen' end body do h1.header { a 'jot', :href => R(Index) } div.content do self << yield end end end end def index if @projects.empty? p 'No projects found.' else ul do for project in @projects li { a project.name, :href => R(ViewProject, project.name) } end end end p { a 'Add Project', :href => R(AddProject) } end def add_project h2.heading 'Add Project' form :method => 'post', :action => R(AddProject) do div.label { label 'Project Name', :for => 'project_name' } div.field { input :name => 'project_name', :type => 'text', :value => '' } input :type => 'submit', :value => 'Submit' end end def start_task h2.heading @project.name form :method => 'post', :action => R(StartTask, @project.name) do div.label { label 'Task Name' } div.field { input :name => 'task_name', :type => 'text', :value => '' } input :type => 'submit', :value => 'Submit' end end def stop_task h2.heading @project.name p "Stopped working on #{@task.name}" end def stop_task_form h2.heading @project.name p "Do you want to stop working on #{@task.name}?" form :method => 'post', :action => R(StopTask, @task.id) do input :type => 'submit', :value => 'Yes, stop it' end end def view_project h2.heading @project.name ul do for task in @tasks li do # This could use a little ajax magic. a task.name, :href => R(StopTask, task.id) if task.is_stopped? then span.task.stopped " (#{task.duration})" else span.task " (Started #{task.duration} ago)" end end end end p { a 'Add Task', :href => R(StartTask, @project.name) } end end