[ANOTHER DRASTIC!] Moving branches/instiki-ar to trunk

This commit is contained in:
Alexey Verkhovsky 2005-11-06 08:28:02 +00:00
commit b876bcc299
199 changed files with 7143 additions and 18857 deletions

View file

@ -1,5 +1,9 @@
* trunk:
Fixed --daemon option.
* instiki-ar:
SQL-based backend (ActiveRecord)
Replaced internal link generator with routing
Fixed --daemon option
Upgraded to Rails 0.14.2
Re-enabled file uploads
* 0.10.2:
Upgraded to Rails 0.13.1

53
README
View file

@ -4,10 +4,20 @@ Admitted, it's YetAnotherWikiClone[http://c2.com/cgi/wiki?WikiWikiClones], but w
on simplicity of installation and running:
Step 1. Download
Step 2. Run "instiki"
Step 3. Chuckle... "There's no step three!" (TM)
Here it should say: "Step 3. Chuckle... "There's no step three!" (TM)"
... but this is a beta version that introduces an SQL-based backend, so:
3. Kill 'instiki'
4. Install SQLite 3 database engine from http://www.sqlite.org/
5. Install SQLite 3 driver for Ruby from http://sqlite-ruby.rubyforge.org/
6. Install Rake from http://rake.rubyforge.org/
7. Execute 'rake db_schema_import create_sessions_table'
8. Make an embarrassed sigh (as I do while writing this)
9. Run 'instiki' again
10. Pat yourself on the shoulder for being such a talented geek
11. At least, there is no step eleven! (TM)
You're now running a perfectly suitable wiki on port 2500
that'll present you with one-step setup, followed by a textarea for the home page
@ -38,21 +48,35 @@ gathering.
===Missing:
* File attachments
===Install from gem:
* Install rubygems
* Run "gem install instiki"
* Change to a directory where you want Instiki to keep its data files (for example, ~/instiki/)
* Run "instiki" - this will create a "storage" directory (for example, ~/instiki/storage), and start a new Wiki service
Make sure that you always launch Instiki from the same working directory, or specify the storage directory in runtime parameters, such as:
instiki --storage ~/instiki/storage
===Command-line options:
* Run "instiki --help"
===History:
* See CHANGELOG
===Migrating Instiki 0.10.2 storage to Instiki-AR database
1. Install Instiki-AR and check that it works (you should be able to create a web, edit and save a HomePage)
2. Execute
ruby script\import_storage \
-t /full/path/to/instiki0.10/storage \
-i /full/path/to/instiki0.10/installation \
-d sqlite (or mysql, or postgres, depending on what you use) \
-o instiki_import.sql
for example:
ruby script\import_storage -t c:\instiki-0.10.2\storage\2500 -i c:\instiki-0.10.2 -d sqlite -o instiki_import.sql
3. This will produce instiki_import.sql file in the current working directory.
Open it in a text editor and inspect carefully.
4. Connect to your production database (e.g., 'sqlite3 db\prod.db'),
and have it execute instiki_import.sql (e.g., '.read instiki_import.sql')
5. Execute ruby script\reset_references
(this script parses all pages for crosslinks between them, so it may take a few minutes)
6. Restart Instiki
7. Go over some pages, especially those with a lot of complex markup, and see if anything is broken.
The most common migration problem is this:
If you open All Pages screen and see a lot of orphaned pages,
you forgot to run ruby script\reset_references after importing the data.
===Download latest from:
* http://rubyforge.org/project/showfiles.php?group_id=186
@ -63,6 +87,11 @@ Make sure that you always launch Instiki from the same working directory, or spe
* same as Ruby's
---
Author:: David Heinemeier Hansson
Authors::
Versions 0.1 to 0.9.1:: David Heinemeier Hansson
Email:: david@loudthinking.com
Weblog:: http://www.loudthinking.com
From 0.9.2 onwards:: Alexey Verkhovsky
Email:: alex@verk.info

View file

@ -46,7 +46,6 @@ class AdminController < ApplicationController
end
def edit_web
system_password = @params['system_password']
if system_password
# form submitted
@ -67,6 +66,7 @@ class AdminController < ApplicationController
flash[:info] = "Web '#{@params['address']}' was successfully updated"
redirect_home(@params['address'])
rescue Instiki::ValidationError => e
logger.warn e.message
@error = e.message
# and re-render the same template again
end

View file

@ -2,42 +2,31 @@
# Likewise will all the methods added be available for all controllers.
class ApplicationController < ActionController::Base
before_filter :set_utf8_http_header, :connect_to_model, :check_snapshot_thread
after_filter :remember_location
before_filter :connect_to_model, :check_authorization, :setup_url_generator, :set_content_type_header, :set_robots_metatag
after_filter :remember_location, :teardown_url_generator
# For injecting a different wiki model implementation. Intended for use in tests
def self.wiki=(the_wiki)
# a global variable is used here because Rails reloads controller and model classes in the
# development environment; therefore, storing it as a class variable does not work
# class variable is, anyway, not much different from a global variable
$instiki_wiki_service = the_wiki
#$instiki_wiki_service = the_wiki
logger.debug("Wiki service: #{the_wiki.to_s}")
end
def self.wiki
$instiki_wiki_service
Wiki.new
end
protected
def authorized?
@web.nil? ||
@web.password.nil? ||
cookies['web_address'] == @web.password ||
password_check(@params['password'])
end
def check_authorization
if in_a_web? and needs_authorization?(@action_name) and not authorized? and
if in_a_web? and authorization_needed? and not authorized?
redirect_to :controller => 'wiki', :action => 'login', :web => @web_name
return false
end
end
def check_snapshot_thread
WikiService.check_snapshot_thread
end
def connect_to_model
@action_name = @params['action'] || 'index'
@web_name = @params['web']
@ -45,14 +34,13 @@ class ApplicationController < ActionController::Base
if @web_name
@web = @wiki.webs[@web_name]
if @web.nil?
render_text "Unknown web '#{@web_name}'", '404 Not Found'
render :status => 404, :text => "Unknown web '#{@web_name}'"
return false
end
end
@page_name = @file_name = @params['id']
@page = @wiki.read_page(@web_name, @page_name) unless @page_name.nil?
@author = cookies['author'] || 'AnonymousCoward'
check_authorization
end
FILE_TYPES = {
@ -71,10 +59,6 @@ class ApplicationController < ActionController::Base
super(file, options)
end
def in_a_web?
not @web_name.nil?
end
def password_check(password)
if password == @web.password
cookies['web_address'] = password
@ -102,28 +86,26 @@ class ApplicationController < ActionController::Base
def redirect_to_page(page_name = @page_name, web = @web_name)
redirect_to :web => web, :controller => 'wiki', :action => 'show',
:id => (page_name || 'HomePage')
:id => (page_name or 'HomePage')
end
@@REMEMBER_NOT = ['locked', 'save', 'back', 'file', 'pic', 'import']
def remember_location
if @response.headers['Status'] == '200 OK'
unless @@REMEMBER_NOT.include? action_name or @request.method != :get
if @request.method == :get and
@response.headers['Status'] == '200 OK' and not
%w(locked save back file pic import).include?(action_name)
@session[:return_to] = @request.request_uri
logger.debug("Session ##{session.object_id}: remembered URL '#{@session[:return_to]}'")
end
logger.debug "Session ##{session.object_id}: remembered URL '#{@session[:return_to]}'"
end
end
def rescue_action_in_public(exception)
message = <<-EOL
render :status => 500, :text => <<-EOL
<html><body>
<h2>Internal Error 500</h2>
<h2>Internal Error</h2>
<p>An application error occurred while processing your request.</p>
<!-- \n#{exception}\n#{exception.backtrace.join("\n")}\n -->
</body></html>
EOL
render_text message, 'Internal Error 500'
end
def return_to_last_remembered
@ -146,16 +128,48 @@ class ApplicationController < ActionController::Base
end
end
def set_utf8_http_header
def set_content_type_header
if %w(rss_with_content rss_with_headlines).include?(action_name)
@response.headers['Content-Type'] = 'text/xml; charset=UTF-8'
else
@response.headers['Content-Type'] = 'text/html; charset=UTF-8'
end
end
def set_robots_metatag
if controller_name == 'wiki' and %w(show published).include? action_name
@robots_metatag_value = 'index,follow'
else
@robots_metatag_value = 'noindex,nofollow'
end
end
def setup_url_generator
PageRenderer.setup_url_generator(UrlGenerator.new(self))
end
def teardown_url_generator
PageRenderer.teardown_url_generator
end
def wiki
$instiki_wiki_service
self.class.wiki
end
def needs_authorization?(action)
not %w( login authenticate published rss_with_content rss_with_headlines ).include?(action)
private
def in_a_web?
not @web_name.nil?
end
def authorization_needed?
not %w( login authenticate published rss_with_content rss_with_headlines ).include?(action_name)
end
def authorized?
@web.password.nil? or
cookies['web_address'] == @web.password or
password_check(@params['password'])
end
end

View file

@ -1,9 +1,4 @@
require 'fileutils'
require 'application'
require 'instiki_errors'
# Controller that is responsible for serving files and pictures.
# Disabled in version 0.10
# Controller responsible for serving files and pictures.
class FileController < ApplicationController
@ -17,7 +12,6 @@ class FileController < ApplicationController
# form supplied
file_yard.upload_file(@file_name, @params['file'])
flash[:info] = "File '#{@file_name}' successfully uploaded"
@web.refresh_pages_with_references(@file_name)
return_to_last_remembered
elsif file_yard.has_file?(@file_name)
send_file(file_yard.file_path(@file_name))
@ -36,7 +30,6 @@ class FileController < ApplicationController
if @params['file']
# form supplied
file_yard.upload_file(@file_name, @params['file'])
@web.refresh_pages_with_references(@file_name)
flash[:info] = "Image '#{@file_name}' successfully uploaded"
return_to_last_remembered
elsif file_yard.has_file?(@file_name)
@ -48,8 +41,6 @@ class FileController < ApplicationController
end
def import
return if file_uploads_disabled?
check_authorization
if @params['file']
@problems = []
@ -71,15 +62,8 @@ class FileController < ApplicationController
protected
def check_allow_uploads
# TODO enable file uploads again after 0.10 release
unless RAILS_ENV == 'test'
render_text 'File uploads are not ready for general use in Instiki 0.10', '403 Forbidden'
return false
end
unless @web.allow_uploads
render_text 'File uploads are blocked by the webmaster', '403 Forbidden'
unless @web.allow_uploads?
render :status => 403, :text => 'File uploads are blocked by the webmaster'
return false
end
end

View file

@ -0,0 +1,32 @@
class RevisionSweeper < ActionController::Caching::Sweeper
observe Revision, Page
def after_save(record)
if record.is_a?(Revision)
expire_caches(record.page)
end
end
def after_delete(record)
if record.is_a?(Page)
expire_caches(record)
end
end
private
def expire_caches(page)
web = page.web
([page.name] + WikiReference.pages_that_reference(page.name)).uniq.each do |page_name|
expire_action :controller => 'wiki', :web => web.address,
:action => %w(show published), :id => page_name
end
expire_action :controller => 'wiki', :web => web.address,
:action => %w(authors recently_revised list)
expire_fragment :controller => 'wiki', :web => web.address,
:action => %w(rss_with_headlines rss_with_content)
end
end

View file

@ -1,10 +1,13 @@
require 'application'
require 'fileutils'
require 'redcloth_for_tex'
require 'parsedate'
require 'zip/zip'
class WikiController < ApplicationController
caches_action :show, :published, :authors, :recently_revised, :list
cache_sweeper :revision_sweeper
layout 'default', :except => [:rss_feed, :rss_with_content, :rss_with_headlines, :tex, :export_tex, :export_html]
def index
@ -42,14 +45,46 @@ class WikiController < ApplicationController
# Within a single web ---------------------------------------------------------
def authors
@authors = @web.select.authors.sort
@page_names_by_author = @web.page_names_by_author
@authors = @page_names_by_author.keys.sort
end
def export_html
stylesheet = File.read(File.join(RAILS_ROOT, 'public', 'stylesheets', 'instiki.css'))
export_pages_as_zip('html') do |page|
@page = page
@link_mode = :export
render_to_string('wiki/print', use_layout = (@params['layout'] != 'no'))
renderer = PageRenderer.new(page.revisions.last)
rendered_page = <<-EOL
<!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">
<head>
<title>#{page.plain_name} in #{@web.name}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
h1#pageName, .newWikiWord a, a.existingWikiWord, .newWikiWord a:hover {
color: ##{@web ? @web.color : "393" };
}
.newWikiWord { background-color: white; font-style: italic; }
#{stylesheet}
</style>
<style type="text/css">
#{@web.additional_style}
</style>
</head>
<body>
#{renderer.display_content_for_export}
<div class="byline">
#{page.revisions? ? "Revised" : "Created" } on #{ page.revised_at.strftime('%B %d, %Y %H:%M:%S') }
by
#{ UrlGenerator.new(self).make_link(page.author.name, @web, nil, { :mode => :export }) }
</div>
</body>
</html>
EOL
rendered_page
end
end
@ -58,7 +93,7 @@ class WikiController < ApplicationController
end
def export_pdf
file_name = "#{@web.address}-tex-#{@web.revised_on.strftime('%Y-%m-%d-%H-%M-%S')}"
file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
file_path = File.join(@wiki.storage_path, file_name)
export_web_to_tex "#{file_path}.tex" unless FileTest.exists? "#{file_path}.tex"
@ -67,7 +102,7 @@ class WikiController < ApplicationController
end
def export_tex
file_name = "#{@web.address}-tex-#{@web.revised_on.strftime('%Y-%m-%d-%H-%M-%S')}.tex"
file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}.tex"
file_path = File.join(@wiki.storage_path, file_name)
export_web_to_tex(file_path) unless FileTest.exists?(file_path)
send_file file_path
@ -141,7 +176,7 @@ class WikiController < ApplicationController
def pdf
page = wiki.read_page(@web_name, @page_name)
safe_page_name = @page.name.gsub(/\W/, '')
file_name = "#{safe_page_name}-#{@web.address}-#{@page.created_at.strftime('%Y-%m-%d-%H-%M-%S')}"
file_name = "#{safe_page_name}-#{@web.address}-#{@page.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
file_path = File.join(@wiki.storage_path, file_name)
export_page_to_tex("#{file_path}.tex") unless FileTest.exists?("#{file_path}.tex")
@ -151,20 +186,30 @@ class WikiController < ApplicationController
end
def print
if @page.nil?
redirect_home
end
@link_mode ||= :show
@renderer = PageRenderer.new(@page.revisions.last)
# to template
end
def published
if @web.published
@page = wiki.read_page(@web_name, @page_name || 'HomePage')
else
redirect_home
if not @web.published?
render(:text => "Published version of web '#{@web_name}' is not available", :status => 404)
return
end
page_name = @page_name || 'HomePage'
page = wiki.read_page(@web_name, page_name)
render(:text => "Page '#{page_name}' not found", status => 404) and return unless page
@renderer = PageRenderer.new(page.revisions.last)
end
def revision
get_page_and_revision
@renderer = PageRenderer.new(@revision)
end
def rollback
@ -172,24 +217,22 @@ class WikiController < ApplicationController
end
def save
redirect_home if @page_name.nil?
cookies['author'] = @params['author']
render(:status => 404, :text => 'Undefined page name') and return if @page_name.nil?
cookies['author'] = { :value => @params['author'], :expires => Time.utc(2030) }
begin
check_for_spam(@params['content'], remote_ip)
check_blocked_ips(remote_ip)
if @page
wiki.revise_page(@web_name, @page_name, @params['content'], Time.now,
Author.new(@params['author'], remote_ip))
Author.new(@params['author'], remote_ip), PageRenderer.new)
@page.unlock
else
wiki.write_page(@web_name, @page_name, @params['content'], Time.now,
Author.new(@params['author'], remote_ip))
Author.new(@params['author'], remote_ip), PageRenderer.new)
end
redirect_to_page @page_name
rescue => e
flash[:error] = e
logger.error e
flash[:content] = @params['content']
if @page
@page.unlock
@ -203,6 +246,7 @@ class WikiController < ApplicationController
def show
if @page
begin
@renderer = PageRenderer.new(@page.revisions.last)
render_action 'page'
# TODO this rescue should differentiate between errors due to rendering and errors in
# the application itself (for application errors, it's better not to rescue the error at all)
@ -231,25 +275,6 @@ class WikiController < ApplicationController
private
def check_blocked_ips(ip)
if defined? BLOCKED_IPS and not BLOCKED_IPS.nil?
BLOCKED_IPS.each do |blocked_ip|
raise Instiki::ValidationError.new('Revision rejected by spam filter') if ip == blocked_ip
end
end
end
def check_for_spam(new_content, ip)
if defined? SPAM_PATTERNS and not SPAM_PATTERNS.nil?
SPAM_PATTERNS.each do |pattern|
if new_content =~ pattern
logger.info "Spam attempt from IP address #{ip}"
raise Instiki::ValidationError.new('Revision rejected by spam filter')
end
end
end
end
def convert_tex_to_pdf(tex_path)
# TODO remove earlier PDF files with the same prefix
# TODO handle gracefully situation where pdflatex is not available
@ -264,13 +289,13 @@ class WikiController < ApplicationController
def export_page_to_tex(file_path)
tex
File.open(file_path, 'w') { |f| f.write(render_to_string('wiki/tex')) }
File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex', :layout => nil)) }
end
def export_pages_as_zip(file_type, &block)
file_prefix = "#{@web.address}-#{file_type}-"
timestamp = @web.revised_on.strftime('%Y-%m-%d-%H-%M-%S')
timestamp = @web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')
file_path = File.join(@wiki.storage_path, file_prefix + timestamp + '.zip')
tmp_path = "#{file_path}.tmp"
@ -292,28 +317,25 @@ class WikiController < ApplicationController
end
def export_web_to_tex(file_path)
@tex_content = table_of_contents(@web.pages['HomePage'].content, render_tex_web)
File.open(file_path, 'w') { |f| f.write(render_to_string('wiki/tex_web')) }
@tex_content = table_of_contents(@web.page('HomePage').content, render_tex_web)
File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex_web', :layout => nil)) }
end
def get_page_and_revision
revision_index = (@params['rev'] || 0).to_i
if @page.nil? or @page.revisions[revision_index].nil?
render_text 'Page not found', '404 Not Found'
else
@revision = @page.revisions[revision_index]
end
@revision_number = @params['rev'].to_i
@revision = @page.revisions[@revision_number]
end
def parse_category
@categories = @web.categories
@category = @params['category']
if @categories.include?(@category)
@pages_in_category = @web.select { |page| page.in_category?(@category) }
@set_name = "category '#{@category}'"
else
@pages_in_category = PageSet.new(@web).by_name
@categories = WikiReference.list_categories.sort
page_names_in_category = WikiReference.pages_in_category(@category)
if (page_names_in_category.empty?)
@pages_in_category = @web.select_all.by_name
@set_name = 'the web'
else
@pages_in_category = @web.select { |page| page_names_in_category.include?(page.name) }.by_name
@set_name = "category '#{@category}'"
end
end
@ -340,15 +362,14 @@ class WikiController < ApplicationController
@pages_by_revision = @web.select.by_revision.first(limit)
else
@pages_by_revision = @web.select.by_revision
@pages_by_revision.reject! { |page| page.created_at < start_date } if start_date
@pages_by_revision.reject! { |page| page.created_at > end_date } if end_date
@pages_by_revision.reject! { |page| page.revised_at < start_date } if start_date
@pages_by_revision.reject! { |page| page.revised_at > end_date } if end_date
end
@hide_description = hide_description
@response.headers['Content-Type'] = 'text/xml'
@link_action = @web.password ? 'published' : 'show'
render 'wiki/rss_feed'
render :action => 'rss_feed'
end
def render_tex_web
@ -358,18 +379,8 @@ class WikiController < ApplicationController
end
end
def render_to_string(template_name, with_layout = false)
add_variables_to_assigns
self.assigns['content_for_layout'] = @template.render_file(template_name)
if with_layout
@template.render_file('layouts/default')
else
self.assigns['content_for_layout']
end
end
def rss_with_content_allowed?
@web.password.nil? or @web.published
@web.password.nil? or @web.published?
end
def truncate(text, length = 30, truncate_string = '...')

View file

@ -44,7 +44,12 @@ module ApplicationHelper
# Creates a hyperlink to a Wiki page, or to a "new page" form if the page doesn't exist yet
def link_to_page(page_name, web = @web, text = nil, options = {})
raise 'Web not defined' if web.nil?
web.make_link(page_name, text, options.merge(:base_url => "#{base_url}/#{web.address}"))
UrlGenerator.new(@controller).make_link(page_name, web, text,
options.merge(:base_url => "#{base_url}/#{web.address}"))
end
def author_link(page, options = {})
UrlGenerator.new(@controller).make_link(page.author.name, page.web, nil, options)
end
def base_url
@ -72,4 +77,19 @@ module ApplicationHelper
h(text).gsub(/\n/, '<br/>')
end
def format_date(date, include_time = true)
# Must use DateTime because Time doesn't support %e on at least some platforms
date_time = DateTime.new(date.year, date.mon, date.day, date.hour, date.min,
date.sec)
if include_time
return date_time.strftime("%B %e, %Y %H:%M:%S")
else
return date_time.strftime("%B %e, %Y")
end
end
def rendered_content(page)
PageRenderer.new(page.revisions.last).display_content
end
end

View file

@ -1,4 +1,18 @@
class Author < String
attr_accessor :ip
def initialize(name, ip) @ip = ip; super(name) end
attr_reader :name
def initialize(name, ip = nil)
@ip = ip
super(name)
end
def name=(value)
self.gsub!(/.+/, value)
end
alias_method :name, :to_s
def <=>(other)
name <=> other.to_s
end
end

View file

@ -1,120 +1,117 @@
require 'date'
require 'page_lock'
require 'revision'
require 'wiki_words'
require 'chunks/wiki'
class Page < ActiveRecord::Base
belongs_to :web
has_many :revisions, :order => 'id'
has_many :wiki_references, :order => 'referenced_name'
has_one :current_revision, :class_name => 'Revision', :order => 'id DESC'
class Page
include PageLock
attr_reader :name, :web
attr_accessor :revisions
def initialize(web, name)
raise 'nil web' if web.nil?
raise 'nil name' if name.nil?
@web, @name, @revisions = web, name, []
end
def revise(content, created_at, author)
if not @revisions.empty? and content == @revisions.last.content
def revise(content, time, author, renderer)
revisions_size = new_record? ? 0 : revisions.size
if (revisions_size > 0) and content == current_revision.content
raise Instiki::ValidationError.new(
"You have tried to save page '#{name}' without changing its content")
end
author = Author.new(author.to_s) unless author.is_a?(Author)
# Try to render content to make sure that markup engine can take it,
# before addin a revision to the page
Revision.new(self, @revisions.length, content, created_at, author).force_rendering
renderer.revision = Revision.new(
:page => self, :content => content, :author => author, :revised_at => time)
renderer.display_content(update_references = true)
# A user may change a page, look at it and make some more changes - several times.
# Not to record every such iteration as a new revision, if the previous revision was done
# by the same author, not more than 30 minutes ago, then update the last revision instead of
# creating a new one
if !@revisions.empty? && continous_revision?(created_at, author)
@revisions.last.created_at = created_at
@revisions.last.content = content
@revisions.last.clear_display_cache
if (revisions_size > 0) && continous_revision?(time, author)
current_revision.update_attributes(:content => content, :revised_at => time)
else
@revisions << Revision.new(self, @revisions.length, content, created_at, author)
revisions.create(:content => content, :author => author, :revised_at => time)
end
self.revisions.last.force_rendering
# at this point the page may not be inserted in the web yet, and therefore
# references to the page itself are rendered as "unresolved". Clearing the cache allows
# the page to re-render itself once again, hopefully _after_ it is inserted in the web
self.revisions.last.clear_display_cache
@web.refresh_pages_with_references(@name) if @revisions.length == 1
save
self
end
def rollback(revision_number, created_at, author_ip = nil)
roll_back_revision = @revisions[revision_number].dup
revise(roll_back_revision.content, created_at, Author.new(roll_back_revision.author, author_ip))
def rollback(revision_number, time, author_ip, renderer)
roll_back_revision = self.revisions[revision_number]
if roll_back_revision.nil?
raise Instiki::ValidationError.new("Revision #{revision_number} not found")
end
author = Author.new(roll_back_revision.author.name, author_ip)
revise(roll_back_revision.content, time, author, renderer)
end
def revisions?
revisions.length > 1
revisions.size > 1
end
def revised_on
created_on
def previous_revision(revision)
revision_index = revisions.each_with_index do |rev, index|
if rev.id == revision.id
break index
else
nil
end
def in_category?(cat)
cat.nil? || cat.empty? || categories.include?(cat)
end
def categories
display_content.find_chunks(Category).map { |cat| cat.list }.flatten
if revision_index.nil? or revision_index == 0
nil
else
revisions[revision_index - 1]
end
def authors
revisions.collect { |rev| rev.author }
end
def references
@web.select.pages_that_reference(name)
web.select.pages_that_reference(name)
end
def linked_from
@web.select.pages_that_link_to(name)
web.select.pages_that_link_to(name)
end
def included_from
@web.select.pages_that_include(name)
web.select.pages_that_include(name)
end
# Returns the original wiki-word name as separate words, so "MyPage" becomes "My Page".
def plain_name
@web.brackets_only ? name : WikiWords.separate(name)
web.brackets_only? ? name : WikiWords.separate(name)
end
# used to build chunk ids.
def id
@id ||= name.unpack('H*').first
LOCKING_PERIOD = 30.minutes
def lock(time, locked_by)
update_attributes(:locked_at => time, :locked_by => locked_by)
end
def link(options = {})
@web.make_link(name, nil, options)
def lock_duration(time)
((time - locked_at) / 60).to_i unless locked_at.nil?
end
def author_link(options = {})
@web.make_link(author, nil, options)
def unlock
update_attribute(:locked_at, nil)
end
def locked?(comparison_time)
locked_at + LOCKING_PERIOD > comparison_time unless locked_at.nil?
end
def to_param
name
end
private
def continous_revision?(created_at, author)
@revisions.last.author == author && @revisions.last.created_at + 30.minutes > created_at
def continous_revision?(time, author)
(current_revision.author == author) && (revised_at + 30.minutes > time)
end
# Forward method calls to the current revision, so the page responds to all revision calls
def method_missing(method_symbol)
revisions.last.send(method_symbol)
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
# Perform a hand-off to AR::Base#method_missing
if @attributes.include?(method_name) or md = /(=|\?|_before_type_cast)$/.match(method_name)
super(method_id, *args, &block)
else
current_revision.send(method_id)
end
end
end

View file

@ -1,23 +0,0 @@
# Contains all the lock methods to be mixed in with the page
module PageLock
LOCKING_PERIOD = 30 * 60 # 30 minutes
attr_reader :locked_by
def lock(time, locked_by)
@locked_at, @locked_by = time, locked_by
end
def lock_duration(time)
((time - @locked_at) / 60).to_i unless @locked_at.nil?
end
def unlock
@locked_at = nil
end
def locked?(comparison_time)
@locked_at + LOCKING_PERIOD > comparison_time unless @locked_at.nil?
end
end

View file

@ -0,0 +1,15 @@
# This class maintains the state of wiki references for newly created or newly deleted pages
class PageObserver < ActiveRecord::Observer
def after_create(page)
WikiReference.update_all("link_type = '#{WikiReference::LINKED_PAGE}'",
['referenced_name = ?', page.name])
end
def before_destroy(page)
WikiReference.delete_all ['page_id = ?', page.id]
WikiReference.update_all("link_type = '#{WikiReference::WANTED_PAGE}'",
['referenced_name = ?', page.name])
end
end

View file

@ -7,7 +7,7 @@ class PageSet < Array
@web = web
# if pages is not specified, make a list of all pages in the web
if pages.nil?
super(web.pages.values)
super(web.pages)
# otherwise use specified pages and condition to produce a set of pages
elsif condition.nil?
super(pages)
@ -17,10 +17,9 @@ class PageSet < Array
end
def most_recent_revision
self.map { |page| page.created_at }.max || Time.at(0)
self.map { |page| page.revised_at }.max || Time.at(0)
end
def by_name
PageSet.new(@web, sort_by { |page| page.name })
end
@ -28,22 +27,28 @@ class PageSet < Array
alias :sort :by_name
def by_revision
PageSet.new(@web, sort_by { |page| page.created_at }).reverse
PageSet.new(@web, sort_by { |page| page.revised_at }).reverse
end
def pages_that_reference(page_name)
self.select { |page| page.wiki_references.include?(page_name) }
all_referring_pages = WikiReference.pages_that_reference(page_name)
self.select { |page| all_referring_pages.include?(page.name) }
end
def pages_that_link_to(page_name)
self.select { |page| page.wiki_words.include?(page_name) }
all_linking_pages = WikiReference.pages_that_link_to(page_name)
self.select { |page| all_linking_pages.include?(page.name) }
end
def pages_that_include(page_name)
self.select { |page| page.wiki_includes.include?(page_name) }
all_including_pages = WikiReference.pages_that_include(page_name)
self.select { |page| all_including_pages.include?(page.name) }
end
def pages_authored_by(author)
all_pages_authored_by_the_author =
Page.connection.select_all(sanitize_sql([
"SELECT page_id FROM revision WHERE author = '?'", author]))
self.select { |page| page.authors.include?(author) }
end
@ -57,7 +62,7 @@ class PageSet < Array
# references and so cannot be orphans
# Pages that refer to themselves and have no links from outside are oprphans.
def orphaned_pages
never_orphans = web.select.authors + ['HomePage']
never_orphans = web.authors + ['HomePage']
self.select { |page|
if never_orphans.include? page.name
false
@ -79,11 +84,11 @@ class PageSet < Array
end
def wiki_words
self.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq
end
def authors
self.inject([]) { |authors, page| authors << page.authors }.flatten.uniq.sort
self.inject([]) { |wiki_words, page|
wiki_words + page.wiki_references.
select { |ref| ref.link_type != WikiReference::CATEGORY }.
map { |ref| ref.referenced_name }
}.flatten.uniq
end
end

View file

@ -1,127 +1,4 @@
require 'diff'
require 'wiki_content'
require 'chunks/wiki'
require 'date'
require 'author'
require 'page'
class Revision
attr_accessor :page, :number, :content, :created_at, :author
def initialize(page, number, content, created_at, author)
@page, @number, @created_at, @author = page, number, created_at, author
self.content = content
@display_cache = nil
end
def created_on
Date.new(@created_at.year, @created_at.mon, @created_at.day)
end
def pretty_created_at
# Must use DateTime because Time doesn't support %e on at least some platforms
DateTime.new(
@created_at.year, @created_at.mon, @created_at.day, @created_at.hour, @created_at.min
).strftime "%B %e, %Y %H:%M"
end
# todo: drop next_revision, previuous_revision and number from here - unused code
def next_revision
page.revisions[number + 1]
end
def previous_revision
number > 0 ? page.revisions[number - 1] : nil
end
# Returns an array of all the WikiIncludes present in the content of this revision.
def wiki_includes
unless @wiki_includes_cache
chunks = display_content.find_chunks(Include)
@wiki_includes_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_includes_cache
end
# Returns an array of all the WikiReferences present in the content of this revision.
def wiki_references
unless @wiki_references_cache
chunks = display_content.find_chunks(WikiChunk::WikiReference)
@wiki_references_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_references_cache
end
# Returns an array of all the WikiWords present in the content of this revision.
def wiki_words
unless @wiki_words_cache
wiki_chunks = display_content.find_chunks(WikiChunk::WikiLink)
@wiki_words_cache = wiki_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_words_cache
end
# Returns an array of all the WikiWords present in the content of this revision.
# that already exists as a page in the web.
def existing_pages
wiki_words.select { |wiki_word| page.web.pages[wiki_word] }
end
# Returns an array of all the WikiWords present in the content of this revision
# that *doesn't* already exists as a page in the web.
def unexisting_pages
wiki_words - existing_pages
end
# Explicit check for new type of display cache with chunks_by_type method.
# Ensures new version works with older snapshots.
def display_content
unless @display_cache && @display_cache.respond_to?(:chunks_by_type)
@display_cache = WikiContent.new(self)
@display_cache.render!
end
@display_cache
end
def display_diff
previous_revision ? HTMLDiff.diff(previous_revision.display_content, display_content) : display_content
end
def clear_display_cache
@wiki_words_cache = @published_cache = @display_cache = @wiki_includes_cache =
@wiki_references_cache = nil
end
def display_published
unless @published_cache && @published_cache.respond_to?(:chunks_by_type)
@published_cache = WikiContent.new(self, {:mode => :publish})
@published_cache.render!
end
@published_cache
end
def display_content_for_export
WikiContent.new(self, {:mode => :export} ).render!
end
def force_rendering
begin
display_content.render!
rescue => e
ApplicationController.logger.error "Failed rendering page #{@name}"
ApplicationController.logger.error e
message = e.message
# substitute content with an error message
self.content = <<-EOL
<p>Markup engine has failed to render this page, raising the following error:</p>
<p>#{message}</p>
<pre>#{self.content}</pre>
EOL
clear_display_cache
raise e
end
end
class Revision < ActiveRecord::Base
belongs_to :page
composed_of :author, :mapping => [ %w(author name), %w(ip ip) ]
end

4
app/models/system.rb Normal file
View file

@ -0,0 +1,4 @@
class System < ActiveRecord::Base
set_table_name 'system'
validates_presence_of :password
end

View file

@ -1,133 +1,114 @@
require 'cgi'
require 'page'
require 'page_set'
require 'wiki_words'
require 'zip/zip'
class Web < ActiveRecord::Base
has_many :pages
class Web
attr_accessor :name, :password, :safe_mode, :pages
attr_accessor :additional_style, :allow_uploads, :published
attr_reader :address
# there are getters for all these attributes, too
attr_writer :markup, :color, :brackets_only, :count_pages, :max_upload_size
def initialize(parent_wiki, name, address, password = nil)
self.address = address
@wiki, @name, @password = parent_wiki, name, password
set_compatible_defaults
@pages = {}
@allow_uploads = true
@additional_style = nil
@published = false
@count_pages = false
def wiki
Wiki.new
end
# Explicitly sets value of some web attributes to defaults, unless they are already set
def set_compatible_defaults
@markup = markup()
@color = color()
@safe_mode = safe_mode()
@brackets_only = brackets_only()
@max_upload_size = max_upload_size()
@wiki = wiki
def file_yard
@file_yard ||= FileYard.new("#{Wiki.storage_path}/#{address}", max_upload_size)
end
# All below getters know their default values. This is necessary to ensure compatibility with
# 0.9 storages, where they were not defined.
def brackets_only() @brackets_only || false end
def color() @color ||= '008B26' end
def count_pages() @count_pages || false end
def markup() @markup ||= :textile end
def max_upload_size() @max_upload_size || 100; end
def wiki() @wiki ||= WikiService.instance; end
def add_page(name, content, created_at, author)
page = Page.new(self, name)
page.revise(content, created_at, author)
@pages[page.name] = page
def settings_changed?(markup, safe_mode, brackets_only)
self.markup != markup ||
self.safe_mode != safe_mode ||
self.brackets_only != brackets_only
end
def address=(the_address)
if the_address != CGI.escape(the_address)
raise Instiki::ValidationError.new('Web name should contain only valid URI characters')
end
@address = the_address
def add_page(name, content, time, author, renderer)
page = page(name) || Page.new(:web => self, :name => name)
page.revise(content, time, author, renderer)
end
def authors
select.authors
connection.select_all(
'SELECT DISTINCT r.author AS author ' +
'FROM revisions r ' +
'JOIN pages p ON p.id = r.page_id ' +
'ORDER by 1').collect { |row| row['author'] }
end
def categories
select.map { |page| page.categories }.flatten.uniq.sort
end
def page(name)
pages.find(:first, :conditions => ['name = ?', name])
end
def has_page?(name)
pages[name]
Page.count(['web_id = ? AND name = ?', id, name]) > 0
end
def has_file?(name)
wiki.file_yard(self).has_file?(name)
end
# Create a link for the given page name and link text based
# on the render mode in options and whether the page exists
# in the this web.
# The links a relative, and will work only if displayed on another WikiPage.
# It should not be used in menus, templates and such - instead, use link_to_page helper
def make_link(name, text = nil, options = {})
text = CGI.escapeHTML(text || WikiWords.separate(name))
mode = options[:mode] || :show
base_url = options[:base_url] || '..'
link_type = options[:link_type] || :show
case link_type.to_sym
when :show
UrlGenerator.new.make_page_link(mode, name, text, base_url, has_page?(name))
when :file
UrlGenerator.new.make_file_link(mode, name, text, base_url, has_file?(name))
when :pic
UrlGenerator.new.make_pic_link(mode, name, text, base_url, has_file?(name))
else
raise "Unknown link type: #{link_type}"
end
def markup
read_attribute('markup').to_sym
end
# Clears the display cache for all the pages with references to
def refresh_pages_with_references(page_name)
select.pages_that_reference(page_name).each { |page|
page.revisions.each { |revision| revision.clear_display_cache }
def page_names_by_author
connection.select_all(
'SELECT DISTINCT r.author AS author, p.name AS page_name ' +
'FROM revisions r ' +
'JOIN pages p ON r.page_id = p.id ' +
"WHERE p.web_id = #{self.id} " +
'ORDER by p.name'
).inject({}) { |result, row|
author, page_name = row['author'], row['page_name']
result[author] = [] unless result.has_key?(author)
result[author] << page_name
result
}
end
def refresh_revisions
select.each { |page| page.revisions.each { |revision| revision.clear_display_cache } }
end
def remove_pages(pages_to_be_removed)
pages.delete_if { |page_name, page| pages_to_be_removed.include?(page) }
pages_to_be_removed.each { |p| p.destroy }
end
def revised_on
def revised_at
select.most_recent_revision
end
def select(&condition)
PageSet.new(self, @pages.values, condition)
PageSet.new(self, pages, condition)
end
def select_all
PageSet.new(self, pages, nil)
end
def to_param
address
end
private
# Returns an array of all the wiki words in any current revision
def wiki_words
pages.values.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq
pages.inject([]) { |wiki_words, page| wiki_words << page.wiki_words }.flatten.uniq
end
# Returns an array of all the page names on this web
def page_names
pages.keys
pages.map { |p| p.name }
end
protected
before_save :sanitize_markup
before_validation :validate_address
validates_uniqueness_of :address
validates_length_of :color, :in => 3..6
def sanitize_markup
self.markup = markup.to_s
end
def validate_address
unless address == CGI.escape(address)
self.errors.add(:address, 'should contain only valid URI characters')
raise Instiki::ValidationError.new("#{self.class.human_attribute_name('address')} #{errors.on(:address)}")
end
end
end

96
app/models/wiki.rb Normal file
View file

@ -0,0 +1,96 @@
class Wiki
cattr_accessor :storage_path, :logger
self.storage_path = "#{RAILS_ROOT}/storage/"
self.logger = RAILS_DEFAULT_LOGGER
def authenticate(password)
password == (system.password || 'instiki')
end
def create_web(name, address, password = nil)
@webs = nil
Web.create(:name => name, :address => address, :password => password)
end
def delete_web(address)
web = Web.find_by_address(address)
unless web.nil?
web.destroy
@webs = nil
end
end
def file_yard(web)
web.file_yard
end
def edit_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false,
password = nil, published = false, brackets_only = false, count_pages = false,
allow_uploads = true, max_upload_size = nil)
if not (web = Web.find_by_address(old_address))
raise Instiki::ValidationError.new("Web with address '#{old_address}' does not exist")
end
web.update_attributes(:address => new_address, :name => name, :markup => markup, :color => color,
:additional_style => additional_style, :safe_mode => safe_mode, :password => password, :published => published,
:brackets_only => brackets_only, :count_pages => count_pages, :allow_uploads => allow_uploads, :max_upload_size => max_upload_size)
@webs = nil
raise Instiki::ValidationError.new("There is already a web with address '#{new_address}'") unless web.errors.on(:address).nil?
web
end
def read_page(web_address, page_name)
self.class.logger.debug "Reading page '#{page_name}' from web '#{web_address}'"
web = Web.find_by_address(web_address)
if web.nil?
self.class.logger.debug "Web '#{web_address}' not found"
return nil
else
page = web.pages.find(:first, :conditions => ['name = ?', page_name])
self.class.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found"
return page
end
end
def remove_orphaned_pages(web_address)
web = Web.find_by_address(web_address)
web.remove_pages(web.select.orphaned_pages)
end
def revise_page(web_address, page_name, content, revised_at, author, renderer)
page = read_page(web_address, page_name)
page.revise(content, revised_at, author, renderer)
end
def rollback_page(web_address, page_name, revision_number, time, author_id = nil)
page = read_page(web_address, page_name)
page.rollback(revision_number, time, author_id)
end
def setup(password, web_name, web_address)
system.update_attribute(:password, password)
create_web(web_name, web_address)
end
def system
@system ||= (System.find(:first) || System.create)
end
def setup?
Web.count > 0
end
def webs
@webs ||= Web.find(:all).inject({}) { |webs, web| webs.merge(web.address => web) }
end
def storage_path
self.class.storage_path
end
def write_page(web_address, page_name, content, written_on, author, renderer)
Web.find_by_address(web_address).add_page(page_name, content, written_on, author, renderer)
end
end

View file

@ -0,0 +1,67 @@
class WikiReference < ActiveRecord::Base
LINKED_PAGE = 'L'
WANTED_PAGE = 'W'
INCLUDED_PAGE = 'I'
CATEGORY = 'C'
AUTHOR = 'A'
belongs_to :page
validates_inclusion_of :link_type, :in => [LINKED_PAGE, WANTED_PAGE, INCLUDED_PAGE, CATEGORY, AUTHOR]
# FIXME all finders below MUST restrict their results to pages belonging to a particular web
def self.link_type(web, page_name)
web.has_page?(page_name) ? LINKED_PAGE : WANTED_PAGE
end
def self.pages_that_reference(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ?' +
"AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}', '#{INCLUDED_PAGE}')"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_that_link_to(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type in ('#{LINKED_PAGE}', '#{WANTED_PAGE}')"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_that_include(page_name)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type = '#{INCLUDED_PAGE}'"
names = connection.select_all(sanitize_sql([query, page_name])).map { |row| row['name'] }
end
def self.pages_in_category(category)
query = 'SELECT name FROM pages JOIN wiki_references ON pages.id = wiki_references.page_id ' +
'WHERE wiki_references.referenced_name = ? ' +
"AND wiki_references.link_type = '#{CATEGORY}'"
names = connection.select_all(sanitize_sql([query, category])).map { |row| row['name'] }
end
def self.list_categories
query = "SELECT DISTINCT referenced_name FROM wiki_references WHERE link_type = '#{CATEGORY}'"
connection.select_all(query).map { |row| row['referenced_name'] }
end
def wiki_link?
linked_page? or wanted_page?
end
def linked_page?
link_type == LINKED_PAGE
end
def wanted_page?
link_type == WANTED_PAGE
end
def included_page?
link_type == INCLUDED_PAGE
end
end

View file

@ -1,259 +0,0 @@
require 'open-uri'
require 'yaml'
require 'madeleine'
require 'madeleine/automatic'
require 'madeleine/zmarshal'
require 'web'
require 'page'
require 'author'
require 'file_yard'
require 'instiki_errors'
module AbstractWikiService
attr_reader :webs, :system
def authenticate(password)
# system['password'] variant is for compatibility with storages from older versions
password == (@system[:password] || @system['password'] || 'instiki')
end
def create_web(name, address, password = nil)
@webs[address] = Web.new(self, name, address, password) unless @webs[address]
end
def delete_web(address)
@webs[address] = nil
end
def file_yard(web)
raise "Web #{@web.name} does not belong to this wiki service" unless @webs.values.include?(web)
# TODO cache FileYards
FileYard.new("#{self.storage_path}/#{web.address}", web.max_upload_size)
end
def init_wiki_service
@webs = {}
@system = {}
end
def edit_web(old_address, new_address, name, markup, color, additional_style, safe_mode = false,
password = nil, published = false, brackets_only = false, count_pages = false,
allow_uploads = true, max_upload_size = nil)
if not @webs.key? old_address
raise Instiki::ValidationError.new("Web with address '#{old_address}' does not exist")
end
if old_address != new_address
if @webs.key? new_address
raise Instiki::ValidationError.new("There is already a web with address '#{new_address}'")
end
@webs[new_address] = @webs[old_address]
@webs.delete(old_address)
@webs[new_address].address = new_address
end
web = @webs[new_address]
web.refresh_revisions if settings_changed?(web, markup, safe_mode, brackets_only)
web.name, web.markup, web.color, web.additional_style, web.safe_mode =
name, markup, color, additional_style, safe_mode
web.password, web.published, web.brackets_only, web.count_pages =
password, published, brackets_only, count_pages, allow_uploads
web.allow_uploads, web.max_upload_size = allow_uploads, max_upload_size.to_i
end
def read_page(web_address, page_name)
ApplicationController.logger.debug "Reading page '#{page_name}' from web '#{web_address}'"
web = @webs[web_address]
if web.nil?
ApplicationController.logger.debug "Web '#{web_address}' not found"
return nil
else
page = web.pages[page_name]
ApplicationController.logger.debug "Page '#{page_name}' #{page.nil? ? 'not' : ''} found"
return page
end
end
def remove_orphaned_pages(web_address)
@webs[web_address].remove_pages(@webs[web_address].select.orphaned_pages)
end
def revise_page(web_address, page_name, content, revised_on, author)
page = read_page(web_address, page_name)
page.revise(content, revised_on, author)
end
def rollback_page(web_address, page_name, revision_number, created_at, author_id = nil)
page = read_page(web_address, page_name)
page.rollback(revision_number, created_at, author_id)
end
def setup(password, web_name, web_address)
@system[:password] = password
create_web(web_name, web_address)
end
def setup?
not (@webs.empty?)
end
def storage_path
self.class.storage_path
end
def write_page(web_address, page_name, content, written_on, author)
@webs[web_address].add_page(page_name, content, written_on, author)
end
private
def settings_changed?(web, markup, safe_mode, brackets_only)
web.markup != markup ||
web.safe_mode != safe_mode ||
web.brackets_only != brackets_only
end
end
class WikiService
include AbstractWikiService
include Madeleine::Automatic::Interceptor
# These methods do not change the state of persistent objects, and
# should not be logged by Madeleine
automatic_read_only :authenticate, :read_page, :setup?, :webs, :storage_path, :file_yard
@@storage_path = './storage/'
class << self
def check_snapshot_thread
# @madeleine may not be initialised in unit tests, and in such case there is no need to do anything
@madeleine.check_snapshot_thread unless @madeleine.nil?
end
def clean_storage
MadeleineServer.clean_storage(self)
end
# One interesting property of Madeleine as persistence mechanism is that it saves
# (and restores) the whole ObjectSpace. And in there, storage from older version may contain
# who knows what in temporary variables, such as caches of various kinds.
# The reason why it is nearly impossible to control is that there may be bugs, people may
# use modified versions of things, etc etc etc
# Therefore, upon loading the storage from a file, it is a good idea to clear all such
# variables. It would be better yet if Madeleine could be somehow instructed not to save that
# data in a snapshot at all. Alas, such a feature is not presently available.
def clear_all_caches
return if @system.webs.nil?
@system.webs.each_value do |web|
next if web.nil? or web.pages.nil?
web.pages.each_value do |page|
next if page.nil? or page.revisions.nil?
page.revisions.each { |revision| revision.clear_display_cache }
end
end
end
def instance
@madeleine ||= MadeleineServer.new(self)
@system = @madeleine.system
clear_all_caches
return @system
end
def snapshot
@madeleine.snapshot
end
def storage_path=(storage_path)
@@storage_path = storage_path
end
def storage_path
@@storage_path
end
end
def initialize
init_wiki_service
end
end
class MadeleineServer
attr_reader :storage_path
# Clears all the command_log and snapshot files located in the storage directory, so the
# database is essentially dropped and recreated as blank
def self.clean_storage(service)
begin
Dir.foreach(service.storage_path) do |file|
if file =~ /(command_log|snapshot)$/
File.delete(File.join(service.storage_path, file))
end
end
rescue
Dir.mkdir(service.storage_path)
end
end
def initialize(service)
@storage_path = service.storage_path
@server = Madeleine::Automatic::AutomaticSnapshotMadeleine.new(service.storage_path,
Madeleine::ZMarshal.new) {
service.new
}
@snapshoot_thread_running = false
end
def command_log_present?
not Dir[storage_path + '/*.command_log'].empty?
end
def snapshot
@server.take_snapshot
end
def check_snapshot_thread
start_snapshot_thread unless @snapshoot_thread_running
end
def start_snapshot_thread
@snapshoot_thread_running = true
Thread.new(@server) {
hours_since_last_snapshot = 0
while true
begin
hours_since_last_snapshot += 1
# Take a snapshot if there is a command log, or 24 hours
# have passed since the last snapshot
if command_log_present? or hours_since_last_snapshot >= 24
ActionController::Base.logger.info "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] " +
'Taking a Madeleine snapshot'
snapshot
hours_since_last_snapshot = 0
end
sleep(1.hour)
rescue => e
ActionController::Base.logger.error(e)
# wait for a minute (not to spoof the log with the same error)
# and go back into the loop, to keep trying
sleep(1.minute)
ActionController::Base.logger.info("Retrying to save a snapshot")
end
end
}
end
def system
@server.system
end
end

View file

@ -14,6 +14,7 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="<%= @robots_metatag_value %>" />
<style type="text/css">
h1#pageName, .newWikiWord a, a.existingWikiWord, .newWikiWord a:hover, #TextileHelp h3 {

View file

@ -0,0 +1,13 @@
<% unless @page.linked_from.empty? %>
<small>
| Linked from:
<%= @page.linked_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %>
</small>
<% end %>
<% unless @page.included_from.empty? %>
<small>
| Included from:
<%= @page.included_from.collect { |referring_page| link_to_existing_page referring_page }.join(", ") %>
</small>
<% end %>

View file

@ -5,7 +5,7 @@
<li>
<%= link_to_page author %>
co- or authored:
<%= @web.select.pages_authored_by(author).collect { |page| link_to_page(page.name) }.sort.join ', ' %>
<%= @page_names_by_author[author].collect { |page_name| link_to_page(page_name) }.sort.join ', ' %>
</li>
<% end %>
</ul>

View file

@ -18,8 +18,9 @@
</p>
<p>
<input type="submit" value="Submit" accesskey="s"/> as
<input type="text" name="author" id="authorName" value="<%= @author %>"
onClick="this.value == 'AnonymousCoward' ? this.value = '' : true" />
<%= text_field_tag :author, @author,
:onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;",
:onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %>
|
<%= link_to('Cancel', {:web => @web.address, :action => 'cancel_edit', :id => @page.name},
{:accesskey => 'c'})

View file

@ -17,7 +17,7 @@
</li>
<% end %></ul>
<% if @web.count_pages %>
<% if @web.count_pages? %>
<% total_chars = @pages_in_category.characters %>
<p><small>All content: <%= total_chars %> chars / <%= sprintf("%-.1f", (total_chars / 2275 )) %> pages</small></p>
<% end %>

View file

@ -18,7 +18,9 @@
</p>
<p>
<input type="submit" value="Submit" accesskey="s"/> as
<input type="text" name="author" id="authorName" value="<%= @author %>" onClick="this.value == 'AnonymousCoward' ? this.value = '' : true" />
<%= text_field_tag :author, @author,
:onfocus => "this.value == 'AnonymousCoward' ? this.value = '' : true;",
:onblur => "this.value == '' ? this.value = 'AnonymousCoward' : true" %>
</p>
<%= end_form_tag %>

View file

@ -4,7 +4,7 @@
%>
<div id="revision">
<%= @page.display_content %>
<%= @renderer.display_content %>
</div>
@ -12,20 +12,20 @@
<div id="changes" style="display: none">
<p style="background: #eee; padding: 3px; border: 1px solid silver">
<small>
Showing changes from revision #<%= @page.number - 1 %> to #<%= @page.number %>:
Showing changes from revision #<%= @page.revisions.size - 2 %> to #<%= @page.revisions.size - 1 %>:
<ins class="diffins">Added</ins> | <del class="diffdel">Removed</del>
</small>
</p>
<%= @page.display_diff %>
<%= @renderer.display_diff %>
</div>
<% end %>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= @page.pretty_created_at %>
by <%= @page.author_link %>
<%= @page.revisions? ? "Revised" : "Created" %> on <%= format_date(@page.revised_at) %>
by <%= author_link(@page) %>
<%= "(#{@page.author.ip})" if @page.author.respond_to?(:ip) %>
<% if @web.count_pages %>
<% if @web.count_pages? %>
<% total_chars = @page.content.length %>
(<%= total_chars %> characters / <%= sprintf("%-.1f", (total_chars / 2275 rescue 0)) %> pages)
<% end %>
@ -71,8 +71,8 @@
<small>
| Views:
<%= link_to('Print',
{:web => @web.address, :action => 'print', :id => @page.name},
{:accesskey => 'p', :name => 'view_print'}) %>
{ :web => @web.address, :action => 'print', :id => @page.name },
{ :accesskey => 'p', :name => 'view_print' }) %>
<% if defined? RedClothForTex and RedClothForTex.available? and @web.markup == :textile %>
|
<%= link_to 'TeX', {:web => @web.address, :action => 'tex', :id => @page.name},
@ -83,24 +83,7 @@
<% end %>
</small>
<% unless @page.linked_from.empty? %>
<small>
| Linked from:
<%= @page.linked_from.collect { |referring_page|
link_to_existing_page referring_page
}.join(", ")
%>
</small>
<% end %>
<% if @page.included_from.length > 0 %>
<small>
| Included from: <%= @page.included_from.collect { |referring_page|
link_to_existing_page referring_page
}.join(", ")
%>
</small>
<% end %>
<%= render :partial => 'inbound_links' %>
</div>
<script language="Javascript" type="text/Javascript">

View file

@ -5,10 +5,10 @@
@inline_style = true
%>
<%= @page.display_content_for_export %>
<%= @renderer.display_content_for_export %>
<div class="byline">
<%= @page.revisions? ? "Revised" : "Created" %> on <%= @page.pretty_created_at %>
<%= @page.revisions? ? "Revised" : "Created" %> on <%= format_date(@page.revised_at) %>
by
<%= @page.author_link({ :mode => (@link_mode || :show) }) %>
<%= author_link(@page, { :mode => (@link_mode || :show) }) %>
</div>

View file

@ -6,4 +6,4 @@
@show_footer = true
%>
<%= @page.display_published %>
<%= @renderer.display_published %>

View file

@ -3,21 +3,21 @@
<%= categories_menu %>
<% unless @pages_by_revision.empty? %>
<% revision_date = @pages_by_revision.first.revised_on %>
<h3><%= revision_date.strftime('%B %e, %Y') %></h3>
<% revision_date = @pages_by_revision.first.revised_at %>
<h3><%= format_date(revision_date, include_time = false) %></h3>
<ul>
<% for page in @pages_by_revision %>
<% if page.revised_on < revision_date %>
<% revision_date = page.revised_on %>
<% if page.revised_at < revision_date %>
<% revision_date = page.revised_at %>
</ul>
<h3><%= revision_date.strftime('%B %e, %Y') %></h3>
<h3><%= format_date(revision_date, include_time = false) %></h3>
<ul>
<% end %>
<li>
<%= link_to_existing_page page %>
<div class="byline" style="margin-bottom: 0px">
by <%= link_to_page(page.author) %>
at <%= page.created_at.strftime "%H:%M" %>
at <%= format_date(page.revised_at) %>
<%= "from #{page.author.ip}" if page.author.respond_to?(:ip) %>
</div>
</li>

View file

@ -1,33 +1,33 @@
<% @title = "#{@page.plain_name} (Rev ##{@revision.number})" %>
<% @title = "#{@page.plain_name} (Rev ##{@revision_number})" %>
<div id="revision">
<%= @revision.display_content %>
<%= @renderer.display_content %>
</div>
<div id="changes" style="display: none">
<p style="background: #eee; padding: 3px; border: 1px solid silver">
<small>
Showing changes from revision #<%= @revision.number - 1 %> to #<%= @revision.number %>:
Showing changes from revision #<%= @revision_number - 1 %> to #<%= @revision_number %>:
<ins class="diffins">Added</ins> | <del class="diffdel">Removed</del>
</small>
</p>
<%= @revision.display_diff %>
<%= @renderer.display_diff %>
</div>
<div class="byline">
<%= "Revision from #{@revision.pretty_created_at} by" %>
<%= "Revision from #{format_date(@revision.revised_at)} by" %>
<%= link_to_page @revision.author %>
</div>
<div class="navigation">
<% if @revision.next_revision %>
<% if @revision.next_revision.number < (@page.revisions.length - 1) %>
<% if @revision_number < @page.revisions.length - 1 %>
<% if @revision_number < @page.revisions.length - 2 %>
<%= link_to('Forward in time',
{:web => @web.address, :action => 'revision', :id => @page.name,
:rev => @revision.next_revision.number},
:rev => @revision_number + 1},
{:class => 'navlink', :name => 'to_next_revision'})
%>
<% else %>
@ -36,20 +36,20 @@
{:class => 'navlink', :name => 'to_next_revision'})
%>
<% end %>
<small>(<%= @revision.page.revisions.length - @revision.next_revision.number %> more)</small>
<small>(<%= @revision.page.revisions.length - @revision_number - 1 %> more)</small>
<% end %>
<% if @revision.next_revision && @revision.previous_revision %>
<% if @revision_number > 0 && @revision_number < @page.revisions.size - 1 %>
|
<% end %>
<% if @revision.previous_revision %>
<% if @revision_number > 0 %>
<%= link_to('Back in time',
{:web => @web.address, :action => 'revision', :id => @page.name,
:rev => @revision.previous_revision.number},
:rev => @revision_number - 1},
{:class => 'navlink', :name => 'to_previous_revision'})
%>
<small>(<%= @revision.previous_revision.number + 1 %> more)</small>
<small>(<%= @revision_number %> more)</small>
<% end %>
|
@ -57,7 +57,7 @@
{:class => 'navlink', :name => 'to_current_revision'})
%>
<% if @revision.previous_revision %>
<% if @revision_number > 0 %>
<span id="show_changes">
| <a href="#" onClick="toggleChanges(); return false;">See changes</a>
</span>
@ -69,21 +69,12 @@
|
<%= link_to('Rollback',
{:web => @web.address, :action => 'rollback', :id => @page.name, :rev => @revision.number},
{:web => @web.address, :action => 'rollback', :id => @page.name, :rev => @revision_number},
{:class => 'navlink', :name => 'rollback'})
%>
<% if @page.references.length > 0 %>
<small>
| Linked from:
<%= @page.references.collect { |ref|
link_to ref.name, :web => @web.address, :action => 'show', :id => ref.name
}.join(", ")
%>
</small>
<% else %>
Orphan page
<% end %>
<%= render :partial => 'inbound_links' %>
</div>
<script language="Javascript">

View file

@ -1,5 +1,5 @@
<%
@title = "Rollback to #{@page.plain_name} Rev ##{@revision.number}"
@title = "Rollback to #{@page.plain_name} Rev ##{@revision_number}"
@content_width = 720
@hide_navigation = true
%>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title><%= @web.name %></title>
<link><%= url_for :only_path => false, :web => @web_name, :action => @link_action, :id => 'HomePage' %></link>
<description>An Instiki wiki</description>
<language>en-us</language>
<ttl>40</ttl>
<% for page in @pages_by_revision %>
<item>
<title><%= h page.plain_name %></title>
<% unless @hide_description %>
<description><%= h page.display_content %></description>
<% end %>
<pubDate><%= page.created_at.getgm.strftime "%a, %d %b %Y %H:%M:%S Z" %></pubDate>
<guid><%= url_for :only_path => false, :web => @web_name, :action => @link_action, :id => page.name %></guid>
<link><%= url_for :only_path => false, :web => @web_name, :action => @link_action, :id => page.name %></link>
<dc:creator><%= WikiWords.separate(page.author) %></dc:creator>
</item>
<% end %>
</channel>
</rss>

View file

@ -0,0 +1,21 @@
xml.rss('version' => '2.0') do
xml.channel do
xml.title(@web.name)
xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => 'HomePage'))
xml.description('An Instiki wiki')
xml.language('en-us')
xml.ttl('40')
for page in @pages_by_revision
xml.item do
xml.title(page.plain_name)
unless @hide_description
xml.description(rendered_content(page))
end
xml.pubDate(page.revised_at.getgm.strftime('%a, %d %b %Y %H:%M:%S Z'))
xml.guid(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name))
xml.link(url_for(:only_path => false, :web => @web_name, :action => @link_action, :id => page.name))
end
end
end
end

17
config/boot.rb Normal file
View file

@ -0,0 +1,17 @@
unless defined?(RAILS_ROOT)
root_path = File.join(File.dirname(__FILE__), '..')
unless RUBY_PLATFORM =~ /mswin32/
require 'pathname'
root_path = Pathname.new(root_path).cleanpath.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)

View file

@ -1,82 +1,31 @@
if RUBY_VERSION < '1.8.1'
puts 'Instiki requires Ruby 1.8.1+'
exit
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
Rails::Initializer.run do |config|
# Skip frameworks you're not going to use
config.frameworks -= [ :action_web_service, :action_mailer ]
# 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 = :page_observer
# 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
# Instiki-specific configuration below
require_dependency 'instiki_errors'
# Enable UTF-8 support
$KCODE = 'u'
require 'jcode'
RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') unless defined? RAILS_ROOT
RAILS_ENV = ENV['RAILS_ENV'] || 'production' unless defined? RAILS_ENV
unless defined? ADDITIONAL_LOAD_PATHS
# Mocks first.
ADDITIONAL_LOAD_PATHS = ["#{RAILS_ROOT}/test/mocks/#{RAILS_ENV}"]
# Then model subdirectories.
ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/app/models/[_a-z]*"])
ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/components/[_a-z]*"])
# Followed by the standard includes.
ADDITIONAL_LOAD_PATHS.concat %w(
app
app/models
app/controllers
app/helpers
app/apis
components
config
lib
vendor
vendor/rails/railties
vendor/rails/railties/lib
vendor/rails/actionpack/lib
vendor/rails/activesupport/lib
vendor/rails/activerecord/lib
vendor/rails/actionmailer/lib
vendor/rails/actionwebservice/lib
vendor/madeleine-0.7.1/lib
vendor/RedCloth-3.0.3/lib
vendor/rubyzip-0.5.8/lib
).map { |dir| "#{File.expand_path(File.join(RAILS_ROOT, dir))}"
}.delete_if { |dir| not File.exist?(dir) }
# Prepend to $LOAD_PATH
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
end
# Require Rails libraries.
require 'rubygems' unless File.directory?("#{RAILS_ROOT}/vendor/rails")
require 'active_support'
require 'action_controller'
require_dependency 'instiki_errors'
require_dependency 'active_record_stub'
# Environment specific configuration
require_dependency "environments/#{RAILS_ENV}"
# Configure defaults if the included environment did not.
unless defined? RAILS_DEFAULT_LOGGER
RAILS_DEFAULT_LOGGER = Logger.new(STDERR)
ActionController::Base.logger ||= RAILS_DEFAULT_LOGGER
if $instiki_debug_logging
RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
ActionController::Base.logger.level = Logger::DEBUG
else
RAILS_DEFAULT_LOGGER.level = Logger::INFO
ActionController::Base.logger.level = Logger::INFO
end
end
ActionController::Base.template_root ||= "#{RAILS_ROOT}/app/views/"
ActionController::Routing::Routes.reload
Controllers = Dependencies::LoadingModule.root(
File.join(RAILS_ROOT, 'app', 'controllers'),
File.join(RAILS_ROOT, 'components')
)
require 'wiki_service'
Socket.do_not_reverse_lookup = true

View file

@ -1,5 +1,17 @@
Dependencies.mechanism = :require
ActionController::Base.consider_all_requests_local = true
ActionController::Base.perform_caching = false
BREAKPOINT_SERVER_PORT = 42531
$instiki_debug_logging = true
# 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

View file

@ -1,16 +1,17 @@
Dependencies.mechanism = :require
ActionController::Base.consider_all_requests_local = false
ActionController::Base.perform_caching = false
# 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
spam_patterns_filename = RAILS_ROOT + '/config/spam_patterns.txt'
if File.exists? spam_patterns_filename
SPAM_PATTERNS = File.readlines(spam_patterns_filename).delete_if { |line| line.strip.empty? }.map {
|line| Regexp.new(line.strip) }
end
# Full error reports are disabled and caching is turned on
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
blocked_ips_filename = RAILS_ROOT + '/config/blocked_ips.txt'
if File.exists? blocked_ips_filename
BLOCKED_IPS = File.readlines(blocked_ips_filename).delete_if { |line| line.strip.empty? }.map {
|line| line.strip }
end
# 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

View file

@ -1,17 +1,23 @@
Dependencies.mechanism = :require
ActionController::Base.consider_all_requests_local = true
ActionController::Base.perform_caching = false
# 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
require 'fileutils'
FileUtils.mkdir_p(RAILS_ROOT + "/log")
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true
unless defined? TEST_LOGGER
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
log_name = RAILS_ROOT + "/log/instiki_test.#{timestamp}.log"
$stderr.puts "To see the Rails log:\n less #{log_name}"
# Show full error reports and disable caching
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
TEST_LOGGER = ActionController::Base.logger = Logger.new(log_name)
$instiki_debug_logging = true
# 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
WikiService.storage_path = RAILS_ROOT + '/storage/test/'
end
# Overwrite the default settings for fixtures in tests. See Fixtures
# for more details about these settings.
# config.transactional_fixtures = true
# config.instantiated_fixtures = false
# config.pre_loaded_fixtures = false

63
db/schema.rb Normal file
View file

@ -0,0 +1,63 @@
# This file is autogenerated. Instead of editing this file, please use the
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
ActiveRecord::Schema.define() do
create_table "pages", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "web_id", :integer, :null => false
t.column "locked_by", :string, :limit => 60
t.column "name", :string, :limit => 60
t.column "locked_at", :datetime
end
create_table "revisions", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "revised_at", :datetime, :null => false
t.column "page_id", :integer, :null => false
t.column "content", :text, :null => false
t.column "author", :string, :limit => 60
t.column "ip", :string, :limit => 60
end
create_table "sessions", :force => true do |t|
t.column "session_id", :string
t.column "data", :text
t.column "updated_at", :datetime
end
add_index "sessions", ["session_id"], :name => "sessions_session_id_index"
create_table "system", :force => true do |t|
t.column "password", :string, :limit => 60
end
create_table "webs", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "name", :string, :limit => 60, :null => false
t.column "address", :string, :limit => 60, :null => false
t.column "password", :string, :limit => 60
t.column "additional_style", :string
t.column "allow_uploads", :integer, :default => 1
t.column "published", :integer, :default => 0
t.column "count_pages", :integer, :default => 0
t.column "markup", :string, :limit => 50, :default => "textile"
t.column "color", :string, :limit => 6, :default => "008B26"
t.column "max_upload_size", :integer, :default => 100
t.column "safe_mode", :integer, :default => 0
t.column "brackets_only", :integer, :default => 0
end
create_table "wiki_references", :force => true do |t|
t.column "created_at", :datetime, :null => false
t.column "updated_at", :datetime, :null => false
t.column "page_id", :integer, :null => false
t.column "referenced_name", :string, :limit => 60, :null => false
t.column "link_type", :string, :limit => 1, :null => false
end
end

View file

@ -1,4 +1,4 @@
#!/usr/bin/ruby
#!/usr/bin/env ruby
# Executable file for a gem
# must be same as ./instiki.rb

View file

@ -7,7 +7,7 @@ spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = 'instiki'
s.version = "0.10.2"
s.summary = 'Easy to install WikiClone running on WEBrick and Madeleine'
s.summary = 'Easy to install WikiClone running on WEBrick and SQLite'
s.description = <<-EOF
Instiki is a Wiki Clone written in Ruby that ships with an embedded
webserver. You can setup up an Instiki in just a few steps.
@ -24,10 +24,10 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = false
s.add_dependency('madeleine', '= 0.7.1')
s.add_dependency('RedCloth', '= 3.0.3')
s.add_dependency('rubyzip', '= 0.5.8')
s.add_dependency('rails', '= 0.13.1')
s.add_dependency('rails', '= 0.14.1')
s.add_dependency('sqlite3-ruby', '= 1.1.0')
s.requirements << 'none'
s.require_path = 'lib'

View file

@ -1,3 +1,3 @@
#!/usr/bin/ruby
#!/usr/bin/env ruby
load File.dirname(__FILE__) + '/script/server'

View file

@ -1,31 +0,0 @@
# This project uses Railties, which has an external dependency on ActiveRecord
# Since ActiveRecord may not be present in Instiki runtime environment, this
# file provides a stub replacement for it
module ActiveRecord
class Base
# dependency in railties/lib/dispatcher.rb
def self.reset_column_information_and_inheritable_attributes_for_all_subclasses
# noop
end
# dependency in actionpack/lib/action_controller/benchmarking.rb
def self.connected?
false
end
# dependency in actionpack/lib/action_controller/benchmarking.rb
def self.connection
return ConnectionStub
end
end
module ConnectionStub
def self.reset_runtime
0
end
end
end

View file

@ -1,4 +1,4 @@
#!/usr/bin/ruby
#!/usr/bin/env ruby
#
# Bluecloth is a Ruby implementation of Markdown, a text-to-HTML conversion
# tool.

View file

@ -27,8 +27,8 @@ module Chunk
# a regexp that matches all chunk_types masks
def Abstract::mask_re(chunk_types)
tmp = chunk_types.map{|klass| klass.mask_string}.join("|")
Regexp.new("chunk([0-9a-f]+n\\d+)(#{tmp})chunk")
chunk_classes = chunk_types.map{|klass| klass.mask_string}.join("|")
/chunk(-?\d+)(#{chunk_classes})chunk/
end
attr_reader :text, :unmask_text, :unmask_mode
@ -53,14 +53,7 @@ module Chunk
# should contain only [a-z0-9]
def mask
@mask ||="chunk#{@id}#{self.class.mask_string}chunk"
end
# We should not use object_id because object_id is not guarantied
# to be unique when we restart the wiki (new object ids can equal old ones
# that were restored from madeleine storage)
def id
@id ||= "#{@content.page_id}n#{@content.chunk_id}"
@mask ||= "chunk#{self.object_id}#{self.class.mask_string}chunk"
end
def unmask

View file

@ -9,12 +9,12 @@ require 'chunks/chunk'
# or RDoc to convert text. This markup occurs when the chunk is required
# to mask itself.
module Engines
class AbstractEngine
class AbstractEngine < Chunk::Abstract
# Convert content to HTML
# Create a new chunk for the whole content and replace it with its mask.
def self.apply_to(content)
engine = self.new(content)
content.replace(engine.to_html)
new_chunk = self.new(content)
content.replace(new_chunk.mask)
end
private
@ -27,7 +27,7 @@ module Engines
end
class Textile < AbstractEngine
def to_html
def mask
redcloth = RedCloth.new(@content, [:hard_breaks] + @content.options[:engine_opts])
redcloth.filter_html = false
redcloth.no_span_caps = false
@ -36,13 +36,13 @@ module Engines
end
class Markdown < AbstractEngine
def to_html
def mask
BlueCloth.new(@content, @content.options[:engine_opts]).to_html
end
end
class Mixed < AbstractEngine
def to_html
def mask
redcloth = RedCloth.new(@content, @content.options[:engine_opts])
redcloth.filter_html = false
redcloth.no_span_caps = false
@ -51,7 +51,7 @@ module Engines
end
class RDoc < AbstractEngine
def to_html
def mask
RDocSupport::RDocFormatter.new(@content).to_html
end
end

View file

@ -12,7 +12,6 @@ class Include < WikiChunk::WikiReference
INCLUDE_PATTERN = /\[\[!include\s+(.*?)\]\]\s*/i
def self.pattern() INCLUDE_PATTERN end
def initialize(match_data, content)
super
@page_name = match_data[1].strip
@ -22,16 +21,18 @@ class Include < WikiChunk::WikiReference
private
def get_unmask_text_avoiding_recursion_loops
if refpage then
refpage.clear_display_cache
if refpage.name == @content.page_name or refpage.wiki_includes.include?(@content.page_name)
if refpage
# TODO This way of instantiating a renderer is ugly.
renderer = PageRenderer.new(refpage.current_revision)
if renderer.wiki_includes.include?(@content.page_name)
# this will break the recursion
@content.delete_chunk(self)
return "<em>Recursive include detected; #{@page_name} --> #{@content.page_name} " +
"--> #{@page_name}</em>\n"
else
@content.merge_chunks(refpage.display_content)
return refpage.display_content.pre_rendered
included_content = renderer.display_content
@content.merge_chunks(included_content)
return included_content.pre_rendered
end
else
return "<em>Could not include #{@page_name}</em>\n"

View file

@ -16,7 +16,7 @@ module WikiChunk
# the referenced page
def refpage
@content.web.pages[@page_name]
@content.web.page(@page_name)
end
end
@ -45,11 +45,6 @@ module WikiChunk
end
end
# the referenced page
def refpage
@content.web.pages[@page_name]
end
def textile_url?
not @textile_link_suffix.nil?
end

46
lib/db_structure.rb Normal file
View file

@ -0,0 +1,46 @@
require 'erb'
def create_options
if @db == 'mysql'
'ENGINE = ' + (mysql_engine rescue @mysql_engine)
end
end
def db_quote(column)
case @db
when 'postgresql'
return "\"#{column}\""
when 'sqlite', 'sqlite3'
return "'#{column}'"
when 'mysql'
return "`#{column}`"
end
end
def db_structure(db)
db.downcase!
@db = db
case db
when 'postgresql'
@pk = 'SERIAL PRIMARY KEY'
@datetime = 'TIMESTAMP'
@boolean = "BOOLEAN"
when 'sqlite', 'sqlite3'
@pk = 'INTEGER PRIMARY KEY'
@datetime = 'DATETIME'
@boolean = "INTEGER"
when 'mysql'
@pk = 'INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY'
@datetime = 'DATETIME'
@boolean = "TINYINT"
@mysql_engine = 'InnoDB'
else
raise "Unknown db type #{db}"
end
s = ''
Dir[RAILS_ROOT + '/db/*.erbsql'].each do |filename|
s += ERB.new(File.read(filename)).result
end
s
end

View file

@ -2,7 +2,8 @@ require 'fileutils'
require 'instiki_errors'
class FileYard
cattr_accessor :restrict_upload_access
restrict_upload_access = true
attr_reader :files_path
def initialize(files_path, max_upload_size)
@ -16,7 +17,7 @@ class FileYard
if io.kind_of?(Tempfile)
io.close
check_upload_size(io.size)
File.chmod(600, file_path(name)) if File.exists? file_path(name)
File.chmod(0600, file_path(name)) if File.exists? file_path(name)
FileUtils.mv(io.path, file_path(name))
else
content = io.read
@ -24,7 +25,7 @@ class FileYard
File.open(file_path(name), 'wb') { |f| f.write(content) }
end
# just in case, restrict read access and prohibit write access to the uploaded file
FileUtils.chmod(0440, file_path(name))
FileUtils.chmod(0440, file_path(name)) if restrict_upload_access
end
def files

130
lib/page_renderer.rb Normal file
View file

@ -0,0 +1,130 @@
require 'diff'
# Temporary class containing all rendering stuff from a Revision
# I want to shift all rendering loguc to the controller eventually
class PageRenderer
def self.setup_url_generator(url_generator)
@@url_generator = url_generator
end
def self.teardown_url_generator
@@url_generator = nil
end
attr_reader :revision
def initialize(revision = nil)
self.revision = revision
end
def revision=(r)
@revision = r
@display_content = @display_published = @wiki_words_cache = @wiki_includes_cache =
@wiki_references_cache = nil
end
def display_content(update_references = false)
@display_content ||= render(:update_references => update_references)
end
def display_content_for_export
render :mode => :export
end
def display_published
@display_published ||= render(:mode => :publish)
end
def display_diff
previous_revision = @revision.page.previous_revision(@revision)
if previous_revision
rendered_previous_revision = WikiContent.new(previous_revision, @@url_generator).render!
HTMLDiff.diff(rendered_previous_revision, display_content)
else
display_content
end
end
# Returns an array of all the WikiIncludes present in the content of this revision.
def wiki_includes
unless @wiki_includes_cache
chunks = display_content.find_chunks(Include)
@wiki_includes_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_includes_cache
end
# Returns an array of all the WikiReferences present in the content of this revision.
def wiki_references
unless @wiki_references_cache
chunks = display_content.find_chunks(WikiChunk::WikiReference)
@wiki_references_cache = chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_references_cache
end
# Returns an array of all the WikiWords present in the content of this revision.
def wiki_words
unless @wiki_words_cache
wiki_chunks = display_content.find_chunks(WikiChunk::WikiLink)
@wiki_words_cache = wiki_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
end
@wiki_words_cache
end
# Returns an array of all the WikiWords present in the content of this revision.
# that already exists as a page in the web.
def existing_pages
wiki_words.select { |wiki_word| @revision.page.web.page(wiki_word) }
end
# Returns an array of all the WikiWords present in the content of this revision
# that *doesn't* already exists as a page in the web.
def unexisting_pages
wiki_words - existing_pages
end
private
def render(options = {})
rendering_result = WikiContent.new(@revision, @@url_generator, options).render!
if options[:update_references]
update_references(rendering_result)
end
rendering_result
end
def update_references(rendering_result)
WikiReference.delete_all ['page_id = ?', @revision.page_id]
references = @revision.page.wiki_references
wiki_word_chunks = rendering_result.find_chunks(WikiChunk::WikiLink)
wiki_words = wiki_word_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
wiki_words.each do |referenced_name|
# Links to self are always considered linked
if referenced_name == @revision.page.name
link_type = WikiReference::LINKED_PAGE
else
link_type = WikiReference.link_type(@revision.page.web, referenced_name)
end
references.create :referenced_name => referenced_name, :link_type => link_type
end
include_chunks = rendering_result.find_chunks(Include)
includes = include_chunks.map { |c| ( c.escaped? ? nil : c.page_name ) }.compact.uniq
includes.each do |included_page_name|
references.create :referenced_name => included_page_name,
:link_type => WikiReference::INCLUDED_PAGE
end
categories = rendering_result.find_chunks(Category).map { |cat| cat.list }.flatten
categories.each do |category|
references.create :referenced_name => category, :link_type => WikiReference::CATEGORY
end
end
end

View file

@ -1,61 +1,121 @@
class UrlGenerator
class AbstractUrlGenerator
def initialize(controller = nil)
@controller = controller or ControllerStub.new
def initialize(controller)
raise 'Controller cannot be nil' if controller.nil?
@controller = controller
end
def make_file_link(mode, name, text, base_url, known_file)
link = CGI.escape(name)
# Create a link for the given page (or file) name and link text based
# on the render mode in options and whether the page (file) exists
# in the web.
def make_link(name, web, text = nil, options = {})
text = CGI.escapeHTML(text || WikiWords.separate(name))
mode = (options[:mode] || :show).to_sym
link_type = (options[:link_type] || :show).to_sym
if (link_type == :show)
known_page = web.has_page?(name)
else
known_page = web.has_file?(name)
end
case link_type
when :show
page_link(mode, name, text, web.address, known_page)
when :file
file_link(mode, name, text, web.address, known_page)
when :pic
pic_link(mode, name, text, web.address, known_page)
else
raise "Unknown link type: #{link_type}"
end
end
end
class UrlGenerator < AbstractUrlGenerator
private
def file_link(mode, name, text, web_address, known_file)
case mode
when :export
if known_file then "<a class=\"existingWikiWord\" href=\"#{link}.html\">#{text}</a>"
else "<span class=\"newWikiWord\">#{text}</span>" end
when :publish
if known_file then "<a class=\"existingWikiWord\" href=\"#{base_url}/published/#{link}\">#{text}</a>"
else "<span class=\"newWikiWord\">#{text}</span>" end
else
if known_file
"<a class=\"existingWikiWord\" href=\"#{base_url}/file/#{link}\">#{text}</a>"
%{<a class="existingWikiWord" href="#{CGI.escape(name)}.html">#{text}</a>}
else
"<span class=\"newWikiWord\">#{text}<a href=\"#{base_url}/file/#{link}\">?</a></span>"
%{<span class="newWikiWord">#{text}</span>}
end
when :publish
if known_file
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'published',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'file',
:id => name
if known_file
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
def make_page_link(mode, name, text, base_url, known_page)
link = CGI.escape(name)
case mode.to_sym
def page_link(mode, name, text, web_address, known_page)
case mode
when :export
if known_page then %{<a class="existingWikiWord" href="#{link}.html">#{text}</a>}
else %{<span class="newWikiWord">#{text}</span>} end
if known_page
%{<a class="existingWikiWord" href="#{CGI.escape(name)}.html">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
when :publish
if known_page then %{<a class="existingWikiWord" href="#{base_url}/published/#{link}">#{text}</a>}
else %{<span class="newWikiWord">#{text}</span>} end
if known_page
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'published',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
if known_page
%{<a class="existingWikiWord" href="#{base_url}/show/#{link}">#{text}</a>}
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'show',
:id => name
%{<a class="existingWikiWord" href="#{href}">#{text}</a>}
else
%{<span class="newWikiWord">#{text}<a href="#{base_url}/show/#{link}">?</a></span>}
href = @controller.url_for :controller => 'wiki', :web => web_address, :action => 'new',
:id => name
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
def make_pic_link(mode, name, text, base_url, known_pic)
link = CGI.escape(name)
case mode.to_sym
def pic_link(mode, name, text, web_address, known_pic)
case mode
when :export
if known_pic then %{<img alt="#{text}" src="#{link}" />}
else %{<img alt="#{text}" src="no image" />} end
when :publish
if known_pic then %{<img alt="#{text}" src="#{link}" />}
else %{<span class="newWikiWord">#{text}</span>} end
if known_pic
%{<img alt="#{text}" src="#{CGI.escape(name)}" />}
else
if known_pic then %{<img alt="#{text}" src="#{base_url}/pic/#{link}" />}
else %{<span class="newWikiWord">#{text}<a href="#{base_url}/pic/#{link}">?</a></span>} end
%{<img alt="#{text}" src="no image" />}
end
when :publish
if known_pic
%{<img alt="#{text}" src="#{CGI.escape(name)}" />}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
href = @controller.url_for @controller => 'file', :web => web_address, :action => 'pic',
:id => name
if known_pic
%{<img alt="#{text}" src="#{href}" />}
else
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
end
class ControllerStub
end

View file

@ -59,14 +59,14 @@ module ChunkManager
def add_chunk(c)
@chunks_by_type[c.class] << c
@chunks_by_id[c.id] = c
@chunks_by_id[c.object_id] = c
@chunks << c
@chunk_id += 1
end
def delete_chunk(c)
@chunks_by_type[c.class].delete(c)
@chunks_by_id.delete(c.id)
@chunks_by_id.delete(c.object_id)
@chunks.delete(c)
end
@ -82,18 +82,15 @@ module ChunkManager
@chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? }
end
# for testing and WikiContentStub; we need a page_id even if we have no page
def page_id
0
end
end
# A simplified version of WikiContent. Useful to avoid recursion problems in
# WikiContent.new
class WikiContentStub < String
attr_reader :options
include ChunkManager
def initialize(content, options)
super(content)
@options = options
@ -103,14 +100,12 @@ class WikiContentStub < String
# Detects the mask strings contained in the text of chunks of type chunk_types
# and yields the corresponding chunk ids
# example: content = "chunk123categorychunk <pre>chunk456categorychunk</pre>"
# inside_chunks([Literal::Pre]) ==> yield 456
# inside_chunks(Literal::Pre) ==> yield 456
def inside_chunks(chunk_types)
chunk_types.each { |chunk_type| chunk_type.apply_to(self) }
chunk_types.each{|chunk_type| chunk_type.apply_to(self) }
chunk_types.each { |chunk_type| @chunks_by_type[chunk_type].each { |chunk|
scan_chunkid(chunk.text) { |id|
yield id
}
chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk|
scan_chunkid(hide_chunk.text){|id| yield id }
}
}
end
@ -131,14 +126,15 @@ class WikiContent < String
# Create a new wiki content string from the given one.
# The options are explained at the top of this file.
def initialize(revision, options = {})
def initialize(revision, url_generator, options = {})
@revision = revision
@url_generator = url_generator
@web = @revision.page.web
@options = DEFAULT_OPTS.dup.merge(options)
@options[:engine] = Engines::MAP[@web.markup]
@options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode
@options[:active_chunks] -= [WikiChunk::Word] if @web.brackets_only
@options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode?
@options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only?
@not_rendered = @pre_rendered = nil
@ -151,7 +147,7 @@ class WikiContent < String
# Call @web.page_link using current options.
def page_link(name, text, link_type)
@options[:link_type] = (link_type || :show)
@web.make_link(name, text, @options)
@url_generator.make_link(name, @web, text, @options)
end
def build_chunks
@ -169,9 +165,7 @@ class WikiContent < String
@options[:engine].apply_to(copy)
copy.inside_chunks(HIDE_CHUNKS) do |id|
# Some markup engines can replicate parts of content while converting to HTML
# Hence the if in the below line
@chunks_by_id[id].revert if @chunks_by_id.key?(id)
@chunks_by_id[id.to_i].revert
end
end
@ -187,14 +181,16 @@ class WikiContent < String
pre_render!
@options[:engine].apply_to(self)
# unmask in one go. $~[1] is the chunk id
gsub!(MASK_RE[ACTIVE_CHUNKS]){
if chunk = @chunks_by_id[$~[1]]
chunk.unmask_text
gsub!(MASK_RE[ACTIVE_CHUNKS]) do
chunk = @chunks_by_id[$~[1].to_i]
if chunk.nil?
# if we match a chunkmask that existed in the original content string
# just keep it as it is
else
$~[0]
end}
else
chunk.unmask_text
end
end
self
end
@ -202,8 +198,5 @@ class WikiContent < String
@revision.page.name
end
def page_id
@revision.page.id
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,152 +0,0 @@
// !$*UTF8*$!
{
174B2765065CE31400ED6208 = {
uiCtxt = {
sepNavIntBoundsRect = "{{0, 0}, {711, 1540}}";
sepNavSelRange = "{1371, 0}";
sepNavVisRect = "{{0, 0}, {711, 429}}";
sepNavWindowFrame = "{{265, 148}, {750, 558}}";
};
};
174B2766065CE31400ED6208 = {
uiCtxt = {
sepNavIntBoundsRect = "{{0, 0}, {711, 429}}";
sepNavSelRange = "{44, 0}";
sepNavVisRect = "{{0, 0}, {711, 429}}";
sepNavWindowFrame = "{{61, 141}, {750, 558}}";
};
};
17C1C6E2065D458D003526E7 = {
uiCtxt = {
sepNavIntBoundsRect = "{{0, 0}, {711, 429}}";
sepNavSelRange = "{0, 0}";
sepNavVisRect = "{{0, 0}, {711, 429}}";
sepNavWindowFrame = "{{84, 120}, {750, 558}}";
};
};
29B97313FDCFA39411CA2CEA = {
activeBuildStyle = 4A9504CDFFE6A4B311CA0CBA;
activeExecutable = 4F32CD80089CC78D003CF12F;
activeTarget = 8D1107260486CEB800E47090;
addToTargets = (
8D1107260486CEB800E47090,
);
codeSenseManager = 4F32CD91089CC7BC003CF12F;
executables = (
4F32CD80089CC78D003CF12F,
);
perUserDictionary = {
PBXConfiguration.PBXFileTableDataSource3.PBXFileTableDataSource = {
PBXFileTableDataSourceColumnSortingDirectionKey = "-1";
PBXFileTableDataSourceColumnSortingKey = PBXFileDataSource_Filename_ColumnID;
PBXFileTableDataSourceColumnWidthsKey = (
20,
243,
20,
48,
43,
43,
20,
);
PBXFileTableDataSourceColumnsKey = (
PBXFileDataSource_FiletypeID,
PBXFileDataSource_Filename_ColumnID,
PBXFileDataSource_Built_ColumnID,
PBXFileDataSource_ObjectSize_ColumnID,
PBXFileDataSource_Errors_ColumnID,
PBXFileDataSource_Warnings_ColumnID,
PBXFileDataSource_Target_ColumnID,
);
};
PBXConfiguration.PBXTargetDataSource.PBXTargetDataSource = {
PBXFileTableDataSourceColumnSortingDirectionKey = "-1";
PBXFileTableDataSourceColumnSortingKey = PBXFileDataSource_Filename_ColumnID;
PBXFileTableDataSourceColumnWidthsKey = (
20,
200,
63,
20,
48,
43,
43,
);
PBXFileTableDataSourceColumnsKey = (
PBXFileDataSource_FiletypeID,
PBXFileDataSource_Filename_ColumnID,
PBXTargetDataSource_PrimaryAttribute,
PBXFileDataSource_Built_ColumnID,
PBXFileDataSource_ObjectSize_ColumnID,
PBXFileDataSource_Errors_ColumnID,
PBXFileDataSource_Warnings_ColumnID,
);
};
PBXPerProjectTemplateStateSaveDate = 144499731;
PBXWorkspaceStateSaveDate = 144499731;
};
perUserProjectItems = {
4FCAF055089CE9E40001C11B = 4FCAF055089CE9E40001C11B;
4FCAF05A089D096D0001C11B = 4FCAF05A089D096D0001C11B;
};
sourceControlManager = 4F32CD90089CC7BC003CF12F;
userBuildSettings = {
};
};
4F32CD80089CC78D003CF12F = {
activeArgIndex = 2147483647;
activeArgIndices = (
);
argumentStrings = (
);
configStateDict = {
};
cppStopOnCatchEnabled = 0;
cppStopOnThrowEnabled = 0;
customDataFormattersEnabled = 1;
debuggerPlugin = GDBDebugging;
disassemblyDisplayState = 0;
enableDebugStr = 1;
environmentEntries = (
);
executableSystemSymbolLevel = 0;
executableUserSymbolLevel = 0;
isa = PBXExecutable;
libgmallocEnabled = 0;
name = Instiki;
shlibInfoDictList_v2 = (
);
sourceDirectories = (
);
};
4F32CD90089CC7BC003CF12F = {
fallbackIsa = XCSourceControlManager;
isSCMEnabled = 0;
isa = PBXSourceControlManager;
scmConfiguration = {
};
scmType = "";
};
4F32CD91089CC7BC003CF12F = {
indexTemplatePath = "";
isa = PBXCodeSenseManager;
};
4FCAF055089CE9E40001C11B = {
fRef = 174B2765065CE31400ED6208;
isa = PBXBookmark;
};
4FCAF05A089D096D0001C11B = {
fRef = 174B2765065CE31400ED6208;
isa = PBXTextBookmark;
name = "AppDelegate.mm: 50";
rLen = 0;
rLoc = 1371;
rType = 0;
vrLen = 680;
vrLoc = 0;
};
8D1107260486CEB800E47090 = {
activeExec = 0;
executables = (
4F32CD80089CC78D003CF12F,
);
};
}

View file

@ -53,7 +53,8 @@
sourceTree = "<group>";
};
1058C7A1FEA54F0111CA2CBB = {
isa = PBXFileReference;
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = Cocoa.framework;
path = /System/Library/Frameworks/Cocoa.framework;
@ -110,10 +111,6 @@
};
17BF6FD9067536EB003F37D6 = {
children = (
4F32CE69089CD9F5003CF12F,
4F32CE72089CD9F5003CF12F,
4F32CE81089CD9F5003CF12F,
4F32CE86089CD9F5003CF12F,
63B86D2F0673A5D300807E13,
63B86D1A0673A5B200807E13,
63B86D100673A58400807E13,
@ -149,7 +146,7 @@
isa = PBXFileReference;
lastKnownFileType = "compiled.mach-o.executable";
name = ruby;
path = /usr/bin/ruby;
path = /usr/local/bin/ruby;
refType = 0;
sourceTree = "<absolute>";
};
@ -163,7 +160,7 @@
isa = PBXFileReference;
lastKnownFileType = folder;
name = ruby;
path = /usr/lib/ruby;
path = /usr/local/lib/ruby;
refType = 0;
sourceTree = "<absolute>";
};
@ -316,7 +313,8 @@
sourceTree = "<group>";
};
29B97324FDCFA39411CA2CEA = {
isa = PBXFileReference;
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = AppKit.framework;
path = /System/Library/Frameworks/AppKit.framework;
@ -324,7 +322,8 @@
sourceTree = "<absolute>";
};
29B97325FDCFA39411CA2CEA = {
isa = PBXFileReference;
fallbackIsa = PBXFileReference;
isa = PBXFrameworkReference;
lastKnownFileType = wrapper.framework;
name = Foundation.framework;
path = /System/Library/Frameworks/Foundation.framework;
@ -360,6 +359,8 @@
//4A3
//4A4
4A9504CCFFE6A4B311CA0CBA = {
buildRules = (
);
buildSettings = {
COPY_PHASE_STRIP = NO;
DEBUGGING_SYMBOLS = YES;
@ -374,6 +375,8 @@
name = Development;
};
4A9504CDFFE6A4B311CA0CBA = {
buildRules = (
);
buildSettings = {
COPY_PHASE_STRIP = YES;
GCC_ENABLE_FIX_AND_CONTINUE = NO;
@ -387,85 +390,6 @@
//4A2
//4A3
//4A4
//4F0
//4F1
//4F2
//4F3
//4F4
4F32CE69089CD9F5003CF12F = {
isa = PBXFileReference;
lastKnownFileType = folder;
name = config;
path = ../../../config;
refType = 2;
sourceTree = SOURCE_ROOT;
};
4F32CE72089CD9F5003CF12F = {
isa = PBXFileReference;
lastKnownFileType = folder;
name = public;
path = ../../../public;
refType = 2;
sourceTree = SOURCE_ROOT;
};
4F32CE81089CD9F5003CF12F = {
isa = PBXFileReference;
lastKnownFileType = folder;
name = script;
path = ../../../script;
refType = 2;
sourceTree = SOURCE_ROOT;
};
4F32CE86089CD9F5003CF12F = {
isa = PBXFileReference;
lastKnownFileType = folder;
name = vendor;
path = ../../../vendor;
refType = 2;
sourceTree = SOURCE_ROOT;
};
4F32D28F089CD9FC003CF12F = {
fileRef = 4F32CE69089CD9F5003CF12F;
isa = PBXBuildFile;
settings = {
};
};
4F32D290089CD9FC003CF12F = {
fileRef = 4F32CE72089CD9F5003CF12F;
isa = PBXBuildFile;
settings = {
};
};
4F32D291089CD9FC003CF12F = {
fileRef = 4F32CE81089CD9F5003CF12F;
isa = PBXBuildFile;
settings = {
};
};
4F32D292089CD9FC003CF12F = {
fileRef = 4F32CE86089CD9F5003CF12F;
isa = PBXBuildFile;
settings = {
};
};
4F32D336089CDDE0003CF12F = {
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
isa = PBXShellScriptBuildPhase;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "";
};
//4F0
//4F1
//4F2
//4F3
//4F4
//630
//631
//632
@ -476,10 +400,6 @@
dstPath = rb_src;
dstSubfolderSpec = 7;
files = (
4F32D290089CD9FC003CF12F,
4F32D291089CD9FC003CF12F,
4F32D28F089CD9FC003CF12F,
4F32D292089CD9FC003CF12F,
63B86D310673A5D600807E13,
63B86D1C0673A5B600807E13,
63B86D120673A59100807E13,
@ -492,9 +412,9 @@
fileEncoding = 4;
isa = PBXFileReference;
name = app;
path = ../../../app;
refType = 2;
sourceTree = SOURCE_ROOT;
path = /Users/duff/Source/rb_src/instiki/app;
refType = 0;
sourceTree = "<absolute>";
};
63B86D120673A59100807E13 = {
fileRef = 63B86D100673A58400807E13;
@ -506,10 +426,10 @@
explicitFileType = folder;
fileEncoding = 4;
isa = PBXFileReference;
name = lib;
path = ../../../lib;
refType = 2;
sourceTree = SOURCE_ROOT;
name = libraries;
path = /Users/duff/Source/rb_src/instiki/libraries;
refType = 0;
sourceTree = "<absolute>";
};
63B86D1C0673A5B600807E13 = {
fileRef = 63B86D1A0673A5B200807E13;
@ -522,9 +442,9 @@
isa = PBXFileReference;
lastKnownFileType = text.script.ruby;
name = instiki.rb;
path = ../../../instiki.rb;
refType = 2;
sourceTree = SOURCE_ROOT;
path = /Users/duff/Source/rb_src/instiki/instiki.rb;
refType = 0;
sourceTree = "<absolute>";
};
63B86D310673A5D600807E13 = {
fileRef = 63B86D2F0673A5D300807E13;
@ -550,7 +470,6 @@
8D11072E0486CEB800E47090,
17F6C3A90662960F007E0BD0,
63B86D0F0673A53100807E13,
4F32D336089CDDE0003CF12F,
);
buildRules = (
);

View file

@ -3,6 +3,13 @@ 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
#
@ -11,6 +18,14 @@ Options +FollowSymLinks +ExecCGI
# Example:
# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
RewriteEngine On
# If your Rails application is accessed via an Alias directive,
# then you MUST also set the RewriteBase in this htaccess file.
#
# Example:
# Alias /myrailsapp /path/to/myrailsapp/public
# RewriteBase /myrailsapp
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f

View file

@ -1,3 +1,5 @@
<!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>

View file

@ -1,3 +1,5 @@
<!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>

10
public/dispatch.cgi Executable file
View file

@ -0,0 +1,10 @@
#!c:/ruby/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 Executable file
View file

@ -0,0 +1,24 @@
#!c:/ruby/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!

View file

@ -1,4 +1,4 @@
#!e:/ruby/bin/ruby
#!c:/ruby/bin/ruby
require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 0 B

708
public/javascripts/controls.js vendored Normal file
View file

@ -0,0 +1,708 @@
// 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) && (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.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);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okText: "ok",
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
ajaxOptions: {}
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function() {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.focus(this.editField);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
this.form.appendChild(okButton);
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
this.form.appendChild(cancelLink);
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
new Ajax.Updater(
{
success: this.element,
// don't update on failure (this could be an option)
failure: null
},
this.url,
Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions)
);
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};

516
public/javascripts/dragdrop.js vendored Normal file
View file

@ -0,0 +1,516 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Element.Class part Copyright (c) 2005 by Rick Olson
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==element });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
isContained: function(element, drop) {
var parentNode = element.parentNode;
return drop._containers.detect(function(c) { return parentNode == c });
},
isAffected: function(pX, pY, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.Class.has_any(element, drop.accept))) &&
Position.within(drop.element, pX, pY) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.Class.remove(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(this.last_active) this.deactivate(this.last_active);
if(drop.hoverclass)
Element.Class.add(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(event, element) {
if(!this.drops.length) return;
var pX = Event.pointerX(event);
var pY = Event.pointerY(event);
Position.prepare();
var i = this.drops.length-1; do {
var drop = this.drops[i];
if(this.isAffected(pX, pY, element, drop)) {
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
if(drop.greedy) {
this.activate(drop);
return;
}
}
} while (i--);
if(this.last_active) this.deactivate(this.last_active);
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
observers: [],
addObserver: function(observer) {
this.observers.push(observer);
},
removeObserver: function(element) { // element instead of obsever fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
},
notify: function(eventName, draggable) { // 'onStart', 'onEnd'
this.observers.invoke(eventName, draggable);
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string'))
this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
Element.makePositioned(this.element); // fix IE
this.offsetX = 0;
this.offsetY = 0;
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
this.originalX = this.element.offsetLeft;
this.originalY = this.element.offsetTop;
this.options = options;
this.active = false;
this.dragging = false;
this.eventMouseDown = this.startDrag.bindAsEventListener(this);
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.update.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
this.registerEvents();
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
this.unregisterEvents();
},
registerEvents: function() {
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
},
unregisterEvents: function() {
//if(!this.active) return;
//Event.stopObserving(document, "mouseup", this.eventMouseUp);
//Event.stopObserving(document, "mousemove", this.eventMouseMove);
//Event.stopObserving(document, "keypress", this.eventKeypress);
},
currentLeft: function() {
return parseInt(this.element.style.left || '0');
},
currentTop: function() {
return parseInt(this.element.style.top || '0')
},
startDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
// this.registerEvents();
this.active = true;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
this.offsetX = (pointer[0] - offsets[0]);
this.offsetY = (pointer[1] - offsets[1]);
Event.stop(event);
}
},
finishDrag: function(event, success) {
// this.unregisterEvents();
this.active = false;
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
this.currentTop()-this.originalTop,
this.currentLeft()-this.originalLeft);
} else {
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Droppables.reset();
},
keyPress: function(event) {
if(this.active) {
if(event.keyCode==Event.KEY_ESC) {
this.finishDrag(event, false);
Event.stop(event);
}
}
},
endDrag: function(event) {
if(this.active && this.dragging) {
this.finishDrag(event, true);
Event.stop(event);
}
this.active = false;
this.dragging = false;
},
draw: function(event) {
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.element);
offsets[0] -= this.currentLeft();
offsets[1] -= this.currentTop();
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = (pointer[1] - offsets[1] - this.offsetY) + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
update: function(event) {
if(this.active) {
if(!this.dragging) {
var style = this.element.style;
this.dragging = true;
if(Element.getStyle(this.element,'position')=='')
style.position = "relative";
if(this.options.zindex) {
this.options.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
Draggables.notify('onStart', this);
if(this.options.starteffect) this.options.starteffect(this.element);
}
Droppables.show(event, this.element);
this.draw(event);
if(this.options.change) this.options.change(this);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
}
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: new Array(),
options: function(element){
element = $(element);
return this.sortables.detect(function(s) { return s.element == element });
},
destroy: function(element){
element = $(element);
this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
});
this.sortables = this.sortables.reject(function(s) { return s.element == element });
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false, // fixme: unimplemented
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
format: null,
onChange: function() {},
onUpdate: function() {}
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass,
onHover: Sortable.onHover,
greedy: !options.dropOnEmpty
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// make it so
// drop on empty handling
if(options.dropOnEmpty) {
Droppables.add(element,
{containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.Class.childrenWith(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
options.droppables.push(e);
});
// keep reference
this.sortables.push(options);
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
if(!element.hasChildNodes()) return null;
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName==options.tag.toUpperCase() &&
(!options.only || (Element.Class.has(e, options.only))))
elements.push(e);
if(options.tree) {
var grandchildren = this.findElements(e, options);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : null);
},
onHover: function(element, dropon, overlap) {
if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon) {
if(element.parentNode!=dropon) {
dropon.appendChild(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.Class.add(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Sortable._marker.style.left = offsets[0] + 'px';
Element.show(Sortable._marker);
},
serialize: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format || /^[^_]*_(.*)$/
}, arguments[1] || {});
return $(this.findElements(element, options) || []).collect( function(item) {
return (encodeURIComponent(options.name) + "[]=" +
encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
}).join("&");
}
}

1101
public/javascripts/effects.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
var Scriptaculous = {
Version: '1.5_rc3',
require: function(libraryName) {
// inserting via DOM fails in Safari 2.0, so brute force approach
document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
},
load: function() {
if((typeof Prototype=='undefined') ||
parseFloat(Prototype.Version.split(".")[0] + "." +
Prototype.Version.split(".")[1]) < 1.4)
throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
var scriptTags = document.getElementsByTagName("script");
for(var i=0;i<scriptTags.length;i++) {
if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js(\?.*)?$/)) {
var path = scriptTags[i].src.replace(/scriptaculous\.js(\?.*)?$/,'');
this.require(path + 'effects.js');
this.require(path + 'dragdrop.js');
this.require(path + 'controls.js');
this.require(path + 'slider.js');
break;
}
}
}
}
Scriptaculous.load();

View file

@ -0,0 +1,258 @@
// Copyright (c) 2005 Marty Haught
//
// See scriptaculous.js for full license.
if(!Control) var Control = {};
Control.Slider = Class.create();
// options:
// axis: 'vertical', or 'horizontal' (default)
// increment: (default: 1)
// step: (default: 1)
//
// callbacks:
// onChange(value)
// onSlide(value)
Control.Slider.prototype = {
initialize: function(handle, track, options) {
this.handle = $(handle);
this.track = $(track);
this.options = options || {};
this.axis = this.options.axis || 'horizontal';
this.increment = this.options.increment || 1;
this.step = parseInt(this.options.step) || 1;
this.value = 0;
var defaultMaximum = Math.round(this.track.offsetWidth / this.increment);
if(this.isVertical()) defaultMaximum = Math.round(this.track.offsetHeight / this.increment);
this.maximum = this.options.maximum || defaultMaximum;
this.minimum = this.options.minimum || 0;
// Will be used to align the handle onto the track, if necessary
this.alignX = parseInt (this.options.alignX) || 0;
this.alignY = parseInt (this.options.alignY) || 0;
// Zero out the slider position
this.setCurrentLeft(Position.cumulativeOffset(this.track)[0] - Position.cumulativeOffset(this.handle)[0] + this.alignX);
this.setCurrentTop(this.trackTop() - Position.cumulativeOffset(this.handle)[1] + this.alignY);
this.offsetX = 0;
this.offsetY = 0;
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
this.originalZ = parseInt(this.handle.style.zIndex || "0");
// Prepopulate Slider value
this.setSliderValue(parseInt(this.options.sliderValue) || 0);
this.active = false;
this.dragging = false;
this.disabled = false;
// FIXME: use css
this.handleImage = $(this.options.handleImage) || false;
this.handleDisabled = this.options.handleDisabled || false;
this.handleEnabled = false;
if(this.handleImage)
this.handleEnabled = this.handleImage.src || false;
if(this.options.disabled)
this.setDisabled();
// Value Array
this.values = this.options.values || false; // Add method to validate and sort??
Element.makePositioned(this.handle); // fix IE
this.eventMouseDown = this.startDrag.bindAsEventListener(this);
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.update.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
},
dispose: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
},
setDisabled: function(){
this.disabled = true;
if(this.handleDisabled)
this.handleImage.src = this.handleDisabled;
},
setEnabled: function(){
this.disabled = false;
if(this.handleEnabled)
this.handleImage.src = this.handleEnabled;
},
currentLeft: function() {
return parseInt(this.handle.style.left || '0');
},
currentTop: function() {
return parseInt(this.handle.style.top || '0');
},
setCurrentLeft: function(left) {
this.handle.style.left = left +"px";
},
setCurrentTop: function(top) {
this.handle.style.top = top +"px";
},
trackLeft: function(){
return Position.cumulativeOffset(this.track)[0];
},
trackTop: function(){
return Position.cumulativeOffset(this.track)[1];
},
getNearestValue: function(value){
if(this.values){
var i = 0;
var offset = Math.abs(this.values[0] - value);
var newValue = this.values[0];
for(i=0; i < this.values.length; i++){
var currentOffset = Math.abs(this.values[i] - value);
if(currentOffset < offset){
newValue = this.values[i];
offset = currentOffset;
}
}
return newValue;
}
return value;
},
setSliderValue: function(sliderValue){
// First check our max and minimum and nearest values
sliderValue = this.getNearestValue(sliderValue);
if(sliderValue > this.maximum) sliderValue = this.maximum;
if(sliderValue < this.minimum) sliderValue = this.minimum;
var offsetDiff = (sliderValue - (this.value||this.minimum)) * this.increment;
if(this.isVertical()){
this.setCurrentTop(offsetDiff + this.currentTop());
} else {
this.setCurrentLeft(offsetDiff + this.currentLeft());
}
this.value = sliderValue;
this.updateFinished();
},
minimumOffset: function(){
return(this.isVertical() ?
this.trackTop() + this.alignY :
this.trackLeft() + this.alignX);
},
maximumOffset: function(){
return(this.isVertical() ?
this.trackTop() + this.alignY + (this.maximum - this.minimum) * this.increment :
this.trackLeft() + this.alignX + (this.maximum - this.minimum) * this.increment);
},
isVertical: function(){
return (this.axis == 'vertical');
},
startDrag: function(event) {
if(Event.isLeftClick(event)) {
if(!this.disabled){
this.active = true;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.handle);
this.offsetX = (pointer[0] - offsets[0]);
this.offsetY = (pointer[1] - offsets[1]);
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
}
Event.stop(event);
}
},
update: function(event) {
if(this.active) {
if(!this.dragging) {
var style = this.handle.style;
this.dragging = true;
if(style.position=="") style.position = "relative";
style.zIndex = this.options.zindex;
}
this.draw(event);
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
}
},
draw: function(event) {
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var offsets = Position.cumulativeOffset(this.handle);
offsets[0] -= this.currentLeft();
offsets[1] -= this.currentTop();
// Adjust for the pointer's position on the handle
pointer[0] -= this.offsetX;
pointer[1] -= this.offsetY;
var style = this.handle.style;
if(this.isVertical()){
if(pointer[1] > this.maximumOffset())
pointer[1] = this.maximumOffset();
if(pointer[1] < this.minimumOffset())
pointer[1] = this.minimumOffset();
// Increment by values
if(this.values){
this.value = this.getNearestValue(Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum);
pointer[1] = this.trackTop() + this.alignY + (this.value - this.minimum) * this.increment;
} else {
this.value = Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum;
}
style.top = pointer[1] - offsets[1] + "px";
} else {
if(pointer[0] > this.maximumOffset()) pointer[0] = this.maximumOffset();
if(pointer[0] < this.minimumOffset()) pointer[0] = this.minimumOffset();
// Increment by values
if(this.values){
this.value = this.getNearestValue(Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum);
pointer[0] = this.trackLeft() + this.alignX + (this.value - this.minimum) * this.increment;
} else {
this.value = Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum;
}
style.left = (pointer[0] - offsets[0]) + "px";
}
if(this.options.onSlide) this.options.onSlide(this.value);
},
endDrag: function(event) {
if(this.active && this.dragging) {
this.finishDrag(event, true);
Event.stop(event);
}
this.active = false;
this.dragging = false;
},
finishDrag: function(event, success) {
this.active = false;
this.dragging = false;
this.handle.style.zIndex = this.originalZ;
this.originalLeft = this.currentLeft();
this.originalTop = this.currentTop();
this.updateFinished();
},
updateFinished: function() {
if(this.options.onChange) this.options.onChange(this.value);
},
keyPress: function(event) {
if(this.active && !this.disabled) {
switch(event.keyCode) {
case Event.KEY_ESC:
this.finishDrag(event, false);
Event.stop(event);
break;
}
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
}
}
}

1
public/robots.txt Normal file
View file

@ -0,0 +1 @@
# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file

View file

@ -1,134 +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/clean'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
$VERBOSE = nil
# Standard Rails tasks
desc 'Run all tests'
task :default => [:test_units, :test_functional]
desc 'Require application environment.'
task :environment do
unless defined? RAILS_ROOT
require File.dirname(__FILE__) + '/config/environment'
end
end
desc 'Generate API documentatio, show coding stats'
task :doc => [ :appdoc, :stats ]
desc 'Run the unit tests in test/unit'
Rake::TestTask.new('test_units') { |t|
t.libs << 'test'
t.pattern = 'test/unit/**/*_test.rb'
t.verbose = true
}
desc 'Run the functional tests in test/functional'
Rake::TestTask.new('test_functional') { |t|
t.libs << 'test'
t.pattern = 'test/functional/**/*_test.rb'
t.verbose = true
}
desc 'Generate documentation for the application'
Rake::RDocTask.new('appdoc') { |rdoc|
rdoc.rdoc_dir = 'doc/app'
rdoc.title = 'Rails Application Documentation'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('doc/README_FOR_APP')
rdoc.rdoc_files.include('app/**/*.rb')
}
desc 'Generate documentation for the Rails framework'
Rake::RDocTask.new("apidoc") { |rdoc|
rdoc.rdoc_dir = 'doc/api'
rdoc.title = 'Rails Framework Documentation'
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('CHANGELOG')
rdoc.rdoc_files.include('vendor/rails/railties/CHANGELOG')
rdoc.rdoc_files.include('vendor/rails/railties/MIT-LICENSE')
rdoc.rdoc_files.include('vendor/rails/activerecord/README')
rdoc.rdoc_files.include('vendor/rails/activerecord/CHANGELOG')
rdoc.rdoc_files.include('vendor/rails/activerecord/lib/active_record/**/*.rb')
rdoc.rdoc_files.exclude('vendor/rails/activerecord/lib/active_record/vendor/*')
rdoc.rdoc_files.include('vendor/rails/actionpack/README')
rdoc.rdoc_files.include('vendor/rails/actionpack/CHANGELOG')
rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_controller/**/*.rb')
rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_view/**/*.rb')
rdoc.rdoc_files.include('vendor/rails/actionmailer/README')
rdoc.rdoc_files.include('vendor/rails/actionmailer/CHANGELOG')
rdoc.rdoc_files.include('vendor/rails/actionmailer/lib/action_mailer/base.rb')
rdoc.rdoc_files.include('vendor/rails/actionwebservice/README')
rdoc.rdoc_files.include('vendor/rails/actionwebservice/ChangeLog')
rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/**/*.rb')
rdoc.rdoc_files.include('vendor/rails/activesupport/README')
rdoc.rdoc_files.include('vendor/rails/activesupport/lib/active_support/**/*.rb')
}
desc 'Report code statistics (KLOCs, etc) from the application'
task :stats => [ :environment ] do
require 'code_statistics'
CodeStatistics.new(
['Helpers', 'app/helpers'],
['Controllers', 'app/controllers'],
['Functionals', 'test/functional'],
['Models', 'app/models'],
['Units', 'test/unit'],
['Miscellaneous (lib)', 'lib']
).to_s
end
# Additional tasks (not standard Rails)
CLEAN << 'pkg' << 'storage' << 'doc' << 'html'
begin
require 'rubygems'
require 'rake/gempackagetask'
rescue Exception => e
nil
end
if defined? Rake::GemPackageTask
gemspec = eval(File.read('instiki.gemspec'))
Rake::GemPackageTask.new(gemspec) do |p|
p.gem_spec = gemspec
p.need_tar = true
p.need_zip = true
end
Rake::PackageTask.new('instiki', gemspec.version) do |p|
p.need_tar = true
p.need_zip = true
# the list of glob expressions for files comes from instiki.gemspec
p.package_files.include($__instiki_source_patterns)
end
# Create a task to build the RDOC documentation tree.
rd = Rake::RDocTask.new("rdoc") { |rdoc|
rdoc.rdoc_dir = 'html'
rdoc.title = 'Instiki -- The Wiki'
rdoc.options << '--line-numbers --inline-source --main README'
rdoc.rdoc_files.include(gemspec.files)
rdoc.main = 'README'
}
else
puts 'Warning: without Rubygems packaging tasks are not available'
end
# Shorthand aliases
desc 'Shorthand for test_units'
task :tu => :test_units
desc 'Shorthand for test_units'
task :ut => :test_units
desc 'Shorthand for test_functional'
task :tf => :test_functional
desc 'Shorthand for test_functional'
task :ft => :test_functional
require 'tasks/rails'

19
script/benchmarker Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env ruby
if ARGV.empty?
puts "Usage: benchmarker times 'Person.expensive_way' 'Person.another_expensive_way' ..."
exit
end
require File.dirname(__FILE__) + '/../config/environment'
require 'benchmark'
include Benchmark
# Don't include compilation in the benchmark
ARGV[1..-1].each { |expression| eval(expression) }
bm(6) do |x|
ARGV[1..-1].each_with_index do |expression, idx|
x.report("##{idx + 1}") { ARGV[0].to_i.times { eval(expression) } }
end
end

View file

@ -1,4 +1,4 @@
#!e:/ruby/bin/ruby
#!/usr/bin/env ruby
require 'rubygems'
require_gem 'rails'
require 'breakpoint_client'

23
script/console Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env ruby
irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb'
require 'optparse'
options = { :sandbox => false, :irb => irb }
OptionParser.new do |opt|
opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |options[:sandbox]| }
opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |options[:irb]| }
opt.parse!(ARGV)
end
libs = " -r irb/completion"
libs << " -r #{File.dirname(__FILE__)}/../config/environment"
libs << " -r console_sandbox" if options[:sandbox]
ENV['RAILS_ENV'] = ARGV.first || 'development'
if options[:sandbox]
puts "Loading #{ENV['RAILS_ENV']} environment in sandbox."
puts "Any modifications you make will be rolled back on exit."
else
puts "Loading #{ENV['RAILS_ENV']} environment."
end
exec "#{options[:irb]} #{libs} --prompt-mode simple"

24
script/create_db Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env ruby
APP_ROOT = File.expand_path(File.dirname(__FILE__)) + '/../'
require APP_ROOT + 'config/environment'
require 'db_structure'
config = ActiveRecord::Base.configurations
['production', 'test', 'development'].each do |target|
begin
ENV['RAILS_ENV'] = target
load APP_ROOT + 'config/environment.rb'
puts "Creating tables for #{target}..."
db_structure(config[target]['adapter']).split(/\s*;\s*/).each do |sql|
ActiveRecord::Base.connection.execute(sql)
end
puts "done."
rescue => e
puts "failed: " + e.inspect
end
end

View file

@ -1,97 +0,0 @@
#!/usr/bin/ruby
=begin
The purpose of this script is to help people poke around in the Madeleine storage.
Two caveats:
1. You MUST be a reasonably good Ruby programmer to use it successfully for anything non-trivial.
2. It's very easy to screw up something by poking in the storage internals. If you do, please
undo your changes by deleting the most recent snapshot(s) and don't ask for help.
Usage example:
E:\eclipse\workspace\instiki\script>irb
irb(main):001:0> load 'debug_storage'
Enter path to storage [E:/eclipse/workspace/instiki/storage/2500]:
Loading storage from the default storage path (E:/eclipse/workspace/instiki/storage/2500)
Instiki storage from E:/eclipse/workspace/instiki/storage/2500 is loaded.
Access it via global variable $wiki.
Happy poking!
=> true
irb(main):003:0> $wiki.system
=> {"password"=>"foo"}
irb(main):005:0> $wiki.system['password'] = 'bar'
=> "bar"
irb(main):006:0> $wiki.webs.keys
=> ["wiki1", "wiki2"]
irb(main):007:0> $wiki.webs['wiki1'].password = 'the_password'
=> "the_password"
irb(main):008:0> WikiService::snapshot
=> []
Things that are possible:
# cleaning old revisions
$wiki.webs['wiki'].pages['HomePage'].revisions = $wiki.webs['wiki'].pages['HomePage'].revisions[-1..-1]
# Changing contents of a revision
$wiki.webs['wiki'].pages['HomePage'].revisions[-1] = 'new content'
# Checking that all pages can be rendered by the markup engine
$wiki.webs['wiki'].pages.each_pair do |name, page|
page.revisions.each_with_index do |revision, i|
begin
revision.display_content
rescue =>
puts "Error when rendering revision ##{i} of page #{name.inspect}:"
puts e.message
puts e.backtrace.join("\n")
end
end
=end
require 'fileutils'
require 'optparse'
require 'webrick'
default_storage_path = File.expand_path(File.dirname(__FILE__) + "/../storage/2500")
print "Enter path to storage [#{default_storage_path}]: "
storage_path = gets.chomp
if storage_path.empty?
storage_path = default_storage_path
puts "Loading storage from the default storage path (#{storage_path})"
else
puts "Loading storage from the path you entered (#{storage_path})"
end
unless File.directory?(storage_path) and not
(Dir["#{storage_path}/*.snapshot"] + Dir["#{storage_path}/*.command_log"]).empty?
raise "Found no storage at #{storage_path}"
end
RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') unless defined? RAILS_ROOT
unless defined? ADDITIONAL_LOAD_PATHS
ADDITIONAL_LOAD_PATHS = %w(
app/models
lib
vendor/madeleine-0.7.1/lib
vendor/RedCloth-3.0.3/lib
vendor/rubyzip-0.5.8/lib
).map { |dir| "#{File.expand_path(File.join(RAILS_ROOT, dir))}"
}.delete_if { |dir| not File.exist?(dir) }
# Prepend to $LOAD_PATH
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
end
require 'wiki_service'
WikiService.storage_path = storage_path
$wiki = WikiService.instance
puts "Instiki storage from #{storage_path} is loaded."
puts 'Access it via global variable $wiki.'
puts 'Happy poking!'
nil

7
script/destroy Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/environment'
require 'rails_generator'
require 'rails_generator/scripts/destroy'
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
Rails::Generator::Scripts::Destroy.new.run(ARGV)

7
script/generate Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/environment'
require 'rails_generator'
require 'rails_generator/scripts/generate'
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
Rails::Generator::Scripts::Generate.new.run(ARGV)

217
script/import_storage Executable file
View file

@ -0,0 +1,217 @@
#!/usr/bin/env ruby
require 'optparse'
OPTIONS = {
:instiki_root => nil,
:storage => nil,
:database => 'mysql'
}
ARGV.options do |opts|
script_name = File.basename($0)
opts.banner = "Usage: ruby #{script_name} [options]"
opts.separator ""
opts.on("-t", "--storage /full/path/to/storage", String,
"Full path to your storage, ",
"such as /home/joe/instiki/storage/2500",
"It should be the directory that ",
"contains .snapshot files.") do |storage|
OPTIONS[:storage] = storage
end
opts.separator ""
opts.on("-i", "--instiki /full/path/to/instiki", String,
"Full path to your Instiki 0.10 installation, ",
"such as /home/joe/instiki-0.10.2") do |instiki|
OPTIONS[:instiki] = instiki
end
opts.separator ""
opts.on("-o", "--outfile /full/path/to/output_file", String,
"Full path (including filename!) to where ",
"you want the SQL output placed, such as ",
"/home/joe/instiki.sql") do |outfile|
OPTIONS[:outfile] = outfile
end
opts.on("-d", "--database {mysql|sqlite|postgres}", String,
"Target database (they have slightly different syntax)",
"default: mysql") do |database|
OPTIONS[:database] = database
end
opts.separator ""
opts.on_tail("-h", "--help",
"Show this help message.") { puts opts; exit }
opts.parse!
end
if OPTIONS[:instiki].nil? or OPTIONS[:storage].nil? or OPTIONS[:outfile].nil?
$stderr.puts "Please specify full paths to Instiki 0.10 installation and storage,"
$stderr.puts "as well as the path to the output file"
$stderr.puts
puts ARGV.options
exit -1
end
if FileTest.exists? OPTIONS[:outfile]
$stderr.puts "Output file #{OPTIONS[:outfile]} already exists!"
$stderr.puts "Please specify a new file"
$stderr.puts
puts ARGV.options
exit -1
end
raise "Directory #{OPTIONS[:instiki]} not found" unless File.directory?(OPTIONS[:instiki])
raise "Directory #{OPTIONS[:storage]} not found" unless File.directory?(OPTIONS[:storage])
expected_page_rb_path = File.join(OPTIONS[:instiki], 'app/models/page.rb')
raise "Instiki installation not found in #{OPTIONS[:instiki]}" unless File.file?(expected_page_rb_path)
expected_snapshot_pattern = File.join(OPTIONS[:storage], '*.snapshot')
raise "No snapshots found in #{expected_snapshot_pattern}" if Dir[expected_snapshot_pattern].empty?
INSTIKI_ROOT = File.expand_path(OPTIONS[:instiki])
ADDITIONAL_LOAD_PATHS = %w(
app/models
lib
vendor/madeleine-0.7.1/lib
vendor/RedCloth-3.0.4/lib
vendor/rubyzip-0.5.8/lib
).map { |dir| "#{File.expand_path(File.join(INSTIKI_ROOT, dir))}"
}.delete_if { |dir| not File.exist?(dir) }
# Prepend to $LOAD_PATH
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
require 'webrick'
require 'wiki_service'
class Revision
alias :__display_content :display_content
def display_content
return self
end
end
class Time
def ansi
strftime('%Y-%m-%d %H:%M:%S')
end
end
def sql_insert(table, hash)
columns = hash.keys
values = columns.map { |column| hash[column] }
values = values.map do |val|
if val.nil?
'NULL'
else
case OPTIONS[:database]
when 'mysql', 'postgres'
escaped_value = val.to_s.gsub("'", "\\\\'")
when 'sqlite'
escaped_value = val.to_s.gsub("'", "''")
else
raise "Unsupported database option #{OPTIONS[:database]}"
end
"'#{escaped_value.gsub("\r\n", "\n")}'"
end
end
output = "INSERT INTO #{table} ("
output << columns.join(", ")
output << ") VALUES ("
output << values.join(", ")
output << ");"
output
end
def delete_all(outfile)
%w(wiki_references revisions pages system webs).each { |table| outfile.puts "DELETE FROM #{table};" }
end
def next_id(key)
$ids ||= {}
if $ids[key].nil?
$ids[key] = 1
else
$ids[key] = $ids[key] + 1
end
$ids[key]
end
def current_id(key)
$ids[key] or raise "No curent ID for #{key.inspect}"
end
WikiService.storage_path = OPTIONS[:storage]
wiki = WikiService.instance
File.open(OPTIONS[:outfile], 'w') { |outfile|
delete_all(outfile)
wiki.webs.each_pair do |web_name, web|
outfile.puts sql_insert(:webs, {
:id => next_id(:web),
:name => web.name,
:address => web.address,
:password => web.password,
:additional_style => web.additional_style,
:allow_uploads => web.allow_uploads,
:published => web.published,
:count_pages => web.count_pages,
:markup => web.markup,
:color => web.color,
:max_upload_size => web.max_upload_size,
:safe_mode => web.safe_mode,
:brackets_only => web.brackets_only,
:created_at => web.pages.values.map { |p| p.revisions.first.created_at }.min.ansi,
:updated_at => web.pages.values.map { |p| p.revisions.last.created_at }.max.ansi
})
puts "Web #{web_name} has #{web.pages.keys.size} pages"
web.pages.each_pair do |page_name, page|
outfile.puts "BEGIN;"
outfile.puts sql_insert(:pages, {
:id => next_id(:page),
:web_id => current_id(:web),
:locked_by => page.locked_by,
:name => page.name,
:created_at => page.revisions.first.created_at.ansi,
:updated_at => page.revisions.last.created_at.ansi
})
puts " Page #{page_name} has #{page.revisions.size} revisions"
page.revisions.each_with_index do |rev, i|
outfile.puts sql_insert(:revisions, {
:id => next_id(:revision),
:page_id => current_id(:page),
:content => rev.content,
:author => rev.author.to_s,
:ip => (rev.author.is_a?(Author) ? rev.author.ip : 'N/A'),
:created_at => rev.created_at.ansi,
:updated_at => rev.created_at.ansi,
:revised_at => rev.created_at.ansi
})
puts " Revision #{i} created at #{rev.created_at.ansi}"
end
outfile.puts "COMMIT;"
end
end
}

34
script/profiler Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env ruby
if ARGV.empty?
$stderr.puts "Usage: profiler 'Person.expensive_method(10)' [times]"
exit(1)
end
# Keep the expensive require out of the profile.
$stderr.puts 'Loading Rails...'
require File.dirname(__FILE__) + '/../config/environment'
# Define a method to profile.
if ARGV[1] and ARGV[1].to_i > 1
eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end"
else
eval "def profile_me() #{ARGV[0]} end"
end
# Use the ruby-prof extension if available. Fall back to stdlib profiler.
begin
require 'prof'
$stderr.puts 'Using the ruby-prof extension.'
Prof.clock_mode = Prof::GETTIMEOFDAY
Prof.start
profile_me
results = Prof.stop
require 'rubyprof_ext'
Prof.print_profile(results, $stderr)
rescue LoadError
$stderr.puts 'Using the standard Ruby profiler.'
Profiler__.start_profile
profile_me
Profiler__.stop_profile
Profiler__.print_profile($stderr)
end

25
script/reset_references Normal file
View file

@ -0,0 +1,25 @@
ENV['RAILS_ENV'] = ARGV.first || 'development'
$stderr.puts "Loading Rails for #{ENV['RAILS_ENV']} environment..."
require File.dirname(__FILE__) + '/../config/environment'
class StubUrlGenerator
def make_link(*args)
'StubLink'
end
end
PageRenderer.setup_url_generator(StubUrlGenerator.new)
WikiReference.delete_all
Web.find_all.each do |web|
web.pages.find(:all, :order => 'name').each do |page|
$stderr.puts "Processing page '#{page.name}'"
begin
PageRenderer.new(page.current_revision).display_content(update_references = true)
rescue => e
puts e
puts e.backtrace
end
end
end

29
script/runner Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
require 'optparse'
options = { :environment => "development" }
ARGV.options do |opts|
script_name = File.basename($0)
opts.banner = "Usage: runner 'puts Person.find(1).name' [options]"
opts.separator ""
opts.on("-e", "--environment=name", String,
"Specifies the environment for the runner to operate under (test/development/production).",
"Default: development") { |options[:environment]| }
opts.separator ""
opts.on("-h", "--help",
"Show this help message.") { puts opts; exit }
opts.parse!
end
ENV["RAILS_ENV"] = options[:environment]
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/environment'
eval(ARGV.first)

View file

@ -1,93 +1,49 @@
#!/usr/bin/ruby
#!/usr/bin/env ruby
require 'webrick'
require 'optparse'
require 'fileutils'
pwd = File.expand_path(File.dirname(__FILE__) + "/..")
OPTIONS = {
# Overridable options
:port => 2500,
:ip => '0.0.0.0',
:environment => 'production',
:server_root => File.expand_path(File.dirname(__FILE__) + '/../public/'),
:server_type => WEBrick::SimpleServer,
:storage => "#{File.expand_path(FileUtils.pwd)}/storage",
:ip => "0.0.0.0",
:environment => "production",
:server_root => File.expand_path(File.dirname(__FILE__) + "/../public/"),
:server_type => WEBrick::SimpleServer
}
ARGV.options do |opts|
script_name = File.basename($0)
opts.banner = "Usage: ruby #{script_name} [options]"
opts.separator ''
opts.separator ""
opts.on('-p', '--port=port', Integer,
'Runs Instiki on the specified port.',
'Default: 2500') { |OPTIONS[:port]| }
opts.on('-b', '--binding=ip', String,
'Binds Rails to the specified ip.',
'Default: 0.0.0.0') { |OPTIONS[:ip]| }
opts.on('-e', '--environment=name', String,
'Specifies the environment to run this server under (test/development/production).',
'Default: production') { |OPTIONS[:environment]| }
opts.on('-d', '--daemon',
'Make Instiki run as a Daemon (only works if fork is available -- meaning on *nix).'
opts.on("-p", "--port=port", Integer,
"Runs Instiki on the specified port.",
"Default: 2500") { |OPTIONS[:port]| }
opts.on("-b", "--binding=ip", String,
"Binds Instiki to the specified ip.",
"Default: 0.0.0.0") { |OPTIONS[:ip]| }
opts.on("-e", "--environment=name", String,
"Specifies the environment to run this server under (test/development/production).",
"Default: development") { |OPTIONS[:environment]| }
opts.on("-d", "--daemon",
"Make Instiki run as a Daemon (only works if fork is available -- meaning on *nix)."
) { OPTIONS[:server_type] = WEBrick::Daemon }
opts.on('-s', '--simple', '--simple-server',
'[deprecated] Forces Instiki not to run as a Daemon if fork is available.',
'Since version 0.10.0 this option is ignored.'
) { puts "Warning: -s (--simple) option is deprecated. See instiki --help for details." }
opts.on('-t', '--storage=storage', String,
'Makes Instiki use the specified directory for storage.',
'Default: ./storage/[port]') { |OPTIONS[:storage]| }
opts.on('-x', '--notex',
'Blocks wiki exports to TeX and PDF, even when pdflatex is available.'
) { |OPTIONS[:notex]| }
opts.on('-v', '--verbose',
'Enables debug-level logging'
) { OPTIONS[:verbose] = true }
opts.separator ''
opts.separator ""
opts.on('-h', '--help',
'Show this help message.') { puts opts; exit }
opts.on("-h", "--help",
"Show this help message.") { puts opts; exit }
opts.parse!
end
if OPTIONS[:environment] == 'production'
storage_path = "#{OPTIONS[:storage]}/#{OPTIONS[:port]}"
else
storage_path = "#{OPTIONS[:storage]}/#{OPTIONS[:environment]}/#{OPTIONS[:port]}"
end
FileUtils.mkdir_p(storage_path)
ENV["RAILS_ENV"] = OPTIONS[:environment]
require File.dirname(__FILE__) + "/../config/environment"
require 'webrick_server'
ENV['RAILS_ENV'] = OPTIONS[:environment]
$instiki_debug_logging = OPTIONS[:verbose]
require File.expand_path(File.dirname(__FILE__) + '/../config/environment')
WikiService.storage_path = storage_path
OPTIONS['working_directory'] = File.expand_path(RAILS_ROOT)
if OPTIONS[:notex]
OPTIONS[:pdflatex] = false
else
begin
OPTIONS[:pdflatex] = system "pdflatex -version"
rescue Errno::ENOENT
OPTIONS[:pdflatex] = false
end
end
if defined? INSTIKI_BATCH_JOB
require 'application'
else
puts "=> Starting Instiki on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}"
puts "=> Data files are stored in #{storage_path}"
require 'webrick_server'
require_dependency 'application'
OPTIONS[:index_controller] = 'wiki'
ApplicationController.wiki = WikiService.instance
DispatchServlet.dispatch(OPTIONS)
end
puts "=> Instiki started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}"
puts "=> Ctrl-C to shutdown; call with --help for options" if OPTIONS[:server_type] == WEBrick::SimpleServer
DispatchServlet.dispatch(OPTIONS)

View file

@ -1,9 +0,0 @@
require 'test_helper'
require 'find'
test_root = File.dirname(__FILE__)
Find.find(test_root) { |path|
if File.file?(path) and path =~ /.*_test\.rb$/
load path
end
}

55
test/fixtures/pages.yml vendored Normal file
View file

@ -0,0 +1,55 @@
home_page:
id: 1
created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %>
updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %>
web_id: 1
name: HomePage
my_way:
id: 2
created_at: <%= 9.days.ago.to_formatted_s(:db) %>
updated_at: <%= 9.days.ago.to_formatted_s(:db) %>
web_id: 1
name: MyWay
smart_engine:
id: 3
created_at: <%= 8.days.ago.to_formatted_s(:db) %>
updated_at: <%= 8.days.ago.to_formatted_s(:db) %>
web_id: 1
name: SmartEngine
that_way:
id: 4
created_at: <%= 7.days.ago.to_formatted_s(:db) %>
updated_at: <%= 7.days.ago.to_formatted_s(:db) %>
web_id: 1
name: ThatWay
no_wiki_word:
id: 5
created_at: <%= 6.days.ago.to_formatted_s(:db) %>
updated_at: <%= 6.days.ago.to_formatted_s(:db) %>
web_id: 1
name: NoWikiWord
first_page:
id: 6
created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %>
updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %>
web_id: 1
name: FirstPage
oak:
id: 7
created_at: <%= 5.days.ago.to_formatted_s(:db) %>
updated_at: <%= 5.days.ago.to_formatted_s(:db) %>
web_id: 1
name: Oak
elephant:
id: 8
created_at: <%= 10.minutes.ago.to_formatted_s(:db) %>
updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %>
web_id: 1
name: Elephant

83
test/fixtures/revisions.yml vendored Normal file
View file

@ -0,0 +1,83 @@
home_page_first_revision:
id: 1
created_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %>
updated_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %>
revised_at: <%= Time.local(2004, 4, 4, 15, 50).to_formatted_s(:db) %>
page_id: 1
content: First revision of the HomePage end
author: AnAuthor
ip: 127.0.0.1
my_way_first_revision:
id: 2
created_at: <%= 9.days.ago.to_formatted_s(:db) %>
updated_at: <%= 9.days.ago.to_formatted_s(:db) %>
revised_at: <%= 9.days.ago.to_formatted_s(:db) %>
page_id: 2
content: MyWay
author: Me
smart_engine_first_revision:
id: 3
created_at: <%= 8.days.ago.to_formatted_s(:db) %>
updated_at: <%= 8.days.ago.to_formatted_s(:db) %>
revised_at: <%= 8.days.ago.to_formatted_s(:db) %>
page_id: 3
content: SmartEngine
author: Me
that_way_first_revision:
id: 4
created_at: <%= 7.days.ago.to_formatted_s(:db) %>
updated_at: <%= 7.days.ago.to_formatted_s(:db) %>
revised_at: <%= 7.days.ago.to_formatted_s(:db) %>
page_id: 4
content: ThatWay
author: Me
no_wiki_word_first_revision:
id: 5
created_at: <%= 6.days.ago.to_formatted_s(:db) %>
updated_at: <%= 6.days.ago.to_formatted_s(:db) %>
revised_at: <%= 6.days.ago.to_formatted_s(:db) %>
page_id: 5
content: hey you
author: Me
home_page_second_revision:
id: 6
created_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %>
updated_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %>
revised_at: <%= Time.local(2004, 4, 4, 16, 50).to_formatted_s(:db) %>
page_id: 1
content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEngine in that SmartEngineGUI
author: DavidHeinemeierHansson
first_page_first_revision:
id: 7
created_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %>
updated_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %>
revised_at: <%= Time.local(2004, 4, 4, 16, 55).to_formatted_s(:db) %>
page_id: 6
content: HisWay would be MyWay in kinda ThatWay in HisWay though MyWay \\OverThere -- see SmartEngine in that SmartEngineGUI
author: DavidHeinemeierHansson
oak_first_revision:
id: 8
created_at: <%= 5.days.ago.to_formatted_s(:db) %>
updated_at: <%= 5.days.ago.to_formatted_s(:db) %>
revised_at: <%= 5.days.ago.to_formatted_s(:db) %>
page_id: 7
content: "All about oak.\ncategory: trees"
author: TreeHugger
ip: 127.0.0.2
elephant_first_revision:
id: 9
created_at: <%= 10.minutes.ago.to_formatted_s(:db) %>
updated_at: <%= 10.minutes.ago.to_formatted_s(:db) %>
revised_at: <%= 10.minutes.ago.to_formatted_s(:db) %>
page_id: 8
content: "All about elephants.\ncategory: animals"
author: Guest
ip: 127.0.0.2

2
test/fixtures/system.yml vendored Normal file
View file

@ -0,0 +1,2 @@
system:
password: test_password

Some files were not shown because too many files have changed in this diff Show more