##// END OF EJS Templates
wiki branch merged into trunk...
Jean-Philippe Lang -
r320:c514316a2efc
parent child
Show More
@@ -0,0 +1,111
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class WikiController < ApplicationController
19 layout 'base'
20 before_filter :find_wiki, :check_project_privacy, :except => [:preview]
21
22 # display a page (in editing mode if it doesn't exist)
23 def index
24 page_title = params[:page]
25 @page = @wiki.find_or_new_page(page_title)
26 if @page.new_record?
27 edit
28 render :action => 'edit' and return
29 end
30 @content = (params[:version] ? @page.content.versions.find_by_version(params[:version]) : @page.content)
31 if params[:export] == 'html'
32 export = render_to_string :action => 'export', :layout => false
33 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
34 return
35 elsif params[:export] == 'txt'
36 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
37 return
38 end
39 render :action => 'show'
40 end
41
42 # edit an existing page or a new one
43 def edit
44 @page = @wiki.find_or_new_page(params[:page])
45 @page.content = WikiContent.new(:page => @page) if @page.new_record?
46 @content = @page.content
47 @content.text = "h1. #{@page.pretty_title}" if @content.text.empty?
48 # don't keep previous comment
49 @content.comment = nil
50 if request.post?
51 if @content.text == params[:content][:text]
52 # don't save if text wasn't changed
53 redirect_to :action => 'index', :id => @project, :page => @page.title
54 return
55 end
56 @content.text = params[:content][:text]
57 @content.comment = params[:content][:comment]
58 @content.author = logged_in_user
59 # if page is new @page.save will also save content, but not if page isn't a new record
60 if (@page.new_record? ? @page.save : @content.save)
61 redirect_to :action => 'index', :id => @project, :page => @page.title
62 end
63 end
64 end
65
66 # show page history
67 def history
68 @page = @wiki.find_page(params[:page])
69 # don't load text
70 @versions = @page.content.versions.find :all,
71 :select => "id, author_id, comment, updated_on, version",
72 :order => 'version DESC'
73 end
74
75 # display special pages
76 def special
77 page_title = params[:page].downcase
78 case page_title
79 # show pages index, sorted by title
80 when 'page_index'
81 # eager load information about last updates, without loading text
82 @pages = @wiki.pages.find :all, :select => "wiki_pages.*, wiki_contents.updated_on",
83 :joins => "LEFT JOIN wiki_contents ON wiki_contents.page_id = wiki_pages.id",
84 :order => 'title'
85 # export wiki to a single html file
86 when 'export'
87 @pages = @wiki.pages.find :all, :order => 'title'
88 export = render_to_string :action => 'export_multiple', :layout => false
89 send_data(export, :type => 'text/html', :filename => "wiki.html")
90 return
91 else
92 # requested special page doesn't exist, redirect to default page
93 redirect_to :action => 'index', :id => @project, :page => nil and return
94 end
95 render :action => "special_#{page_title}"
96 end
97
98 def preview
99 @text = params[:content][:text]
100 render :partial => 'preview'
101 end
102
103 private
104
105 def find_wiki
106 @project = Project.find(params[:id])
107 @wiki = @project.wiki
108 rescue ActiveRecord::RecordNotFound
109 render_404
110 end
111 end
@@ -0,0 +1,19
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module WikiHelper
19 end
@@ -0,0 +1,44
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class Wiki < ActiveRecord::Base
19 belongs_to :project
20 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
21
22 validates_presence_of :project_id, :start_page
23
24 # find the page with the given title
25 # if page doesn't exist, return a new page
26 def find_or_new_page(title)
27 title = Wiki.titleize(title || start_page)
28 find_page(title) || WikiPage.new(:wiki => self, :title => title)
29 end
30
31 # find the page with the given title
32 def find_page(title)
33 pages.find_by_title(Wiki.titleize(title || start_page))
34 end
35
36 # turn a string into a valid page title
37 def self.titleize(title)
38 # replace spaces with _ and remove unwanted caracters
39 title = title.gsub(/\s+/, '_').delete(',;|') if title
40 # upcase the first letter
41 title = title[0..0].upcase + title[1..-1] if title
42 title
43 end
44 end
@@ -0,0 +1,58
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require 'zlib'
19
20 class WikiContent < ActiveRecord::Base
21 belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 validates_presence_of :text
24
25 acts_as_versioned
26 class Version
27 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
28 attr_protected :data
29
30 def text=(plain)
31 case Setting.wiki_compression
32 when 'gzip'
33 begin
34 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
35 self.compression = 'gzip'
36 rescue
37 self.data = plain
38 self.compression = ''
39 end
40 else
41 self.data = plain
42 self.compression = ''
43 end
44 plain
45 end
46
47 def text
48 @text ||= case compression
49 when 'gzip'
50 Zlib::Inflate.inflate(data)
51 else
52 # uncompressed data
53 data
54 end
55 end
56 end
57
58 end
@@ -0,0 +1,34
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class WikiPage < ActiveRecord::Base
19 belongs_to :wiki
20 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
21
22 validates_presence_of :title
23 validates_format_of :title, :with => /^[^,\s]*$/
24 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
25 validates_associated :content
26
27 def before_save
28 self.title = Wiki.titleize(title)
29 end
30
31 def pretty_title
32 title.tr '_', ' '
33 end
34 end
@@ -0,0 +1,3
1 <fieldset class="preview"><legend><%= l(:label_preview) %></legend>
2 <%= textilizable @text %>
3 </fieldset>
@@ -0,0 +1,39
1 <div class="contextual">
2 <%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
3 </div>
4
5 <h2><%= @page.pretty_title %></h2>
6
7 <% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:id => 'wiki_form'} do |f| %>
8 <%= error_messages_for 'content' %>
9 <p><%= f.text_area :text, :cols => 100, :rows => 25, :style => "width:99%;" %></p>
10 <p><label><%= l(:field_comment) %></label><br /><%= f.text_field :comment, :size => 120 %></p>
11 <p><%= submit_tag l(:button_save) %>
12 <%= link_to_remote l(:label_preview),
13 { :url => { :controller => 'wiki', :action => 'preview' },
14 :method => 'get',
15 :update => 'preview',
16 :with => "Form.serialize('wiki_form')",
17 :loading => "Element.show('indicator')",
18 :loaded => "Element.hide('indicator')"
19 } %>
20 <span id="indicator" style="display:none"><%= image_tag "loading.gif", :align => "absmiddle" %></span>
21 </p>
22
23 <% end %>
24
25 <% if Setting.text_formatting == 'textile' %>
26 <%= javascript_include_tag 'jstoolbar' %>
27 <script type="text/javascript">
28 //<![CDATA[
29 if (document.getElementById) {
30 if (document.getElementById('content_text')) {
31 var commentTb = new jsToolBar(document.getElementById('content_text'));
32 commentTb.draw();
33 }
34 }
35 //]]>
36 </script>
37 <% end %>
38
39 <div id="preview" class="wiki"></div> No newline at end of file
@@ -0,0 +1,14
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
4 <title><%=h @page.pretty_title %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <style>
7 body { font:80% Verdana,Tahoma,Arial,sans-serif; }
8 h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
9 </style>
10 </head>
11 <body>
12 <%= textilizable @content.text, :wiki_links => :local %>
13 </body>
14 </html>
@@ -0,0 +1,26
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
4 <title><%=h @wiki.project.name %></title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <style>
7 body { font:80% Verdana,Tahoma,Arial,sans-serif; }
8 h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
9 </style>
10 </head>
11 <body>
12
13 <strong><%= l(:label_page_index) %></strong>
14 <ul>
15 <% @pages.each do |page| %>
16 <li><a href="#<%= page.title %>"><%= page.pretty_title %></a></li>
17 <% end %>
18 </ul>
19
20 <% @pages.each do |page| %>
21 <hr />
22 <%= textilizable page.content.text, :wiki_links => :anchor %>
23 <% end %>
24
25 </body>
26 </html>
@@ -0,0 +1,28
1 <div class="contextual">
2 <%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
3 </div>
4
5 <h2><%= @page.pretty_title %></h2>
6
7 <h3><%= l(:label_history) %></h3>
8
9 <table class="list">
10 <thead><tr>
11 <th>#</th>
12 <th><%= l(:field_updated_on) %></th>
13 <th><%= l(:field_author) %></th>
14 <th><%= l(:field_comment) %></th>
15 </tr></thead>
16 <tbody>
17 <% @versions.each do |ver| %>
18 <tr class="<%= cycle("odd", "even") %>">
19 <th align="center"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></th>
20 <td align="center"><%= format_time(ver.updated_on) %></td>
21 <td><em><%= ver.author ? ver.author.name : "anonyme" %></em></td>
22 <td><%=h ver.comment %></td>
23 </tr>
24 <% end %>
25 </tbody>
26 </table>
27
28 <p><%= link_to l(:button_back), :action => 'index', :page => @page.title %></p> No newline at end of file
@@ -0,0 +1,30
1 <div class="contextual">
2 <%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') if @content.version == @page.content.version %>
3 <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
4 <%= link_to(l(:label_page_index), {:action => 'special', :page => 'Page_index'}, :class => 'icon icon-index') %>
5 </div>
6
7 <% if @content.version != @page.content.version %>
8 <p>
9 <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
10 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %> -
11 <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
12 <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
13 <br />
14 <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
15 <%=h @content.comment %>
16 </p>
17 <hr />
18 <% end %>
19
20 <div class="wiki">
21 <% cache "wiki/show/#{@page.id}/#{@content.version}" do %>
22 <%= textilizable @content.text %>
23 <% end %>
24 </div>
25
26 <div class="contextual">
27 <%= l(:label_export_to) %>
28 <%= link_to 'HTML', {:export => 'html', :version => @content.version}, :class => 'icon icon-html' %>,
29 <%= link_to 'TXT', {:export => 'txt', :version => @content.version}, :class => 'icon icon-txt' %>
30 </div> No newline at end of file
@@ -0,0 +1,13
1 <div class="contextual">
2 <% unless @pages.empty? %>
3 <%= l(:label_export_to) %> <%= link_to 'HTML', {:action => 'special', :page => 'export'}, :class => 'icon icon-html' %>
4 <% end %>
5 </div>
6
7 <h2><%= l(:label_page_index) %></h2>
8
9 <% if @pages.empty? %><p><i><%= l(:label_no_data) %></i></p><% end %>
10 <ul><% @pages.each do |page| %>
11 <li><%= link_to page.pretty_title, :action => 'index', :page => page.title %> -
12 <%= l(:label_last_updates) %>: <%= format_time(page.updated_on) %></li>
13 <% end %></ul> No newline at end of file
@@ -0,0 +1,14
1 class CreateWikis < ActiveRecord::Migration
2 def self.up
3 create_table :wikis do |t|
4 t.column :project_id, :integer, :null => false
5 t.column :start_page, :string, :limit => 255, :null => false
6 t.column :status, :integer, :default => 1, :null => false
7 end
8 add_index :wikis, :project_id, :name => :wikis_project_id
9 end
10
11 def self.down
12 drop_table :wikis
13 end
14 end
@@ -0,0 +1,14
1 class CreateWikiPages < ActiveRecord::Migration
2 def self.up
3 create_table :wiki_pages do |t|
4 t.column :wiki_id, :integer, :null => false
5 t.column :title, :string, :limit => 255, :null => false
6 t.column :created_on, :datetime, :null => false
7 end
8 add_index :wiki_pages, [:wiki_id, :title], :name => :wiki_pages_wiki_id_title
9 end
10
11 def self.down
12 drop_table :wiki_pages
13 end
14 end
@@ -0,0 +1,30
1 class CreateWikiContents < ActiveRecord::Migration
2 def self.up
3 create_table :wiki_contents do |t|
4 t.column :page_id, :integer, :null => false
5 t.column :author_id, :integer
6 t.column :text, :text, :default => "", :null => false
7 t.column :comment, :string, :limit => 255, :default => ""
8 t.column :updated_on, :datetime, :null => false
9 t.column :version, :integer, :null => false
10 end
11 add_index :wiki_contents, :page_id, :name => :wiki_contents_page_id
12
13 create_table :wiki_content_versions do |t|
14 t.column :wiki_content_id, :integer, :null => false
15 t.column :page_id, :integer, :null => false
16 t.column :author_id, :integer
17 t.column :data, :binary
18 t.column :compression, :string, :limit => 6, :default => ""
19 t.column :comment, :string, :limit => 255, :default => ""
20 t.column :updated_on, :datetime, :null => false
21 t.column :version, :integer, :null => false
22 end
23 add_index :wiki_content_versions, :wiki_content_id, :name => :wiki_content_versions_wcid
24 end
25
26 def self.down
27 drop_table :wiki_contents
28 drop_table :wiki_content_versions
29 end
30 end
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,40
1 ---
2 wiki_content_versions_001:
3 updated_on: 2007-03-07 00:08:07 +01:00
4 page_id: 1
5 id: 1
6 version: 1
7 author_id: 1
8 comment: Page creation
9 wiki_content_id: 1
10 compression: ""
11 data: |-
12 h1. CookBook documentation
13
14
15
16 Some [[documentation]] here...
17 wiki_content_versions_002:
18 updated_on: 2007-03-07 00:08:34 +01:00
19 page_id: 1
20 id: 2
21 version: 2
22 author_id: 1
23 comment: Small update
24 wiki_content_id: 1
25 compression: ""
26 data: |-
27 h1. CookBook documentation
28
29
30
31 Some updated [[documentation]] here...
32 wiki_content_versions_003:
33 updated_on: 2007-03-07 00:10:51 +01:00
34 page_id: 1
35 id: 3
36 version: 3
37 author_id: 1
38 comment: ""
39 wiki_content_id: 1
40 compression: ""
@@ -0,0 +1,12
1 ---
2 wiki_contents_001:
3 text: |-
4 h1. CookBook documentation
5
6
7
8 Some updated [[documentation]] here with gzipped history
9 updated_on: 2007-03-07 00:10:51 +01:00
10 page_id: 1
11 id: 1
12 version: 3
@@ -0,0 +1,6
1 ---
2 wiki_pages_001:
3 created_on: 2007-03-07 00:08:07 +01:00
4 title: CookBook_documentation
5 id: 1
6 wiki_id: 1
@@ -0,0 +1,6
1 ---
2 wikis_001:
3 status: 1
4 start_page: CookBook documentation
5 project_id: 1
6 id: 1
@@ -0,0 +1,60
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class WikiContentTest < Test::Unit::TestCase
21 fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :users
22
23 def setup
24 @wiki = Wiki.find(1)
25 @page = @wiki.pages.first
26 end
27
28 def test_create
29 page = WikiPage.new(:wiki => @wiki, :title => "Page")
30 page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comment => "My comment")
31 assert page.save
32 page.reload
33
34 content = page.content
35 assert_kind_of WikiContent, content
36 assert_equal 1, content.version
37 assert_equal 1, content.versions.length
38 assert_equal "Content text", content.text
39 assert_equal "My comment", content.comment
40 assert_equal User.find(1), content.author
41 assert_equal content.text, content.versions.last.text
42 end
43
44 def test_update
45 content = @page.content
46 version_count = content.version
47 content.text = "My new content"
48 assert content.save
49 content.reload
50 assert_equal version_count+1, content.version
51 assert_equal version_count+1, content.versions.length
52 end
53
54 def test_fetch_history
55 assert !@page.content.versions.empty?
56 @page.content.versions.each do |version|
57 assert_kind_of String, version.text
58 end
59 end
60 end
@@ -0,0 +1,50
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class WikiPageTest < Test::Unit::TestCase
21 fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
22
23 def setup
24 @wiki = Wiki.find(1)
25 @page = @wiki.pages.first
26 end
27
28 def test_create
29 page = WikiPage.new(:wiki => @wiki)
30 assert !page.save
31 assert_equal 1, page.errors.count
32
33 page.title = "Page"
34 assert page.save
35 page.reload
36
37 @wiki.reload
38 assert @wiki.pages.include?(page)
39 end
40
41 def test_find_or_new_page
42 page = @wiki.find_or_new_page("CookBook documentation")
43 assert_kind_of WikiPage, page
44 assert !page.new_record?
45
46 page = @wiki.find_or_new_page("Non existing page")
47 assert_kind_of WikiPage, page
48 assert page.new_record?
49 end
50 end
@@ -0,0 +1,39
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class WikiTest < Test::Unit::TestCase
21 fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
22
23 def test_create
24 wiki = Wiki.new(:project => Project.find(2))
25 assert !wiki.save
26 assert_equal 1, wiki.errors.count
27
28 wiki.start_page = "Start page"
29 assert wiki.save
30 end
31
32 def test_update
33 @wiki = Wiki.find(1)
34 @wiki.start_page = "Another start page"
35 assert @wiki.save
36 @wiki.reload
37 assert_equal "Another start page", @wiki.start_page
38 end
39 end
@@ -0,0 +1,74
1 *SVN* (version numbers are overrated)
2
3 * (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
4
5 *0.5.1*
6
7 * (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
8
9 *0.5* # do versions even matter for plugins?
10
11 * (21 Apr 2006) Added without_locking and without_revision methods.
12
13 Foo.without_revision do
14 @foo.update_attributes ...
15 end
16
17 *0.4*
18
19 * (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
20 * (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
21
22 *0.3.1*
23
24 * (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
25 * (7 Jan 2006) added tests to prove has_many :through joins work
26
27 *0.3*
28
29 * (2 Jan 2006) added ability to share a mixin with versioned class
30 * (2 Jan 2006) changed the dynamic version model to MyModel::Version
31
32 *0.2.4*
33
34 * (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
35
36 *0.2.3*
37
38 * (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
39 * (12 Nov 2005) updated tests to use ActiveRecord Schema
40
41 *0.2.2*
42
43 * (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
44
45 *0.2.1*
46
47 * (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
48
49 *0.2*
50
51 * (6 Oct 2005) added find_versions and find_version class methods.
52
53 * (6 Oct 2005) removed transaction from create_versioned_table().
54 this way you can specify your own transaction around a group of operations.
55
56 * (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
57
58 * (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
59
60 *0.1.3* (18 Sep 2005)
61
62 * First RubyForge release
63
64 *0.1.2*
65
66 * check if module is already included when acts_as_versioned is called
67
68 *0.1.1*
69
70 * Adding tests and rdocs
71
72 *0.1*
73
74 * Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974 No newline at end of file
@@ -0,0 +1,20
1 Copyright (c) 2005 Rick Olson
2
3 Permission is hereby granted, free of charge, to any person obtaining
4 a copy of this software and associated documentation files (the
5 "Software"), to deal in the Software without restriction, including
6 without limitation the rights to use, copy, modify, merge, publish,
7 distribute, sublicense, and/or sell copies of the Software, and to
8 permit persons to whom the Software is furnished to do so, subject to
9 the following conditions:
10
11 The above copyright notice and this permission notice shall be
12 included in all copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. No newline at end of file
@@ -0,0 +1,28
1 = acts_as_versioned
2
3 This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
4
5 == Resources
6
7 Install
8
9 * gem install acts_as_versioned
10
11 Rubyforge project
12
13 * http://rubyforge.org/projects/ar-versioned
14
15 RDocs
16
17 * http://ar-versioned.rubyforge.org
18
19 Subversion
20
21 * http://techno-weenie.net/svn/projects/acts_as_versioned
22
23 Collaboa
24
25 * http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
26
27 Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
28 was the first project to use acts_as_versioned <em>in the wild</em>. No newline at end of file
@@ -0,0 +1,41
1 == Creating the test database
2
3 The default name for the test databases is "activerecord_versioned". If you
4 want to use another database name then be sure to update the connection
5 adapter setups you want to test with in test/connections/<your database>/connection.rb.
6 When you have the database online, you can import the fixture tables with
7 the test/fixtures/db_definitions/*.sql files.
8
9 Make sure that you create database objects with the same user that you specified in i
10 connection.rb otherwise (on Postgres, at least) tests for default values will fail.
11
12 == Running with Rake
13
14 The easiest way to run the unit tests is through Rake. The default task runs
15 the entire test suite for all the adapters. You can also run the suite on just
16 one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
17 or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
18
19 Rake can be found at http://rake.rubyforge.org
20
21 == Running by hand
22
23 Unit tests are located in test directory. If you only want to run a single test suite,
24 or don't want to bother with Rake, you can do so with something like:
25
26 cd test; ruby -I "connections/native_mysql" base_test.rb
27
28 That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
29 and test suite name as needed.
30
31 == Faster tests
32
33 If you are using a database that supports transactions, you can set the
34 "AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
35 This gives a very large speed boost. With rake:
36
37 rake AR_TX_FIXTURES=yes
38
39 Or, by hand:
40
41 AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
@@ -0,0 +1,182
1 require 'rubygems'
2
3 Gem::manage_gems
4
5 require 'rake/rdoctask'
6 require 'rake/packagetask'
7 require 'rake/gempackagetask'
8 require 'rake/testtask'
9 require 'rake/contrib/rubyforgepublisher'
10
11 PKG_NAME = 'acts_as_versioned'
12 PKG_VERSION = '0.3.1'
13 PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14 PROD_HOST = "technoweenie@bidwell.textdrive.com"
15 RUBY_FORGE_PROJECT = 'ar-versioned'
16 RUBY_FORGE_USER = 'technoweenie'
17
18 desc 'Default: run unit tests.'
19 task :default => :test
20
21 desc 'Test the calculations plugin.'
22 Rake::TestTask.new(:test) do |t|
23 t.libs << 'lib'
24 t.pattern = 'test/**/*_test.rb'
25 t.verbose = true
26 end
27
28 desc 'Generate documentation for the calculations plugin.'
29 Rake::RDocTask.new(:rdoc) do |rdoc|
30 rdoc.rdoc_dir = 'rdoc'
31 rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
32 rdoc.options << '--line-numbers --inline-source'
33 rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
34 rdoc.rdoc_files.include('lib/**/*.rb')
35 end
36
37 spec = Gem::Specification.new do |s|
38 s.name = PKG_NAME
39 s.version = PKG_VERSION
40 s.platform = Gem::Platform::RUBY
41 s.summary = "Simple versioning with active record models"
42 s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
43 s.files.delete "acts_as_versioned_plugin.sqlite.db"
44 s.files.delete "acts_as_versioned_plugin.sqlite3.db"
45 s.files.delete "test/debug.log"
46 s.require_path = 'lib'
47 s.autorequire = 'acts_as_versioned'
48 s.has_rdoc = true
49 s.test_files = Dir['test/**/*_test.rb']
50 s.add_dependency 'activerecord', '>= 1.10.1'
51 s.add_dependency 'activesupport', '>= 1.1.1'
52 s.author = "Rick Olson"
53 s.email = "technoweenie@gmail.com"
54 s.homepage = "http://techno-weenie.net"
55 end
56
57 Rake::GemPackageTask.new(spec) do |pkg|
58 pkg.need_tar = true
59 end
60
61 desc "Publish the API documentation"
62 task :pdoc => [:rdoc] do
63 Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
64 end
65
66 desc 'Publish the gem and API docs'
67 task :publish => [:pdoc, :rubyforge_upload]
68
69 desc "Publish the release files to RubyForge."
70 task :rubyforge_upload => :package do
71 files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
72
73 if RUBY_FORGE_PROJECT then
74 require 'net/http'
75 require 'open-uri'
76
77 project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
78 project_data = open(project_uri) { |data| data.read }
79 group_id = project_data[/[?&]group_id=(\d+)/, 1]
80 raise "Couldn't get group id" unless group_id
81
82 # This echos password to shell which is a bit sucky
83 if ENV["RUBY_FORGE_PASSWORD"]
84 password = ENV["RUBY_FORGE_PASSWORD"]
85 else
86 print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
87 password = STDIN.gets.chomp
88 end
89
90 login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
91 data = [
92 "login=1",
93 "form_loginname=#{RUBY_FORGE_USER}",
94 "form_pw=#{password}"
95 ].join("&")
96 http.post("/account/login.php", data)
97 end
98
99 cookie = login_response["set-cookie"]
100 raise "Login failed" unless cookie
101 headers = { "Cookie" => cookie }
102
103 release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
104 release_data = open(release_uri, headers) { |data| data.read }
105 package_id = release_data[/[?&]package_id=(\d+)/, 1]
106 raise "Couldn't get package id" unless package_id
107
108 first_file = true
109 release_id = ""
110
111 files.each do |filename|
112 basename = File.basename(filename)
113 file_ext = File.extname(filename)
114 file_data = File.open(filename, "rb") { |file| file.read }
115
116 puts "Releasing #{basename}..."
117
118 release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
119 release_date = Time.now.strftime("%Y-%m-%d %H:%M")
120 type_map = {
121 ".zip" => "3000",
122 ".tgz" => "3110",
123 ".gz" => "3110",
124 ".gem" => "1400"
125 }; type_map.default = "9999"
126 type = type_map[file_ext]
127 boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
128
129 query_hash = if first_file then
130 {
131 "group_id" => group_id,
132 "package_id" => package_id,
133 "release_name" => PKG_FILE_NAME,
134 "release_date" => release_date,
135 "type_id" => type,
136 "processor_id" => "8000", # Any
137 "release_notes" => "",
138 "release_changes" => "",
139 "preformatted" => "1",
140 "submit" => "1"
141 }
142 else
143 {
144 "group_id" => group_id,
145 "release_id" => release_id,
146 "package_id" => package_id,
147 "step2" => "1",
148 "type_id" => type,
149 "processor_id" => "8000", # Any
150 "submit" => "Add This File"
151 }
152 end
153
154 query = "?" + query_hash.map do |(name, value)|
155 [name, URI.encode(value)].join("=")
156 end.join("&")
157
158 data = [
159 "--" + boundary,
160 "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
161 "Content-Type: application/octet-stream",
162 "Content-Transfer-Encoding: binary",
163 "", file_data, ""
164 ].join("\x0D\x0A")
165
166 release_headers = headers.merge(
167 "Content-Type" => "multipart/form-data; boundary=#{boundary}"
168 )
169
170 target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
171 http.post(target + query, data, release_headers)
172 end
173
174 if first_file then
175 release_id = release_response.body[/release_id=(\d+)/, 1]
176 raise("Couldn't get release id") unless release_id
177 end
178
179 first_file = false
180 end
181 end
182 end No newline at end of file
@@ -0,0 +1,1
1 require 'acts_as_versioned' No newline at end of file
This diff has been collapsed as it changes many lines, (511 lines changed) Show them Hide them
@@ -0,0 +1,511
1 # Copyright (c) 2005 Rick Olson
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 module ActiveRecord #:nodoc:
23 module Acts #:nodoc:
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 # versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version
26 # column is present as well.
27 #
28 # 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
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 #
31 # class Page < ActiveRecord::Base
32 # # assumes pages_versions table
33 # acts_as_versioned
34 # end
35 #
36 # Example:
37 #
38 # page = Page.create(:title => 'hello world!')
39 # page.version # => 1
40 #
41 # page.title = 'hello world'
42 # page.save
43 # page.version # => 2
44 # page.versions.size # => 2
45 #
46 # page.revert_to(1) # using version number
47 # page.title # => 'hello world!'
48 #
49 # page.revert_to(page.versions.last) # using versioned instance
50 # page.title # => 'hello world'
51 #
52 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
53 module Versioned
54 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes]
55 def self.included(base) # :nodoc:
56 base.extend ClassMethods
57 end
58
59 module ClassMethods
60 # == Configuration options
61 #
62 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
63 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
64 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
65 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
66 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
67 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
68 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
69 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
70 # For finer control, pass either a Proc or modify Model#version_condition_met?
71 #
72 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
73 #
74 # or...
75 #
76 # class Auction
77 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
78 # !expired?
79 # end
80 # end
81 #
82 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
83 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
84 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
85 #
86 # def name=(new_name)
87 # write_changed_attribute :name, new_name
88 # end
89 #
90 # * <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
91 # to create an anonymous mixin:
92 #
93 # class Auction
94 # acts_as_versioned do
95 # def started?
96 # !started_at.nil?
97 # end
98 # end
99 # end
100 #
101 # or...
102 #
103 # module AuctionExtension
104 # def started?
105 # !started_at.nil?
106 # end
107 # end
108 # class Auction
109 # acts_as_versioned :extend => AuctionExtension
110 # end
111 #
112 # Example code:
113 #
114 # @auction = Auction.find(1)
115 # @auction.started?
116 # @auction.versions.first.started?
117 #
118 # == Database Schema
119 #
120 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
121 # into a table called #{model}_versions where the model name is singlular. The _versions table should
122 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
123 #
124 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
125 # then that field is reflected in the versioned model as 'versioned_type' by default.
126 #
127 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
128 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
129 #
130 # class AddVersions < ActiveRecord::Migration
131 # def self.up
132 # # create_versioned_table takes the same options hash
133 # # that create_table does
134 # Post.create_versioned_table
135 # end
136 #
137 # def self.down
138 # Post.drop_versioned_table
139 # end
140 # end
141 #
142 # == Changing What Fields Are Versioned
143 #
144 # By default, acts_as_versioned will version all but these fields:
145 #
146 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
147 #
148 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
149 #
150 # class Post < ActiveRecord::Base
151 # acts_as_versioned
152 # self.non_versioned_columns << 'comments_count'
153 # end
154 #
155 def acts_as_versioned(options = {}, &extension)
156 # don't allow multiple calls
157 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
158
159 send :include, ActiveRecord::Acts::Versioned::ActMethods
160
161 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
162 :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
163 :version_association_options
164
165 # legacy
166 alias_method :non_versioned_fields, :non_versioned_columns
167 alias_method :non_versioned_fields=, :non_versioned_columns=
168
169 class << self
170 alias_method :non_versioned_fields, :non_versioned_columns
171 alias_method :non_versioned_fields=, :non_versioned_columns=
172 end
173
174 send :attr_accessor, :changed_attributes
175
176 self.versioned_class_name = options[:class_name] || "Version"
177 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
178 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
179 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
180 self.version_column = options[:version_column] || 'version'
181 self.version_sequence_name = options[:sequence_name]
182 self.max_version_limit = options[:limit].to_i
183 self.version_condition = options[:if] || true
184 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
185 self.version_association_options = {
186 :class_name => "#{self.to_s}::#{versioned_class_name}",
187 :foreign_key => "#{versioned_foreign_key}",
188 :order => 'version',
189 :dependent => :delete_all
190 }.merge(options[:association_options] || {})
191
192 if block_given?
193 extension_module_name = "#{versioned_class_name}Extension"
194 silence_warnings do
195 self.const_set(extension_module_name, Module.new(&extension))
196 end
197
198 options[:extend] = self.const_get(extension_module_name)
199 end
200
201 class_eval do
202 has_many :versions, version_association_options
203 before_save :set_new_version
204 after_create :save_version_on_create
205 after_update :save_version
206 after_save :clear_old_versions
207 after_save :clear_changed_attributes
208
209 unless options[:if_changed].nil?
210 self.track_changed_attributes = true
211 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
212 options[:if_changed].each do |attr_name|
213 define_method("#{attr_name}=") do |value|
214 write_changed_attribute attr_name, value
215 end
216 end
217 end
218
219 include options[:extend] if options[:extend].is_a?(Module)
220 end
221
222 # create the dynamic versioned model
223 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
224 def self.reloadable? ; false ; end
225 end
226
227 versioned_class.set_table_name versioned_table_name
228 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
229 :class_name => "::#{self.to_s}",
230 :foreign_key => versioned_foreign_key
231 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
232 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
233 end
234 end
235
236 module ActMethods
237 def self.included(base) # :nodoc:
238 base.extend ClassMethods
239 end
240
241 # Saves a version of the model if applicable
242 def save_version
243 save_version_on_create if save_version?
244 end
245
246 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
247 def save_version_on_create
248 rev = self.class.versioned_class.new
249 self.clone_versioned_model(self, rev)
250 rev.version = send(self.class.version_column)
251 rev.send("#{self.class.versioned_foreign_key}=", self.id)
252 rev.save
253 end
254
255 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
256 # Override this method to set your own criteria for clearing old versions.
257 def clear_old_versions
258 return if self.class.max_version_limit == 0
259 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
260 if excess_baggage > 0
261 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
262 self.class.versioned_class.connection.execute sql
263 end
264 end
265
266 # Finds a specific version of this model.
267 def find_version(version)
268 return version if version.is_a?(self.class.versioned_class)
269 return nil if version.is_a?(ActiveRecord::Base)
270 find_versions(:conditions => ['version = ?', version], :limit => 1).first
271 end
272
273 # Finds versions of this model. Takes an options hash like <tt>find</tt>
274 def find_versions(options = {})
275 versions.find(:all, options)
276 end
277
278 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
279 def revert_to(version)
280 if version.is_a?(self.class.versioned_class)
281 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
282 else
283 return false unless version = find_version(version)
284 end
285 self.clone_versioned_model(version, self)
286 self.send("#{self.class.version_column}=", version.version)
287 true
288 end
289
290 # Reverts a model to a given version and saves the model.
291 # Takes either a version number or an instance of the versioned model
292 def revert_to!(version)
293 revert_to(version) ? save_without_revision : false
294 end
295
296 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
297 def save_without_revision
298 save_without_revision!
299 true
300 rescue
301 false
302 end
303
304 def save_without_revision!
305 without_locking do
306 without_revision do
307 save!
308 end
309 end
310 end
311
312 # Returns an array of attribute keys that are versioned. See non_versioned_columns
313 def versioned_attributes
314 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
315 end
316
317 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
318 # If called with a single parameter, gets whether the parameter has changed.
319 def changed?(attr_name = nil)
320 attr_name.nil? ?
321 (!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) :
322 (changed_attributes && changed_attributes.include?(attr_name.to_s))
323 end
324
325 # keep old dirty? method
326 alias_method :dirty?, :changed?
327
328 # Clones a model. Used when saving a new version or reverting a model's version.
329 def clone_versioned_model(orig_model, new_model)
330 self.versioned_attributes.each do |key|
331 new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.has_attribute?(key)
332 end
333
334 if orig_model.is_a?(self.class.versioned_class)
335 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
336 elsif new_model.is_a?(self.class.versioned_class)
337 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
338 end
339 end
340
341 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
342 def save_version?
343 version_condition_met? && changed?
344 end
345
346 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
347 # custom version condition checking.
348 def version_condition_met?
349 case
350 when version_condition.is_a?(Symbol)
351 send(version_condition)
352 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
353 version_condition.call(self)
354 else
355 version_condition
356 end
357 end
358
359 # Executes the block with the versioning callbacks disabled.
360 #
361 # @foo.without_revision do
362 # @foo.save
363 # end
364 #
365 def without_revision(&block)
366 self.class.without_revision(&block)
367 end
368
369 # Turns off optimistic locking for the duration of the block
370 #
371 # @foo.without_locking do
372 # @foo.save
373 # end
374 #
375 def without_locking(&block)
376 self.class.without_locking(&block)
377 end
378
379 def empty_callback() end #:nodoc:
380
381 protected
382 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
383 def set_new_version
384 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
385 end
386
387 # Gets the next available version for the current record, or 1 for a new record
388 def next_version
389 return 1 if new_record?
390 (versions.calculate(:max, :version) || 0) + 1
391 end
392
393 # clears current changed attributes. Called after save.
394 def clear_changed_attributes
395 self.changed_attributes = []
396 end
397
398 def write_changed_attribute(attr_name, attr_value)
399 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
400 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
401 (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
402 write_attribute(attr_name, attr_value_for_db)
403 end
404
405 private
406 CALLBACKS.each do |attr_name|
407 alias_method "orig_#{attr_name}".to_sym, attr_name
408 end
409
410 module ClassMethods
411 # Finds a specific version of a specific row of this model
412 def find_version(id, version)
413 find_versions(id,
414 :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version],
415 :limit => 1).first
416 end
417
418 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
419 def find_versions(id, options = {})
420 versioned_class.find :all, {
421 :conditions => ["#{versioned_foreign_key} = ?", id],
422 :order => 'version' }.merge(options)
423 end
424
425 # Returns an array of columns that are versioned. See non_versioned_columns
426 def versioned_columns
427 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
428 end
429
430 # Returns an instance of the dynamic versioned model
431 def versioned_class
432 const_get versioned_class_name
433 end
434
435 # Rake migration task to create the versioned table using options passed to acts_as_versioned
436 def create_versioned_table(create_table_options = {})
437 # create version column in main table if it does not exist
438 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
439 self.connection.add_column table_name, :version, :integer
440 end
441
442 self.connection.create_table(versioned_table_name, create_table_options) do |t|
443 t.column versioned_foreign_key, :integer
444 t.column :version, :integer
445 end
446
447 updated_col = nil
448 self.versioned_columns.each do |col|
449 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
450 self.connection.add_column versioned_table_name, col.name, col.type,
451 :limit => col.limit,
452 :default => col.default
453 end
454
455 if type_col = self.columns_hash[inheritance_column]
456 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
457 :limit => type_col.limit,
458 :default => type_col.default
459 end
460
461 if updated_col.nil?
462 self.connection.add_column versioned_table_name, :updated_at, :timestamp
463 end
464 end
465
466 # Rake migration task to drop the versioned table
467 def drop_versioned_table
468 self.connection.drop_table versioned_table_name
469 end
470
471 # Executes the block with the versioning callbacks disabled.
472 #
473 # Foo.without_revision do
474 # @foo.save
475 # end
476 #
477 def without_revision(&block)
478 class_eval do
479 CALLBACKS.each do |attr_name|
480 alias_method attr_name, :empty_callback
481 end
482 end
483 result = block.call
484 class_eval do
485 CALLBACKS.each do |attr_name|
486 alias_method attr_name, "orig_#{attr_name}".to_sym
487 end
488 end
489 result
490 end
491
492 # Turns off optimistic locking for the duration of the block
493 #
494 # Foo.without_locking do
495 # @foo.save
496 # end
497 #
498 def without_locking(&block)
499 current = ActiveRecord::Base.lock_optimistically
500 ActiveRecord::Base.lock_optimistically = false if current
501 result = block.call
502 ActiveRecord::Base.lock_optimistically = true if current
503 result
504 end
505 end
506 end
507 end
508 end
509 end
510
511 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
@@ -0,0 +1,40
1 $:.unshift(File.dirname(__FILE__) + '/../lib')
2
3 require 'test/unit'
4 require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
5 require 'active_record/fixtures'
6
7 config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
8 ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
9 ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
10
11 load(File.dirname(__FILE__) + "/schema.rb")
12
13 # set up custom sequence on widget_versions for DBs that support sequences
14 if ENV['DB'] == 'postgresql'
15 ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
16 ActiveRecord::Base.connection.remove_column :widget_versions, :id
17 ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
18 ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
19 end
20
21 Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
22 $LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
23
24 class Test::Unit::TestCase #:nodoc:
25 def create_fixtures(*table_names)
26 if block_given?
27 Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
28 else
29 Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
30 end
31 end
32
33 # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
34 self.use_transactional_fixtures = true
35
36 # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
37 self.use_instantiated_fixtures = false
38
39 # Add more helper methods to be used by all tests here...
40 end No newline at end of file
@@ -0,0 +1,18
1 sqlite:
2 :adapter: sqlite
3 :dbfile: acts_as_versioned_plugin.sqlite.db
4 sqlite3:
5 :adapter: sqlite3
6 :dbfile: acts_as_versioned_plugin.sqlite3.db
7 postgresql:
8 :adapter: postgresql
9 :username: postgres
10 :password: postgres
11 :database: acts_as_versioned_plugin_test
12 :min_messages: ERROR
13 mysql:
14 :adapter: mysql
15 :host: localhost
16 :username: rails
17 :password:
18 :database: acts_as_versioned_plugin_test No newline at end of file
@@ -0,0 +1,6
1 caged:
2 id: 1
3 name: caged
4 mly:
5 id: 2
6 name: mly No newline at end of file
@@ -0,0 +1,3
1 class Landmark < ActiveRecord::Base
2 acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
3 end
@@ -0,0 +1,7
1 washington:
2 id: 1
3 landmark_id: 1
4 version: 1
5 name: Washington, D.C.
6 latitude: 38.895
7 longitude: -77.036667
@@ -0,0 +1,6
1 washington:
2 id: 1
3 name: Washington, D.C.
4 latitude: 38.895
5 longitude: -77.036667
6 version: 1
@@ -0,0 +1,10
1 welcome:
2 id: 1
3 title: Welcome to the weblog
4 lock_version: 24
5 type: LockedPage
6 thinking:
7 id: 2
8 title: So I was thinking
9 lock_version: 24
10 type: SpecialLockedPage
@@ -0,0 +1,27
1 welcome_1:
2 id: 1
3 page_id: 1
4 title: Welcome to the weblg
5 version: 23
6 version_type: LockedPage
7
8 welcome_2:
9 id: 2
10 page_id: 1
11 title: Welcome to the weblog
12 version: 24
13 version_type: LockedPage
14
15 thinking_1:
16 id: 3
17 page_id: 2
18 title: So I was thinking!!!
19 version: 23
20 version_type: SpecialLockedPage
21
22 thinking_2:
23 id: 4
24 page_id: 2
25 title: So I was thinking
26 version: 24
27 version_type: SpecialLockedPage
@@ -0,0 +1,13
1 class AddVersionedTables < ActiveRecord::Migration
2 def self.up
3 create_table("things") do |t|
4 t.column :title, :text
5 end
6 Thing.create_versioned_table
7 end
8
9 def self.down
10 Thing.drop_versioned_table
11 drop_table "things" rescue nil
12 end
13 end No newline at end of file
@@ -0,0 +1,43
1 class Page < ActiveRecord::Base
2 belongs_to :author
3 has_many :authors, :through => :versions, :order => 'name'
4 belongs_to :revisor, :class_name => 'Author'
5 has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
6 acts_as_versioned :if => :feeling_good? do
7 def self.included(base)
8 base.cattr_accessor :feeling_good
9 base.feeling_good = true
10 base.belongs_to :author
11 base.belongs_to :revisor, :class_name => 'Author'
12 end
13
14 def feeling_good?
15 @@feeling_good == true
16 end
17 end
18 end
19
20 module LockedPageExtension
21 def hello_world
22 'hello_world'
23 end
24 end
25
26 class LockedPage < ActiveRecord::Base
27 acts_as_versioned \
28 :inheritance_column => :version_type,
29 :foreign_key => :page_id,
30 :table_name => :locked_pages_revisions,
31 :class_name => 'LockedPageRevision',
32 :version_column => :lock_version,
33 :limit => 2,
34 :if_changed => :title,
35 :extend => LockedPageExtension
36 end
37
38 class SpecialLockedPage < LockedPage
39 end
40
41 class Author < ActiveRecord::Base
42 has_many :pages
43 end No newline at end of file
@@ -0,0 +1,16
1 welcome_2:
2 id: 1
3 page_id: 1
4 title: Welcome to the weblog
5 body: Such a lovely day
6 version: 24
7 author_id: 1
8 revisor_id: 1
9 welcome_1:
10 id: 2
11 page_id: 1
12 title: Welcome to the weblg
13 body: Such a lovely day
14 version: 23
15 author_id: 2
16 revisor_id: 2
@@ -0,0 +1,7
1 welcome:
2 id: 1
3 title: Welcome to the weblog
4 body: Such a lovely day
5 version: 24
6 author_id: 1
7 revisor_id: 1 No newline at end of file
@@ -0,0 +1,6
1 class Widget < ActiveRecord::Base
2 acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
3 :dependent => nil, :order => 'version desc'
4 }
5 non_versioned_columns << 'foo'
6 end No newline at end of file
@@ -0,0 +1,32
1 require File.join(File.dirname(__FILE__), 'abstract_unit')
2
3 if ActiveRecord::Base.connection.supports_migrations?
4 class Thing < ActiveRecord::Base
5 attr_accessor :version
6 acts_as_versioned
7 end
8
9 class MigrationTest < Test::Unit::TestCase
10 self.use_transactional_fixtures = false
11 def teardown
12 ActiveRecord::Base.connection.initialize_schema_information
13 ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
14
15 Thing.connection.drop_table "things" rescue nil
16 Thing.connection.drop_table "thing_versions" rescue nil
17 Thing.reset_column_information
18 end
19
20 def test_versioned_migration
21 assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
22 # take 'er up
23 ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
24 t = Thing.create :title => 'blah blah'
25 assert_equal 1, t.versions.size
26
27 # now lets take 'er back down
28 ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
29 assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
30 end
31 end
32 end
@@ -0,0 +1,68
1 ActiveRecord::Schema.define(:version => 0) do
2 create_table :pages, :force => true do |t|
3 t.column :version, :integer
4 t.column :title, :string, :limit => 255
5 t.column :body, :text
6 t.column :updated_on, :datetime
7 t.column :author_id, :integer
8 t.column :revisor_id, :integer
9 end
10
11 create_table :page_versions, :force => true do |t|
12 t.column :page_id, :integer
13 t.column :version, :integer
14 t.column :title, :string, :limit => 255
15 t.column :body, :text
16 t.column :updated_on, :datetime
17 t.column :author_id, :integer
18 t.column :revisor_id, :integer
19 end
20
21 create_table :authors, :force => true do |t|
22 t.column :page_id, :integer
23 t.column :name, :string
24 end
25
26 create_table :locked_pages, :force => true do |t|
27 t.column :lock_version, :integer
28 t.column :title, :string, :limit => 255
29 t.column :type, :string, :limit => 255
30 end
31
32 create_table :locked_pages_revisions, :force => true do |t|
33 t.column :page_id, :integer
34 t.column :version, :integer
35 t.column :title, :string, :limit => 255
36 t.column :version_type, :string, :limit => 255
37 t.column :updated_at, :datetime
38 end
39
40 create_table :widgets, :force => true do |t|
41 t.column :name, :string, :limit => 50
42 t.column :foo, :string
43 t.column :version, :integer
44 t.column :updated_at, :datetime
45 end
46
47 create_table :widget_versions, :force => true do |t|
48 t.column :widget_id, :integer
49 t.column :name, :string, :limit => 50
50 t.column :version, :integer
51 t.column :updated_at, :datetime
52 end
53
54 create_table :landmarks, :force => true do |t|
55 t.column :name, :string
56 t.column :latitude, :float
57 t.column :longitude, :float
58 t.column :version, :integer
59 end
60
61 create_table :landmark_versions, :force => true do |t|
62 t.column :landmark_id, :integer
63 t.column :name, :string
64 t.column :latitude, :float
65 t.column :longitude, :float
66 t.column :version, :integer
67 end
68 end
@@ -0,0 +1,313
1 require File.join(File.dirname(__FILE__), 'abstract_unit')
2 require File.join(File.dirname(__FILE__), 'fixtures/page')
3 require File.join(File.dirname(__FILE__), 'fixtures/widget')
4
5 class VersionedTest < Test::Unit::TestCase
6 fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
7
8 def test_saves_versioned_copy
9 p = Page.create :title => 'first title', :body => 'first body'
10 assert !p.new_record?
11 assert_equal 1, p.versions.size
12 assert_equal 1, p.version
13 assert_instance_of Page.versioned_class, p.versions.first
14 end
15
16 def test_saves_without_revision
17 p = pages(:welcome)
18 old_versions = p.versions.count
19
20 p.save_without_revision
21
22 p.without_revision do
23 p.update_attributes :title => 'changed'
24 end
25
26 assert_equal old_versions, p.versions.count
27 end
28
29 def test_rollback_with_version_number
30 p = pages(:welcome)
31 assert_equal 24, p.version
32 assert_equal 'Welcome to the weblog', p.title
33
34 assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
35 assert_equal 23, p.version
36 assert_equal 'Welcome to the weblg', p.title
37 end
38
39 def test_versioned_class_name
40 assert_equal 'Version', Page.versioned_class_name
41 assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
42 end
43
44 def test_versioned_class
45 assert_equal Page::Version, Page.versioned_class
46 assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
47 end
48
49 def test_special_methods
50 assert_nothing_raised { pages(:welcome).feeling_good? }
51 assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
52 assert_nothing_raised { locked_pages(:welcome).hello_world }
53 assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
54 end
55
56 def test_rollback_with_version_class
57 p = pages(:welcome)
58 assert_equal 24, p.version
59 assert_equal 'Welcome to the weblog', p.title
60
61 assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
62 assert_equal 23, p.version
63 assert_equal 'Welcome to the weblg', p.title
64 end
65
66 def test_rollback_fails_with_invalid_revision
67 p = locked_pages(:welcome)
68 assert !p.revert_to!(locked_pages(:thinking))
69 end
70
71 def test_saves_versioned_copy_with_options
72 p = LockedPage.create :title => 'first title'
73 assert !p.new_record?
74 assert_equal 1, p.versions.size
75 assert_instance_of LockedPage.versioned_class, p.versions.first
76 end
77
78 def test_rollback_with_version_number_with_options
79 p = locked_pages(:welcome)
80 assert_equal 'Welcome to the weblog', p.title
81 assert_equal 'LockedPage', p.versions.first.version_type
82
83 assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
84 assert_equal 'Welcome to the weblg', p.title
85 assert_equal 'LockedPage', p.versions.first.version_type
86 end
87
88 def test_rollback_with_version_class_with_options
89 p = locked_pages(:welcome)
90 assert_equal 'Welcome to the weblog', p.title
91 assert_equal 'LockedPage', p.versions.first.version_type
92
93 assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
94 assert_equal 'Welcome to the weblg', p.title
95 assert_equal 'LockedPage', p.versions.first.version_type
96 end
97
98 def test_saves_versioned_copy_with_sti
99 p = SpecialLockedPage.create :title => 'first title'
100 assert !p.new_record?
101 assert_equal 1, p.versions.size
102 assert_instance_of LockedPage.versioned_class, p.versions.first
103 assert_equal 'SpecialLockedPage', p.versions.first.version_type
104 end
105
106 def test_rollback_with_version_number_with_sti
107 p = locked_pages(:thinking)
108 assert_equal 'So I was thinking', p.title
109
110 assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
111 assert_equal 'So I was thinking!!!', p.title
112 assert_equal 'SpecialLockedPage', p.versions.first.version_type
113 end
114
115 def test_lock_version_works_with_versioning
116 p = locked_pages(:thinking)
117 p2 = LockedPage.find(p.id)
118
119 p.title = 'fresh title'
120 p.save
121 assert_equal 2, p.versions.size # limit!
122
123 assert_raises(ActiveRecord::StaleObjectError) do
124 p2.title = 'stale title'
125 p2.save
126 end
127 end
128
129 def test_version_if_condition
130 p = Page.create :title => "title"
131 assert_equal 1, p.version
132
133 Page.feeling_good = false
134 p.save
135 assert_equal 1, p.version
136 Page.feeling_good = true
137 end
138
139 def test_version_if_condition2
140 # set new if condition
141 Page.class_eval do
142 def new_feeling_good() title[0..0] == 'a'; end
143 alias_method :old_feeling_good, :feeling_good?
144 alias_method :feeling_good?, :new_feeling_good
145 end
146
147 p = Page.create :title => "title"
148 assert_equal 1, p.version # version does not increment
149 assert_equal 1, p.versions(true).size
150
151 p.update_attributes(:title => 'new title')
152 assert_equal 1, p.version # version does not increment
153 assert_equal 1, p.versions(true).size
154
155 p.update_attributes(:title => 'a title')
156 assert_equal 2, p.version
157 assert_equal 2, p.versions(true).size
158
159 # reset original if condition
160 Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
161 end
162
163 def test_version_if_condition_with_block
164 # set new if condition
165 old_condition = Page.version_condition
166 Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
167
168 p = Page.create :title => "title"
169 assert_equal 1, p.version # version does not increment
170 assert_equal 1, p.versions(true).size
171
172 p.update_attributes(:title => 'a title')
173 assert_equal 1, p.version # version does not increment
174 assert_equal 1, p.versions(true).size
175
176 p.update_attributes(:title => 'b title')
177 assert_equal 2, p.version
178 assert_equal 2, p.versions(true).size
179
180 # reset original if condition
181 Page.version_condition = old_condition
182 end
183
184 def test_version_no_limit
185 p = Page.create :title => "title", :body => 'first body'
186 p.save
187 p.save
188 5.times do |i|
189 assert_page_title p, i
190 end
191 end
192
193 def test_version_max_limit
194 p = LockedPage.create :title => "title"
195 p.update_attributes(:title => "title1")
196 p.update_attributes(:title => "title2")
197 5.times do |i|
198 assert_page_title p, i, :lock_version
199 assert p.versions(true).size <= 2, "locked version can only store 2 versions"
200 end
201 end
202
203 def test_track_changed_attributes_default_value
204 assert !Page.track_changed_attributes
205 assert LockedPage.track_changed_attributes
206 assert SpecialLockedPage.track_changed_attributes
207 end
208
209 def test_version_order
210 assert_equal 23, pages(:welcome).versions.first.version
211 assert_equal 24, pages(:welcome).versions.last.version
212 assert_equal 23, pages(:welcome).find_versions.first.version
213 assert_equal 24, pages(:welcome).find_versions.last.version
214 end
215
216 def test_track_changed_attributes
217 p = LockedPage.create :title => "title"
218 assert_equal 1, p.lock_version
219 assert_equal 1, p.versions(true).size
220
221 p.title = 'title'
222 assert !p.save_version?
223 p.save
224 assert_equal 2, p.lock_version # still increments version because of optimistic locking
225 assert_equal 1, p.versions(true).size
226
227 p.title = 'updated title'
228 assert p.save_version?
229 p.save
230 assert_equal 3, p.lock_version
231 assert_equal 1, p.versions(true).size # version 1 deleted
232
233 p.title = 'updated title!'
234 assert p.save_version?
235 p.save
236 assert_equal 4, p.lock_version
237 assert_equal 2, p.versions(true).size # version 1 deleted
238 end
239
240 def assert_page_title(p, i, version_field = :version)
241 p.title = "title#{i}"
242 p.save
243 assert_equal "title#{i}", p.title
244 assert_equal (i+4), p.send(version_field)
245 end
246
247 def test_find_versions
248 assert_equal 2, locked_pages(:welcome).versions.size
249 assert_equal 1, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length
250 assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length
251 assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length
252 assert_equal 2, locked_pages(:welcome).find_versions.length
253 end
254
255 def test_with_sequence
256 assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
257 Widget.create :name => 'new widget'
258 Widget.create :name => 'new widget'
259 Widget.create :name => 'new widget'
260 assert_equal 3, Widget.count
261 assert_equal 3, Widget.versioned_class.count
262 end
263
264 def test_has_many_through
265 assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
266 end
267
268 def test_has_many_through_with_custom_association
269 assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
270 end
271
272 def test_referential_integrity
273 pages(:welcome).destroy
274 assert_equal 0, Page.count
275 assert_equal 0, Page::Version.count
276 end
277
278 def test_association_options
279 association = Page.reflect_on_association(:versions)
280 options = association.options
281 assert_equal :delete_all, options[:dependent]
282 assert_equal 'version', options[:order]
283
284 association = Widget.reflect_on_association(:versions)
285 options = association.options
286 assert_nil options[:dependent]
287 assert_equal 'version desc', options[:order]
288 assert_equal 'widget_id', options[:foreign_key]
289
290 widget = Widget.create :name => 'new widget'
291 assert_equal 1, Widget.count
292 assert_equal 1, Widget.versioned_class.count
293 widget.destroy
294 assert_equal 0, Widget.count
295 assert_equal 1, Widget.versioned_class.count
296 end
297
298 def test_versioned_records_should_belong_to_parent
299 page = pages(:welcome)
300 page_version = page.versions.last
301 assert_equal page, page_version.page
302 end
303
304 def test_unchanged_attributes
305 landmarks(:washington).attributes = landmarks(:washington).attributes
306 assert !landmarks(:washington).changed?
307 end
308
309 def test_unchanged_string_attributes
310 landmarks(:washington).attributes = landmarks(:washington).attributes.inject({}) { |params, (key, value)| params.update key => value.to_s }
311 assert !landmarks(:washington).changed?
312 end
313 end
@@ -32,6 +32,10 class ApplicationController < ActionController::Base
32 end
32 end
33 end
33 end
34
34
35 def logged_in_user_membership
36 @user_membership ||= Member.find(:first, :conditions => ["user_id=? and project_id=?", self.logged_in_user.id, @project.id])
37 end
38
35 # check if login is globally required to access the application
39 # check if login is globally required to access the application
36 def check_if_login_required
40 def check_if_login_required
37 require_login if Setting.login_required?
41 require_login if Setting.login_required?
@@ -89,6 +93,16 class ApplicationController < ActionController::Base
89 render :nothing => true, :status => 403
93 render :nothing => true, :status => 403
90 false
94 false
91 end
95 end
96
97 # make sure that the user is a member of the project (or admin) if project is private
98 # used as a before_filter for actions that do not require any particular permission on the project
99 def check_project_privacy
100 return true if @project.is_public?
101 return false unless logged_in_user
102 return true if logged_in_user.admin? || logged_in_user_membership
103 render :nothing => true, :status => 403
104 false
105 end
92
106
93 # store current uri in session.
107 # store current uri in session.
94 # return to this location by calling redirect_back_or_default
108 # return to this location by calling redirect_back_or_default
@@ -68,6 +68,10 class ProjectsController < ApplicationController
68 @project.repository = Repository.new
68 @project.repository = Repository.new
69 @project.repository.attributes = params[:repository]
69 @project.repository.attributes = params[:repository]
70 end
70 end
71 if "1" == params[:wiki_enabled]
72 @project.wiki = Wiki.new
73 @project.wiki.attributes = params[:wiki]
74 end
71 if @project.save
75 if @project.save
72 flash[:notice] = l(:notice_successful_create)
76 flash[:notice] = l(:notice_successful_create)
73 redirect_to :controller => 'admin', :action => 'projects'
77 redirect_to :controller => 'admin', :action => 'projects'
@@ -113,6 +117,15 class ProjectsController < ApplicationController
113 @project.repository.update_attributes params[:repository]
117 @project.repository.update_attributes params[:repository]
114 end
118 end
115 end
119 end
120 if params[:wiki_enabled]
121 case params[:wiki_enabled]
122 when "0"
123 @project.wiki.destroy
124 when "1"
125 @project.wiki ||= Wiki.new
126 @project.wiki.update_attributes params[:wiki]
127 end
128 end
116 @project.attributes = params[:project]
129 @project.attributes = params[:project]
117 if @project.save
130 if @project.save
118 flash[:notice] = l(:notice_successful_update)
131 flash[:notice] = l(:notice_successful_update)
@@ -63,7 +63,7 module ApplicationHelper
63 end
63 end
64
64
65 def format_time(time)
65 def format_time(time)
66 l_datetime(time) if time
66 l_datetime((time.is_a? String) ? time.to_time : time) if time
67 end
67 end
68
68
69 def day_name(day)
69 def day_name(day)
@@ -92,10 +92,42 module ApplicationHelper
92 html
92 html
93 end
93 end
94
94
95 def textilizable(text)
95 # textilize text according to system settings and RedCloth availability
96 text = (Setting.text_formatting == 'textile') && (ActionView::Helpers::TextHelper.method_defined? "textilize") ? RedCloth.new(h(text)).to_html : simple_format(auto_link(h(text)))
96 def textilizable(text, options = {})
97 # turn "#id" patterns into links to issues
97 # different methods for formatting wiki links
98 text = text.gsub(/#(\d+)([^;\d])/, "<a href='/issues/show/\\1'>#\\1</a>\\2")
98 case options[:wiki_links]
99 when :local
100 # used for local links to html files
101 format_wiki_link = Proc.new {|title| "#{title}.html" }
102 when :anchor
103 # used for single-file wiki export
104 format_wiki_link = Proc.new {|title| "##{title}" }
105 else
106 if @project
107 format_wiki_link = Proc.new {|title| url_for :controller => 'wiki', :action => 'index', :id => @project, :page => title }
108 else
109 format_wiki_link = Proc.new {|title| title }
110 end
111 end
112
113 # turn wiki links into textile links:
114 # example:
115 # [[link]] -> "link":link
116 # [[link|title]] -> "title":link
117 text = text.gsub(/\[\[([^\]\|]+)(\|([^\]\|]+))?\]\]/) {|m| "\"#{$3 || $1}\":" + format_wiki_link.call(Wiki.titleize($1)) }
118
119 # turn issue ids to textile links
120 # example:
121 # #52 -> "#52":/issues/show/52
122 text = text.gsub(/#(\d+)([\s\.\(\)\-,:;])/) {|m| "\"##{$1}\":" + url_for(:controller => 'issues', :action => 'show', :id => $1) + $2 }
123
124 # turn revision ids to textile links (@project needed)
125 # example:
126 # r52 -> "r52":/repositories/revision/6?rev=52 (@project.id is 6)
127 text = text.gsub(/r(\d+)([\s\.\(\)\-,:;])/) {|m| "\"r#{$1}\":" + url_for(:controller => 'repositories', :action => 'revision', :id => @project.id, :rev => $1) + $2 } if @project
128
129 # finally textilize text
130 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)))
99 end
131 end
100
132
101 def error_messages_for(object_name, options = {})
133 def error_messages_for(object_name, options = {})
@@ -26,13 +26,14 class Project < ActiveRecord::Base
26 has_many :news, :dependent => :delete_all, :include => :author
26 has_many :news, :dependent => :delete_all, :include => :author
27 has_many :issue_categories, :dependent => :delete_all, :order => "issue_categories.name"
27 has_many :issue_categories, :dependent => :delete_all, :order => "issue_categories.name"
28 has_one :repository, :dependent => :destroy
28 has_one :repository, :dependent => :destroy
29 has_one :wiki, :dependent => :destroy
29 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id'
30 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => 'custom_fields_projects', :association_foreign_key => 'custom_field_id'
30 acts_as_tree :order => "name", :counter_cache => true
31 acts_as_tree :order => "name", :counter_cache => true
31
32
32 validates_presence_of :name, :description
33 validates_presence_of :name, :description
33 validates_uniqueness_of :name
34 validates_uniqueness_of :name
34 validates_associated :custom_values, :on => :update
35 validates_associated :custom_values, :on => :update
35 validates_associated :repository
36 validates_associated :repository, :wiki
36 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
37 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
37
38
38 # returns latest created projects
39 # returns latest created projects
@@ -91,6 +91,7
91 <%= link_to l(:label_change_log), {:controller => 'projects', :action => 'changelog', :id => @project }, :class => "menuItem" %>
91 <%= link_to l(:label_change_log), {:controller => 'projects', :action => 'changelog', :id => @project }, :class => "menuItem" %>
92 <%= link_to l(:label_roadmap), {:controller => 'projects', :action => 'roadmap', :id => @project }, :class => "menuItem" %>
92 <%= link_to l(:label_roadmap), {:controller => 'projects', :action => 'roadmap', :id => @project }, :class => "menuItem" %>
93 <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
93 <%= link_to l(:label_document_plural), {:controller => 'projects', :action => 'list_documents', :id => @project }, :class => "menuItem" %>
94 <%= link_to l(:label_wiki), {:controller => 'wiki', :id => @project, :page => nil }, :class => "menuItem" if @project.wiki and !@project.wiki.new_record? %>
94 <%= link_to l(:label_member_plural), {:controller => 'projects', :action => 'list_members', :id => @project }, :class => "menuItem" %>
95 <%= link_to l(:label_member_plural), {:controller => 'projects', :action => 'list_members', :id => @project }, :class => "menuItem" %>
95 <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
96 <%= link_to l(:label_attachment_plural), {:controller => 'projects', :action => 'list_files', :id => @project }, :class => "menuItem" %>
96 <%= link_to l(:label_search), {:controller => 'projects', :action => 'search', :id => @project }, :class => "menuItem" %>
97 <%= link_to l(:label_search), {:controller => 'projects', :action => 'search', :id => @project }, :class => "menuItem" %>
@@ -115,6 +116,7
115 <li><%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %></li>
116 <li><%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %></li>
116 <li><%= link_to l(:label_roadmap), :controller => 'projects', :action => 'roadmap', :id => @project %></li>
117 <li><%= link_to l(:label_roadmap), :controller => 'projects', :action => 'roadmap', :id => @project %></li>
117 <li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
118 <li><%= link_to l(:label_document_plural), :controller => 'projects', :action => 'list_documents', :id => @project %></li>
119 <li><%= link_to l(:label_wiki), :controller => 'wiki', :id => @project, :page => nil if @project.wiki and !@project.wiki.new_record? %></li>
118 <li><%= link_to l(:label_member_plural), :controller => 'projects', :action => 'list_members', :id => @project %></li>
120 <li><%= link_to l(:label_member_plural), :controller => 'projects', :action => 'list_members', :id => @project %></li>
119 <li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
121 <li><%= link_to l(:label_attachment_plural), :controller => 'projects', :action => 'list_files', :id => @project %></li>
120 <li><%= link_to l(:label_search), :controller => 'projects', :action => 'search', :id => @project %></li>
122 <li><%= link_to l(:label_search), :controller => 'projects', :action => 'search', :id => @project %></li>
@@ -38,6 +38,20
38 <%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
38 <%= javascript_tag "Element.hide('repository');" if @project.repository.nil? %>
39 </div>
39 </div>
40
40
41 <div class="box">
42 <h3><%= check_box_tag "wiki_enabled", 1, !@project.wiki.nil?, :onclick => "Element.toggle('wiki');" %> <%= l(:label_wiki) %></h3>
43 <%= hidden_field_tag "wiki_enabled", 0 %>
44 <div id="wiki">
45 <% fields_for :wiki, @project.wiki, { :builder => TabularFormBuilder, :lang => current_language} do |wiki| %>
46 <p><%= wiki.text_field :start_page, :size => 60, :required => true %></p>
47 <% # content_tag("div", "", :id => "wiki_start_page_auto_complete", :class => "auto_complete") +
48 # auto_complete_field("wiki_start_page", { :url => { :controller => 'wiki', :action => 'auto_complete_for_wiki_page', :id => @project } })
49 %>
50 <% end %>
51 </div>
52 <%= javascript_tag "Element.hide('wiki');" if @project.wiki.nil? %>
53 </div>
54
41 <% content_for :header_tags do %>
55 <% content_for :header_tags do %>
42 <%= javascript_include_tag 'calendar/calendar' %>
56 <%= javascript_include_tag 'calendar/calendar' %>
43 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
57 <%= javascript_include_tag "calendar/lang/calendar-#{current_language}.js" %>
@@ -38,6 +38,9
38 <p><label><%= l(:setting_text_formatting) %></label>
38 <p><label><%= l(:setting_text_formatting) %></label>
39 <%= select_tag 'settings[text_formatting]', options_for_select( [[l(:label_none), 0], ["textile", "textile"]], Setting.text_formatting) %></p>
39 <%= select_tag 'settings[text_formatting]', options_for_select( [[l(:label_none), 0], ["textile", "textile"]], Setting.text_formatting) %></p>
40
40
41 <p><label><%= l(:setting_wiki_compression) %></label>
42 <%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p>
43
41 </div>
44 </div>
42 <%= submit_tag l(:button_save) %>
45 <%= submit_tag l(:button_save) %>
43 <% end %> No newline at end of file
46 <% end %>
@@ -9,7 +9,8 ActionController::Routing::Routes.draw do |map|
9 # You can have the root of your site routed by hooking up ''
9 # You can have the root of your site routed by hooking up ''
10 # -- just remember to delete public/index.html.
10 # -- just remember to delete public/index.html.
11 map.connect '', :controller => "welcome"
11 map.connect '', :controller => "welcome"
12
12
13 map.connect 'wiki/:id/:page/:action', :controller => 'wiki', :page => nil
13 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
14 map.connect 'roles/workflow/:id/:role_id/:tracker_id', :controller => 'roles', :action => 'workflow'
14 map.connect 'help/:ctrl/:page', :controller => 'help'
15 map.connect 'help/:ctrl/:page', :controller => 'help'
15 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
16 #map.connect ':controller/:action/:id/:sort_key/:sort_order'
@@ -41,6 +41,8 mail_from:
41 default: redmine@somenet.foo
41 default: redmine@somenet.foo
42 text_formatting:
42 text_formatting:
43 default: textile
43 default: textile
44 wiki_compression:
45 default: ""
44 default_language:
46 default_language:
45 default: en
47 default: en
46 host_name:
48 host_name:
@@ -142,6 +142,7 field_auth_source: Authentisierung Modus
142 field_hide_mail: Mein email address verstecken
142 field_hide_mail: Mein email address verstecken
143 field_comment: Anmerkung
143 field_comment: Anmerkung
144 field_url: URL
144 field_url: URL
145 field_start_page: Hauptseite
145
146
146 setting_app_title: Applikation Titel
147 setting_app_title: Applikation Titel
147 setting_app_subtitle: Applikation Untertitel
148 setting_app_subtitle: Applikation Untertitel
@@ -154,6 +155,7 setting_issues_export_limit: Issues export limit
154 setting_mail_from: Emission address
155 setting_mail_from: Emission address
155 setting_host_name: Host Name
156 setting_host_name: Host Name
156 setting_text_formatting: Textformatierung
157 setting_text_formatting: Textformatierung
158 setting_wiki_compression: Wiki Geschichte Kompression
157
159
158 label_user: Benutzer
160 label_user: Benutzer
159 label_user_plural: Benutzer
161 label_user_plural: Benutzer
@@ -322,6 +324,10 label_search: Suche
322 label_result: %d Resultat
324 label_result: %d Resultat
323 label_result_plural: %d Resultate
325 label_result_plural: %d Resultate
324 label_all_words: Alle Wörter
326 label_all_words: Alle Wörter
327 label_wiki: Wiki
328 label_page_index: Index
329 label_current_version: Gegenwärtige Version
330 label_preview: Vorbetrachtung
325
331
326 button_login: Einloggen
332 button_login: Einloggen
327 button_submit: Einreichen
333 button_submit: Einreichen
@@ -142,6 +142,7 field_auth_source: Authentication mode
142 field_hide_mail: Hide my email address
142 field_hide_mail: Hide my email address
143 field_comment: Comment
143 field_comment: Comment
144 field_url: URL
144 field_url: URL
145 field_start_page: Start page
145
146
146 setting_app_title: Application title
147 setting_app_title: Application title
147 setting_app_subtitle: Application subtitle
148 setting_app_subtitle: Application subtitle
@@ -154,6 +155,7 setting_issues_export_limit: Issues export limit
154 setting_mail_from: Emission mail address
155 setting_mail_from: Emission mail address
155 setting_host_name: Host name
156 setting_host_name: Host name
156 setting_text_formatting: Text formatting
157 setting_text_formatting: Text formatting
158 setting_wiki_compression: Wiki history compression
157
159
158 label_user: User
160 label_user: User
159 label_user_plural: Users
161 label_user_plural: Users
@@ -322,6 +324,10 label_search: Search
322 label_result: %d result
324 label_result: %d result
323 label_result_plural: %d results
325 label_result_plural: %d results
324 label_all_words: All words
326 label_all_words: All words
327 label_wiki: Wiki
328 label_page_index: Index
329 label_current_version: Current version
330 label_preview: Preview
325
331
326 button_login: Login
332 button_login: Login
327 button_submit: Submit
333 button_submit: Submit
@@ -142,6 +142,7 field_auth_source: Modo de la autentificación
142 field_hide_mail: Ocultar mi email address
142 field_hide_mail: Ocultar mi email address
143 field_comment: Comentario
143 field_comment: Comentario
144 field_url: URL
144 field_url: URL
145 field_start_page: Página principal
145
146
146 setting_app_title: Título del aplicación
147 setting_app_title: Título del aplicación
147 setting_app_subtitle: Subtítulo del aplicación
148 setting_app_subtitle: Subtítulo del aplicación
@@ -154,6 +155,7 setting_issues_export_limit: Issues export limit
154 setting_mail_from: Email de la emisión
155 setting_mail_from: Email de la emisión
155 setting_host_name: Nombre de anfitrión
156 setting_host_name: Nombre de anfitrión
156 setting_text_formatting: Formato de texto
157 setting_text_formatting: Formato de texto
158 setting_wiki_compression: Compresión de la historia de Wiki
157
159
158 label_user: Usuario
160 label_user: Usuario
159 label_user_plural: Usuarios
161 label_user_plural: Usuarios
@@ -322,6 +324,10 label_search: Búsqueda
322 label_result: %d resultado
324 label_result: %d resultado
323 label_result_plural: %d resultados
325 label_result_plural: %d resultados
324 label_all_words: Todas las palabras
326 label_all_words: Todas las palabras
327 label_wiki: Wiki
328 label_page_index: Índice
329 label_current_version: Versión actual
330 label_preview: Previo
325
331
326 button_login: Conexión
332 button_login: Conexión
327 button_submit: Someter
333 button_submit: Someter
@@ -142,6 +142,7 field_auth_source: Mode d'authentification
142 field_hide_mail: Cacher mon adresse mail
142 field_hide_mail: Cacher mon adresse mail
143 field_comment: Commentaire
143 field_comment: Commentaire
144 field_url: URL
144 field_url: URL
145 field_start_page: Page de démarrage
145
146
146 setting_app_title: Titre de l'application
147 setting_app_title: Titre de l'application
147 setting_app_subtitle: Sous-titre de l'application
148 setting_app_subtitle: Sous-titre de l'application
@@ -154,6 +155,7 setting_issues_export_limit: Limite export demandes
154 setting_mail_from: Adresse d'émission
155 setting_mail_from: Adresse d'émission
155 setting_host_name: Nom d'hôte
156 setting_host_name: Nom d'hôte
156 setting_text_formatting: Formatage du texte
157 setting_text_formatting: Formatage du texte
158 setting_wiki_compression: Compression historique wiki
157
159
158 label_user: Utilisateur
160 label_user: Utilisateur
159 label_user_plural: Utilisateurs
161 label_user_plural: Utilisateurs
@@ -322,6 +324,10 label_search: Recherche
322 label_result: %d résultat
324 label_result: %d résultat
323 label_result_plural: %d résultats
325 label_result_plural: %d résultats
324 label_all_words: Tous les mots
326 label_all_words: Tous les mots
327 label_wiki: Wiki
328 label_page_index: Index
329 label_current_version: Version actuelle
330 label_preview: Prévisualisation
325
331
326 button_login: Connexion
332 button_login: Connexion
327 button_submit: Soumettre
333 button_submit: Soumettre
@@ -143,6 +143,7 field_auth_source: 認証モード
143 field_hide_mail: Emailアドレスを隠す
143 field_hide_mail: Emailアドレスを隠す
144 field_comment: コメント
144 field_comment: コメント
145 field_url: URL
145 field_url: URL
146 field_start_page: メインページ
146
147
147 setting_app_title: アプリケーションのタイトル
148 setting_app_title: アプリケーションのタイトル
148 setting_app_subtitle: アプリケーションのサブタイトル
149 setting_app_subtitle: アプリケーションのサブタイトル
@@ -155,6 +156,7 setting_issues_export_limit: 出力する問題数の上限
155 setting_mail_from: Emission メールアドレス
156 setting_mail_from: Emission メールアドレス
156 setting_host_name: ホスト名
157 setting_host_name: ホスト名
157 setting_text_formatting: テキストの書式
158 setting_text_formatting: テキストの書式
159 setting_wiki_compression: Wiki history compression
158
160
159 label_user: ユーザ
161 label_user: ユーザ
160 label_user_plural: ユーザ
162 label_user_plural: ユーザ
@@ -323,6 +325,10 label_search: 検索
323 label_result: %d 件の結果
325 label_result: %d 件の結果
324 label_result_plural: %d 件の結果
326 label_result_plural: %d 件の結果
325 label_all_words: すべての単語
327 label_all_words: すべての単語
328 label_wiki: Wiki
329 label_page_index: 索引
330 label_current_version: 最近版
331 label_preview: 下検分
326
332
327 button_login: ログイン
333 button_login: ログイン
328 button_submit: 変更
334 button_submit: 変更
@@ -139,6 +139,8 vertical-align: middle;
139 .icon-cancel { background-image: url(../images/cancel.png); }
139 .icon-cancel { background-image: url(../images/cancel.png); }
140 .icon-pdf { background-image: url(../images/pdf.png); }
140 .icon-pdf { background-image: url(../images/pdf.png); }
141 .icon-csv { background-image: url(../images/csv.png); }
141 .icon-csv { background-image: url(../images/csv.png); }
142 .icon-html { background-image: url(../images/html.png); }
143 .icon-txt { background-image: url(../images/txt.png); }
142 .icon-file { background-image: url(../images/file.png); }
144 .icon-file { background-image: url(../images/file.png); }
143 .icon-folder { background-image: url(../images/folder.png); }
145 .icon-folder { background-image: url(../images/folder.png); }
144 .icon-package { background-image: url(../images/package.png); }
146 .icon-package { background-image: url(../images/package.png); }
@@ -150,6 +152,8 vertical-align: middle;
150 .icon-logout { background-image: url(../images/logout.png); }
152 .icon-logout { background-image: url(../images/logout.png); }
151 .icon-help { background-image: url(../images/help.png); }
153 .icon-help { background-image: url(../images/help.png); }
152 .icon-attachment { background-image: url(../images/attachment.png); }
154 .icon-attachment { background-image: url(../images/attachment.png); }
155 .icon-index { background-image: url(../images/index.png); }
156 .icon-history { background-image: url(../images/history.png); }
153
157
154 .icon22-projects { background-image: url(../images/22x22/projects.png); }
158 .icon22-projects { background-image: url(../images/22x22/projects.png); }
155 .icon22-users { background-image: url(../images/22x22/users.png); }
159 .icon22-users { background-image: url(../images/22x22/users.png); }
@@ -181,7 +185,7 border-left: 1px dashed #c0c0c0;
181
185
182 }
186 }
183
187
184 #content h2{
188 #content h2, #content div.wiki h1 {
185 display:block;
189 display:block;
186 margin:0 0 16px 0;
190 margin:0 0 16px 0;
187 font-size:1.7em;
191 font-size:1.7em;
@@ -576,4 +580,23 to account for 3 pixel bug: http://www.positioniseverything.net/explorer/threepx
576
580
577 * html .threepxfix{
581 * html .threepxfix{
578 margin-left: 3px;
582 margin-left: 3px;
579 } No newline at end of file
583 }
584
585 /***** Wiki sections ****/
586 #content div.wiki { font-size: 110%}
587
588 #content div.wiki h2, div.wiki h3 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; color:#606060; }
589 #content div.wiki h2 { font-size: 1.4em;}
590 #content div.wiki h3 { font-size: 1.2em;}
591
592 div.wiki table {
593 border: 1px solid #505050;
594 border-collapse: collapse;
595 }
596
597 div.wiki table, div.wiki td {
598 border: 1px solid #bbb;
599 padding: 4px;
600 }
601
602 #preview .preview { background: #fafbfc url(../images/draft.png); }
@@ -449,6 +449,15 permissions_005:
449 mail_option: false
449 mail_option: false
450 sort: 151
450 sort: 151
451 is_public: false
451 is_public: false
452 permissions_061:
453 action: search
454 id: 62
455 description: label_search
456 controller: projects
457 mail_enabled: false
458 mail_option: false
459 sort: 130
460 is_public: true
452 permissions_050:
461 permissions_050:
453 action: history
462 action: history
454 id: 50
463 id: 50
@@ -1,379 +1,163
1 ---
1 ---
2 permissions_roles_075:
2 permissions_roles_054:
3 role_id: 3
4 permission_id: 34
5 permissions_roles_047:
6 role_id: 1
7 permission_id: 15
8 permissions_roles_102:
9 role_id: 2
10 permission_id: 4
11 permissions_roles_019:
12 role_id: 3
3 role_id: 3
13 permission_id: 30
4 permission_id: 44
14 permissions_roles_048:
5 permissions_roles_043:
15 role_id: 2
16 permission_id: 24
17 permissions_roles_103:
18 role_id: 2
19 permission_id: 27
20 permissions_roles_076:
21 role_id: 2
6 role_id: 2
22 permission_id: 41
7 permission_id: 25
23 permissions_roles_049:
8 permissions_roles_032:
24 role_id: 1
9 role_id: 1
25 permission_id: 3
10 permission_id: 42
26 permissions_roles_104:
27 role_id: 2
28 permission_id: 36
29 permissions_roles_077:
30 role_id: 2
31 permission_id: 7
32 permissions_roles_105:
33 role_id: 2
34 permission_id: 32
35 permissions_roles_078:
36 role_id: 3
37 permission_id: 38
38 permissions_roles_106:
39 role_id: 2
40 permission_id: 14
41 permissions_roles_020:
42 role_id: 2
43 permission_id: 9
44 permissions_roles_079:
45 role_id: 2
46 permission_id: 18
47 permissions_roles_107:
48 role_id: 3
49 permission_id: 40
50 permissions_roles_021:
11 permissions_roles_021:
51 role_id: 1
12 role_id: 1
52 permission_id: 13
13 permission_id: 22
53 permissions_roles_108:
14 permissions_roles_010:
54 role_id: 1
15 role_id: 1
55 permission_id: 29
56 permissions_roles_050:
57 role_id: 2
58 permission_id: 29
59 permissions_roles_022:
60 role_id: 3
61 permission_id: 4
16 permission_id: 4
62 permissions_roles_109:
17 permissions_roles_044:
63 role_id: 3
18 role_id: 3
64 permission_id: 22
19 permission_id: 22
65 permissions_roles_051:
20 permissions_roles_033:
66 role_id: 3
67 permission_id: 37
68 permissions_roles_023:
69 role_id: 1
70 permission_id: 23
71 permissions_roles_052:
72 role_id: 2
73 permission_id: 33
74 permissions_roles_024:
75 role_id: 1
76 permission_id: 1
77 permissions_roles_080:
78 role_id: 2
79 permission_id: 13
80 permissions_roles_053:
81 role_id: 2
82 permission_id: 1
83 permissions_roles_025:
84 role_id: 2
85 permission_id: 10
86 permissions_roles_081:
87 role_id: 3
88 permission_id: 20
89 permissions_roles_054:
90 role_id: 2
21 role_id: 2
91 permission_id: 12
22 permission_id: 22
92 permissions_roles_026:
23 permissions_roles_022:
93 role_id: 1
94 permission_id: 36
95 permissions_roles_082:
96 role_id: 1
97 permission_id: 39
98 permissions_roles_110:
99 role_id: 3
100 permission_id: 6
101 permissions_roles_027:
102 role_id: 3
103 permission_id: 31
104 permissions_roles_083:
105 role_id: 1
106 permission_id: 33
107 permissions_roles_055:
108 role_id: 1
24 role_id: 1
109 permission_id: 38
25 permission_id: 38
110 permissions_roles_111:
26 permissions_roles_011:
111 role_id: 3
112 permission_id: 1
113 permissions_roles_028:
114 role_id: 1
115 permission_id: 24
116 permissions_roles_084:
117 role_id: 3
118 permission_id: 16
119 permissions_roles_056:
120 role_id: 2
121 permission_id: 5
122 permissions_roles_029:
123 role_id: 1
124 permission_id: 9
125 permissions_roles_085:
126 role_id: 3
127 permission_id: 27
128 permissions_roles_057:
129 role_id: 1
130 permission_id: 16
131 permissions_roles_112:
132 role_id: 1
27 role_id: 1
133 permission_id: 20
28 permission_id: 20
134 permissions_roles_086:
29 permissions_roles_045:
135 role_id: 3
136 permission_id: 12
137 permissions_roles_058:
138 role_id: 1
139 permission_id: 26
140 permissions_roles_113:
141 role_id: 2
142 permission_id: 37
143 permissions_roles_087:
144 role_id: 1
30 role_id: 1
145 permission_id: 5
31 permission_id: 12
146 permissions_roles_059:
32 permissions_roles_034:
147 role_id: 3
148 permission_id: 18
149 permissions_roles_114:
150 role_id: 2
33 role_id: 2
151 permission_id: 20
34 permission_id: 44
152 permissions_roles_115:
35 permissions_roles_023:
153 role_id: 2
36 role_id: 2
154 permission_id: 15
37 permission_id: 15
155 permissions_roles_088:
38 permissions_roles_012:
156 role_id: 2
39 role_id: 1
157 permission_id: 3
40 permission_id: 36
158 permissions_roles_001:
41 permissions_roles_001:
159 role_id: 2
160 permission_id: 21
161 permissions_roles_116:
162 role_id: 3
163 permission_id: 23
164 permissions_roles_030:
165 role_id: 1
42 role_id: 1
166 permission_id: 30
43 permission_id: 14
167 permissions_roles_089:
44 permissions_roles_046:
168 role_id: 1
45 role_id: 1
169 permission_id: 28
170 permissions_roles_002:
171 role_id: 3
172 permission_id: 29
46 permission_id: 29
173 permissions_roles_117:
47 permissions_roles_035:
174 role_id: 3
175 permission_id: 28
176 permissions_roles_031:
177 role_id: 2
178 permission_id: 38
179 permissions_roles_003:
180 role_id: 3
181 permission_id: 41
182 permissions_roles_118:
183 role_id: 1
48 role_id: 1
184 permission_id: 34
49 permission_id: 10
185 permissions_roles_032:
50 permissions_roles_024:
186 role_id: 3
187 permission_id: 9
188 permissions_roles_004:
189 role_id: 2
51 role_id: 2
190 permission_id: 8
52 permission_id: 42
191 permissions_roles_060:
53 permissions_roles_013:
192 role_id: 2
54 role_id: 2
193 permission_id: 2
55 permission_id: 13
194 permissions_roles_119:
56 permissions_roles_002:
195 role_id: 1
57 role_id: 1
196 permission_id: 21
58 permission_id: 34
197 permissions_roles_033:
59 permissions_roles_047:
198 role_id: 2
199 permission_id: 28
200 permissions_roles_005:
201 role_id: 3
202 permission_id: 3
203 permissions_roles_061:
204 role_id: 2
205 permission_id: 40
206 permissions_roles_006:
207 role_id: 3
208 permission_id: 14
209 permissions_roles_090:
210 role_id: 2
60 role_id: 2
211 permission_id: 26
61 permission_id: 4
212 permissions_roles_062:
62 permissions_roles_036:
213 role_id: 1
63 role_id: 1
214 permission_id: 19
64 permission_id: 25
215 permissions_roles_034:
65 permissions_roles_025:
216 role_id: 2
217 permission_id: 11
218 permissions_roles_007:
219 role_id: 1
66 role_id: 1
220 permission_id: 35
67 permission_id: 8
221 permissions_roles_091:
68 permissions_roles_014:
222 role_id: 3
223 permission_id: 35
224 permissions_roles_063:
225 role_id: 2
226 permission_id: 30
227 permissions_roles_035:
228 role_id: 2
229 permission_id: 23
230 permissions_roles_008:
231 role_id: 2
69 role_id: 2
232 permission_id: 17
70 permission_id: 38
233 permissions_roles_092:
71 permissions_roles_003:
234 role_id: 2
72 role_id: 2
235 permission_id: 31
73 permission_id: 11
236 permissions_roles_064:
74 permissions_roles_048:
237 role_id: 3
238 permission_id: 33
239 permissions_roles_036:
240 role_id: 3
241 permission_id: 5
242 permissions_roles_120:
243 role_id: 3
244 permission_id: 13
245 permissions_roles_009:
246 role_id: 1
247 permission_id: 12
248 permissions_roles_093:
249 role_id: 2
75 role_id: 2
250 permission_id: 42
76 permission_id: 34
251 permissions_roles_065:
252 role_id: 3
253 permission_id: 26
254 permissions_roles_037:
77 permissions_roles_037:
255 role_id: 1
78 role_id: 1
256 permission_id: 42
79 permission_id: 43
257 permissions_roles_121:
80 permissions_roles_026:
258 role_id: 3
259 permission_id: 2
260 permissions_roles_094:
261 role_id: 3
262 permission_id: 39
263 permissions_roles_066:
264 role_id: 2
265 permission_id: 6
266 permissions_roles_038:
267 role_id: 1
81 role_id: 1
268 permission_id: 25
82 permission_id: 23
269 permissions_roles_122:
83 permissions_roles_015:
270 role_id: 1
84 role_id: 1
271 permission_id: 7
85 permission_id: 5
272 permissions_roles_095:
86 permissions_roles_004:
273 role_id: 2
87 role_id: 2
274 permission_id: 19
275 permissions_roles_067:
276 role_id: 1
277 permission_id: 17
278 permissions_roles_039:
279 role_id: 3
280 permission_id: 36
88 permission_id: 36
281 permissions_roles_123:
89 permissions_roles_049:
282 role_id: 3
90 role_id: 3
283 permission_id: 24
91 permission_id: 24
284 permissions_roles_096:
92 permissions_roles_038:
285 role_id: 1
93 role_id: 2
286 permission_id: 18
94 permission_id: 24
287 permissions_roles_068:
95 permissions_roles_027:
288 role_id: 1
289 permission_id: 32
290 permissions_roles_124:
291 role_id: 1
96 role_id: 1
292 permission_id: 11
97 permission_id: 41
293 permissions_roles_010:
98 permissions_roles_016:
294 role_id: 1
99 role_id: 1
295 permission_id: 8
100 permission_id: 21
296 permissions_roles_069:
101 permissions_roles_005:
297 role_id: 3
298 permission_id: 19
299 permissions_roles_097:
300 role_id: 2
301 permission_id: 35
302 permissions_roles_125:
303 role_id: 2
304 permission_id: 16
305 permissions_roles_011:
306 role_id: 3
307 permission_id: 42
308 permissions_roles_098:
309 role_id: 1
102 role_id: 1
310 permission_id: 6
103 permission_id: 53
311 permissions_roles_126:
104 permissions_roles_050:
312 role_id: 3
313 permission_id: 7
314 permissions_roles_012:
315 role_id: 3
316 permission_id: 8
317 permissions_roles_040:
318 role_id: 1
105 role_id: 1
319 permission_id: 2
106 permission_id: 13
320 permissions_roles_099:
107 permissions_roles_039:
321 role_id: 3
108 role_id: 3
322 permission_id: 17
109 permission_id: 20
323 permissions_roles_041:
110 permissions_roles_028:
324 role_id: 2
111 role_id: 2
325 permission_id: 39
112 permission_id: 20
326 permissions_roles_013:
113 permissions_roles_017:
327 role_id: 1
328 permission_id: 40
329 permissions_roles_070:
330 role_id: 3
331 permission_id: 11
332 permissions_roles_042:
333 role_id: 1
114 role_id: 1
334 permission_id: 37
115 permission_id: 37
335 permissions_roles_014:
116 permissions_roles_006:
336 role_id: 1
337 permission_id: 22
338 permissions_roles_071:
339 role_id: 1
117 role_id: 1
340 permission_id: 4
118 permission_id: 15
341 permissions_roles_043:
119 permissions_roles_051:
342 role_id: 3
343 permission_id: 32
344 permissions_roles_015:
345 role_id: 2
346 permission_id: 22
347 permissions_roles_072:
348 role_id: 1
120 role_id: 1
349 permission_id: 27
121 permission_id: 30
350 permissions_roles_044:
122 permissions_roles_040:
351 role_id: 1
123 role_id: 1
124 permission_id: 11
125 permissions_roles_029:
126 role_id: 2
127 permission_id: 43
128 permissions_roles_018:
129 role_id: 2
352 permission_id: 14
130 permission_id: 14
353 permissions_roles_016:
131 permissions_roles_007:
354 role_id: 3
132 role_id: 1
355 permission_id: 15
133 permission_id: 35
356 permissions_roles_073:
134 permissions_roles_052:
357 role_id: 2
135 role_id: 2
358 permission_id: 34
359 permissions_roles_045:
360 role_id: 3
361 permission_id: 10
136 permission_id: 10
362 permissions_roles_100:
137 permissions_roles_041:
363 role_id: 1
138 role_id: 1
364 permission_id: 10
139 permission_id: 28
365 permissions_roles_017:
140 permissions_roles_030:
366 role_id: 3
141 role_id: 1
367 permission_id: 25
142 permission_id: 9
368 permissions_roles_074:
143 permissions_roles_019:
369 role_id: 2
144 role_id: 2
370 permission_id: 25
145 permission_id: 41
371 permissions_roles_046:
146 permissions_roles_008:
147 role_id: 2
148 permission_id: 12
149 permissions_roles_053:
150 role_id: 2
151 permission_id: 35
152 permissions_roles_042:
372 role_id: 1
153 role_id: 1
373 permission_id: 31
154 permission_id: 44
374 permissions_roles_101:
155 permissions_roles_031:
375 role_id: 3
376 permission_id: 21
377 permissions_roles_018:
378 role_id: 1
156 role_id: 1
379 permission_id: 41
157 permission_id: 24
158 permissions_roles_020:
159 role_id: 1
160 permission_id: 7
161 permissions_roles_009:
162 role_id: 2
163 permission_id: 37
@@ -24,7 +24,7 class MailerTest < Test::Unit::TestCase
24 def test_issue_add
24 def test_issue_add
25 issue = Issue.find(1)
25 issue = Issue.find(1)
26 GLoc.valid_languages.each do |lang|
26 GLoc.valid_languages.each do |lang|
27 Setting.default_language = lang
27 Setting.default_language = lang.to_s
28 assert Mailer.deliver_issue_add(issue)
28 assert Mailer.deliver_issue_add(issue)
29 end
29 end
30 end
30 end
@@ -32,7 +32,7 class MailerTest < Test::Unit::TestCase
32 def test_issue_edit
32 def test_issue_edit
33 journal = Journal.find(1)
33 journal = Journal.find(1)
34 GLoc.valid_languages.each do |lang|
34 GLoc.valid_languages.each do |lang|
35 Setting.default_language = lang
35 Setting.default_language = lang.to_s
36 assert Mailer.deliver_issue_edit(journal)
36 assert Mailer.deliver_issue_edit(journal)
37 end
37 end
38 end
38 end
@@ -40,7 +40,7 class MailerTest < Test::Unit::TestCase
40 def test_document_add
40 def test_document_add
41 document = Document.find(1)
41 document = Document.find(1)
42 GLoc.valid_languages.each do |lang|
42 GLoc.valid_languages.each do |lang|
43 Setting.default_language = lang
43 Setting.default_language = lang.to_s
44 assert Mailer.deliver_document_add(document)
44 assert Mailer.deliver_document_add(document)
45 end
45 end
46 end
46 end
@@ -48,7 +48,7 class MailerTest < Test::Unit::TestCase
48 def test_lost_password
48 def test_lost_password
49 token = Token.find(2)
49 token = Token.find(2)
50 GLoc.valid_languages.each do |lang|
50 GLoc.valid_languages.each do |lang|
51 token.user.update_attribute :language, lang
51 token.user.update_attribute :language, lang.to_s
52 assert Mailer.deliver_lost_password(token)
52 assert Mailer.deliver_lost_password(token)
53 end
53 end
54 end
54 end
@@ -56,7 +56,7 class MailerTest < Test::Unit::TestCase
56 def test_register
56 def test_register
57 token = Token.find(1)
57 token = Token.find(1)
58 GLoc.valid_languages.each do |lang|
58 GLoc.valid_languages.each do |lang|
59 token.user.update_attribute :language, lang
59 token.user.update_attribute :language, lang.to_s
60 assert Mailer.deliver_register(token)
60 assert Mailer.deliver_register(token)
61 end
61 end
62 end
62 end
General Comments 0
You need to be logged in to leave comments. Login now