Skip to content
Browse files

Initial import

git-svn-id: svn://rubyforge.org/var/svn/ozimodo/trunk@1 bbd52f5d-c10c-0410-8bd0-8d55da062a3b
  • Loading branch information...
0 parents commit fa01c4aa08c27b0ecf1e9eb2d36a3aba27f13feb ozmm committed Feb 21, 2006
Showing with 13,233 additions and 0 deletions.
  1. +3 −0 README
  2. +10 −0 Rakefile
  3. +231 −0 app/controllers/admin_controller.rb
  4. +80 −0 app/controllers/application.rb
  5. +16 −0 app/controllers/feed_controller.rb
  6. +97 −0 app/controllers/tumble_controller.rb
  7. +3 −0 app/helpers/application_helper.rb
  8. +17 −0 app/helpers/feed_helper.rb
  9. +2 −0 app/helpers/tag_helper.rb
  10. +128 −0 app/helpers/tumble_helper.rb
  11. +37 −0 app/models/cache_sweeper.rb
  12. +49 −0 app/models/post.rb
  13. +8 −0 app/models/tag.rb
  14. +54 −0 app/models/user.rb
  15. +28 −0 app/views/admin/list.rhtml
  16. +27 −0 app/views/admin/list_tags.rhtml
  17. +28 −0 app/views/admin/login.rhtml
  18. +44 −0 app/views/admin/new.rhtml
  19. +25 −0 app/views/admin/password.rhtml
  20. +36 −0 app/views/admin/show.rhtml
  21. +33 −0 app/views/feed/atom.rxml
  22. +26 −0 app/views/feed/rss.rxml
  23. +53 −0 app/views/layouts/admin.rhtml
  24. +87 −0 components/your_tumblelog/tumble/_post.rhtml
  25. +3 −0 components/your_tumblelog/tumble/error.rhtml
  26. +51 −0 components/your_tumblelog/tumble/layout.rhtml
  27. +1 −0 components/your_tumblelog/tumble/list.rhtml
  28. +1 −0 components/your_tumblelog/tumble/show.rhtml
  29. +1 −0 components/your_tumblelog/tumble/types/_image.rhtml
  30. +1 −0 components/your_tumblelog/tumble/types/_link.rhtml
  31. +1 −0 components/your_tumblelog/tumble/types/_post.rhtml
  32. +1 −0 components/your_tumblelog/tumble/types/_quip.rhtml
  33. +3 −0 components/your_tumblelog/tumble/types/_quote.rhtml
  34. +1 −0 components/your_tumblelog/tumble/types/_ruby_code.rhtml
  35. +25 −0 components/your_tumblelog/tumble_controller.rb
  36. +47 −0 components/your_tumblelog/types_helper.rb
  37. +19 −0 config/boot.rb
  38. +20 −0 config/database.yml
  39. +59 −0 config/environment.rb
  40. +19 −0 config/environments/development.rb
  41. +19 −0 config/environments/production.rb
  42. +19 −0 config/environments/test.rb
  43. +32 −0 config/routes.rb
  44. +15 −0 config/tumble.yml
  45. +46 −0 db/migrate/1_get_your_tumble_on.rb
  46. +33 −0 public/.htaccess
  47. +8 −0 public/404.html
  48. +8 −0 public/500.html
  49. +10 −0 public/dispatch.cgi
  50. +24 −0 public/dispatch.fcgi
  51. +10 −0 public/dispatch.rb
  52. 0 public/favicon.ico
  53. +750 −0 public/javascripts/controls.js
  54. +584 −0 public/javascripts/dragdrop.js
  55. +854 −0 public/javascripts/effects.js
  56. +1,785 −0 public/javascripts/prototype.js
  57. +1 −0 public/robots.txt
  58. +80 −0 public/stylesheets/admin.css
  59. +100 −0 public/stylesheets/tumble.css
  60. +77 −0 public/stylesheets/types.css
  61. +3 −0 script/about
  62. +19 −0 script/benchmarker
  63. +3 −0 script/breakpointer
  64. +3 −0 script/console
  65. +3 −0 script/destroy
  66. +3 −0 script/generate
  67. +3 −0 script/performance/benchmarker
  68. +3 −0 script/performance/profiler
  69. +3 −0 script/plugin
  70. +3 −0 script/process/reaper
  71. +3 −0 script/process/spawner
  72. +3 −0 script/process/spinner
  73. +34 −0 script/profiler
  74. +3 −0 script/runner
  75. +3 −0 script/server
  76. +31 −0 test/fixtures/posts.yml
  77. +27 −0 test/fixtures/posts_tags.yml
  78. +24 −0 test/fixtures/tags.yml
  79. +5 −0 test/fixtures/users.yml
  80. +28 −0 test/test_helper.rb
  81. +48 −0 test/unit/post_test.rb
  82. +12 −0 test/unit/tag_test.rb
  83. +50 −0 test/unit/user_test.rb
  84. +3 −0 vendor/RedCloth/bin/redcloth
  85. +160 −0 vendor/RedCloth/doc/CHANGELOG
  86. +25 −0 vendor/RedCloth/doc/COPYING
  87. +106 −0 vendor/RedCloth/doc/README
  88. +216 −0 vendor/RedCloth/doc/REFERENCE
  89. +359 −0 vendor/RedCloth/doc/make.rb
  90. +1,130 −0 vendor/RedCloth/lib/redcloth.rb
  91. +28 −0 vendor/RedCloth/run-tests.rb
  92. +1,376 −0 vendor/RedCloth/setup.rb
  93. +105 −0 vendor/RedCloth/tests/code.yml
  94. +26 −0 vendor/RedCloth/tests/hard_breaks.yml
  95. +171 −0 vendor/RedCloth/tests/images.yml
  96. +39 −0 vendor/RedCloth/tests/instiki.yml
  97. +155 −0 vendor/RedCloth/tests/links.yml
  98. +77 −0 vendor/RedCloth/tests/lists.yml
  99. +218 −0 vendor/RedCloth/tests/markdown.yml
  100. +64 −0 vendor/RedCloth/tests/poignant.yml
  101. +198 −0 vendor/RedCloth/tests/table.yml
  102. +406 −0 vendor/RedCloth/tests/textism.yml
  103. +18 −0 vendor/syntax/data/ruby.css
  104. +8 −0 vendor/syntax/data/xml.css
  105. +12 −0 vendor/syntax/data/yaml.css
  106. +38 −0 vendor/syntax/lib/syntax.rb
  107. +163 −0 vendor/syntax/lib/syntax/common.rb
  108. +27 −0 vendor/syntax/lib/syntax/convertors/abstract.rb
  109. +51 −0 vendor/syntax/lib/syntax/convertors/html.rb
  110. +317 −0 vendor/syntax/lib/syntax/lang/ruby.rb
  111. +108 −0 vendor/syntax/lib/syntax/lang/xml.rb
  112. +105 −0 vendor/syntax/lib/syntax/lang/yaml.rb
  113. +9 −0 vendor/syntax/lib/syntax/version.rb
  114. +5 −0 vendor/syntax/test/ALL-TESTS.rb
  115. +871 −0 vendor/syntax/test/syntax/tc_ruby.rb
  116. +202 −0 vendor/syntax/test/syntax/tc_xml.rb
  117. +228 −0 vendor/syntax/test/syntax/tc_yaml.rb
  118. +40 −0 vendor/syntax/test/syntax/tokenizer_testcase.rb
  119. +22 −0 vendor/syntax/test/tc_syntax.rb
3 README
@@ -0,0 +1,3 @@
+Coming soon.
+
+http://ozimodo.rubyforge.org
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
231 app/controllers/admin_controller.rb
@@ -0,0 +1,231 @@
+class AdminController < ApplicationController
+ before_filter :authorize, # run the authorize method before we execute
+ :except => :login # any methods (except login) to see if the user
+ # is actually logged in or not
+
+ layout "admin" # admin layout
+ helper :tumble, :types # load up our helpers
+
+ # app/models/cache_sweeper.rb's after_save method is invoked if a Post
+ # or Tag is saved within any of these methods. it then clears the appropriate
+ # caches.
+ cache_sweeper :cache_sweeper, :only => [ :new, :edit, :delete, :kill_cache,
+ :ajax_edit_title, :ajax_edit_content,
+ :ajax_edit_tag_names ]
+
+
+ #
+ # post management
+ #
+
+ # we do this a lot. hrm.
+ def index
+ list
+ render :action => 'list'
+ end
+
+ # new and edit just wrap to the same method
+ def new; save_post; end
+ def edit; save_post; end
+
+ # this method handles creation of new posts and editing of existing posts
+ def save_post
+ if params[:id] and request.get?
+ # want to edit a specific post -- go for it
+ @post = Post.find(params[:id])
+ @tags = @post.tag_names
+ render :action => :new
+
+ elsif !params[:id] and request.get?
+ # want to create a new post -- go for it
+ @post = Post.new
+ @tags = nil
+ render :action => :new
+
+ elsif request.post?
+ # post request means something big is going to happen.
+ # set post variable to the post in question if we're editing, otherwise
+ # open a new object
+ post = params[:id] ? Post.find(params[:id]) : Post.new
+
+ # reset all of post's attributes, replacing them with those submitted
+ # from the form
+ post.attributes = params[:post]
+
+ # if post has no user_id set, give it the id of the logged in user
+ post.user_id ||= session[:user_id]
+
+ # reset all the tag names attached to those submitted
+ post.tag_names = params[:tags]
+
+ # save the post - if it fails, send the user back from whence she came
+ if post.save
+ flash[:notice] = 'Post was successfully saved.'
+ redirect_to :action => 'show', :id => post.id
+ else
+ flash[:notice] = "There was an error saving your post."
+ render :action => self.action_name
+ end
+ else
+ # i don't know how you'd ever get here but i don't know a lot of things
+ redirect_to :action => :list
+ end
+ end
+
+ # be polite and show a post
+ def show
+ if params[:id]
+ @post = Post.find(params[:id])
+ @tags = @post.tag_names
+ else
+ redirect_to :action => :list
+ end
+ end
+
+ # ooo, pagination.
+ def list
+ @post_pages, @posts = paginate :post, {:per_page => 20, :order_by => 'id DESC'}
+ end
+
+ # grab the post and destroy it. simple enough.
+ def delete
+ post = Post.find(params[:id])
+ post.destroy
+ flash[:notice] = 'Post deleted.'
+ redirect_to :action => :list
+ end
+
+ # could be better. grab the last post and save it, forcing a refresh of
+ # most of our cache.
+ def kill_cache
+ Post.find(:first, :order => "created_at DESC").save
+ flash[:notice] = "Cache killed."
+ redirect_to request.env['HTTP_REFERER']
+ end
+
+ #
+ # ajax editing of posts
+ #
+
+ # a hash of methods we want to define... ajax_edit_#{key} will be the
+ # method name. the attribute we care about will be params[:post_#{key}].
+ # we will set it to post.#{key}. the values in the hash are the code we will
+ # run upon success, what we return.
+ ajax_methods = {
+ :title => 'render_text post.title.blank? ? "empty-title" : post.title',
+ :tag_names => %q[render_text(if post.tag_names.size>0;post.tag_names.split.map { |t|
+ "<a href=\"#{t}\">#{t}</a>"}.join(' '); else;'empty-tags';end)],
+ :content => %q[render :partial => "tumble/types/"+post.post_type,
+ :locals => {:content=>post.content.blank? ? '&nbsp;' : post.content}]
+ }
+
+ # define our methods three
+ ajax_methods.each do |m, r|
+ define_method("ajax_edit_#{m}".to_sym) do
+ if request.post?
+ post = Post.find(params[:post_id])
+ post.send("#{m}=", self.instance_eval("params[:post_#{m}]"))
+ post.save
+ self.instance_eval r
+ end
+ end
+ end
+
+ #
+ # tag management
+ #
+
+ # ajaxly rename the tag
+ def rename_tag
+ if request.post?
+ tag = Tag.find(params[:tag_id])
+ tag.name = params[:tag_name]
+ tag.save
+ render_text tag.name
+ end
+ end
+
+ # up and delete a tag
+ def delete_tag
+ tag = Tag.find(params[:id])
+ tag.destroy
+ flash[:notice] = 'Tag deleted.'
+ redirect_to :action => :list_tags
+ end
+
+ # up and paginate a tag
+ def list_tags
+ @tag_pages, @tags = paginate :tag, {:per_page => 10, :order_by => 'id DESC'}
+ end
+
+ #
+ # user handling
+ #
+
+ # see if the poor user is authorized
+ def authorize
+ unless session[:user_id]
+ redirect_to :controller => 'admin', :action => 'login'
+ end
+ end
+
+ # try to login, obviously
+ def login
+ if request.post?
+ @user = User.new(params[:user])
+ # login check is built into the user model
+ logged_in_user = @user.try_to_login
+ if logged_in_user
+ # the mere existence of :user_id in the session hash means a user
+ # is logged in
+ session[:user_id] = logged_in_user.id
+ redirect_to :action => 'list'
+ else
+ # hax.
+ flash[:notice] = 'Username or password incorrect.'
+ end
+ elsif session[:user_id]
+ # user's already logged in
+ redirect_to :action => 'list'
+ else
+ @user = User.new
+ end
+ end
+
+ def logout
+ # important to get rid of the session variable
+ session[:user_id] = nil
+ flash[:notice] = "Logged out."
+ # if they got here from a page other than the admin section, send them back
+ ref = request.env['HTTP_REFERER']
+ redirect_to ref =~ /admin/ ? {:action => :login} : ref
+ end
+
+ # change your password
+ def password
+ if request.post?
+ # grab our lovely user
+ user = User.find(session[:user_id])
+
+ # see if there's anything wrong with the submitted passwords
+ flash[:error] = case true
+ when User.hash_password(params[:password][:old]) != user.hashed_password
+ "Old password is wrong, sorry."
+ when params[:password][:new].length < 6
+ "You gotta make your password longer than six letters... come one."
+ when params[:password][:new] != params[:password][:confirm]
+ "Passwords don't match."
+ end
+
+ # end this transaction if we got an error
+ return flash[:error] unless flash[:error].nil?
+
+ # okay, change the password.
+ user.password = params[:password][:new]
+ user.save
+
+ flash[:notice] = "Password changed."
+ end
+ end
+
+end
80 app/controllers/application.rb
@@ -0,0 +1,80 @@
+# Here is a good place to add your microplugins.
+# All controllers inherit from this one.
+class ApplicationController < ActionController::Base
+ attr_accessor :cache # we locally cache fragments in a hash after grabbing
+ # them from Rails' cache
+ #
+ # note that app/models/cache_sweeper.rb oversees when and how cached
+ # data is purged.
+ #
+
+ # check to see if we already have a cached version of whatever we're looking
+ # to grab.
+ def check_cache(id, tags = nil)
+ # if caching is off, we don't have a cached version. simple enough.
+ return false if perform_caching == false or session[:user_id]
+
+ # create the cache instance variable if it's not here
+ @cache ||= Hash.new
+
+ # convert the id to a symbol if it's not an int, otherwise leave it alone.
+ # if the id is a symbol it's probably something like ':list_tags' or
+ # ':list_posts'
+ id = id.to_sym unless id.to_i > 0
+
+ # now that we have a good id, see if we already did this check recently
+ # by looking for the existance of @cache[id]
+ return @cache[id] if @cache[id]
+
+ # figure out the unique cache identifier for this tasty nugget
+ cache_id_hash = build_cache_hash(id, tags)
+
+ # set our cached info into the cache instance variable, using the symbol/int
+ # id as the key.
+ @cache[id] = read_fragment(cache_id_hash)
+
+ # if what we got 'twas false, say so.
+ return false if @cache[id] == nil || @cache[id] == false
+
+ # all clear!
+ true
+ end
+
+ # will check to see if a particular cached item exists and return it.
+ # if the item does not exist, will execute the attached block and cache
+ # the results, returning them as well.
+ # this is called from helper functions, but you might call it from in here.
+ # who knows.
+ def return_cache(id, tags = nil, &block)
+ # execute and return the block if caching is off or the user is logged in.
+ # we don't show cached info to the administrator. she can always log out.
+ return(yield block) if perform_caching == false or session[:user_id]
+
+ # check to see if the cached item already exists. if it doesn't, set it.
+ if check_cache(id, tags) == false
+ # figure out the unique cache identifier for this tasty nugget
+ cache_id_hash = build_cache_hash(id, tags)
+
+ # save the block's return
+ data_to_cache = yield block
+ write_fragment(cache_id_hash, data_to_cache)
+
+ # save the cached info in our cache instance variablec
+ @cache[id] = data_to_cache
+ end
+ @cache[id].to_s
+ end
+
+
+ # build a hash to identify a cached fragment with. we keep everything in the
+ # tumble/cache namespace (except feeds)
+ def build_cache_hash(id, tags = nil)
+ cache_id_hash = { :controller => 'tumble', :action => 'cache', :id => id }
+
+ # if we got tags, include those in our hash identifier
+ cache_id_hash.merge!({ :tags => tags * ' ' }) unless tags.nil?
+
+ # return the finished product
+ cache_id_hash
+ end
+end
16 app/controllers/feed_controller.rb
@@ -0,0 +1,16 @@
+class FeedController < ApplicationController
+ helper :types, :tumble
+
+ # the feed method.
+ def feed
+ @posts = Post.find(:all, :order => 'created_at DESC', :limit => 10)
+ end
+
+ # cache the full output of the rss and atom methods.
+ # this is a disk based cache, with the files stored in yourapp/public/feeds.
+ # atom and rss methods are both identical, they just call the feed method.
+ %w[atom rss].each do |f|
+ define_method(f.to_sym) { feed }
+ caches_page f.to_sym
+ end
+end
97 app/controllers/tumble_controller.rb
@@ -0,0 +1,97 @@
+class TumbleController < ApplicationController
+
+ # show all the posts for a specific date
+ def show_for_date
+ # build a date string
+ datestring = "#{params[:year]}-#{params[:month]}-#{params[:day]}"
+ begin
+ # try and find posts for this day in history - tag on a wildcard
+ posts = Post.find(:all, :conditions => ["created_at LIKE ?", datestring + '%'],
+ :order => 'created_at DESC') unless check_cache("show_for_date_#{datestring}") == true
+ render_component_list( {:posts => posts}.merge(@params) )
+ rescue ActiveRecord::RecordNotFound
+ error "No posts found for that date."
+ end
+ end
+
+ # list all the posts for the index page
+ # do nothing if this info is already cached
+ # behavior is configured in config/tumble.yml
+ def list
+ # how many posts/days to show?
+ limit = TUMBLE['limit']
+ if TUMBLE['show'] == 'by date'
+ # show by day -- find posts within limit days ago
+ start = Date.today - (limit-1)
+ params = { :conditions => ["created_at >= ?", start.strftime("%Y-%m-%d 00:00:00")] }
+ else
+ # show by post -- find last limit posts
+ params = { :limit => limit }
+ end
+ # don't grab anything if cached
+ posts = Post.find(:all, params.merge(:order => 'created_at DESC')) unless check_cache('list_posts') == true
+ render_component_list(posts)
+ end
+
+ # display all the posts associated with a tag
+ def tag
+ tags = params[:tag].split(' ')
+ # if more than one tag is specified, get the posts containing all the
+ # passed tags. otherwise get all the posts with just the one tag.
+ # don't do any processing if this information is already cached.
+ begin
+ if tags.is_a? Array
+ posts = Post.find_by_tags(tags)
+ else
+ posts = Tag.find_by_name(tags).posts
+ end unless check_cache(:list_tags, tags) == true # gates of madness.
+ render_component_list( :posts => posts, :tag => params[:tag] )
+ rescue NoMethodError
+ error "Tag not found."
+ end
+ end
+
+ # show a post based on its id, or redirect if we got here through hackery.
+ # again, don't grab any info if we've already cached this momma.
+ def show
+ begin
+ if params[:id]
+ post = Post.find(params[:id]) unless check_cache(params[:id]) == true
+ render_component_show(post)
+ else
+ redirect_to :action => 'list'
+ end
+ rescue ActiveRecord::RecordNotFound
+ error "Post not found."
+ end
+ end
+
+ # error method, for redirecting to our hand rolled 404 page
+ # with a custom error message
+ def error(x = nil)
+ render_tumblelog_component( 'error', { :error_msg => x } )
+ end
+
+ private
+
+ # shortcut to calling our component 'list' action
+ def render_component_list(params = nil)
+ params = { :posts => params } unless params.nil? or params.is_a? Hash
+ render_tumblelog_component( 'list', params )
+ end
+
+ # shortcut to calling our component 'show' action
+ def render_component_show(params = nil)
+ params = { :post => params } unless params.nil? or params.is_a? Hash
+ render_tumblelog_component( 'show', params )
+ end
+
+ # in case something changes, or we need to spiff up the params
+ def render_tumblelog_component(action, params)
+ params ||= Hash.new
+ params.merge( :id => @params[:id] )
+ render_component :controller => 'your_tumblelog/tumble',
+ :action => action, :params => params
+ end
+
+end
3 app/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# The methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
17 app/helpers/feed_helper.rb
@@ -0,0 +1,17 @@
+module FeedHelper
+ # customized for special types. needs some work.
+ def feed_content(content, type)
+ if type == 'image'
+ "<img src=\"#{content}\">"
+ elsif type =~ /code/
+ content
+ else
+ r content
+ end
+ end
+
+ # for feeds
+ def strip_textile(x)
+ x.gsub(/<.*?>/,'').gsub(/\"(.*?)\":http:\/\/([^ ]*)( )?/,'\1 ')
+ end
+end
2 app/helpers/tag_helper.rb
@@ -0,0 +1,2 @@
+module TagHelper
+end
128 app/helpers/tumble_helper.rb
@@ -0,0 +1,128 @@
+module TumbleHelper
+ # try and grab the partial for this post type. if it doesn't exist, grab the
+ # default partial. re-raise any actionview errors we're not interested in.
+ def oz_render_post_type(post)
+ begin
+ render :partial => "your_tumblelog/tumble/types/" + post.post_type, :locals => { :content => post.content }
+ rescue ActionView::ActionViewError => e
+ if e.to_s =~ /No rhtml/
+ render :partial => "your_tumblelog/tumble/types/post", :locals => { :content => post.content }
+ else
+ raise e
+ end
+ end
+ end
+
+ # clean date
+ def oz_clean_date(date)
+ sprintf "%d/%02d/%02d", date.year, date.month, date.day
+ end
+
+ # the 5 most recent tags
+ def oz_recent_tags(separator = ' . ')
+ return_cache(:recent_tags) do
+ tags = Tag.find(:all, :order => "updated_at DESC", :limit => 5)
+ recent = ''
+ tags.each do |t|
+ recent << link_to(t.name, :controller => 'tumble', :action => 'tag',
+ :tag => t.name) + separator
+ end
+ recent.chomp(separator)
+ end
+ end
+
+ # display all the tags
+ def oz_all_tags
+ tags = @params[:tag].split(' ') if @params[:tag]
+ return_cache(:all_tags, tags) do
+ tags = Tag.find(:all, :order => 'name ASC')
+ all = String.new
+ tags.each { |t| all << add_tag_link(t.name) + link_to(t.name,
+ :controller => 'tumble', :action => 'tag', :tag => t.name) + ' ' }
+ all
+ end
+ end
+
+ # the current tag
+ def oz_current_tag(default = 'tumble')
+ if @error_msg
+ "error"
+ elsif @params[:tag]
+ @params[:tag].gsub(' ','+')
+ else
+ default
+ end
+ end
+
+ # return a list of posts
+ def oz_show_list
+ list_block = lambda do
+ output = String.new
+ for post in @posts
+ output << render(:partial => 'post', :locals => { :post => post })
+ end if @posts
+ return output unless output.empty?
+ %[<div id="error-box">I tried to find what you're looking for really hard.
+ No matches, though. Sorry.</div>]
+ end
+ if @params[:tag]
+ tags = @params[:tag].split(' ')
+ return_cache(:list_tags, tags) { list_block.call }
+ elsif @params[:year] and @params[:month] and @params[:day]
+ datestring = "#{params[:year]}-#{params[:month]}-#{params[:day]}"
+ return_cache(datestring) { list_block.call }
+ else
+ return_cache(:list_posts) { list_block.call }
+ end
+ end
+
+ # return the post
+ def oz_show_post
+ return_cache(@params[:id]) { output = render(:partial => 'post', :locals => { :post => @post }) }
+ end
+
+ # the relative date
+ def oz_relative_date(date = Date.today)
+ date = Date.parse(date, true) unless /Date.*/ =~ date.class.to_s
+ days = (date - Date.today).to_i
+ case
+ when (days >= 0 and days < 1) then 'today'
+ when (days >= 1 and days < 2) then 'tomorrow'
+ when (days >= -1 and days < 0) then 'yesterday'
+ when (days.abs < 60 and days > 0) then "in #{days} days"
+ when (days.abs < 60 and days < 0) then "#{days.abs} days ago"
+ when days.abs < 182 then date.strftime('%A, %B %e')
+ else date.strftime('%A, %B %e, %Y')
+ end
+ end
+
+ # if we're looking at a tag, give the option to add (or remove) another tag
+ def tag_link(t)
+ %[<a href="#{t}" class="tag-link">#{t}</a>]
+ end
+
+ # add a + or - in front of tags if we're looking at a tag's listing
+ def add_tag_link(tag)
+ if @params[:tag] and !@params[:tag].split.select { |x| x =~ /^#{tag}$/ }.empty?
+ link = @params[:tag].split
+ link = link.reject { |x| x =~ /^#{tag}$/ } * '+' unless link.size == 1
+ link = '/' if link.size == 1
+ %[<a href="#{link}" class="remove-tag">-</a>]
+ elsif @params[:tag]
+ "<a href=\"#{@params[:tag].gsub(' ','+')}+#{tag}\" class=\"add-tag\">+</a>"
+ else
+ ""
+ end
+ end
+
+ # fetch the value of our cached fragment, or write it
+ def return_cache(id, tags = nil, &block)
+ controller.return_cache(id, tags, &block)
+ end
+
+ # is caching on or off?
+ def caching?; self.caching?; end
+ def self.caching?
+ ActionController::Base.perform_caching
+ end
+end
37 app/models/cache_sweeper.rb
@@ -0,0 +1,37 @@
+class CacheSweeper < ActionController::Caching::Sweeper
+ observe Post, Tag
+
+ # this method is invoked is a Post or Tag is saved in certain methods we
+ # registered within app/controllers/admin/post_controller.rb
+ def after_save(record)
+ if record.is_a?(Post)
+ # we're saving a new post. get its id.
+ id = record.id
+
+ # clear out tag list caches
+ expire_fragment /all_tags/
+ expire_fragment /list_tags/
+
+ # clear out the cache for that post's day
+ date = record.created_at
+ date_id = "show_for_date_" + sprintf("%d-%02d-%02d", date.year, date.month, date.day )
+ expire_fragment :controller => '/tumble', :action => 'cache', :id => date_id
+
+ # expire the cache for this posts page
+ expire_fragment :controller => '/tumble', :action => 'cache', :id => id
+
+ # grab the 20 most recent posts (those appearing on the front page)
+ # and paint an array of their ids
+ rposts = Post.find(:all, :order => 'created_at DESC', :limit => 20).map { |p| p.id }
+
+ # if the post we're saving appears on the front page, we need to reset the
+ # feed caches as well as the recent_tags cache and the list_posts cache
+ if rposts.include?(id)
+ expire_page :controller => "/feed", :action => "rss.xml"
+ expire_page :controller => "/feed", :action => "atom.xml"
+ expire_fragment :controller => '/tumble', :action => 'cache', :id => :recent_tags
+ expire_fragment :controller => '/tumble', :action => 'cache', :id => :list_posts
+ end
+ end
+ end
+end
49 app/models/post.rb
@@ -0,0 +1,49 @@
+class MultiPostFindExpectsArray < StandardError; end
+
+class Post < ActiveRecord::Base
+ has_and_belongs_to_many :tags
+ belongs_to :user
+
+ # Returns a space separated string of all this post's tags
+ def tag_names
+ self.tags.map { |t| t.name }.join ' '
+ end
+
+ # take a space separated string or array of tags and sets this post's
+ # tags to those, touching their updated_at time as well.
+ def tag_names=(tag_names)
+ # if we don't get an array or string, abort
+ return false unless tag_names.is_a?(Array) or tag_names.is_a?(String)
+
+ # get rid of the current tags
+ self.tags.clear
+
+ # for each tag, set its update_at to now and then add it to the array of
+ # tags associated with this post
+ (tag_names.is_a?(String) ? tag_names.split : tag_names).each do |t|
+ tag = Tag.find_by_name(t) || Tag.new(:name => t)
+ tag.updated_at = Time.now
+ self.tags << tag
+ end
+ end
+
+ # get posts with more than one tag.
+ # accepts an array or space separated string of tag names
+ def self.find_by_tags(tags)
+ # if it's a space separated string, break it up
+ tags = tags.is_a?(String) ? tags.split : tags
+
+ # die if we didn't get an array
+ raise MultiPostFindExpectsArray unless tags.is_a?(Array)
+
+ # get all the posts containing all the tags in our list
+ tags.inject(Tag.find_by_name(tags.shift).posts) do |posts, tag|
+ posts = posts & Tag.find_by_name(tag).posts
+ end
+ end
+
+ # get rid of any tags which are not associated with any posts
+ def after_save
+ Tag.prune_tags
+ end
+end
8 app/models/tag.rb
@@ -0,0 +1,8 @@
+class Tag < ActiveRecord::Base
+ has_and_belongs_to_many :posts, :order => 'created_at DESC'
+
+ # get rid of any tags that aren't attached to a post
+ def self.prune_tags
+ find(:all).each { |t| t.destroy if t.posts.size == 0 }
+ end
+end
54 app/models/user.rb
@@ -0,0 +1,54 @@
+#
+# Inspired by the authentication example in the Pragmatic Programmers'
+# Agile Web Development with Ruby on Rails. Nothing too crazy.
+#
+
+class CantDestroyAdminUser < StandardError; end
+
+
+class User < ActiveRecord::Base
+ has_many :posts, :order => 'created_at DESC'
+
+ before_destroy :dont_destroy_admin
+
+ attr_accessor :password
+ attr_accessible :name, :password
+
+ validates_uniqueness_of :name
+ validates_presence_of :name
+ validates_presence_of :password, :on => :create
+
+ def before_save
+ # hash the plaintext password and set it to an instance variable
+ self.hashed_password = User.hash_password(self.password)
+ end
+
+ def after_save
+ # get rid of the plaintext password
+ @password = nil
+ end
+
+ def self.hash_password(password)
+ require 'digest/sha1'
+
+ # create a hash of plaintext
+ Digest::SHA1.hexdigest(password)
+ end
+
+ def self.login(name, password)
+ # get the user info for anyone matching name and password
+ hashed_password = hash_password(password || "")
+ find(:first, :conditions => ["name = ? and hashed_password = ?",
+ name, hashed_password])
+ end
+
+ def try_to_login
+ # return a user if we find anything
+ User.login(self.name,self.password)
+ end
+
+ def dont_destroy_admin
+ # that's you.
+ raise CantDestroyAdminUser if self.id == 1
+ end
+end
28 app/views/admin/list.rhtml
@@ -0,0 +1,28 @@
+<% @page_name = 'listing posts' -%>
+
+<table>
+ <tr>
+ <th>Title</th>
+ <th>Type</th>
+ <th>Date</th>
+ <th>&nbsp;</th>
+ </tr>
+
+<% @posts.each do |p| -%>
+ <tr>
+ <td><%=r p.title.length > 0 ? p.title : '&nbsp;' %></td>
+ <td><%= p.post_type %></td>
+ <% date = p.created_at -%>
+ <td><%= date.year %>-<%= sprintf('%02d', date.month.to_s) %>-<%= sprintf('%02d', date.day.to_s) %></td>
+ <td><small><%= link_to 'show', :action => :show, :id => p %> |
+ <%= link_to 'edit', :action => :edit, :id => p %> |
+ <%= link_to 'delete', {:action => :delete, :id => p},
+ :confirm => 'Are you sure you want to delete this post?' %></small>
+ </td>
+ </tr>
+<% end -%>
+
+</table>
+
+<%= link_to "prev page", { :page => @post_pages.current.previous } if @post_pages.current.previous %>
+<%= link_to "next page", { :page => @post_pages.current.next } if @post_pages.current.next %>
27 app/views/admin/list_tags.rhtml
@@ -0,0 +1,27 @@
+<% @page_name = 'listing tags' -%>
+
+<table>
+ <tr><th colspan="2">Tags</th></tr>
+<% @tags.each do |t| -%>
+ <tr>
+ <td><div id="name-<%= t.id %>"><%= t.name %></div>
+ <div id="rename-<%= t.id %>" style="display:none;">
+ <%= form_remote_tag :update => "name-#{t.id}", :url => { :action => 'rename_tag' },
+ :complete => "Element.toggle('rename-#{t.id}');Element.toggle('name-#{t.id}')" %>
+ <%= hidden_field_tag 'tag_id', t.id %>
+ <%= text_field_tag 'tag_name', t.name, :size => 10 %><%= submit_tag 'save' %>
+ <%= end_form_tag %>
+ </div>
+
+ </td>
+ <td><%= link_to_function 'rename', "Element.toggle('rename-#{t.id}');Element.toggle('name-#{t.id}')" %> |
+ <%= link_to 'delete', {:action => :delete_tag, :id => t},
+ :confirm => 'Are you sure you want to delete this tag?' %>
+ </td>
+ </tr>
+<% end -%>
+
+</table>
+
+<%= link_to "prev page", { :page => @tag_pages.current.previous } if @tag_pages.current.previous %>
+<%= link_to "next page", { :page => @tag_pages.current.next } if @tag_pages.current.next %>
28 app/views/admin/login.rhtml
@@ -0,0 +1,28 @@
+<% @page_name = "Login" %>
+<%= error_messages_for 'user' %>
+
+<% unless session[:user_id] %>
+
+ <%= form_tag %>
+ <table border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td>User name:</td>
+ <td><%= text_field 'user', 'name', :size => 20 %></td>
+ </tr>
+ <tr>
+ <td>Password:</td>
+ <td><%= password_field 'user', 'password', :size => 20 %></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><%= submit_tag "Login" %></td>
+ </tr>
+ </table>
+
+ <%= end_form_tag %>
+
+<% else %>
+ <div id="notice">
+ You are already logged in. <%= link_to 'Log out.', :action => 'logout' %>
+ </div>
+<% end %>
44 app/views/admin/new.rhtml
@@ -0,0 +1,44 @@
+<% @page_name = "#{controller.action_name} post" -%>
+
+<%= form_tag %>
+<table>
+ <tr>
+ <th>Title:</th>
+ <td><%= text_field 'post', 'title', :size => 40 %></td>
+ </tr>
+ <tr>
+ <th>Post Type:</th>
+ <td>
+ <% if controller.action_name = :new %>
+ <select id="post_post_type" name="post[post_type]">
+ <%= options_for_select( TYPES, @post.post_type ? @post.post_type : ( TYPES.include?('post') ? 'post' : '') ) %>
+ </select>
+ <% else %>
+ <%= select 'post', 'post_type', TYPES, :size => 40 %>
+ <% end %>
+ </td>
+ </tr>
+
+ <tr>
+ <th>Tags:</th>
+ <td><%= text_field_tag 'tags', @tags, :size => 40 %></td>
+ </tr>
+
+ <tr>
+ <th>Content:</th>
+<!-- <small>[you can use <a href="http://hobix.com/textile/quick.html" target="_blank">textile markup</a> in the post]</small><br/> -->
+ <td><%= text_area 'post', 'content', :cols => 38, :rows => 15 %></td>
+ </tr>
+
+ <tr>
+ <th>Created at:</th>
+ <td><%= datetime_select 'post', 'created_at' %></td>
+ </tr>
+
+ <tr><th colspan="2" align="center"><%= submit_tag 'Save Your Tumble' %></th></tr>
+</table>
+
+ <%# no real support for multiple users yet%>
+ <%= hidden_field 'post', 'user_id' %>
+
+<%= end_form_tag %>
25 app/views/admin/password.rhtml
@@ -0,0 +1,25 @@
+<% @page_name = "Change Your Password" %>
+
+<div id="error"><%= flash[:error] %></div>
+
+<%= form_tag %>
+ <table border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td>Old Password:</td>
+ <td><input type="password" name="password[old]" size="20" /></td>
+ </tr>
+ <tr>
+ <td>New Password:</td>
+ <td><input type="password" name="password[new]" size="20" /></td>
+ </tr>
+ <tr>
+ <td>Confirm:</td>
+ <td><input type="password" name="password[confirm]" size="20" /></td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><%= submit_tag "Change Password" %></td>
+ </tr>
+ </table>
+
+<%= end_form_tag %>
36 app/views/admin/show.rhtml
@@ -0,0 +1,36 @@
+<% @page_name = "show post" -%>
+
+<table>
+ <tr>
+ <th colspan="2" align="center">
+ <%= link_to 'edit', :action => :edit, :id => @post %> |
+ <%= link_to 'delete', {:action => :delete, :id => @post}, :confirm => 'Are you sure you want to delete this post?' %>
+ </th>
+ </tr>
+ <tr>
+ <td><strong>id:</strong></td>
+ <td><%= @post.id %></td>
+ </tr>
+ <tr>
+ <td><strong>title:</strong></td>
+ <td><%= @post.title %></td>
+ </tr>
+ <tr>
+ <td><strong>post type:</strong></td>
+ <td><%= @post.post_type %></td>
+ </tr>
+ <tr>
+ <td><strong>tags:</strong></td>
+ <td><%= @tags %></td>
+ </tr>
+ <tr>
+ <td><strong>created at:</strong></td>
+ <td><%= @post.created_at %></td>
+ </tr>
+</table>
+
+<div class="show-post">
+ <% partial = "#{RAILS_ROOT}/components/your_tumblelog/tumble/types/_#{@post.post_type}.rhtml" -%>
+ <% partial = "#{RAILS_ROOT}/components/your_tumblelog/tumble/types/_post.rhtml" unless File.exists?(partial) %>
+ <%= render_file partial, false, { :content => @post.content } %>
+</div>
33 app/views/feed/atom.rxml
@@ -0,0 +1,33 @@
+xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
+xml.feed "version" => "0.3", "xml:lang"=>"en-US", "xmlns"=>"http://purl.org/atom/ns#", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do
+
+ xml.title TUMBLE['name']
+ xml.tagline "mode"=>"escaped", "type"=>"text/html"
+ xml.id "tag:#{controller.request.host},2005:ozimodo"
+ xml.generator "ozimodo", "url" => "http://ozimodo.rubyforge.org/"
+ xml.link "rel" => "alternate", "type" => "text/html", "href" => TUMBLE['url']
+
+ xml.modified @posts.first.created_at.xmlschema unless @posts.empty?
+
+ for entry in @posts
+
+ xml.entry do
+
+# xml.author { xml.name entry.user.name }
+ xml.id "tag:#{controller.request.host},2005:ozimodo-#{entry.id}"
+
+ xml.issued entry.created_at.xmlschema
+ xml.modified entry.created_at.xmlschema
+ xml.title strip_textile(entry.title)
+
+ xml.link "rel" => "alternate", "type" => "text/html", "href" => TUMBLE['url'] + oz_clean_date(post.created_at) + '#' + entry.id.to_s
+ for tag in entry.tags
+ xml.dc :subject, tag.name
+ end
+
+ content = feed_content(entry.content,entry.post_type)
+ xml.content content, "type"=>"text/html", "mode"=>"escaped"
+
+ end
+ end
+end
26 app/views/feed/rss.rxml
@@ -0,0 +1,26 @@
+xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
+
+xml.rss "version"=>"2.0", "xmlns:dc"=>"http://purl.org/dc/elements/1.1/" do
+ xml.channel do
+ xml.title TUMBLE['name']
+ xml.link TUMBLE['url']
+ xml.language "en-us"
+ xml.ttl "40"
+ xml.description TUMBLE['name']
+
+ for entry in @posts
+ xml.item do
+ xml.title strip_textile(entry.title)
+ content = feed_content(entry.content,entry.post_type)
+ xml.description content[0,500]
+ xml.pubDate entry.created_at.strftime "%a, %e %b %Y %H:%M:%S %Z"
+ link = TUMBLE['url'] + oz_clean_date(entry.created_at) + '#' + entry.id.to_s
+ xml.guid link
+ xml.link link
+ for tag in entry.tags
+ xml.category tag.name
+ end
+ end
+ end
+ end
+end
53 app/views/layouts/admin.rhtml
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+ <title>ozimodo::<%= @page_name %></title>
+ <%= stylesheet_link_tag 'admin' %>
+ <%= stylesheet_link_tag 'types' %>
+ <%= javascript_include_tag 'prototype' %>
+</head>
+<body>
+
+<div id="main" align="center">
+ <div id="header">
+ <table><tr>
+ <td class="bracket">{</td>
+ <td class="logo">tumblelog maintenance</td>
+ <td class="bracket">}</td>
+ </tr></table>
+ </div>
+
+ <div id="nav">
+ [
+ <%= link_to 'posts', :action => :list %> |
+ <%= link_to 'tags', :action => :list_tags %> |
+ <%= link_to 'tumble', :action => :new %> |
+ <%= link_to 'password', :action => :password %> |
+ <%= link_to 'kill cache', :action => :kill_cache %> |
+ <%= link_to 'tumblelog', "/" %> |
+ <% if session[:user_id] %>
+ <%= link_to 'logout', :action => :logout %>
+ <% else %>
+ <%= link_to 'login', :action => :login %>
+ <% end %>
+ ]
+ </div>
+
+ <% if flash[:notice] %>
+ <div id="notice">
+ <%= flash[:notice] %>
+ </div>
+ <% end %>
+
+ <div id="content">
+ <%= @content_for_layout %>
+ </div>
+
+ <div id="footer">
+ you're on the admin side, where we keep it casual ~
+ <a href="http://ozimodo.rubyforge.org/">ozimodo</a>
+ </div>
+</div>
+
+</body>
+</html>
87 components/your_tumblelog/tumble/_post.rhtml
@@ -0,0 +1,87 @@
+<%# code to display the date of the current post group %>
+<% postdate = post.created_at.strftime("%Y-%m-%d")%>
+<% if @date != postdate %>
+ <% @date = postdate %>
+ <div class="post-date">
+ <%= link_to oz_relative_date(@date.to_s), '/' + oz_clean_date(post.created_at) + '/',
+ :class => 'post-date' %>
+ </div>
+<% end %>
+
+<div class="post">
+
+ <% if session[:user_id] %>
+ <div class="edit-title" id="edit-title<%= post.id %>" style="display:none;">
+ <%= form_remote_tag :update => "post-title#{post.id}", :url => { :controller => 'admin',
+ :action => 'ajax_edit_title' },
+ :complete => "Element.toggle('edit-title#{post.id}');
+ header = document.getElementById('post-header#{post.id}');
+ div = document.getElementById('post-title#{post.id}');
+ if (div.innerHTML == 'empty-title') { header.style.display = 'none'; } else { header.style.display = 'visible'; }
+ " %>
+ <%= hidden_field_tag 'post_id', post.id %>
+ <%= text_field_tag 'post_title', post.title %>
+ <%= submit_tag 'save' %>
+ <%= end_form_tag %>
+ </div>
+ <% end %>
+ <a name="<%= post.id %>"></a>
+ <div class="post-title" id="post-header<%= post.id %>" <% if post.title.empty? %>style="display:none;"<% end %>>
+ <span id="post-title<%= post.id %>">title: <%=t post.title %></span>
+ <span class="post-date">
+ <%= link_to '#',
+ '/' + oz_clean_date(post.created_at) + '/' %>
+ </span>
+ </div>
+
+ <% if session[:user_id] %>
+ <div class="admin-actions"><span class="admin-bracket">admin: </span>
+ <%= link_to_function 'title', "Element.toggle('edit-title#{post.id}');if (document.getElementById('post-title#{post.id}').innerHTML != 'empty-title') { Element.toggle('post-header#{post.id}') }" %> |
+ <%= link_to_function 'body', "Element.toggle('edit-body#{post.id}');Element.toggle('post-body#{post.id}')" %> |
+ <%= link_to_function 'tags', "Element.toggle('edit-tags#{post.id}');if (document.getElementById('post-tags#{post.id}').innerHTML != 'empty-tags') { Element.toggle('post-tags#{post.id}') }" %>
+ </div>
+ <% end %>
+
+
+ <div class="post-body" id="post-body<%= post.id %>">
+ <%= oz_render_post_type(post) %>
+ </div>
+
+ <% if session[:user_id] %>
+ <div class="edit-body" id="edit-body<%= post.id %>" style="display:none;">
+ <%= form_remote_tag :update => "post-body#{post.id}", :url => { :controller => 'admin',
+ :action => 'ajax_edit_content' },
+ :complete => "Element.toggle('edit-body#{post.id}');Element.toggle('post-body#{post.id}')" %>
+ <%= hidden_field_tag 'post_id', post.id %>
+ <%= text_area_tag 'post_content', post.content, :cols => 50, :rows => 4 %><br/>
+ <%= submit_tag 'save' %>
+ <%= end_form_tag %>
+ </div>
+ <% end %>
+
+
+ <div class="post-tags">
+ tags:
+ <span id="post-tags<%= post.id %>" <% if post.tags.empty? %>style="display:none;"<% end %>>
+ <% for tag in post.tags %>
+ <%= link_to tag.name, :controller => 'tumble', :action => 'tag', :tag => tag.name %>
+ <% end %>
+ </span>
+ </div>
+
+ <% if session[:user_id] %>
+ <div id="edit-tags<%= post.id %>" style="display:none;">
+ <%= form_remote_tag :update => "post-tags#{post.id}", :url => { :controller => 'admin',
+ :action => 'ajax_edit_tag_names' },
+ :complete => "Element.toggle('edit-tags#{post.id}');
+ div = document.getElementById('post-tags#{post.id}');
+ if (div.innerHTML == 'empty-tags') { div.style.display = 'none'; } else { div.style.display = 'visible'; }
+ " %>
+ <%= hidden_field_tag 'post_id', post.id %>
+ <%= text_field_tag 'post_tag_names', post.tag_names %>
+ <%= submit_tag 'save' %>
+ <%= end_form_tag %>
+ </div>
+ <% end %>
+</div>
+
3 components/your_tumblelog/tumble/error.rhtml
@@ -0,0 +1,3 @@
+<div id="error-box">
+ <strong>An error!</strong> <%= @error_msg %>
+</div>
51 components/your_tumblelog/tumble/layout.rhtml
@@ -0,0 +1,51 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+ <title>your tumblelog<%= " &raquo; #{params[:tag]}" if params[:tag] %></title>
+ <%= stylesheet_link_tag 'tumble' %>
+ <%= stylesheet_link_tag 'types' %>
+ <%= javascript_include_tag 'prototype' if session[:user_id] %>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <link rel="alternate" type="application/atom+xml" title="Atom feed" href="<%= url_for(:controller => 'feed', :action => 'atom.xml') %>"/>
+ <link rel="alternate" type="application/rss+xml" title="RSS feed" href="<%= url_for(:controller => 'feed', :action => 'rss.xml') %>"/>
+</head>
+<body>
+<div id="main">
+ <div id="header">
+ <span class="admin">
+ <a href="/admin">admin</a>
+ </span>
+ <span class="logo"><a href="/" class="logo">your tumblelog</a></span>
+ <span class="ready-set"><span style="color:red;">&raquo;</span> ready, set, <%= oz_current_tag('tumble') %></span>
+ </div>
+
+ <div id="tags">
+ tags: <%= oz_all_tags %>
+ </div>
+
+ <div id="content">
+ <%= @content_for_layout %>
+ </div>
+
+ <div class="gem_test">
+ <!-- get rid of this -->
+ RubyGems: <%= require_test('rubygems') %> |
+ RedCloth: <%= require_test('RedCloth', true) %> |
+ Syntax: <%= require_test('syntax', true) %>
+ </div>
+
+ <div id="footer">
+ <div class="powered-by">
+ <span class="bracket">{</span>
+ <a href="http://ozimodo.rubyforge.org">powered by ozimodo</a>
+ <span class="bracket">}</span>
+ </div>
+ <span class="feeds">
+ feeds:
+ <a href="/feed/rss.xml">rss</a>,
+ <a href="/feed/atom.xml">atom</a>
+ </span>
+ </div>
+</div>
+</body>
+</html>
1 components/your_tumblelog/tumble/list.rhtml
@@ -0,0 +1 @@
+<%= oz_show_list %>
1 components/your_tumblelog/tumble/show.rhtml
@@ -0,0 +1 @@
+<%= oz_show_post %>
1 components/your_tumblelog/tumble/types/_image.rhtml
@@ -0,0 +1 @@
+<img src="<%= content %>" class="type-img" alt="an image." />
1 components/your_tumblelog/tumble/types/_link.rhtml
@@ -0,0 +1 @@
+<div class="type-link"><%=r content %></div>
1 components/your_tumblelog/tumble/types/_post.rhtml
@@ -0,0 +1 @@
+<%=r content %>
1 components/your_tumblelog/tumble/types/_quip.rhtml
@@ -0,0 +1 @@
+<%=r "*%{color:red}&raquo;%* " + content %>
3 components/your_tumblelog/tumble/types/_quote.rhtml
@@ -0,0 +1,3 @@
+<div class="type-quote">
+ <%=q content %>
+</div>
1 components/your_tumblelog/tumble/types/_ruby_code.rhtml
@@ -0,0 +1 @@
+<div class="ruby-code"><%=rc content %></div>
25 components/your_tumblelog/tumble_controller.rb
@@ -0,0 +1,25 @@
+class YourTumblelog::TumbleController < ApplicationController
+ uses_component_template_root # know where our templates are
+ helper :tumble, :types
+ layout "your_tumblelog/tumble/layout"
+
+ # show a list of posts -- by date, tag, or the main page
+ def list
+ @posts = params[:posts]
+ end
+
+ # show a single post
+ def show
+ @post = params[:post]
+ end
+
+ # error method, for redirecting to our hand rolled 404 page
+ # with a custom error message
+ def error(x = nil)
+ x ||= params[:error_msg]
+ @error_msg = x unless x.nil?
+ self.action_name = :error
+ render :action => 'error'
+ end
+
+end
47 components/your_tumblelog/types_helper.rb
@@ -0,0 +1,47 @@
+#
+# where all the helper functions related to your tumble types go.
+#
+module TypesHelper
+ # for the quote type. redcloth adds p tags, we strip them.
+ def q(x, div_class = 'type-quote-quotation')
+ r("<sub><span class=\"#{div_class}\">&ldquo;</span></sub>" + x).gsub(/<(\/)?p>/,'')
+ end
+
+ # for titles -- protect irc channel names, specifically
+ def t(x)
+ return r(x).gsub(/<(\/)?p>/,'') unless x[0,1] == '#'
+ x
+ end
+
+ # RedCloth wrapper -- clean this up if you have redcloth installed for sure
+ def r(x)
+ begin
+ require_gem 'RedCloth'
+ RedCloth.new(x).to_html
+ rescue
+ x
+ end
+ end
+
+ # syntax highlight ruby code -- same as redcloth, clean it up
+ def rc(x)
+ begin
+ require_gem 'syntax'
+ require 'syntax/convertors/html'
+ Syntax::Convertors::HTML.for_syntax("ruby").convert(x)
+ rescue
+ x
+ end
+ end
+
+ # see if we have dependencies -- feel free to nuke this
+ def require_test(file, gem = false)
+ begin
+ require file unless gem == true
+ require_gem file if gem == true
+ %[<span style="color: green;">found</span>]
+ rescue MissingSourceFile, Gem::LoadError
+ %[<span style="color: red;">not found</span>]
+ end
+ end
+end
19 config/boot.rb
@@ -0,0 +1,19 @@
+# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+
+unless defined?(RAILS_ROOT)
+ root_path = File.join(File.dirname(__FILE__), '..')
+ unless RUBY_PLATFORM =~ /mswin32/
+ require 'pathname'
+ root_path = Pathname.new(root_path).cleanpath(true).to_s
+ end
+ RAILS_ROOT = root_path
+end
+
+if File.directory?("#{RAILS_ROOT}/vendor/rails")
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+else
+ require 'rubygems'
+ require 'initializer'
+end
+
+Rails::Initializer.run(:set_load_path)
20 config/database.yml
@@ -0,0 +1,20 @@
+development:
+ adapter: mysql
+ database: tumblelog_dev
+ host: localhost
+ username: root
+ password:
+
+test:
+ adapter: mysql
+ database: tumblelog_test
+ host: localhost
+ username: root
+ password:
+
+production:
+ adapter: mysql
+ database: tumblelog_production
+ host: localhost
+ username: root
+ password:
59 config/environment.rb
@@ -0,0 +1,59 @@
+# Be sure to restart your web server when you modify this file.
+
+# Uncomment below to force Rails into production mode when
+# you don't control web/app server and can't set it the proper way
+# ENV['RAILS_ENV'] ||= 'production'
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+ # Settings in config/environments/* take precedence those specified here
+
+ # Skip frameworks you're not going to use
+ # config.frameworks -= [ :action_web_service, :action_mailer ]
+
+ # Add additional load paths for your own custom dirs
+ # config.load_paths += %W( #{RAILS_ROOT}/extras )
+
+ # Force all environments to use the same logger level
+ # (by default production uses :info, the others :debug)
+ # config.log_level = :debug
+
+ # Use the database for sessions instead of the file system
+ # (create the session table with 'rake create_sessions_table')
+ # config.action_controller.session_store = :active_record_store
+
+ # Enable page/fragment caching by setting a file-based store
+ # (remember to create the caching directory and make it readable to the application)
+ # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache"
+
+ # Activate observers that should always be running
+ # config.active_record.observers = :cacher, :garbage_collector
+
+ # Make Active Record use UTC-base instead of local time
+ # config.active_record.default_timezone = :utc
+
+ # Use Active Record's schema dumper instead of SQL when creating the test database
+ # (enables use of different database adapters for development and test environments)
+ # config.active_record.schema_format = :ruby
+
+ # See Rails::Configuration for more options
+end
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# load yaml config file, mostly for rss and api
+TUMBLE = YAML.load( File.open( File.dirname(__FILE__) + '/tumble.yml' ) )
+
+# figure out all of our types based on partials in components/your_tumblelog/tumble/types
+TYPES = Dir[File.dirname(__FILE__) + '/../components/your_tumblelog/tumble/types/*'].map do |f|
+ File.basename(f).sub(/^_/,'').sub('.rhtml','')
+end
19 config/environments/development.rb
@@ -0,0 +1,19 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# In the development environment your application's code is reloaded on
+# every request. This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+config.cache_classes = false
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Enable the breakpoint server that script/breakpointer connects to
+config.breakpoint_server = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false
19 config/environments/production.rb
@@ -0,0 +1,19 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, "live" apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable delivery errors if you bad email addresses should just be ignored
+# config.action_mailer.raise_delivery_errors = false
19 config/environments/test.rb
@@ -0,0 +1,19 @@
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+
+# Tell ActionMailer not to deliver emails to the real world.
+# The :test delivery method accumulates sent emails in the
+# ActionMailer::Base.deliveries array.
+config.action_mailer.delivery_method = :test
32 config/routes.rb
@@ -0,0 +1,32 @@
+ActionController::Routing::Routes.draw do |map|
+ # Here's a sample route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # pretty-fy the feed urls
+ map.connect 'feed/atom.xml', :controller => 'feed', :action => 'atom'
+ map.connect 'feed/rss.xml', :controller => 'feed', :action => 'rss'
+
+ # our admin stuffs
+ map.connect 'admin', :controller => 'admin', :action => 'list'
+ map.connect 'admin/:action', :controller => 'admin', :action => :action
+
+ # show posts by date
+ map.connect ':year/:month/:day', :controller => 'tumble',
+ :action => 'show_for_date', :year => /\d{4}/, :month => /\d{1,2}/,
+ :day => /\d{1,2}/
+
+ # default
+ map.connect '', :controller => 'tumble', :action => 'list'
+
+ # show a single post
+ map.connect ':id', :controller => 'tumble', :action => 'show',
+ :id => /\d+/
+
+ # show a tag
+ map.connect ':tag', :controller => 'tumble', :action => 'tag',
+ :tag => /[A-Za-z0-9+]+/
+
+ # Install the default route as the lowest priority.
+ map.connect ':controller/:action/:id'
+end
15 config/tumble.yml
@@ -0,0 +1,15 @@
+# Your tumblelog's URL. Make sure it ends with a /
+url: http://ozimodo.rubyforge.org/
+
+# The name of your tumblelog
+name: your tumblelog
+
+# how do you want your posts displayed on the main page?
+# options are 'by date' or 'by post'
+show: by date
+
+# now, how many to show?
+# if you picked 'by date' this is how many days to show -- note that today
+# counts as a day.
+# if 'by post', then how many posts
+limit: 7
46 db/migrate/1_get_your_tumble_on.rb
@@ -0,0 +1,46 @@
+class GetYourTumbleOn < ActiveRecord::Migration
+ def self.up
+ create_table :posts do |t|
+ t.column :id, :int, :null => false
+ t.column :user_id, :int
+ t.column :title, :string, :limit => 100
+ t.column :post_type, :string, :limit => 15
+ t.column :content, :text
+ t.column :created_at, :datetime
+ end
+ add_index :posts, :id, :unique
+
+ create_table :posts_tags do |t|
+ t.column :post_id, :int
+ t.column :tag_id, :int
+ end
+ add_index :posts_tags, [:post_id, :tag_id]
+
+ create_table :tags do |t|
+ t.column :id, :int, :null => false
+ t.column :name, :string, :limit => 25
+ t.column :updated_at, :datetime
+ end
+ add_index :tags, :id, :unique
+
+ create_table :users do |t|
+ t.column :id, :int, :null => false
+ t.column :name, :string, :limit => 20
+ t.column :hashed_password, :string, :limit => 40
+ end
+ add_index :users, :id, :unique
+
+ User.new( :name => "admin", :password => 'changeme' ).save
+
+ Post.new( :title => "first post!", :post_type => "post",
+ :content => %[Hello, world. I'm tumblin'!
+ Check out
+ <a href="http://ozimodo.rubyforge.org/configure.html">http://ozimodo.rubyforge.org/configure.html</a>
+ for info on how to totally customize your tumblelog.],
+ :user_id => 1 ).save
+ end
+
+ def self.down
+ %w[posts posts_tags tags users].each { |t| drop_table t.to_sym }
+ end
+end
33 public/.htaccess
@@ -0,0 +1,33 @@
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# If you don't want Rails to look in certain directories,
+# use the following rewrite rules so that Apache won't rewrite certain requests
+#
+# Example:
+# RewriteCond %{REQUEST_URI} ^/notrails.*
+# RewriteRule .* - [L]
+
+# Redirect all requests not available on the filesystem to Rails
+# By default the cgi dispatcher is used which is very slow
+#
+# For better performance replace the dispatcher with the fastcgi one
+#
+# Example:
+# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+RewriteEngine On
+
+RewriteRule ^$ index.html [QSA]
+RewriteRule ^([^.]+)$ $1.html [QSA]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
+
+# In case Rails experiences terminal errors
+# Instead of displaying this message you can supply a file here which will be rendered instead
+#
+# Example:
+# ErrorDocument 500 /500.html
+
+ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
8 public/404.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>File not found</h1>
+ <p>Change this error message for pages not found in public/404.html</p>
+</body>
+</html>
8 public/500.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<body>
+ <h1>Application error (Apache)</h1>
+ <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
+</body>
+</html>
10 public/dispatch.cgi
@@ -0,0 +1,10 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch
24 public/dispatch.fcgi
@@ -0,0 +1,24 @@
+#!/usr/local/bin/ruby
+#
+# You may specify the path to the FastCGI crash log (a log of unhandled
+# exceptions which forced the FastCGI instance to exit, great for debugging)
+# and the number of requests to process before running garbage collection.
+#
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
+# and the GC period is nil (turned off). A reasonable number of requests
+# could range from 10-100 depending on the memory footprint of your app.
+#
+# Example:
+# # Default log path, normal GC behavior.
+# RailsFCGIHandler.process!
+#
+# # Default log path, 50 requests between GC.
+# RailsFCGIHandler.process! nil, 50
+#
+# # Custom log path, normal GC behavior.
+# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
+#
+require File.dirname(__FILE__) + "/../config/environment"
+require 'fcgi_handler'
+
+RailsFCGIHandler.process!
10 public/dispatch.rb
@@ -0,0 +1,10 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+Dispatcher.dispatch
0 public/favicon.ico
No changes.
750 public/javascripts/controls.js
@@ -0,0 +1,750 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ baseInitialize: function(element, update, options) {
+ this.element = $(element);
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+
+ if (this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || {};
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if (typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (navigator.appVersion.indexOf('MSIE')>0) &&
+ (navigator.userAgent.indexOf('Opera')<0) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix);
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
+ return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--
+ else this.index = this.entryCount-1;
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++
+ else this.index = 0;
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+
+ var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+ var lastTokenPos = this.findLastToken();
+ if (lastTokenPos != -1) {
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value;
+ } else {
+ this.element.value = value;
+ }
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },