wiki branch merged into trunk

git-svn-id: http://redmine.rubyforge.org/svn/trunk@323 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2007-03-10 15:09:49 +00:00
parent 8b98ceb92c
commit c514316a2e
68 changed files with 2443 additions and 353 deletions

View File

@ -32,6 +32,10 @@ class ApplicationController < ActionController::Base
end
end
def logged_in_user_membership
@user_membership ||= Member.find(:first, :conditions => ["user_id=? and project_id=?", self.logged_in_user.id, @project.id])
end
# check if login is globally required to access the application
def check_if_login_required
require_login if Setting.login_required?
@ -89,6 +93,16 @@ class ApplicationController < ActionController::Base
render :nothing => true, :status => 403
false
end
# make sure that the user is a member of the project (or admin) if project is private
# used as a before_filter for actions that do not require any particular permission on the project
def check_project_privacy
return true if @project.is_public?
return false unless logged_in_user
return true if logged_in_user.admin? || logged_in_user_membership
render :nothing => true, :status => 403
false
end
# store current uri in session.
# return to this location by calling redirect_back_or_default

View File

@ -68,6 +68,10 @@ class ProjectsController < ApplicationController
@project.repository = Repository.new
@project.repository.attributes = params[:repository]
end
if "1" == params[:wiki_enabled]
@project.wiki = Wiki.new
@project.wiki.attributes = params[:wiki]
end
if @project.save
flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects'
@ -113,6 +117,15 @@ class ProjectsController < ApplicationController
@project.repository.update_attributes params[:repository]
end
end
if params[:wiki_enabled]
case params[:wiki_enabled]
when "0"
@project.wiki.destroy
when "1"
@project.wiki ||= Wiki.new
@project.wiki.update_attributes params[:wiki]
end
end
@project.attributes = params[:project]
if @project.save
flash[:notice] = l(:notice_successful_update)

View File

@ -0,0 +1,111 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikiController < ApplicationController
layout 'base'
before_filter :find_wiki, :check_project_privacy, :except => [:preview]
# display a page (in editing mode if it doesn't exist)
def index
page_title = params[:page]
@page = @wiki.find_or_new_page(page_title)
if @page.new_record?
edit
render :action => 'edit' and return
end
@content = (params[:version] ? @page.content.versions.find_by_version(params[:version]) : @page.content)
if params[:export] == 'html'
export = render_to_string :action => 'export', :layout => false
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
return
elsif params[:export] == 'txt'
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
end
render :action => 'show'
end
# edit an existing page or a new one
def edit
@page = @wiki.find_or_new_page(params[:page])
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content
@content.text = "h1. #{@page.pretty_title}" if @content.text.empty?
# don't keep previous comment
@content.comment = nil
if request.post?
if @content.text == params[:content][:text]
# don't save if text wasn't changed
redirect_to :action => 'index', :id => @project, :page => @page.title
return
end
@content.text = params[:content][:text]
@content.comment = params[:content][:comment]
@content.author = logged_in_user
# if page is new @page.save will also save content, but not if page isn't a new record
if (@page.new_record? ? @page.save : @content.save)
redirect_to :action => 'index', :id => @project, :page => @page.title
end
end
end
# show page history
def history
@page = @wiki.find_page(params[:page])
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comment, updated_on, version",
:order => 'version DESC'
end
# display special pages
def special
page_title = params[:page].downcase
case page_title
# show pages index, sorted by title
when 'page_index'
# eager load information about last updates, without loading text
@pages = @wiki.pages.find :all, :select => "wiki_pages.*, wiki_contents.updated_on",
:joins => "LEFT JOIN wiki_contents ON wiki_contents.page_id = wiki_pages.id",
:order => 'title'
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title'
export = render_to_string :action => 'export_multiple', :layout => false
send_data(export, :type => 'text/html', :filename => "wiki.html")
return
else
# requested special page doesn't exist, redirect to default page
redirect_to :action => 'index', :id => @project, :page => nil and return
end
render :action => "special_#{page_title}"
end
def preview
@text = params[:content][:text]
render :partial => 'preview'
end
private
def find_wiki
@project = Project.find(params[:id])
@wiki = @project.wiki
rescue ActiveRecord::RecordNotFound
render_404
end
end

View File

@ -63,7 +63,7 @@ module ApplicationHelper
end
def format_time(time)
l_datetime(time) if time
l_datetime((time.is_a? String) ? time.to_time : time) if time
end
def day_name(day)
@ -92,10 +92,42 @@ module ApplicationHelper
html
end
def textilizable(text)
text = (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize") ? RedCloth.new(h(text)).to_html : simple_format(auto_link(h(text)))
# turn "#id" patterns into links to issues
text = text.gsub(/#(\d+)([^;\d])/, "<a href='/issues/show/\\1'>#\\1</a>\\2")
# textilize text according to system settings and RedCloth availability
def textilizable(text, options = {})
# different methods for formatting wiki links
case options[:wiki_links]
when :local
# used for local links to html files
format_wiki_link = Proc.new {|title| "#{title}.html" }
when :anchor
# used for single-file wiki export
format_wiki_link = Proc.new {|title| "##{title}" }
else
if @project
format_wiki_link = Proc.new {|title| url_for :controller => 'wiki', :action => 'index', :id => @project, :page => title }
else
format_wiki_link = Proc.new {|title| title }
end
end
# turn wiki links into textile links:
# example:
# [[link]] -> "link":link
# [[link|title]] -> "title":link
text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) {|m| "\"#{$3 || $1}\":" + format_wiki_link.call(Wiki.titleize($1)) }
# turn issue ids to textile links
# example:
# #52 -> "#52":/issues/show/52
text = text.gsub(/#(\d+)([\s\.\(\)\-,:;])/) {|m| "\"##{$1}\":" + url_for(:controller => 'issues', :action => 'show', :id => $1) + $2 }
# turn revision ids to textile links (@project needed)
# example:
# r52 -> "r52":/repositories/revision/6?rev=52 (@project.id is 6)
text = text.gsub(/r(\d+)([\s\.\(\)\-,:;])/) {|m| "\"r#{$1}\":" + url_for(:controller => 'repositories', :action => 'revision', :id => @project.id, :rev => $1) + $2 } if @project
# finally textilize text
text = (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize") ? auto_link(RedCloth.new(text, [:filter_html]).to_html) : simple_format(auto_link(h(text)))
end
def error_messages_for(object_name, options = {})

View File

@ -0,0 +1,19 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module WikiHelper
end

View File

@ -26,13 +26,14 @@ class Project < ActiveRecord::Base
has_many :news, :dependent => :delete_all, :include => :author
has_many :issue_categories, :dependent => :delete_all, :order => "issue_categories.name"
has_one :repository, :dependent => :destroy
has_one :wiki, :dependent => :destroy
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id'
acts_as_tree :order => "name", :counter_cache => true
validates_presence_of :name, :description
validates_uniqueness_of :name
validates_associated :custom_values, :on => :update
validates_associated :repository
validates_associated :repository, :wiki
validates_format_of :name, :with => /^[\w\s\'\-]*$/i
# returns latest created projects

44
app/models/wiki.rb Normal file
View File

@ -0,0 +1,44 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Wiki < ActiveRecord::Base
belongs_to :project
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
validates_presence_of :project_id, :start_page
# find the page with the given title
# if page doesn't exist, return a new page
def find_or_new_page(title)
title = Wiki.titleize(title || start_page)
find_page(title) || WikiPage.new(:wiki => self, :title => title)
end
# find the page with the given title
def find_page(title)
pages.find_by_title(Wiki.titleize(title || start_page))
end
# turn a string into a valid page title
def self.titleize(title)
# replace spaces with _ and remove unwanted caracters
title = title.gsub(/\s+/, '_').delete(',;|') if title
# upcase the first letter
title = title[0..0].upcase + title[1..-1] if title
title
end
end

View File

@ -0,0 +1,58 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'zlib'
class WikiContent < ActiveRecord::Base
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :text
acts_as_versioned
class Version
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
attr_protected :data
def text=(plain)
case Setting.wiki_compression
when 'gzip'
begin
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
self.compression = 'gzip'
rescue
self.data = plain
self.compression = ''
end
else
self.data = plain
self.compression = ''
end
plain
end
def text
@text ||= case compression
when 'gzip'
Zlib::Inflate.inflate(data)
else
# uncompressed data
data
end
end
end
end

34
app/models/wiki_page.rb Normal file
View File

@ -0,0 +1,34 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
validates_presence_of :title
validates_format_of :title, :with => /^[^,\s]*$/
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
validates_associated :content
def before_save
self.title = Wiki.titleize(title)
end
def pretty_title
title.tr '_', ' '
end
end

View File

@ -91,6 +91,7 @@
<%= link_to l(:label_change_log), {:controller => 'projects', :action => 'changelog', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_roadmap), {:controller => 'projects', :action => 'roadmap', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_wiki), {:controller => 'wiki', :id => @project, :page => nil }, :class => "menuItem" if @project.wiki and !@project.wiki.new_record? %>
<%= link_to l(:label_member_plural), {:controller => 'projects', :action => 'list_members', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
<%= link_to l(:label_search), {:controller => 'projects', :action => 'search', :id => @project }, :class => "menuItem" %>
@ -115,6 +116,7 @@
<li><%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %></li>
<li><%= link_to l(:label_roadmap), :controller => 'projects', :action => 'roadmap', :id => @project %></li>
<li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
<li><%= link_to l(:label_wiki), :controller => 'wiki', :id => @project, :page => nil if @project.wiki and !@project.wiki.new_record? %></li>
<li><%= link_to l(:label_member_plural), :controller => 'projects', :action => 'list_members', :id => @project %></li>
<li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
<li><%= link_to l(:label_search), :controller => 'projects', :action => 'search', :id => @project %></li>

View File

@ -38,6 +38,20 @@
<%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
</div>
<div class="box">
<h3><%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %></h3>
<%= hidden_field_tag "wiki_enabled", 0 %>
<div id="wiki">
<% fields_for :wiki, @project.wiki, { :builder => TabularFormBuilder, :lang => current_language} do |wiki| %>
<p><%= wiki.text_field :start_page, :size => 60, :required => true %></p>
<% # content_tag("div", "", :id => "wiki_start_page_auto_complete", :class => "auto_complete") +
# auto_complete_field("wiki_start_page", { :url => { :controller => 'wiki', :action => 'auto_complete_for_wiki_page', :id => @project } })
%>
<% end %>
</div>
<%= javascript_tag "Element.hide('wiki');" if @project.wiki.nil? %>
</div>
<% content_for :header_tags do %>
<%= javascript_include_tag 'calendar/calendar' %>
<%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>

View File

@ -38,6 +38,9 @@
<p><label><%= l(:setting_text_formatting) %></label>
<%= select_tag 'settings[text_formatting]', options_for_select( [[l(:label_none), 0], ["textile", "textile"]], Setting.text_formatting) %></p>
<p><label><%= l(:setting_wiki_compression) %></label>
<%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p>
</div>
<%= submit_tag l(:button_save) %>
<% end %>

View File

@ -0,0 +1,3 @@
<fieldset class="preview"><legend><%= l(:label_preview) %></legend>
<%= textilizable @text %>
</fieldset>

39
app/views/wiki/edit.rhtml Normal file
View File

@ -0,0 +1,39 @@
<div class="contextual">
<%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
</div>
<h2><%= @page.pretty_title %></h2>
<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %>
<%= error_messages_for 'content' %>
<p><%= f.text_area :text, :cols => 100, :rows => 25, :style => "width:99%;" %></p>
<p><label><%= l(:field_comment) %></label><br /><%= f.text_field :comment, :size => 120 %></p>
<p><%= submit_tag l(:button_save) %>
<%= link_to_remote l(:label_preview),
{ :url => { :controller => 'wiki', :action => 'preview' },
:method => 'get',
:update => 'preview',
:with => "Form.serialize('wiki_form')",
:loading => "Element.show('indicator')",
:loaded => "Element.hide('indicator')"
} %>
<span id="indicator" style="display:none"><%= image_tag "loading.gif", :align => "absmiddle" %></span>
</p>
<% end %>
<% if Setting.text_formatting == 'textile' %>
<%= javascript_include_tag 'jstoolbar' %>
<script type="text/javascript">
//<![CDATA[
if (document.getElementById) {
if (document.getElementById('content_text')) {
var commentTb = new jsToolBar(document.getElementById('content_text'));
commentTb.draw();
}
}
//]]>
</script>
<% end %>
<div id="preview" class="wiki"></div>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title><%=h @page.pretty_title %></title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style>
body { font:80% Verdana,Tahoma,Arial,sans-serif; }
h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
</style>
</head>
<body>
<%= textilizable @content.text, :wiki_links => :local %>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<title><%=h @wiki.project.name %></title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style>
body { font:80% Verdana,Tahoma,Arial,sans-serif; }
h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
</style>
</head>
<body>
<strong><%= l(:label_page_index) %></strong>
<ul>
<% @pages.each do |page| %>
<li><a href="#<%= page.title %>"><%= page.pretty_title %></a></li>
<% end %>
</ul>
<% @pages.each do |page| %>
<hr />
<%= textilizable page.content.text, :wiki_links => :anchor %>
<% end %>
</body>
</html>

View File

@ -0,0 +1,28 @@
<div class="contextual">
<%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
</div>
<h2><%= @page.pretty_title %></h2>
<h3><%= l(:label_history) %></h3>
<table class="list">
<thead><tr>
<th>#</th>
<th><%= l(:field_updated_on) %></th>
<th><%= l(:field_author) %></th>
<th><%= l(:field_comment) %></th>
</tr></thead>
<tbody>
<% @versions.each do |ver| %>
<tr class="<%= cycle("odd", "even") %>">
<th align="center"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></th>
<td align="center"><%= format_time(ver.updated_on) %></td>
<td><em><%= ver.author ? ver.author.name : "anonyme" %></em></td>
<td><%=h ver.comment %></td>
</tr>
<% end %>
</tbody>
</table>
<p><%= link_to l(:button_back), :action => 'index', :page => @page.title %></p>

30
app/views/wiki/show.rhtml Normal file
View File

@ -0,0 +1,30 @@
<div class="contextual">
<%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') if @content.version == @page.content.version %>
<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
<%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
</div>
<% if @content.version != @page.content.version %>
<p>
<%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
<%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> -
<%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
<%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
<br />
<em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
<%=h @content.comment %>
</p>
<hr />
<% end %>
<div class="wiki">
<% cache "wiki/show/#{@page.id}/#{@content.version}" do %>
<%= textilizable @content.text %>
<% end %>
</div>
<div class="contextual">
<%= l(:label_export_to) %>
<%= link_to 'HTML', {:export => 'html', :version => @content.version}, :class => 'icon icon-html' %>,
<%= link_to 'TXT', {:export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %>
</div>

View File

@ -0,0 +1,13 @@
<div class="contextual">
<% unless @pages.empty? %>
<%= l(:label_export_to) %> <%= link_to 'HTML', {:action => 'special', :page => 'export'}, :class => 'icon icon-html' %>
<% end %>
</div>
<h2><%= l(:label_page_index) %></h2>
<% if @pages.empty? %><p><i><%= l(:label_no_data) %></i></p><% end %>
<ul><% @pages.each do |page| %>
<li><%= link_to page.pretty_title, :action => 'index', :page => page.title %> -
<%= l(:label_last_updates) %>: <%= format_time(page.updated_on) %></li>
<% end %></ul>

View File

@ -9,7 +9,8 @@ ActionController::Routing::Routes.draw do |map|
# You can have the root of your site routed by hooking up ''
# -- just remember to delete public/index.html.
map.connect '', :controller => "welcome"
map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
map.connect 'help/:ctrl/:page', :controller => 'help'
#map.connect ':controller/:action/:id/:sort_key/:sort_order'

View File

@ -41,6 +41,8 @@ mail_from:
default: redmine@somenet.foo
text_formatting:
default: textile
wiki_compression:
default: ""
default_language:
default: en
host_name:

View File

@ -0,0 +1,14 @@
class CreateWikis < ActiveRecord::Migration
def self.up
create_table :wikis do |t|
t.column :project_id, :integer, :null => false
t.column :start_page, :string, :limit => 255, :null => false
t.column :status, :integer, :default => 1, :null => false
end
add_index :wikis, :project_id, :name => :wikis_project_id
end
def self.down
drop_table :wikis
end
end

View File

@ -0,0 +1,14 @@
class CreateWikiPages < ActiveRecord::Migration
def self.up
create_table :wiki_pages do |t|
t.column :wiki_id, :integer, :null => false
t.column :title, :string, :limit => 255, :null => false
t.column :created_on, :datetime, :null => false
end
add_index :wiki_pages, [:wiki_id, :title], :name => :wiki_pages_wiki_id_title
end
def self.down
drop_table :wiki_pages
end
end

View File

@ -0,0 +1,30 @@
class CreateWikiContents < ActiveRecord::Migration
def self.up
create_table :wiki_contents do |t|
t.column :page_id, :integer, :null => false
t.column :author_id, :integer
t.column :text, :text, :default => "", :null => false
t.column :comment, :string, :limit => 255, :default => ""
t.column :updated_on, :datetime, :null => false
t.column :version, :integer, :null => false
end
add_index :wiki_contents, :page_id, :name => :wiki_contents_page_id
create_table :wiki_content_versions do |t|
t.column :wiki_content_id, :integer, :null => false
t.column :page_id, :integer, :null => false
t.column :author_id, :integer
t.column :data, :binary
t.column :compression, :string, :limit => 6, :default => ""
t.column :comment, :string, :limit => 255, :default => ""
t.column :updated_on, :datetime, :null => false
t.column :version, :integer, :null => false
end
add_index :wiki_content_versions, :wiki_content_id, :name => :wiki_content_versions_wcid
end
def self.down
drop_table :wiki_contents
drop_table :wiki_content_versions
end
end

View File

@ -142,6 +142,7 @@ field_auth_source: Authentisierung Modus
field_hide_mail: Mein email address verstecken
field_comment: Anmerkung
field_url: URL
field_start_page: Hauptseite
setting_app_title: Applikation Titel
setting_app_subtitle: Applikation Untertitel
@ -154,6 +155,7 @@ setting_issues_export_limit: Issues export limit
setting_mail_from: Emission address
setting_host_name: Host Name
setting_text_formatting: Textformatierung
setting_wiki_compression: Wiki Geschichte Kompression
label_user: Benutzer
label_user_plural: Benutzer
@ -322,6 +324,10 @@ label_search: Suche
label_result: %d Resultat
label_result_plural: %d Resultate
label_all_words: Alle Wörter
label_wiki: Wiki
label_page_index: Index
label_current_version: Gegenwärtige Version
label_preview: Vorbetrachtung
button_login: Einloggen
button_submit: Einreichen

View File

@ -142,6 +142,7 @@ field_auth_source: Authentication mode
field_hide_mail: Hide my email address
field_comment: Comment
field_url: URL
field_start_page: Start page
setting_app_title: Application title
setting_app_subtitle: Application subtitle
@ -154,6 +155,7 @@ setting_issues_export_limit: Issues export limit
setting_mail_from: Emission mail address
setting_host_name: Host name
setting_text_formatting: Text formatting
setting_wiki_compression: Wiki history compression
label_user: User
label_user_plural: Users
@ -322,6 +324,10 @@ label_search: Search
label_result: %d result
label_result_plural: %d results
label_all_words: All words
label_wiki: Wiki
label_page_index: Index
label_current_version: Current version
label_preview: Preview
button_login: Login
button_submit: Submit

View File

@ -142,6 +142,7 @@ field_auth_source: Modo de la autentificación
field_hide_mail: Ocultar mi email address
field_comment: Comentario
field_url: URL
field_start_page: Página principal
setting_app_title: Título del aplicación
setting_app_subtitle: Subtítulo del aplicación
@ -154,6 +155,7 @@ setting_issues_export_limit: Issues export limit
setting_mail_from: Email de la emisión
setting_host_name: Nombre de anfitrión
setting_text_formatting: Formato de texto
setting_wiki_compression: Compresión de la historia de Wiki
label_user: Usuario
label_user_plural: Usuarios
@ -322,6 +324,10 @@ label_search: Búsqueda
label_result: %d resultado
label_result_plural: %d resultados
label_all_words: Todas las palabras
label_wiki: Wiki
label_page_index: Índice
label_current_version: Versión actual
label_preview: Previo
button_login: Conexión
button_submit: Someter

View File

@ -142,6 +142,7 @@ field_auth_source: Mode d'authentification
field_hide_mail: Cacher mon adresse mail
field_comment: Commentaire
field_url: URL
field_start_page: Page de démarrage
setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
@ -154,6 +155,7 @@ setting_issues_export_limit: Limite export demandes
setting_mail_from: Adresse d'émission
setting_host_name: Nom d'hôte
setting_text_formatting: Formatage du texte
setting_wiki_compression: Compression historique wiki
label_user: Utilisateur
label_user_plural: Utilisateurs
@ -322,6 +324,10 @@ label_search: Recherche
label_result: %d résultat
label_result_plural: %d résultats
label_all_words: Tous les mots
label_wiki: Wiki
label_page_index: Index
label_current_version: Version actuelle
label_preview: Prévisualisation
button_login: Connexion
button_submit: Soumettre

View File

@ -143,6 +143,7 @@ field_auth_source: 認証モード
field_hide_mail: Emailアドレスを隠す
field_comment: コメント
field_url: URL
field_start_page: メインページ
setting_app_title: アプリケーションのタイトル
setting_app_subtitle: アプリケーションのサブタイトル
@ -155,6 +156,7 @@ setting_issues_export_limit: 出力する問題数の上限
setting_mail_from: Emission メールアドレス
setting_host_name: ホスト名
setting_text_formatting: テキストの書式
setting_wiki_compression: Wiki history compression
label_user: ユーザ
label_user_plural: ユーザ
@ -323,6 +325,10 @@ label_search: 検索
label_result: %d 件の結果
label_result_plural: %d 件の結果
label_all_words: すべての単語
label_wiki: Wiki
label_page_index: 索引
label_current_version: 最近版
label_preview: 下検分
button_login: ログイン
button_submit: 変更

BIN
public/images/draft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/images/history.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

BIN
public/images/html.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

BIN
public/images/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
public/images/txt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

View File

@ -139,6 +139,8 @@ vertical-align: middle;
.icon-cancel { background-image: url(../images/cancel.png); }
.icon-pdf { background-image: url(../images/pdf.png); }
.icon-csv { background-image: url(../images/csv.png); }
.icon-html { background-image: url(../images/html.png); }
.icon-txt { background-image: url(../images/txt.png); }
.icon-file { background-image: url(../images/file.png); }
.icon-folder { background-image: url(../images/folder.png); }
.icon-package { background-image: url(../images/package.png); }
@ -150,6 +152,8 @@ vertical-align: middle;
.icon-logout { background-image: url(../images/logout.png); }
.icon-help { background-image: url(../images/help.png); }
.icon-attachment { background-image: url(../images/attachment.png); }
.icon-index { background-image: url(../images/index.png); }
.icon-history { background-image: url(../images/history.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); }
@ -181,7 +185,7 @@ border-left: 1px dashed #c0c0c0;
}
#content h2{
#content h2, #content div.wiki h1 {
display:block;
margin:0 0 16px 0;
font-size:1.7em;
@ -576,4 +580,23 @@ to account for 3 pixel bug: http://www.positioniseverything.net/explorer/threepx
* html .threepxfix{
margin-left: 3px;
}
}
/***** Wiki sections ****/
#content div.wiki { font-size: 110%}
#content div.wiki h2, div.wiki h3 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; color:#606060; }
#content div.wiki h2 { font-size: 1.4em;}
#content div.wiki h3 { font-size: 1.2em;}
div.wiki table {
border: 1px solid #505050;
border-collapse: collapse;
}
div.wiki table, div.wiki td {
border: 1px solid #bbb;
padding: 4px;
}
#preview .preview { background: #fafbfc url(../images/draft.png); }

View File

@ -449,6 +449,15 @@ permissions_005:
mail_option: false
sort: 151
is_public: false
permissions_061:
action: search
id: 62
description: label_search
controller: projects
mail_enabled: false
mail_option: false
sort: 130
is_public: true
permissions_050:
action: history
id: 50

View File

@ -1,379 +1,163 @@
---
permissions_roles_075:
permissions_roles_054:
role_id: 3
permission_id: 34
permissions_roles_047:
permission_id: 44
permissions_roles_043:
role_id: 2
permission_id: 25
permissions_roles_032:
role_id: 1
permission_id: 15
permissions_roles_102:
role_id: 2
permission_id: 4
permissions_roles_019:
role_id: 3
permission_id: 30
permissions_roles_048:
role_id: 2
permission_id: 24
permissions_roles_103:
role_id: 2
permission_id: 27
permissions_roles_076:
role_id: 2
permission_id: 41
permissions_roles_049:
role_id: 1
permission_id: 3
permissions_roles_104:
role_id: 2
permission_id: 36
permissions_roles_077:
role_id: 2
permission_id: 7
permissions_roles_105:
role_id: 2
permission_id: 32
permissions_roles_078:
role_id: 3
permission_id: 38
permissions_roles_106:
role_id: 2
permission_id: 14
permissions_roles_020:
role_id: 2
permission_id: 9
permissions_roles_079:
role_id: 2
permission_id: 18
permissions_roles_107:
role_id: 3
permission_id: 40
permission_id: 42
permissions_roles_021:
role_id: 1
permission_id: 13
permissions_roles_108:
permission_id: 22
permissions_roles_010:
role_id: 1
permission_id: 29
permissions_roles_050:
role_id: 2
permission_id: 29
permissions_roles_022:
role_id: 3
permission_id: 4
permissions_roles_109:
permissions_roles_044:
role_id: 3
permission_id: 22
permissions_roles_051:
role_id: 3
permission_id: 37
permissions_roles_023:
role_id: 1
permission_id: 23
permissions_roles_052:
permissions_roles_033:
role_id: 2
permission_id: 33
permissions_roles_024:
role_id: 1
permission_id: 1
permissions_roles_080:
role_id: 2
permission_id: 13
permissions_roles_053:
role_id: 2
permission_id: 1
permissions_roles_025:
role_id: 2
permission_id: 10
permissions_roles_081:
role_id: 3
permission_id: 20
permissions_roles_054:
role_id: 2
permission_id: 12
permissions_roles_026:
role_id: 1
permission_id: 36
permissions_roles_082:
role_id: 1
permission_id: 39
permissions_roles_110:
role_id: 3
permission_id: 6
permissions_roles_027:
role_id: 3
permission_id: 31
permissions_roles_083:
role_id: 1
permission_id: 33
permissions_roles_055:
permission_id: 22
permissions_roles_022:
role_id: 1
permission_id: 38
permissions_roles_111:
role_id: 3
permission_id: 1
permissions_roles_028:
role_id: 1
permission_id: 24
permissions_roles_084:
role_id: 3
permission_id: 16
permissions_roles_056:
role_id: 2
permission_id: 5
permissions_roles_029:
role_id: 1
permission_id: 9
permissions_roles_085:
role_id: 3
permission_id: 27
permissions_roles_057:
role_id: 1
permission_id: 16
permissions_roles_112:
permissions_roles_011:
role_id: 1
permission_id: 20
permissions_roles_086:
role_id: 3
permissions_roles_045:
role_id: 1
permission_id: 12
permissions_roles_058:
role_id: 1
permission_id: 26
permissions_roles_113:
permissions_roles_034:
role_id: 2
permission_id: 37
permissions_roles_087:
role_id: 1
permission_id: 5
permissions_roles_059:
role_id: 3
permission_id: 18
permissions_roles_114:
role_id: 2
permission_id: 20
permissions_roles_115:
permission_id: 44
permissions_roles_023:
role_id: 2
permission_id: 15
permissions_roles_088:
role_id: 2
permission_id: 3
permissions_roles_012:
role_id: 1
permission_id: 36
permissions_roles_001:
role_id: 2
permission_id: 21
permissions_roles_116:
role_id: 3
permission_id: 23
permissions_roles_030:
role_id: 1
permission_id: 30
permissions_roles_089:
permission_id: 14
permissions_roles_046:
role_id: 1
permission_id: 28
permissions_roles_002:
role_id: 3
permission_id: 29
permissions_roles_117:
role_id: 3
permission_id: 28
permissions_roles_031:
permissions_roles_035:
role_id: 1
permission_id: 10
permissions_roles_024:
role_id: 2
permission_id: 42
permissions_roles_013:
role_id: 2
permission_id: 13
permissions_roles_002:
role_id: 1
permission_id: 34
permissions_roles_047:
role_id: 2
permission_id: 4
permissions_roles_036:
role_id: 1
permission_id: 25
permissions_roles_025:
role_id: 1
permission_id: 8
permissions_roles_014:
role_id: 2
permission_id: 38
permissions_roles_003:
role_id: 3
permission_id: 41
permissions_roles_118:
role_id: 1
permission_id: 34
permissions_roles_032:
role_id: 3
permission_id: 9
permissions_roles_004:
role_id: 2
permission_id: 8
permissions_roles_060:
role_id: 2
permission_id: 2
permissions_roles_119:
role_id: 1
permission_id: 21
permissions_roles_033:
role_id: 2
permission_id: 28
permissions_roles_005:
role_id: 3
permission_id: 3
permissions_roles_061:
role_id: 2
permission_id: 40
permissions_roles_006:
role_id: 3
permission_id: 14
permissions_roles_090:
role_id: 2
permission_id: 26
permissions_roles_062:
role_id: 1
permission_id: 19
permissions_roles_034:
role_id: 2
permission_id: 11
permissions_roles_048:
role_id: 2
permission_id: 34
permissions_roles_037:
role_id: 1
permission_id: 43
permissions_roles_026:
role_id: 1
permission_id: 23
permissions_roles_015:
role_id: 1
permission_id: 5
permissions_roles_004:
role_id: 2
permission_id: 36
permissions_roles_049:
role_id: 3
permission_id: 24
permissions_roles_038:
role_id: 2
permission_id: 24
permissions_roles_027:
role_id: 1
permission_id: 41
permissions_roles_016:
role_id: 1
permission_id: 21
permissions_roles_005:
role_id: 1
permission_id: 53
permissions_roles_050:
role_id: 1
permission_id: 13
permissions_roles_039:
role_id: 3
permission_id: 20
permissions_roles_028:
role_id: 2
permission_id: 20
permissions_roles_017:
role_id: 1
permission_id: 37
permissions_roles_006:
role_id: 1
permission_id: 15
permissions_roles_051:
role_id: 1
permission_id: 30
permissions_roles_040:
role_id: 1
permission_id: 11
permissions_roles_029:
role_id: 2
permission_id: 43
permissions_roles_018:
role_id: 2
permission_id: 14
permissions_roles_007:
role_id: 1
permission_id: 35
permissions_roles_091:
role_id: 3
permission_id: 35
permissions_roles_063:
permissions_roles_052:
role_id: 2
permission_id: 30
permissions_roles_035:
permission_id: 10
permissions_roles_041:
role_id: 1
permission_id: 28
permissions_roles_030:
role_id: 1
permission_id: 9
permissions_roles_019:
role_id: 2
permission_id: 23
permission_id: 41
permissions_roles_008:
role_id: 2
permission_id: 17
permissions_roles_092:
role_id: 2
permission_id: 31
permissions_roles_064:
role_id: 3
permission_id: 33
permissions_roles_036:
role_id: 3
permission_id: 5
permissions_roles_120:
role_id: 3
permission_id: 13
permissions_roles_009:
role_id: 1
permission_id: 12
permissions_roles_093:
role_id: 2
permission_id: 42
permissions_roles_065:
role_id: 3
permission_id: 26
permissions_roles_037:
role_id: 1
permission_id: 42
permissions_roles_121:
role_id: 3
permission_id: 2
permissions_roles_094:
role_id: 3
permission_id: 39
permissions_roles_066:
role_id: 2
permission_id: 6
permissions_roles_038:
role_id: 1
permission_id: 25
permissions_roles_122:
role_id: 1
permission_id: 7
permissions_roles_095:
role_id: 2
permission_id: 19
permissions_roles_067:
role_id: 1
permission_id: 17
permissions_roles_039:
role_id: 3
permission_id: 36
permissions_roles_123:
role_id: 3
permission_id: 24
permissions_roles_096:
role_id: 1
permission_id: 18
permissions_roles_068:
role_id: 1
permission_id: 32
permissions_roles_124:
role_id: 1
permission_id: 11
permissions_roles_010:
role_id: 1
permission_id: 8
permissions_roles_069:
role_id: 3
permission_id: 19
permissions_roles_097:
permissions_roles_053:
role_id: 2
permission_id: 35
permissions_roles_125:
role_id: 2
permission_id: 16
permissions_roles_011:
role_id: 3
permission_id: 42
permissions_roles_098:
role_id: 1
permission_id: 6
permissions_roles_126:
role_id: 3
permission_id: 7
permissions_roles_012:
role_id: 3
permission_id: 8
permissions_roles_040:
role_id: 1
permission_id: 2
permissions_roles_099:
role_id: 3
permission_id: 17
permissions_roles_041:
role_id: 2
permission_id: 39
permissions_roles_013:
role_id: 1
permission_id: 40
permissions_roles_070:
role_id: 3
permission_id: 11
permissions_roles_042:
role_id: 1
permission_id: 44
permissions_roles_031:
role_id: 1
permission_id: 24
permissions_roles_020:
role_id: 1
permission_id: 7
permissions_roles_009:
role_id: 2
permission_id: 37
permissions_roles_014:
role_id: 1
permission_id: 22
permissions_roles_071:
role_id: 1
permission_id: 4
permissions_roles_043:
role_id: 3
permission_id: 32
permissions_roles_015:
role_id: 2
permission_id: 22
permissions_roles_072:
role_id: 1
permission_id: 27
permissions_roles_044:
role_id: 1
permission_id: 14
permissions_roles_016:
role_id: 3
permission_id: 15
permissions_roles_073:
role_id: 2
permission_id: 34
permissions_roles_045:
role_id: 3
permission_id: 10
permissions_roles_100:
role_id: 1
permission_id: 10
permissions_roles_017:
role_id: 3
permission_id: 25
permissions_roles_074:
role_id: 2
permission_id: 25
permissions_roles_046:
role_id: 1
permission_id: 31
permissions_roles_101:
role_id: 3
permission_id: 21
permissions_roles_018:
role_id: 1
permission_id: 41

40
test/fixtures/wiki_content_versions.yml vendored Normal file
View File

@ -0,0 +1,40 @@
---
wiki_content_versions_001:
updated_on: 2007-03-07 00:08:07 +01:00
page_id: 1
id: 1
version: 1
author_id: 1
comment: Page creation
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some [[documentation]] here...
wiki_content_versions_002:
updated_on: 2007-03-07 00:08:34 +01:00
page_id: 1
id: 2
version: 2
author_id: 1
comment: Small update
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
wiki_content_versions_003:
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
id: 3
version: 3
author_id: 1
comment: ""
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...

12
test/fixtures/wiki_contents.yml vendored Normal file
View File

@ -0,0 +1,12 @@
---
wiki_contents_001:
text: |-
h1. CookBook documentation
Some updated [[documentation]] here with gzipped history
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
id: 1
version: 3
author_id: 1
comment: Gzip compression activated

6
test/fixtures/wiki_pages.yml vendored Normal file
View File

@ -0,0 +1,6 @@
---
wiki_pages_001:
created_on: 2007-03-07 00:08:07 +01:00
title: CookBook_documentation
id: 1
wiki_id: 1

6
test/fixtures/wikis.yml vendored Normal file
View File

@ -0,0 +1,6 @@
---
wikis_001:
status: 1
start_page: CookBook documentation
project_id: 1
id: 1

View File

@ -24,7 +24,7 @@ class MailerTest < Test::Unit::TestCase
def test_issue_add
issue = Issue.find(1)
GLoc.valid_languages.each do |lang|
Setting.default_language = lang
Setting.default_language = lang.to_s
assert Mailer.deliver_issue_add(issue)
end
end
@ -32,7 +32,7 @@ class MailerTest < Test::Unit::TestCase
def test_issue_edit
journal = Journal.find(1)
GLoc.valid_languages.each do |lang|
Setting.default_language = lang
Setting.default_language = lang.to_s
assert Mailer.deliver_issue_edit(journal)
end
end
@ -40,7 +40,7 @@ class MailerTest < Test::Unit::TestCase
def test_document_add
document = Document.find(1)
GLoc.valid_languages.each do |lang|
Setting.default_language = lang
Setting.default_language = lang.to_s
assert Mailer.deliver_document_add(document)
end
end
@ -48,7 +48,7 @@ class MailerTest < Test::Unit::TestCase
def test_lost_password
token = Token.find(2)
GLoc.valid_languages.each do |lang|
token.user.update_attribute :language, lang
token.user.update_attribute :language, lang.to_s
assert Mailer.deliver_lost_password(token)
end
end
@ -56,7 +56,7 @@ class MailerTest < Test::Unit::TestCase
def test_register
token = Token.find(1)
GLoc.valid_languages.each do |lang|
token.user.update_attribute :language, lang
token.user.update_attribute :language, lang.to_s
assert Mailer.deliver_register(token)
end
end

View File

@ -0,0 +1,60 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../test_helper'
class WikiContentTest < Test::Unit::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :users
def setup
@wiki = Wiki.find(1)
@page = @wiki.pages.first
end
def test_create
page = WikiPage.new(:wiki => @wiki, :title => "Page")
page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comment => "My comment")
assert page.save
page.reload
content = page.content
assert_kind_of WikiContent, content
assert_equal 1, content.version
assert_equal 1, content.versions.length
assert_equal "Content text", content.text
assert_equal "My comment", content.comment
assert_equal User.find(1), content.author
assert_equal content.text, content.versions.last.text
end
def test_update
content = @page.content
version_count = content.version
content.text = "My new content"
assert content.save
content.reload
assert_equal version_count+1, content.version
assert_equal version_count+1, content.versions.length
end
def test_fetch_history
assert !@page.content.versions.empty?
@page.content.versions.each do |version|
assert_kind_of String, version.text
end
end
end

View File

@ -0,0 +1,50 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../test_helper'
class WikiPageTest < Test::Unit::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
def setup
@wiki = Wiki.find(1)
@page = @wiki.pages.first
end
def test_create
page = WikiPage.new(:wiki => @wiki)
assert !page.save
assert_equal 1, page.errors.count
page.title = "Page"
assert page.save
page.reload
@wiki.reload
assert @wiki.pages.include?(page)
end
def test_find_or_new_page
page = @wiki.find_or_new_page("CookBook documentation")
assert_kind_of WikiPage, page
assert !page.new_record?
page = @wiki.find_or_new_page("Non existing page")
assert_kind_of WikiPage, page
assert page.new_record?
end
end

39
test/unit/wiki_test.rb Normal file
View File

@ -0,0 +1,39 @@
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../test_helper'
class WikiTest < Test::Unit::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
def test_create
wiki = Wiki.new(:project => Project.find(2))
assert !wiki.save
assert_equal 1, wiki.errors.count
wiki.start_page = "Start page"
assert wiki.save
end
def test_update
@wiki = Wiki.find(1)
@wiki.start_page = "Another start page"
assert @wiki.save
@wiki.reload
assert_equal "Another start page", @wiki.start_page
end
end

View File

@ -0,0 +1,74 @@
*SVN* (version numbers are overrated)
* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
*0.5.1*
* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
*0.5* # do versions even matter for plugins?
* (21 Apr 2006) Added without_locking and without_revision methods.
Foo.without_revision do
@foo.update_attributes ...
end
*0.4*
* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
*0.3.1*
* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
* (7 Jan 2006) added tests to prove has_many :through joins work
*0.3*
* (2 Jan 2006) added ability to share a mixin with versioned class
* (2 Jan 2006) changed the dynamic version model to MyModel::Version
*0.2.4*
* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
*0.2.3*
* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
* (12 Nov 2005) updated tests to use ActiveRecord Schema
*0.2.2*
* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
*0.2.1*
* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
*0.2*
* (6 Oct 2005) added find_versions and find_version class methods.
* (6 Oct 2005) removed transaction from create_versioned_table().
this way you can specify your own transaction around a group of operations.
* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
*0.1.3* (18 Sep 2005)
* First RubyForge release
*0.1.2*
* check if module is already included when acts_as_versioned is called
*0.1.1*
* Adding tests and rdocs
*0.1*
* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974

View File

@ -0,0 +1,20 @@
Copyright (c) 2005 Rick Olson
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.

28
vendor/plugins/acts_as_versioned/README vendored Normal file
View File

@ -0,0 +1,28 @@
= acts_as_versioned
This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
== Resources
Install
* gem install acts_as_versioned
Rubyforge project
* http://rubyforge.org/projects/ar-versioned
RDocs
* http://ar-versioned.rubyforge.org
Subversion
* http://techno-weenie.net/svn/projects/acts_as_versioned
Collaboa
* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
was the first project to use acts_as_versioned <em>in the wild</em>.

View File

@ -0,0 +1,41 @@
== Creating the test database
The default name for the test databases is "activerecord_versioned". If you
want to use another database name then be sure to update the connection
adapter setups you want to test with in test/connections/<your database>/connection.rb.
When you have the database online, you can import the fixture tables with
the test/fixtures/db_definitions/*.sql files.
Make sure that you create database objects with the same user that you specified in i
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all the adapters. You can also run the suite on just
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
Unit tests are located in test directory. If you only want to run a single test suite,
or don't want to bother with Rake, you can do so with something like:
cd test; ruby -I "connections/native_mysql" base_test.rb
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
and test suite name as needed.
== Faster tests
If you are using a database that supports transactions, you can set the
"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
This gives a very large speed boost. With rake:
rake AR_TX_FIXTURES=yes
Or, by hand:
AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb

View File

@ -0,0 +1,182 @@
require 'rubygems'
Gem::manage_gems
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/testtask'
require 'rake/contrib/rubyforgepublisher'
PKG_NAME = 'acts_as_versioned'
PKG_VERSION = '0.3.1'
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
PROD_HOST = "technoweenie@bidwell.textdrive.com"
RUBY_FORGE_PROJECT = 'ar-versioned'
RUBY_FORGE_USER = 'technoweenie'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the calculations plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the calculations plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
rdoc.rdoc_files.include('lib/**/*.rb')
end
spec = Gem::Specification.new do |s|
s.name = PKG_NAME
s.version = PKG_VERSION
s.platform = Gem::Platform::RUBY
s.summary = "Simple versioning with active record models"
s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
s.files.delete "acts_as_versioned_plugin.sqlite.db"
s.files.delete "acts_as_versioned_plugin.sqlite3.db"
s.files.delete "test/debug.log"
s.require_path = 'lib'
s.autorequire = 'acts_as_versioned'
s.has_rdoc = true
s.test_files = Dir['test/**/*_test.rb']
s.add_dependency 'activerecord', '>= 1.10.1'
s.add_dependency 'activesupport', '>= 1.1.1'
s.author = "Rick Olson"
s.email = "technoweenie@gmail.com"
s.homepage = "http://techno-weenie.net"
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_tar = true
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
end
desc 'Publish the gem and API docs'
task :publish => [:pdoc, :rubyforge_upload]
desc "Publish the release files to RubyForge."
task :rubyforge_upload => :package do
files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
if RUBY_FORGE_PROJECT then
require 'net/http'
require 'open-uri'
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
project_data = open(project_uri) { |data| data.read }
group_id = project_data[/[?&]group_id=(\d+)/, 1]
raise "Couldn't get group id" unless group_id
# This echos password to shell which is a bit sucky
if ENV["RUBY_FORGE_PASSWORD"]
password = ENV["RUBY_FORGE_PASSWORD"]
else
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
password = STDIN.gets.chomp
end
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
data = [
"login=1",
"form_loginname=#{RUBY_FORGE_USER}",
"form_pw=#{password}"
].join("&")
http.post("/account/login.php", data)
end
cookie = login_response["set-cookie"]
raise "Login failed" unless cookie
headers = { "Cookie" => cookie }
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
release_data = open(release_uri, headers) { |data| data.read }
package_id = release_data[/[?&]package_id=(\d+)/, 1]
raise "Couldn't get package id" unless package_id
first_file = true
release_id = ""
files.each do |filename|
basename = File.basename(filename)
file_ext = File.extname(filename)
file_data = File.open(filename, "rb") { |file| file.read }
puts "Releasing #{basename}..."
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
type_map = {
".zip" => "3000",
".tgz" => "3110",
".gz" => "3110",
".gem" => "1400"
}; type_map.default = "9999"
type = type_map[file_ext]
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
query_hash = if first_file then
{
"group_id" => group_id,
"package_id" => package_id,
"release_name" => PKG_FILE_NAME,
"release_date" => release_date,
"type_id" => type,
"processor_id" => "8000", # Any
"release_notes" => "",
"release_changes" => "",
"preformatted" => "1",
"submit" => "1"
}
else
{
"group_id" => group_id,
"release_id" => release_id,
"package_id" => package_id,
"step2" => "1",
"type_id" => type,
"processor_id" => "8000", # Any
"submit" => "Add This File"
}
end
query = "?" + query_hash.map do |(name, value)|
[name, URI.encode(value)].join("=")
end.join("&")
data = [
"--" + boundary,
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
"Content-Type: application/octet-stream",
"Content-Transfer-Encoding: binary",
"", file_data, ""
].join("\x0D\x0A")
release_headers = headers.merge(
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
)
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
http.post(target + query, data, release_headers)
end
if first_file then
release_id = release_response.body[/release_id=(\d+)/, 1]
raise("Couldn't get release id") unless release_id
end
first_file = false
end
end
end

View File

@ -0,0 +1 @@
require 'acts_as_versioned'

View File

@ -0,0 +1,511 @@
# Copyright (c) 2005 Rick Olson
#
# 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.
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
# versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version
# column is present as well.
#
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
#
# class Page < ActiveRecord::Base
# # assumes pages_versions table
# acts_as_versioned
# end
#
# Example:
#
# page = Page.create(:title => 'hello world!')
# page.version # => 1
#
# page.title = 'hello world'
# page.save
# page.version # => 2
# page.versions.size # => 2
#
# page.revert_to(1) # using version number
# page.title # => 'hello world!'
#
# page.revert_to(page.versions.last) # using versioned instance
# page.title # => 'hello world'
#
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
module Versioned
CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes]
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
# == Configuration options
#
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
# For finer control, pass either a Proc or modify Model#version_condition_met?
#
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
#
# or...
#
# class Auction
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
# !expired?
# end
# end
#
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
# either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
#
# def name=(new_name)
# write_changed_attribute :name, new_name
# end
#
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
# to create an anonymous mixin:
#
# class Auction
# acts_as_versioned do
# def started?
# !started_at.nil?
# end
# end
# end
#
# or...
#
# module AuctionExtension
# def started?
# !started_at.nil?
# end
# end
# class Auction
# acts_as_versioned :extend => AuctionExtension
# end
#
# Example code:
#
# @auction = Auction.find(1)
# @auction.started?
# @auction.versions.first.started?
#
# == Database Schema
#
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
# into a table called #{model}_versions where the model name is singlular. The _versions table should
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
#
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
# then that field is reflected in the versioned model as 'versioned_type' by default.
#
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
#
# class AddVersions < ActiveRecord::Migration
# def self.up
# # create_versioned_table takes the same options hash
# # that create_table does
# Post.create_versioned_table
# end
#
# def self.down
# Post.drop_versioned_table
# end
# end
#
# == Changing What Fields Are Versioned
#
# By default, acts_as_versioned will version all but these fields:
#
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
#
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
#
# class Post < ActiveRecord::Base
# acts_as_versioned
# self.non_versioned_columns << 'comments_count'
# end
#
def acts_as_versioned(options = {}, &extension)
# don't allow multiple calls
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
send :include, ActiveRecord::Acts::Versioned::ActMethods
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
:version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
:version_association_options
# legacy
alias_method :non_versioned_fields, :non_versioned_columns
alias_method :non_versioned_fields=, :non_versioned_columns=
class << self
alias_method :non_versioned_fields, :non_versioned_columns
alias_method :non_versioned_fields=, :non_versioned_columns=
end
send :attr_accessor, :changed_attributes
self.versioned_class_name = options[:class_name] || "Version"
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
self.version_column = options[:version_column] || 'version'
self.version_sequence_name = options[:sequence_name]
self.max_version_limit = options[:limit].to_i
self.version_condition = options[:if] || true
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
self.version_association_options = {
:class_name => "#{self.to_s}::#{versioned_class_name}",
:foreign_key => "#{versioned_foreign_key}",
:order => 'version',
:dependent => :delete_all
}.merge(options[:association_options] || {})
if block_given?
extension_module_name = "#{versioned_class_name}Extension"
silence_warnings do
self.const_set(extension_module_name, Module.new(&extension))
end
options[:extend] = self.const_get(extension_module_name)
end
class_eval do
has_many :versions, version_association_options
before_save :set_new_version
after_create :save_version_on_create
after_update :save_version
after_save :clear_old_versions
after_save :clear_changed_attributes
unless options[:if_changed].nil?
self.track_changed_attributes = true
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
options[:if_changed].each do |attr_name|
define_method("#{attr_name}=") do |value|
write_changed_attribute attr_name, value
end
end
end
include options[:extend] if options[:extend].is_a?(Module)
end
# create the dynamic versioned model
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
def self.reloadable? ; false ; end
end
versioned_class.set_table_name versioned_table_name
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
:class_name => "::#{self.to_s}",
:foreign_key => versioned_foreign_key
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end
end
module ActMethods
def self.included(base) # :nodoc:
base.extend ClassMethods
end
# Saves a version of the model if applicable
def save_version
save_version_on_create if save_version?
end
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
def save_version_on_create
rev = self.class.versioned_class.new
self.clone_versioned_model(self, rev)
rev.version = send(self.class.version_column)
rev.send("#{self.class.versioned_foreign_key}=", self.id)
rev.save
end
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
# Override this method to set your own criteria for clearing old versions.
def clear_old_versions
return if self.class.max_version_limit == 0
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
if excess_baggage > 0
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
self.class.versioned_class.connection.execute sql
end
end
# Finds a specific version of this model.
def find_version(version)
return version if version.is_a?(self.class.versioned_class)
return nil if version.is_a?(ActiveRecord::Base)
find_versions(:conditions => ['version = ?', version], :limit => 1).first
end
# Finds versions of this model. Takes an options hash like <tt>find</tt>
def find_versions(options = {})
versions.find(:all, options)
end
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
def revert_to(version)
if version.is_a?(self.class.versioned_class)
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
else
return false unless version = find_version(version)
end
self.clone_versioned_model(version, self)
self.send("#{self.class.version_column}=", version.version)
true
end
# Reverts a model to a given version and saves the model.
# Takes either a version number or an instance of the versioned model
def revert_to!(version)
revert_to(version) ? save_without_revision : false
end
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
def save_without_revision
save_without_revision!
true
rescue
false
end
def save_without_revision!
without_locking do
without_revision do
save!
end
end
end
# Returns an array of attribute keys that are versioned. See non_versioned_columns
def versioned_attributes
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
end
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
# If called with a single parameter, gets whether the parameter has changed.
def changed?(attr_name = nil)
attr_name.nil? ?
(!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) :
(changed_attributes && changed_attributes.include?(attr_name.to_s))
end
# keep old dirty? method
alias_method :dirty?, :changed?
# Clones a model. Used when saving a new version or reverting a model's version.
def clone_versioned_model(orig_model, new_model)
self.versioned_attributes.each do |key|
new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.has_attribute?(key)
end
if orig_model.is_a?(self.class.versioned_class)
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
elsif new_model.is_a?(self.class.versioned_class)
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
end
end
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
def save_version?
version_condition_met? && changed?
end
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
# custom version condition checking.
def version_condition_met?
case
when version_condition.is_a?(Symbol)
send(version_condition)
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
version_condition.call(self)
else
version_condition
end
end
# Executes the block with the versioning callbacks disabled.
#
# @foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
self.class.without_revision(&block)
end
# Turns off optimistic locking for the duration of the block
#
# @foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
self.class.without_locking(&block)
end
def empty_callback() end #:nodoc:
protected
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
def set_new_version
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
end
# Gets the next available version for the current record, or 1 for a new record
def next_version
return 1 if new_record?
(versions.calculate(:max, :version) || 0) + 1
end
# clears current changed attributes. Called after save.
def clear_changed_attributes
self.changed_attributes = []
end
def write_changed_attribute(attr_name, attr_value)
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
(self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
write_attribute(attr_name, attr_value_for_db)
end
private
CALLBACKS.each do |attr_name|
alias_method "orig_#{attr_name}".to_sym, attr_name
end
module ClassMethods
# Finds a specific version of a specific row of this model
def find_version(id, version)
find_versions(id,
:conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version],
:limit => 1).first
end
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
def find_versions(id, options = {})
versioned_class.find :all, {
:conditions => ["#{versioned_foreign_key} = ?", id],
:order => 'version' }.merge(options)
end
# Returns an array of columns that are versioned. See non_versioned_columns
def versioned_columns
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
end
# Returns an instance of the dynamic versioned model
def versioned_class
const_get versioned_class_name
end
# Rake migration task to create the versioned table using options passed to acts_as_versioned
def create_versioned_table(create_table_options = {})
# create version column in main table if it does not exist
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
self.connection.add_column table_name, :version, :integer
end
self.connection.create_table(versioned_table_name, create_table_options) do |t|
t.column versioned_foreign_key, :integer
t.column :version, :integer
end
updated_col = nil
self.versioned_columns.each do |col|
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
self.connection.add_column versioned_table_name, col.name, col.type,
:limit => col.limit,
:default => col.default
end
if type_col = self.columns_hash[inheritance_column]
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
:limit => type_col.limit,
:default => type_col.default
end
if updated_col.nil?
self.connection.add_column versioned_table_name, :updated_at, :timestamp
end
end
# Rake migration task to drop the versioned table
def drop_versioned_table
self.connection.drop_table versioned_table_name
end
# Executes the block with the versioning callbacks disabled.
#
# Foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, :empty_callback
end
end
result = block.call
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, "orig_#{attr_name}".to_sym
end
end
result
end
# Turns off optimistic locking for the duration of the block
#
# Foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
current = ActiveRecord::Base.lock_optimistically
ActiveRecord::Base.lock_optimistically = false if current
result = block.call
ActiveRecord::Base.lock_optimistically = true if current
result
end
end
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned

View File

@ -0,0 +1,40 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
require 'active_record/fixtures'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
load(File.dirname(__FILE__) + "/schema.rb")
# set up custom sequence on widget_versions for DBs that support sequences
if ENV['DB'] == 'postgresql'
ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
ActiveRecord::Base.connection.remove_column :widget_versions, :id
ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
end
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

View File

@ -0,0 +1,18 @@
sqlite:
:adapter: sqlite
:dbfile: acts_as_versioned_plugin.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: acts_as_versioned_plugin.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: acts_as_versioned_plugin_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: acts_as_versioned_plugin_test

View File

@ -0,0 +1,6 @@
caged:
id: 1
name: caged
mly:
id: 2
name: mly

View File

@ -0,0 +1,3 @@
class Landmark < ActiveRecord::Base
acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
end

View File

@ -0,0 +1,7 @@
washington:
id: 1
landmark_id: 1
version: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667

View File

@ -0,0 +1,6 @@
washington:
id: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667
version: 1

View File

@ -0,0 +1,10 @@
welcome:
id: 1
title: Welcome to the weblog
lock_version: 24
type: LockedPage
thinking:
id: 2
title: So I was thinking
lock_version: 24
type: SpecialLockedPage

View File

@ -0,0 +1,27 @@
welcome_1:
id: 1
page_id: 1
title: Welcome to the weblg
version: 23
version_type: LockedPage
welcome_2:
id: 2
page_id: 1
title: Welcome to the weblog
version: 24
version_type: LockedPage
thinking_1:
id: 3
page_id: 2
title: So I was thinking!!!
version: 23
version_type: SpecialLockedPage
thinking_2:
id: 4
page_id: 2
title: So I was thinking
version: 24
version_type: SpecialLockedPage

View File

@ -0,0 +1,13 @@
class AddVersionedTables < ActiveRecord::Migration
def self.up
create_table("things") do |t|
t.column :title, :text
end
Thing.create_versioned_table
end
def self.down
Thing.drop_versioned_table
drop_table "things" rescue nil
end
end

View File

@ -0,0 +1,43 @@
class Page < ActiveRecord::Base
belongs_to :author
has_many :authors, :through => :versions, :order => 'name'
belongs_to :revisor, :class_name => 'Author'
has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
acts_as_versioned :if => :feeling_good? do
def self.included(base)
base.cattr_accessor :feeling_good
base.feeling_good = true
base.belongs_to :author
base.belongs_to :revisor, :class_name => 'Author'
end
def feeling_good?
@@feeling_good == true
end
end
end
module LockedPageExtension
def hello_world
'hello_world'
end
end
class LockedPage < ActiveRecord::Base
acts_as_versioned \
:inheritance_column => :version_type,
:foreign_key => :page_id,
:table_name => :locked_pages_revisions,
:class_name => 'LockedPageRevision',
:version_column => :lock_version,
:limit => 2,
:if_changed => :title,
:extend => LockedPageExtension
end
class SpecialLockedPage < LockedPage
end
class Author < ActiveRecord::Base
has_many :pages
end

View File

@ -0,0 +1,16 @@
welcome_2:
id: 1
page_id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1
welcome_1:
id: 2
page_id: 1
title: Welcome to the weblg
body: Such a lovely day
version: 23
author_id: 2
revisor_id: 2

View File

@ -0,0 +1,7 @@
welcome:
id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1

View File

@ -0,0 +1,6 @@
class Widget < ActiveRecord::Base
acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
:dependent => nil, :order => 'version desc'
}
non_versioned_columns << 'foo'
end

View File

@ -0,0 +1,32 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
if ActiveRecord::Base.connection.supports_migrations?
class Thing < ActiveRecord::Base
attr_accessor :version
acts_as_versioned
end
class MigrationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
def teardown
ActiveRecord::Base.connection.initialize_schema_information
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
Thing.connection.drop_table "things" rescue nil
Thing.connection.drop_table "thing_versions" rescue nil
Thing.reset_column_information
end
def test_versioned_migration
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
# take 'er up
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
t = Thing.create :title => 'blah blah'
assert_equal 1, t.versions.size
# now lets take 'er back down
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
end
end
end

View File

@ -0,0 +1,68 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :pages, :force => true do |t|
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
create_table :page_versions, :force => true do |t|
t.column :page_id, :integer
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
create_table :authors, :force => true do |t|
t.column :page_id, :integer
t.column :name, :string
end
create_table :locked_pages, :force => true do |t|
t.column :lock_version, :integer
t.column :title, :string, :limit => 255
t.column :type, :string, :limit => 255
end
create_table :locked_pages_revisions, :force => true do |t|
t.column :page_id, :integer
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :version_type, :string, :limit => 255
t.column :updated_at, :datetime
end
create_table :widgets, :force => true do |t|
t.column :name, :string, :limit => 50
t.column :foo, :string
t.column :version, :integer
t.column :updated_at, :datetime
end
create_table :widget_versions, :force => true do |t|
t.column :widget_id, :integer
t.column :name, :string, :limit => 50
t.column :version, :integer
t.column :updated_at, :datetime
end
create_table :landmarks, :force => true do |t|
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :version, :integer
end
create_table :landmark_versions, :force => true do |t|
t.column :landmark_id, :integer
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :version, :integer
end
end

View File

@ -0,0 +1,313 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
require File.join(File.dirname(__FILE__), 'fixtures/page')
require File.join(File.dirname(__FILE__), 'fixtures/widget')
class VersionedTest < Test::Unit::TestCase
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
def test_saves_versioned_copy
p = Page.create :title => 'first title', :body => 'first body'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_equal 1, p.version
assert_instance_of Page.versioned_class, p.versions.first
end
def test_saves_without_revision
p = pages(:welcome)
old_versions = p.versions.count
p.save_without_revision
p.without_revision do
p.update_attributes :title => 'changed'
end
assert_equal old_versions, p.versions.count
end
def test_rollback_with_version_number
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_versioned_class_name
assert_equal 'Version', Page.versioned_class_name
assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
end
def test_versioned_class
assert_equal Page::Version, Page.versioned_class
assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
end
def test_special_methods
assert_nothing_raised { pages(:welcome).feeling_good? }
assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
assert_nothing_raised { locked_pages(:welcome).hello_world }
assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
end
def test_rollback_with_version_class
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_rollback_fails_with_invalid_revision
p = locked_pages(:welcome)
assert !p.revert_to!(locked_pages(:thinking))
end
def test_saves_versioned_copy_with_options
p = LockedPage.create :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
end
def test_rollback_with_version_number_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_rollback_with_version_class_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_saves_versioned_copy_with_sti
p = SpecialLockedPage.create :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_rollback_with_version_number_with_sti
p = locked_pages(:thinking)
assert_equal 'So I was thinking', p.title
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
assert_equal 'So I was thinking!!!', p.title
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_lock_version_works_with_versioning
p = locked_pages(:thinking)
p2 = LockedPage.find(p.id)
p.title = 'fresh title'
p.save
assert_equal 2, p.versions.size # limit!
assert_raises(ActiveRecord::StaleObjectError) do
p2.title = 'stale title'
p2.save
end
end
def test_version_if_condition
p = Page.create :title => "title"
assert_equal 1, p.version
Page.feeling_good = false
p.save
assert_equal 1, p.version
Page.feeling_good = true
end
def test_version_if_condition2
# set new if condition
Page.class_eval do
def new_feeling_good() title[0..0] == 'a'; end
alias_method :old_feeling_good, :feeling_good?
alias_method :feeling_good?, :new_feeling_good
end
p = Page.create :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
p.update_attributes(:title => 'new title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
p.update_attributes(:title => 'a title')
assert_equal 2, p.version
assert_equal 2, p.versions(true).size
# reset original if condition
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
end
def test_version_if_condition_with_block
# set new if condition
old_condition = Page.version_condition
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
p = Page.create :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
p.update_attributes(:title => 'a title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
p.update_attributes(:title => 'b title')
assert_equal 2, p.version
assert_equal 2, p.versions(true).size
# reset original if condition
Page.version_condition = old_condition
end
def test_version_no_limit
p = Page.create :title => "title", :body => 'first body'
p.save
p.save
5.times do |i|
assert_page_title p, i
end
end
def test_version_max_limit
p = LockedPage.create :title => "title"
p.update_attributes(:title => "title1")
p.update_attributes(:title => "title2")
5.times do |i|
assert_page_title p, i, :lock_version
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
end
end
def test_track_changed_attributes_default_value
assert !Page.track_changed_attributes
assert LockedPage.track_changed_attributes
assert SpecialLockedPage.track_changed_attributes
end
def test_version_order
assert_equal 23, pages(:welcome).versions.first.version
assert_equal 24, pages(:welcome).versions.last.version
assert_equal 23, pages(:welcome).find_versions.first.version
assert_equal 24, pages(:welcome).find_versions.last.version
end
def test_track_changed_attributes
p = LockedPage.create :title => "title"
assert_equal 1, p.lock_version
assert_equal 1, p.versions(true).size
p.title = 'title'
assert !p.save_version?
p.save
assert_equal 2, p.lock_version # still increments version because of optimistic locking
assert_equal 1, p.versions(true).size
p.title = 'updated title'
assert p.save_version?
p.save
assert_equal 3, p.lock_version
assert_equal 1, p.versions(true).size # version 1 deleted
p.title = 'updated title!'
assert p.save_version?
p.save
assert_equal 4, p.lock_version
assert_equal 2, p.versions(true).size # version 1 deleted
end
def assert_page_title(p, i, version_field = :version)
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+4), p.send(version_field)
end
def test_find_versions
assert_equal 2, locked_pages(:welcome).versions.size
assert_equal 1, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length
assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length
assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length
assert_equal 2, locked_pages(:welcome).find_versions.length
end
def test_with_sequence
assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
Widget.create :name => 'new widget'
Widget.create :name => 'new widget'
Widget.create :name => 'new widget'
assert_equal 3, Widget.count
assert_equal 3, Widget.versioned_class.count
end
def test_has_many_through
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
end
def test_has_many_through_with_custom_association
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
end
def test_referential_integrity
pages(:welcome).destroy
assert_equal 0, Page.count
assert_equal 0, Page::Version.count
end
def test_association_options
association = Page.reflect_on_association(:versions)
options = association.options
assert_equal :delete_all, options[:dependent]
assert_equal 'version', options[:order]
association = Widget.reflect_on_association(:versions)
options = association.options
assert_nil options[:dependent]
assert_equal 'version desc', options[:order]
assert_equal 'widget_id', options[:foreign_key]
widget = Widget.create :name => 'new widget'
assert_equal 1, Widget.count
assert_equal 1, Widget.versioned_class.count
widget.destroy
assert_equal 0, Widget.count
assert_equal 1, Widget.versioned_class.count
end
def test_versioned_records_should_belong_to_parent
page = pages(:welcome)
page_version = page.versions.last
assert_equal page, page_version.page
end
def test_unchanged_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes
assert !landmarks(:washington).changed?
end
def test_unchanged_string_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes.inject({}) { |params, (key, value)| params.update key => value.to_s }
assert !landmarks(:washington).changed?
end
end