Skip to content
Browse files

* initial import.

  • Loading branch information...
0 parents commit cfdee638e5c0b61c40ee79d0dfac1416a259b0c9 @nahi committed Jan 27, 2009
Showing with 9,622 additions and 0 deletions.
  1. +7 −0 COPYING
  2. +340 −0 GPL
  3. +56 −0 RUBYS
  4. +10 −0 Rakefile
  5. +55 −0 app/controllers/application.rb
  6. +132 −0 app/controllers/entry_controller.rb
  7. +32 −0 app/controllers/login_controller.rb
  8. +103 −0 app/helpers/application_helper.rb
  9. +264 −0 app/helpers/entry_helper.rb
  10. +2 −0 app/helpers/login_helper.rb
  11. +5 −0 app/models/user.rb
  12. +77 −0 app/views/entry/list.html.erb
  13. +16 −0 app/views/entry/new.html.erb
  14. +29 −0 app/views/login/index.html.erb
  15. +109 −0 config/boot.rb
  16. +22 −0 config/database.yml
  17. +75 −0 config/environment.rb
  18. +17 −0 config/environments/development.rb
  19. +24 −0 config/environments/production.rb
  20. +22 −0 config/environments/test.rb
  21. +10 −0 config/initializers/inflections.rb
  22. +5 −0 config/initializers/mime_types.rb
  23. +17 −0 config/initializers/new_rails_defaults.rb
  24. +5 −0 config/locales/en.yml
  25. +43 −0 config/routes.rb
  26. +13 −0 db/migrate/20090124133001_create_users.rb
  27. +163 −0 lib/ff.rb
  28. +15 −0 lib/tasks/rcov.rake
  29. +30 −0 public/404.html
  30. +30 −0 public/422.html
  31. +33 −0 public/500.html
  32. +10 −0 public/dispatch.cgi
  33. +24 −0 public/dispatch.fcgi
  34. +10 −0 public/dispatch.rb
  35. 0 public/favicon.ico
  36. BIN public/images/rails.png
  37. +274 −0 public/index.html
  38. +2 −0 public/javascripts/application.js
  39. +963 −0 public/javascripts/controls.js
  40. +973 −0 public/javascripts/dragdrop.js
  41. +1,128 −0 public/javascripts/effects.js
  42. +4,320 −0 public/javascripts/prototype.js
  43. +5 −0 public/robots.txt
  44. +4 −0 script/about
  45. +3 −0 script/console
  46. +3 −0 script/dbconsole
  47. +3 −0 script/destroy
  48. +3 −0 script/generate
  49. +3 −0 script/performance/benchmarker
  50. +3 −0 script/performance/profiler
  51. +3 −0 script/performance/request
  52. +3 −0 script/plugin
  53. +3 −0 script/process/inspector
  54. +3 −0 script/process/reaper
  55. +3 −0 script/process/spawner
  56. +3 −0 script/runner
  57. +3 −0 script/server
  58. +9 −0 test/fixtures/users.yml
  59. +8 −0 test/functional/entry_controller_test.rb
  60. +12 −0 test/functional/login_controller_test.rb
  61. +9 −0 test/performance/browsing_test.rb
  62. +38 −0 test/test_helper.rb
  63. +33 −0 test/unit/user_test.rb
7 COPYING
@@ -0,0 +1,7 @@
+ffp
+Copyright (C) 2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
+
+This program is copyrighted free software by NAKAMURA, Hiroshi. You can
+redistribute it and/or modify it under the same terms of Ruby's license;
+either the dual license version in 2003 (see the file RUBYS), or any later
+version.
340 GPL
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
56 RUBYS
@@ -0,0 +1,56 @@
+Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
+You can redistribute it and/or modify it under either the terms of the GPL
+(see the file GPL), or the conditions below:
+
+ 1. You may make and give away verbatim copies of the source form of the
+ software without restriction, provided that you duplicate all of the
+ original copyright notices and associated disclaimers.
+
+ 2. You may modify your copy of the software in any way, provided that
+ you do at least ONE of the following:
+
+ a) place your modifications in the Public Domain or otherwise
+ make them Freely Available, such as by posting said
+ modifications to Usenet or an equivalent medium, or by allowing
+ the author to include your modifications in the software.
+
+ b) use the modified software only within your corporation or
+ organization.
+
+ c) give non-standard binaries non-standard names, with
+ instructions on where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+ 3. You may distribute the software in object code or binary form,
+ provided that you do at least ONE of the following:
+
+ a) distribute the binaries and library files of the software,
+ together with instructions (in the manual page or equivalent)
+ on where to get the original distribution.
+
+ b) accompany the distribution with the machine-readable source of
+ the software.
+
+ c) give non-standard binaries non-standard names, with
+ instructions on where to get the original software distribution.
+
+ d) make other distribution arrangements with the author.
+
+ 4. You may modify and include the part of the software into any other
+ software (possibly commercial). But some files in the distribution
+ are not written by the author, so that they are not under these terms.
+
+ For the list of those files and their copying conditions, see the
+ file LEGAL.
+
+ 5. The scripts and library files supplied as input to or produced as
+ output from the software do not automatically fall under the
+ copyright of the software, but belong to whomever generated them,
+ and may be sold commercially, and may be aggregated with this
+ software.
+
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE.
10 Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.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'
55 app/controllers/application.rb
@@ -0,0 +1,55 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+require 'ff'
+
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery # :secret => 'b2dff3fcf9a2f9a3980713aebb79677f'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ # filter_parameter_logging :password
+
+ def self.ff_client
+ @@ff ||= FriendFeed::APIClient.new
+ end
+
+ def self.ff_client=(ff_client)
+ @@ff = ff_client
+ end
+
+ def ff_client
+ self.class.ff_client
+ end
+
+private
+
+ def login_required
+ unless ensure_login
+ redirect_to :controller => 'login', :action => 'index'
+ end
+ end
+
+ def ensure_login
+ @user_id ||= session[:user_id]
+ if @user_id
+ @auth = User.find(@user_id)
+ end
+ end
+
+ def set_user(user)
+ @user_id = session[:user_id] = user.id
+ @auth = user
+ end
+
+ def logout
+ @user_id = session[:user_id] = nil
+ @auth = nil
+ end
+end
132 app/controllers/entry_controller.rb
@@ -0,0 +1,132 @@
+class EntryController < ApplicationController
+ before_filter :login_required
+
+ NUM_DEFAULT = '30'
+
+ verify :only => :list,
+ :method => :get,
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def list
+ @room = params[:room]
+ @user = params[:user]
+ @service = params[:service]
+ @start = (params[:start] || '0').to_i
+ @num = (params[:num] || NUM_DEFAULT).to_i
+ @entry_fold = (!@user and !@service and params[:fold] != 'no')
+ opt = {
+ :start => @start,
+ :num => @num,
+ :service => @service
+ }
+ if @user
+ @entries = ff_client.get_user_entries(@auth.name, @auth.remote_key, @user, opt)
+ elsif @room
+ room = @room
+ room = nil if room == '*'
+ @entries = ff_client.get_room_entries(@auth.name, @auth.remote_key, room, opt)
+ else
+ @entries = ff_client.get_home_entries(@auth.name, @auth.remote_key, opt)
+ end
+ @compact = true
+ @post = true
+ @post_comment = false
+ @entries ||= []
+ end
+
+ def index
+ redirect_to :action => 'list'
+ end
+
+ verify :only => :show,
+ :method => :get,
+ :params => [:id],
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def show
+ @eid = params[:id]
+ @entries = ff_client.get_entry(@auth.name, @auth.remote_key, @eid)
+ @compact = false
+ @post = false
+ @post_comment = true
+ @entries ||= []
+ render :action => 'list'
+ end
+
+ def new
+ @room = params[:room]
+ end
+
+ verify :only => :add,
+ :method => :post,
+ :params => [:body],
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def add
+ body = params[:body]
+ link = params[:link]
+ room = params[:room]
+ link = nil if link and link.empty?
+ room = nil if room and room.empty?
+ if body
+ ff_client.post(@auth.name, @auth.remote_key, body, link, nil, nil, nil, room)
+ end
+ redirect_to :action => 'list'
+ end
+
+ verify :only => :delete,
+ :method => :get,
+ :params => [:id],
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def delete
+ id = params[:id]
+ comment = params[:comment]
+ do_delete(id, comment, false)
+ flash[:deleted_id] = id
+ flash[:deleted_comment] = comment
+ redirect_to :action => 'list'
+ end
+
+ verify :only => :undelete,
+ :method => :get,
+ :params => [:id],
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def undelete
+ id = params[:id]
+ comment = params[:comment]
+ do_delete(id, comment, true)
+ redirect_to :action => 'list'
+ end
+
+ def do_delete(id, comment = nil, undelete = false)
+ if comment and !comment.empty?
+ logger.info([id, comment, undelete].inspect)
+ ff_client.delete_comment(@auth.name, @auth.remote_key, id, comment, undelete)
+ else
+ logger.info([id, comment, undelete].inspect)
+ ff_client.delete(@auth.name, @auth.remote_key, id, undelete)
+ end
+ end
+
+ verify :only => :add_comment,
+ :method => :post,
+ :params => [:id, :body],
+ :add_flash => {:error => 'verify failed'},
+ :redirect_to => {:action => 'list'}
+
+ def add_comment
+ eid = params[:id]
+ body = params[:body]
+ if eid and body
+ ff_client.post_comment(@auth.name, @auth.remote_key, eid, body)
+ end
+ redirect_to :action => 'list'
+ end
+end
32 app/controllers/login_controller.rb
@@ -0,0 +1,32 @@
+require 'httpclient'
+
+
+class LoginController < ApplicationController
+ def index
+ if ensure_login
+ redirect_to :controller => 'entry'
+ end
+ end
+
+ def clear
+ logout
+ redirect_to :action => 'index'
+ end
+
+ def authenticate
+ name, remote_key = params[:name], params[:remote_key]
+ if ff_client.validate(name, remote_key)
+ user = User.new
+ user.name = name
+ user.remote_key = remote_key
+ unless user.save
+ flash[:error] = 'illegal auth credentials given'
+ redirect_to :action => 'index'
+ end
+ set_user(user)
+ redirect_to :controller => 'entry'
+ else
+ redirect_to :action => 'index'
+ end
+ end
+end
103 app/helpers/application_helper.rb
@@ -0,0 +1,103 @@
+# Methods added to this helper will be available to all templates in the application.
+
+require 'time'
+
+
+module ApplicationHelper
+ APPNAME = 'ffp'
+ DATE_THRESHOLD = 24 - 8
+
+ def appname
+ h(APPNAME)
+ end
+
+ def service_icon(service)
+ icon_url = v(service, 'iconUrl')
+ name = v(service, 'name')
+ if icon_url and name
+ image_tag(icon_url, :alt => h(name))
+ end
+ end
+
+ def room(room)
+ name = v(room, 'name')
+ nickname = v(room, 'nickname')
+ if name and nickname
+ link_to(h(name), :controller => 'entry', :action => 'list', :room => u(nickname))
+ end
+ end
+
+ def user(user)
+ nickname = v(user, 'nickname')
+ name = v(user, 'name')
+ if name
+ if nickname
+ link_to(h(name), :controller => 'entry', :action => 'list', :user => u(nickname))
+ else
+ h(name)
+ end
+ end
+ end
+
+ def via(via)
+ name = v(via, 'name')
+ link = v(via, 'url')
+ if link
+ %Q[via #{link_to(h(name), link)}</a>]
+ elsif name
+ %Q[via #{h(name)}]
+ end
+ end
+
+ def image_size(width, height)
+ "#{h(width)}x#{h(height)}"
+ end
+
+ def date(time, compact = true)
+ return unless time
+ unless time.is_a?(Time)
+ time = Time.parse(time.to_s).localtime
+ end
+ if !compact
+ body = h(time.strftime("%Y/%m/%d %H:%M:%S"))
+ elsif Time.now - time < DATE_THRESHOLD.hour
+ body = h(time.strftime("%H:%M"))
+ else
+ body = h(time.strftime("%m/%d"))
+ end
+ latest(time, body)
+ end
+
+ def latest(time, body)
+ case elapsed(time)
+ when (-1.hour)..(1.hour) # may have a time lag
+ %Q[<span class="latest1">#{body}</span>]
+ when 0..3.hour
+ %Q[<span class="latest2">#{body}</span>]
+ when 0..6.hour
+ %Q[<span class="latest3">#{body}</span>]
+ else
+ body
+ end
+ end
+
+ def elapsed(time)
+ if time
+ Time.now - time
+ end
+ end
+
+ def link_url(url)
+ link_to(h(url), url)
+ end
+
+ def q(str)
+ %Q["#{str}"]
+ end
+
+ def v(hash, *keywords)
+ keywords.inject(hash) { |r, k|
+ r[k] if r
+ }
+ end
+end
264 app/helpers/entry_helper.rb
@@ -0,0 +1,264 @@
+module EntryHelper
+ FF_ICON_URL_BASE = 'http://friendfeed.com/static/images/'
+ LIKE_ICON_URL = FF_ICON_URL_BASE + 'smile.png'
+ COMMENT_ICON_URL = FF_ICON_URL_BASE + 'comment-lighter.png'
+ DELETE_ICON_URL = FF_ICON_URL_BASE + 'delete.png'
+
+ def icon(entry)
+ service_icon(v(entry, 'service'))
+ end
+
+ def service(entry)
+ room = v(entry, 'room')
+ if room
+ room(room)
+ else
+ name = v(entry, 'service', 'name')
+ service_id = v(entry, 'service', 'id')
+ user = v(entry, 'user', 'nickname')
+ if name and service_id
+ if user
+ link_to(h(name), :controller => 'entry', :action => 'list', :user => u(user), :service => u(service_id))
+ else
+ # pseudo friend!
+ h(name)
+ end
+ end
+ end
+ end
+
+ def content(entry)
+ common = common_content(entry)
+ service_id = v(entry, 'service', 'id')
+ case service_id
+ when 'brightkite'
+ brightkite_content(common, entry)
+ when 'twitter'
+ twitter_content(common, entry)
+ else
+ common
+ end
+ end
+
+ def common_content(entry)
+ title = v(entry, 'title')
+ link = v(entry, 'link')
+ if link and with_link?(v(entry, 'service'))
+ content = link_to(h(q(title)), link)
+ else
+ content = h(q(title))
+ end
+ medias = v(entry, 'media')
+ if medias and !medias.empty?
+ content += "<br/>\n" + content_with_media(title, medias)
+ end
+ content
+ end
+
+ def with_link?(service)
+ service_id = v(service, 'id')
+ entry_type = v(service, 'entryType')
+ entry_type != 'message' and !['twitter'].include?(service_id)
+ end
+
+ def content_with_media(title, medias)
+ medias.collect { |media|
+ media_title = v(media, 'title')
+ media_link = v(media, 'link')
+ tbs = v(media, 'thumbnails')
+ if tbs and !tbs.empty?
+ safe_content = tbs.collect { |tb|
+ tb_url = v(tb, 'url')
+ tb_width = v(tb, 'width')
+ tb_height = v(tb, 'height')
+ if tb_url
+ image_tag(tb_url,
+ :alt => h(media_title), :size => image_size(tb_width, tb_height))
+ end
+ }.join(' ')
+ else
+ safe_content = h(media_title)
+ end
+ link_to(safe_content, media_link)
+ }.join(' ')
+ end
+
+ def brightkite_content(common, entry)
+ lat = v(entry, 'geo', 'lat')
+ long = v(entry, 'geo', 'long')
+ if lat and long
+ zoom = 13
+ width = 160
+ height = 80
+ tb = "http://maps.google.com/staticmap?zoom=#{h(zoom)}&size=#{image_size(width, height)}&maptype=mobile&markers=#{lat},#{long}"
+ title = v(entry, 'title')
+ link = "http://maps.google.com/maps?q=#{lat},#{long}+%28#{u(title)}%29"
+ content = link_to(image_tag(tb, :alt => h(title), :size => image_size(width, height)), link)
+ medias = v(entry, 'media')
+ if medias and !medias.empty?
+ common + ' ' + content
+ else
+ common + "<br/>\n" + content
+ end
+ else
+ common
+ end
+ end
+
+ # TODO: uglish
+ def twitter_content(common, entry)
+ common = common.sub(/\A&quot;(.*)&quot;\z/) { $1 }
+ common = common.gsub(/((?:http|https):\/\/\S+)/) { link_to(h($1), $1) }
+ common = common.gsub(/@([a-zA-Z0-9_]+)/) {
+ link_to('@' + $1, "http://twitter.com/#{$1}")
+ }
+ '&quot;' + common + '&quot;'
+ end
+
+ def via(entry)
+ super(v(entry, 'via'))
+ end
+
+ def likes(entry)
+ likes = v(entry, 'likes')
+ if likes and !likes.empty?
+ like_icon + v(entry, 'likes').collect { |like| user(like) }.join(' ')
+ end
+ end
+
+ def published(entry, compact)
+ published = v(entry, 'published')
+ date(published, compact)
+ end
+
+ def user(entry)
+ super(v(entry, 'user'))
+ end
+
+ def like_icon
+ image_tag(LIKE_ICON_URL, :alt => 'like')
+ end
+
+ def comment_icon
+ image_tag(COMMENT_ICON_URL, :alt => 'comment')
+ end
+
+ def delete_icon
+ image_tag(DELETE_ICON_URL, :alt => 'delete')
+ end
+
+ def comment(comment)
+ h(v(comment, 'body'))
+ end
+
+ def post_comment_form
+ str = ''
+ room = (@room != '*') ? @room : nil
+ if room
+ str = hidden_field_tag('room', room) + h(room) + ': '
+ end
+ str += text_field_tag('body') + submit_tag('post')
+ str += ' ' + link_to(h('[extended]'), :action => 'new', :room => u(room))
+ str
+ end
+
+ def logout_link
+ link_to(h('[logout]'), :controller => 'login', :action => 'clear')
+ end
+
+ def page_links
+ return unless defined?(@start)
+ links = []
+ label = '[<<]'
+ if @start - @num >= 0
+ links << link_to(h(label), list_opt(:action => 'list', :start => 0, :num => @num))
+ else
+ links << h(label)
+ end
+ label = '[<]'
+ if @start - @num >= 0
+ links << link_to(h(label), list_opt(:action => 'list', :start => @start - @num, :num => @num))
+ else
+ links << h(label)
+ end
+ label = '[home]'
+ if @room or @user or @service
+ links << link_to(h(label), :action => 'list')
+ else
+ links << h(label)
+ end
+ label = '[rooms]'
+ if @room == '*'
+ links << h(label)
+ else
+ links << link_to(h(label), :action => 'list', :room => '*')
+ end
+ label = '[>]'
+ links << link_to(h(label), list_opt(:action => 'list', :start => @start + @num, :num => @num))
+ links.join(' ')
+ end
+
+ def post_comment_link(entry)
+ eid = v(entry, 'id')
+ link_to(comment_icon, :action => 'show', :id => u(eid))
+ end
+
+ def delete_link(entry)
+ eid = v(entry, 'id')
+ name = v(entry, 'user', 'nickname')
+ if name == @auth.name
+ link_to(delete_icon, {:action => 'delete', :id => u(eid)}, :confirm => 'Are you sure?')
+ end
+ end
+
+ def undelete_link(id, comment)
+ link_to(h('Deleted. UNDO?'), :action => 'undelete', :id => u(id), :comment => u(comment))
+ end
+
+ def delete_comment_link(entry, comment)
+ eid = v(entry, 'id')
+ cid = v(comment, 'id')
+ name = v(comment, 'user', 'nickname')
+ if name == @auth.name
+ link_to(delete_icon, {:action => 'delete', :id => u(eid), :comment => u(cid)}, :confirm => 'Are you sure?')
+ end
+ end
+
+ def list_opt(hash = {})
+ {
+ :room => @room,
+ :user => @user,
+ :service => @service
+ }.merge(hash)
+ end
+
+ class Fold
+ attr_accessor :fold_entries
+
+ def initialize(fold_entries)
+ @fold_entries = fold_entries
+ end
+ end
+
+ def fold(entries)
+ result = []
+ seq = 0
+ prev = nil
+ entries.each do |entry|
+ pair = [v(entry, 'user', 'nickname'), v(entry, 'service', 'id')]
+ if pair == prev
+ seq += 1
+ else
+ if seq >= 2
+ result << Fold.new(seq - 1)
+ end
+ seq = 0
+ end
+ prev = pair
+ if seq < 2
+ result << entry
+ end
+ end
+ result
+ end
+end
2 app/helpers/login_helper.rb
@@ -0,0 +1,2 @@
+module LoginHelper
+end
5 app/models/user.rb
@@ -0,0 +1,5 @@
+class User < ActiveRecord::Base
+ TEXT_MAXLEN = 255
+ validates_length_of :name, :in => 1..TEXT_MAXLEN
+ validates_length_of :remote_key, :in => 1..TEXT_MAXLEN
+end
77 app/views/entry/list.html.erb
@@ -0,0 +1,77 @@
+<html>
+ <head>
+ <title><%= appname %> entries</title>
+ <style type="text/css">
+ a img { border: none; }
+ p.error { color: red; }
+ .latest1 { color: #FF0000; }
+ .latest2 { color: #C00000; }
+ .latest3 { color: #800000; }
+ </style>
+ </head>
+ <body>
+ <h1><%= link_to(appname, :action => 'list') %> entries</h1>
+ <% if flash[:deleted_id] %>
+ <p><%= undelete_link(flash[:deleted_id], flash[:deleted_comment]) %></p>
+ <% end %>
+ <% if @post %>
+ <% form_tag(:action => 'add') do %>
+ <p><%= post_comment_form %> <%= logout_link %></p>
+ <% end %>
+ <% end %>
+ <%= page_links %>
+ <ul>
+ <%
+ (@entry_fold ? fold(@entries) : @entries).each do |entry|
+ if entry.respond_to?(:fold_entries)
+ %>
+ <ul>
+ <li><%= link_to(h("#{entry.fold_entries} entrie(s) from same service"), list_opt(:action => 'list', :start => @start, :num => @num, :fold => 'no')) %></li>
+ </ul>
+ <%
+ next
+ end
+ next if v(entry, 'hidden')
+ %>
+ <li>
+ <%= icon(entry) %>
+ <%= published(entry, @compact) %>
+ <%= user(entry) %>@<%= service(entry) %>:
+ <%= content(entry) %>
+ <%= via(entry) %>
+ <%= likes(entry) %>
+ <%= delete_link(entry) %>
+ <% comments = v(entry, 'comments') %>
+ <% if comments and !comments.empty? %>
+ <ul>
+ <% if @compact and comments.size > 4 %>
+ <li><%= link_to("#{comments.size - 3} more comment(s)", :action => 'show', :id => u(v(entry, 'id'))) %></li>
+ <% comments = comments[-3, 3] %>
+ <% end %>
+ <% comments.each do |c| %>
+ <li>
+ <%= date(v(c, 'date'), @compact) %></a>
+ <%= comment(c) %>
+ by <%= user(c) %>
+ <%= via(c) %>
+ <%= delete_comment_link(entry, c) %>
+ <%= post_comment_link(entry) if !@post_comment and c == comments.last %>
+ </li>
+ <% end %>
+ </ul>
+ <% else %>
+ <%= post_comment_link(entry) unless @post_comment %>
+ <% end %>
+ <% if @post_comment %>
+ <% form_tag(:action => 'add_comment', :id => u(@eid)) do %>
+ <p>
+ <%= text_field_tag('body') %> <%= submit_tag('post') %>
+ </p>
+ <% end %>
+ <% end %>
+ </li>
+ <% end %>
+ </ul>
+ <%= page_links %>
+ </body>
+</html>
16 app/views/entry/new.html.erb
@@ -0,0 +1,16 @@
+<html>
+ <head>
+ <title><%= appname %> new entry</title>
+ </head>
+ <body>
+ <h1><%= link_to(appname, :action => 'list') %> new entry</h1>
+ <% form_tag :action => 'add' do %>
+ <p>
+ message: <%= text_field_tag('body') %><br/>
+ link: <%= text_field_tag('link') %><br/>
+ room: <%= text_field_tag('room', @room) %><br/>
+ <%= submit_tag 'post' %>
+ </p>
+ <% end %>
+ </body>
+</html>
29 app/views/login/index.html.erb
@@ -0,0 +1,29 @@
+<html>
+ <head>
+ <title><%= appname %> login</title>
+ </head>
+ <body>
+ <h1><%= appname %> login</h1>
+ <% form_tag :action => 'authenticate' do %>
+ <% if flash[:error] %>
+ <p class="error"><%= flash[:error] %></p>
+ <% end %>
+ <p>
+ FriendFeed user nickname: <%= text_field_tag 'name' %><br/>
+ Remote key(NOT A PASSWORD): <%= password_field_tag 'remote_key' %><br/>
+ <%= submit_tag 'login' %>
+ </p>
+ <% end %>
+ <hr/>
+ <p>Bear in mind...</p>
+ <ul>
+ <li>Do not enter your FriendFeed PASSWORD here. You can check <%= link_to('your remote key', 'https://friendfeed.com/account/api') %> by yourself.</li>
+ <li>NO WARRANTY; This service is provided on an "AS IS" basis without warranty of any kind.</li>
+ <li>The pairs of nickname and remote key are stored in DB of this server for accessing FriendFeed API.</li>
+ <li>Your feed is NOT stored in the DB.</li>
+ <li>Let me know if you have any trouble on this service at <%= link_url('http://friendfeed.com/ffpd') %>.</li>
+ </ul>
+ <hr/>
+ <p>NAKAMURA, Hiroshi a.k.a. NaHi - <%= link_url('http://friendfeed.com/nahi') %></p>
+ </body>
+</html>
109 config/boot.rb
@@ -0,0 +1,109 @@
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+ class << self
+ def boot!
+ unless booted?
+ preinitialize
+ pick_boot.run
+ end
+ end
+
+ def booted?
+ defined? Rails::Initializer
+ end
+
+ def pick_boot
+ (vendor_rails? ? VendorBoot : GemBoot).new
+ end
+
+ def vendor_rails?
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
+ end
+
+ def preinitialize
+ load(preinitializer_path) if File.exist?(preinitializer_path)
+ end
+
+ def preinitializer_path
+ "#{RAILS_ROOT}/config/preinitializer.rb"
+ end
+ end
+
+ class Boot
+ def run
+ load_initializer
+ Rails::Initializer.run(:set_load_path)
+ end
+ end
+
+ class VendorBoot < Boot
+ def load_initializer
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+ Rails::Initializer.run(:install_gem_spec_stubs)
+ end
+ end
+
+ class GemBoot < Boot
+ def load_initializer
+ self.class.load_rubygems
+ load_rails_gem
+ require 'initializer'
+ end
+
+ def load_rails_gem
+ if version = self.class.gem_version
+ gem 'rails', version
+ else
+ gem 'rails'
+ end
+ rescue Gem::LoadError => load_error
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+ exit 1
+ end
+
+ class << self
+ def rubygems_version
+ Gem::RubyGemsVersion rescue nil
+ end
+
+ def gem_version
+ if defined? RAILS_GEM_VERSION
+ RAILS_GEM_VERSION
+ elsif ENV.include?('RAILS_GEM_VERSION')
+ ENV['RAILS_GEM_VERSION']
+ else
+ parse_gem_version(read_environment_rb)
+ end
+ end
+
+ def load_rubygems
+ require 'rubygems'
+ min_version = '1.3.1'
+ unless rubygems_version >= min_version
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+ exit 1
+ end
+
+ rescue LoadError
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+ exit 1
+ end
+
+ def parse_gem_version(text)
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+ end
+
+ private
+ def read_environment_rb
+ File.read("#{RAILS_ROOT}/config/environment.rb")
+ end
+ end
+ end
+end
+
+# All that for this:
+Rails.boot!
22 config/database.yml
@@ -0,0 +1,22 @@
+# SQLite version 3.x
+# gem install sqlite3-ruby (not necessary on OS X Leopard)
+development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ pool: 5
+ timeout: 5000
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: sqlite3
+ database: db/production.sqlite3
+ pool: 5
+ timeout: 5000
75 config/environment.rb
@@ -0,0 +1,75 @@
+# Be sure to restart your 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'
+
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION
+
+# 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 over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+ # See Rails::Configuration for more options.
+
+ # Skip frameworks you're not going to use. To use Rails without a database
+ # you must remove the Active Record framework.
+ # config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
+
+ # Specify gems that this application depends on.
+ # They can then be installed with "rake gems:install" on new installations.
+ # You have to specify the :lib option for libraries, where the Gem name (sqlite3-ruby) differs from the file itself (sqlite3)
+ # config.gem "bj"
+ # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net"
+ # config.gem "sqlite3-ruby", :lib => "sqlite3"
+ # config.gem "aws-s3", :lib => "aws/s3"
+
+ # Only load the plugins named here, in the order given. By default, all plugins
+ # in vendor/plugins are loaded in alphabetical order.
+ # :all can be used as a placeholder for all plugins not explicitly named
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+
+ # 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
+
+ # Make Time.zone default to the specified zone, and make Active Record store time values
+ # in the database in UTC, and return them converted to the specified local zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time.
+ config.time_zone = 'UTC'
+
+ # The internationalization framework can be changed to have another default locale (standard is :en) or more load paths.
+ # All files from config/locales/*.rb,yml are added automatically.
+ # config.i18n.load_path << Dir[File.join(RAILS_ROOT, 'my', 'locales', '*.{rb,yml}')]
+ # config.i18n.default_locale = :de
+
+ # Your secret key for verifying cookie session data integrity.
+ # If you change this key, all old sessions will become invalid!
+ # Make sure the secret is at least 30 characters and all random,
+ # no regular words or you'll be exposed to dictionary attacks.
+ config.action_controller.session = {
+ :session_key => '_ff1_session',
+ :secret => 'cb8724bb405f34efcaea391cce2380850d80455f8d01de24a0d385a2452a8772ec7a71d765c85c321d17a1f6705d9cfc642c62c39b9be9594f05b49e8e22e6ae'
+ }
+
+ # Use the database for sessions instead of the cookie-based default,
+ # which shouldn't be used to store highly confidential information
+ # (create the session table with "rake db:sessions:create")
+ # config.action_controller.session_store = :active_record_store
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Activate observers that should always be running
+ # Please note that observers generated using script/generate observer need to have an _observer suffix
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+end
17 config/environments/development.rb
@@ -0,0 +1,17 @@
+# 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
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_view.debug_rjs = true
+config.action_controller.perform_caching = false
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false
24 config/environments/production.rb
@@ -0,0 +1,24 @@
+# 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
+
+# Enable threaded mode
+# config.threadsafe!
+
+# 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
+
+# Use a different cache store in production
+# config.cache_store = :mem_cache_store
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host = "http://assets.example.com"
+
+# Disable delivery errors, bad email addresses will be ignored
+# config.action_mailer.raise_delivery_errors = false
22 config/environments/test.rb
@@ -0,0 +1,22 @@
+# 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
+
+# Disable request forgery protection in test environment
+config.action_controller.allow_forgery_protection = false
+
+# Tell Action Mailer 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
10 config/initializers/inflections.rb
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::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
5 config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
+# Mime::Type.register_alias "text/html", :iphone
17 config/initializers/new_rails_defaults.rb
@@ -0,0 +1,17 @@
+# These settings change the behavior of Rails 2 apps and will be defaults
+# for Rails 3. You can remove this initializer when Rails 3 is released.
+
+if defined?(ActiveRecord)
+ # Include Active Record class name as root for JSON serialized output.
+ ActiveRecord::Base.include_root_in_json = true
+
+ # Store the full class name (including module namespace) in STI type column.
+ ActiveRecord::Base.store_full_sti_class = true
+end
+
+# Use ISO 8601 format for JSON serialized times and dates.
+ActiveSupport.use_standard_json_time_format = true
+
+# Don't escape HTML entities in JSON, leave that for the #json_escape helper.
+# if you're including raw json in an HTML page.
+ActiveSupport.escape_html_entities_in_json = false
5 config/locales/en.yml
@@ -0,0 +1,5 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ hello: "Hello world"
43 config/routes.rb
@@ -0,0 +1,43 @@
+ActionController::Routing::Routes.draw do |map|
+ # The priority is based upon order of creation: first created -> highest priority.
+
+ # Sample of regular route:
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
+ # Keep in mind you can assign values other than :controller and :action
+
+ # Sample of named route:
+ # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
+ # This route can be invoked with purchase_url(:id => product.id)
+
+ # Sample resource route (maps HTTP verbs to controller actions automatically):
+ # map.resources :products
+
+ # Sample resource route with options:
+ # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
+
+ # Sample resource route with sub-resources:
+ # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
+
+ # Sample resource route with more complex sub-resources
+ # map.resources :products do |products|
+ # products.resources :comments
+ # products.resources :sales, :collection => { :recent => :get }
+ # end
+
+ # Sample resource route within a namespace:
+ # map.namespace :admin do |admin|
+ # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb)
+ # admin.resources :products
+ # end
+
+ # You can have the root of your site routed with map.root -- just remember to delete public/index.html.
+ # map.root :controller => "welcome"
+
+ # See how all your routes lay out with "rake routes"
+
+ # Install the default routes as the lowest priority.
+ # Note: These default routes make all actions in every controller accessible via GET requests. You should
+ # consider removing the them or commenting them out if you're using named routes and resources.
+ map.connect ':controller/:action/:id'
+ map.connect ':controller/:action/:id.:format'
+end
13 db/migrate/20090124133001_create_users.rb
@@ -0,0 +1,13 @@
+class CreateUsers < ActiveRecord::Migration
+ def self.up
+ create_table :users do |t|
+ t.string :name, :null => false
+ t.string :remote_key, :null => false
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :users
+ end
+end
163 lib/ff.rb
@@ -0,0 +1,163 @@
+require 'httpclient'
+require 'uri'
+require 'json'
+require 'monitor'
+
+
+module FriendFeed
+ class APIClient
+ URL_BASE = 'http://friendfeed.com/api/'
+
+ def initialize
+ @client = HTTPClient.new
+ @client.extend(MonitorMixin)
+ end
+
+ def validate(name, remote_key)
+ uri = uri('validate')
+ client_sync(uri, name, remote_key) do |client|
+ client.get(uri).status == 200
+ end
+ end
+
+ def get_profile(name, remote_key)
+ uri = uri("user/#{name}/profile")
+ client_sync(uri, name, remote_key) do |client|
+ JSON.parse(client.get(uri).content)
+ end
+ end
+
+ def get_entry(name, remote_key, eid)
+ uri = uri("feed/entry/#{eid}")
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri)
+ end
+ end
+
+ def get_home_entries(name, remote_key, opt = {})
+ uri = uri("feed/home")
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri, opt)
+ end
+ end
+
+ def get_user_entries(name, remote_key, user, opt = {})
+ uri = uri("feed/user/#{user}")
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri, opt)
+ end
+ end
+
+ def get_room_entries(name, remote_key, room = nil, opt = {})
+ if room.nil?
+ uri = uri("feed/rooms")
+ else
+ uri = uri("feed/room/#{room}")
+ end
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri, opt)
+ end
+ end
+
+ def get_comments(name, remote_key)
+ uri = uri("feed/user/#{name}/comments")
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri)
+ end
+ end
+
+ def get_likes(name, remote_key)
+ uri = uri("feed/user/#{name}/likes")
+ client_sync(uri, name, remote_key) do |client|
+ get_feed(client, uri)
+ end
+ end
+
+ def post(name, remote_key, title, link = nil, comment = nil, images = nil, files = nil, room = nil)
+ uri = uri("share")
+ query = { 'title' => title }
+ query['link'] = link if link
+ query['comment'] = comment if comment
+ if images
+ images.each_with_index do |image, idx|
+ image_url, image_link = image
+ query["image#{idx}_url"] = image_url
+ query["image#{idx}_link"] = image_link
+ end
+ end
+ if files
+ files.each_with_index do |file, idx|
+ file, file_link = file
+ query["file#{idx}"] = file
+ query["file#{idx}_link"] = file_link
+ end
+ end
+ query['room'] = room if room
+ client_sync(uri, name, remote_key) do |client|
+ client.post(uri, query)
+ end
+ end
+
+ def delete(name, remote_key, entry, undelete = false)
+ uri = uri("entry/delete")
+ query = { 'entry' => entry }
+ query['undelete'] = 1 if undelete
+ client_sync(uri, name, remote_key) do |client|
+ client.post(uri, query)
+ end
+ end
+
+ def post_comment(name, remote_key, entry, body)
+ uri = uri("comment")
+ query = {
+ 'entry' => entry,
+ 'body' => body
+ }
+ client_sync(uri, name, remote_key) do |client|
+ client.post(uri, query)
+ end
+ end
+
+ def delete_comment(name, remote_key, entry, comment, undelete = false)
+ uri = uri("comment/delete")
+ query = {
+ 'entry' => entry,
+ 'comment' => comment
+ }
+ query['undelete'] = 1 if undelete
+ client_sync(uri, name, remote_key) do |client|
+ client.post(uri, query)
+ end
+ end
+
+ private
+
+ def uri(part)
+ uri = URI.parse(File.join(URL_BASE, part))
+ end
+
+ def client_sync(uri, name, remote_key)
+ @client.synchronize do
+ @client.www_auth.basic_auth.challenge(uri, true)
+ @client.set_auth(nil, name, remote_key)
+ result = yield(@client)
+ @client.set_auth(nil, nil, nil)
+ result
+ end
+ end
+
+ def get_feed(client, uri, query = {})
+ JSON.parse(client.get(uri, query).content)['entries']
+ end
+ end
+end
+
+
+if $0 == __FILE__
+ name = ARGV.shift or raise
+ remote_key = ARGV.shift or raise
+ client = FriendFeed::APIClient.new
+ require 'pp'
+ #pp client.get_home_entries(name, remote_key)
+ pp client.get_rooms(name, remote_key)
+end
15 lib/tasks/rcov.rake
@@ -0,0 +1,15 @@
+begin
+ require 'rcov/rcovtask'
+
+ namespace :test do
+ Rcov::RcovTask.new("coverage") do |rcov|
+ rcov.libs << 'test'
+ rcov.test_files = FileList['test/**/*_test.rb']
+ rcov.verbose = true
+ rcov.rcov_opts << '--rails' <<
+ '-x' << File.expand_path('~/.gem') <<
+ '-x' << 'lib'
+ end
+ end
+rescue LoadError
+end
30 public/404.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>The page you were looking for doesn't exist (404)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/404.html -->
+ <div class="dialog">
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+</body>
+</html>
30 public/422.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>The change you wanted was rejected (422)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+</body>
+</html>
33 public/500.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+ <title>We're sorry, but something went wrong (500)</title>
+ <style type="text/css">
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+ div.dialog {
+ width: 25em;
+ padding: 0 4em;
+ margin: 4em auto 0 auto;
+ border: 1px solid #ccc;
+ border-right-color: #999;
+ border-bottom-color: #999;
+ }
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <h1>We're sorry, but something went wrong.</h1>
+ <p>We've been notified about this issue and we'll take a look at it shortly.</p>
+ <p><small>(If you're the administrator of this website, then please read
+ the log file "<%=h RAILS_ENV %>.log"
+ to find out what went wrong.)</small></p>
+ </div>
+</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.
BIN public/images/rails.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 public/index.html
@@ -0,0 +1,274 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <title>Ruby on Rails: Welcome aboard</title>
+ <style type="text/css" media="screen">
+ body {
+ margin: 0;
+ margin-bottom: 25px;
+ padding: 0;
+ background-color: #f0f0f0;
+ font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
+ font-size: 13px;
+ color: #333;
+ }
+
+ h1 {
+ font-size: 28px;
+ color: #000;
+ }
+
+ a {color: #03c}
+ a:hover {
+ background-color: #03c;
+ color: white;
+ text-decoration: none;
+ }
+
+
+ #page {
+ background-color: #f0f0f0;
+ width: 750px;
+ margin: 0;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ #content {
+ float: left;
+ background-color: white;
+ border: 3px solid #aaa;
+ border-top: none;
+ padding: 25px;
+ width: 500px;
+ }
+
+ #sidebar {
+ float: right;
+ width: 175px;
+ }
+
+ #footer {
+ clear: both;
+ }
+
+
+ #header, #about, #getting-started {
+ padding-left: 75px;
+ padding-right: 30px;
+ }
+
+
+ #header {
+ background-image: url("images/rails.png");
+ background-repeat: no-repeat;
+ background-position: top left;
+ height: 64px;
+ }
+ #header h1, #header h2 {margin: 0}
+ #header h2 {
+ color: #888;
+ font-weight: normal;
+ font-size: 16px;
+ }
+
+
+ #about h3 {
+ margin: 0;
+ margin-bottom: 10px;
+ font-size: 14px;
+ }
+
+ #about-content {
+ background-color: #ffd;
+ border: 1px solid #fc0;
+ margin-left: -11px;
+ }
+ #about-content table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 11px;
+ border-collapse: collapse;
+ }
+ #about-content td {
+ padding: 10px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+ #about-content td.name {color: #555}
+ #about-content td.value {color: #000}
+
+ #about-content.failure {
+ background-color: #fcc;
+ border: 1px solid #f00;
+ }
+ #about-content.failure p {
+ margin: 0;
+ padding: 10px;
+ }
+
+
+ #getting-started {
+ border-top: 1px solid #ccc;
+ margin-top: 25px;
+ padding-top: 15px;
+ }
+ #getting-started h1 {
+ margin: 0;
+ font-size: 20px;
+ }
+ #getting-started h2 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: normal;
+ color: #333;
+ margin-bottom: 25px;
+ }
+ #getting-started ol {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #getting-started li {
+ font-size: 18px;
+ color: #888;
+ margin-bottom: 25px;
+ }
+ #getting-started li h2 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 18px;
+ color: #333;
+ }
+ #getting-started li p {
+ color: #555;
+ font-size: 13px;
+ }
+
+
+ #search {
+ margin: 0;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ font-size: 11px;
+ }
+ #search input {
+ font-size: 11px;
+ margin: 2px;
+ }
+ #search-text {width: 170px}
+
+
+ #sidebar ul {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #sidebar ul h3 {
+ margin-top: 25px;
+ font-size: 16px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ccc;
+ }
+ #sidebar li {
+ list-style-type: none;
+ }
+ #sidebar ul.links li {
+ margin-bottom: 5px;
+ }
+
+ </style>
+ <script type="text/javascript" src="javascripts/prototype.js"></script>
+ <script type="text/javascript" src="javascripts/effects.js"></script>
+ <script type="text/javascript">
+ function about() {
+ if (Element.empty('about-content')) {
+ new Ajax.Updater('about-content', 'rails/info/properties', {
+ method: 'get',
+ onFailure: function() {Element.classNames('about-content').add('failure')},
+ onComplete: function() {new Effect.BlindDown('about-content', {duration: 0.25})}
+ });
+ } else {
+ new Effect[Element.visible('about-content') ?
+ 'BlindUp' : 'BlindDown']('about-content', {duration: 0.25});
+ }
+ }
+
+ window.onload = function() {
+ $('search-text').value = '';
+ $('search').onsubmit = function() {
+ $('search-text').value = 'site:rubyonrails.org ' + $F('search-text');
+ }
+ }
+ </script>
+ </head>
+ <body>
+ <div id="page">
+ <div id="sidebar">
+ <ul id="sidebar-items">
+ <li>
+ <form id="search" action="http://www.google.com/search" method="get">
+ <input type="hidden" name="hl" value="en" />
+ <input type="text" id="search-text" name="q" value="site:rubyonrails.org " />
+ <input type="submit" value="Search" /> the Rails site
+ </form>
+ </li>
+
+ <li>
+ <h3>Join the community</h3>
+ <ul class="links">
+ <li><a href="http://www.rubyonrails.org/">Ruby on Rails</a></li>
+ <li><a href="http://weblog.rubyonrails.org/">Official weblog</a></li>
+ <li><a href="http://wiki.rubyonrails.org/">Wiki</a></li>
+ </ul>
+ </li>
+
+ <li>
+ <h3>Browse the documentation</h3>
+ <ul class="links">
+ <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
+ <li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li>
+ <li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ <div id="content">
+ <div id="header">
+ <h1>Welcome aboard</h1>
+ <h2>You&rsquo;re riding Ruby on Rails!</h2>
+ </div>
+
+ <div id="about">
+ <h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
+ <div id="about-content" style="display: none"></div>
+ </div>
+
+ <div id="getting-started">
+ <h1>Getting started</h1>
+ <h2>Here&rsquo;s how to get rolling:</h2>
+
+ <ol>
+ <li>
+ <h2>Use <tt>script/generate</tt> to create your models and controllers</h2>
+ <p>To see all available options, run it without parameters.</p>
+ </li>
+
+ <li>
+ <h2>Set up a default route and remove or rename this file</h2>
+ <p>Routes are set up in config/routes.rb.</p>
+ </li>
+
+ <li>
+ <h2>Create your database</h2>
+ <p>Run <tt>rake db:migrate</tt> to create your database. If you're not using SQLite (the default), edit <tt>config/database.yml</tt> with your username and password.</p>
+ </li>
+ </ol>
+ </div>
+ </div>
+
+ <div id="footer">&nbsp;</div>
+ </div>
+ </body>
+</html>
2 public/javascripts/application.js
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
963 public/javascripts/controls.js
@@ -0,0 +1,963 @@
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// 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.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options