Merge branch 'journals' into 1.0-stable

Conflicts:
	app/controllers/issues_controller.rb
	app/controllers/journals_controller.rb
	app/helpers/journals_helper.rb
	app/views/issues/_history.rhtml
	lib/redmine/activity/fetcher.rb
	test/functional/issues_controller_test.rb
	test/test_helper.rb
pull/351/head
Tim Felgentreff 14 years ago
commit 4b098d7774
  1. 3
      .gitmodules
  2. 58
      app/controllers/issues_controller.rb
  3. 73
      app/controllers/journals_controller.rb
  4. 9
      app/controllers/wiki_controller.rb
  5. 76
      app/helpers/issues_helper.rb
  6. 42
      app/helpers/journals_helper.rb
  7. 49
      app/models/attachment.rb
  8. 18
      app/models/changeset.rb
  9. 15
      app/models/document.rb
  10. 97
      app/models/issue.rb
  11. 68
      app/models/journal.rb
  12. 25
      app/models/journal_detail.rb
  13. 22
      app/models/journal_observer.rb
  14. 60
      app/models/mail_handler.rb
  15. 5
      app/models/mailer.rb
  16. 23
      app/models/message.rb
  17. 6
      app/models/news.rb
  18. 14
      app/models/time_entry.rb
  19. 3
      app/models/version.rb
  20. 95
      app/models/wiki_content.rb
  21. 18
      app/views/issues/_history.rhtml
  22. 8
      app/views/journals/_notes_form.rhtml
  23. 3
      app/views/journals/edit.rjs
  24. 10
      app/views/journals/update.rjs
  25. 2
      app/views/mailer/issue_edit.text.html.rhtml
  26. 2
      app/views/mailer/issue_edit.text.plain.rhtml
  27. 2
      app/views/wiki/edit.rhtml
  28. 6
      app/views/wiki/history.rhtml
  29. 4
      config/environment.rb
  30. 2
      db/migrate/041_rename_comment_to_comments.rb
  31. 12
      extra/sample_plugin/app/models/meeting.rb
  32. 4
      lib/redmine.rb
  33. 44
      lib/redmine/activity/fetcher.rb
  34. 2
      lib/redmine/export/pdf.rb
  35. 2
      lib/redmine/wiki_formatting/macros.rb
  36. 22
      test/fixtures/journal_details.yml
  37. 220
      test/fixtures/journals.yml
  38. 56
      test/fixtures/wiki_content_versions.yml
  39. 51
      test/fixtures/wiki_contents.yml
  40. 512
      test/functional/issues_controller_test.rb
  41. 3
      test/functional/issues_controller_transaction_test.rb
  42. 4
      test/functional/journals_controller_test.rb
  43. 2
      test/functional/projects_controller_test.rb
  44. 72
      test/functional/wiki_controller_test.rb
  45. 1
      test/integration/issues_api_test.rb
  46. 2
      test/integration/projects_api_test.rb
  47. 34
      test/test_helper.rb
  48. 30
      test/unit/activity_test.rb
  49. 175
      test/unit/issue_test.rb
  50. 14
      test/unit/journal_test.rb
  51. 2
      test/unit/mail_handler_test.rb
  52. 6
      test/unit/mailer_test.rb
  53. 1
      test/unit/search_test.rb
  54. 28
      test/unit/wiki_content_test.rb
  55. 41
      test/unit/wiki_page_test.rb
  56. 20
      test/unit/wiki_test.rb
  57. 10
      vendor/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb
  58. 1
      vendor/plugins/acts_as_journalized
  59. 74
      vendor/plugins/acts_as_versioned/CHANGELOG
  60. 20
      vendor/plugins/acts_as_versioned/MIT-LICENSE
  61. 28
      vendor/plugins/acts_as_versioned/README
  62. 41
      vendor/plugins/acts_as_versioned/RUNNING_UNIT_TESTS
  63. 182
      vendor/plugins/acts_as_versioned/Rakefile
  64. 1
      vendor/plugins/acts_as_versioned/init.rb
  65. 564
      vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb
  66. 41
      vendor/plugins/acts_as_versioned/test/abstract_unit.rb
  67. 18
      vendor/plugins/acts_as_versioned/test/database.yml
  68. 6
      vendor/plugins/acts_as_versioned/test/fixtures/authors.yml
  69. 3
      vendor/plugins/acts_as_versioned/test/fixtures/landmark.rb
  70. 7
      vendor/plugins/acts_as_versioned/test/fixtures/landmark_versions.yml
  71. 6
      vendor/plugins/acts_as_versioned/test/fixtures/landmarks.yml
  72. 10
      vendor/plugins/acts_as_versioned/test/fixtures/locked_pages.yml
  73. 27
      vendor/plugins/acts_as_versioned/test/fixtures/locked_pages_revisions.yml
  74. 13
      vendor/plugins/acts_as_versioned/test/fixtures/migrations/1_add_versioned_tables.rb
  75. 43
      vendor/plugins/acts_as_versioned/test/fixtures/page.rb
  76. 16
      vendor/plugins/acts_as_versioned/test/fixtures/page_versions.yml
  77. 7
      vendor/plugins/acts_as_versioned/test/fixtures/pages.yml
  78. 6
      vendor/plugins/acts_as_versioned/test/fixtures/widget.rb
  79. 46
      vendor/plugins/acts_as_versioned/test/migration_test.rb
  80. 68
      vendor/plugins/acts_as_versioned/test/schema.rb
  81. 347
      vendor/plugins/acts_as_versioned/test/versioned_test.rb

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "vendor/plugins/acts_as_journalized"]
path = vendor/plugins/acts_as_journalized
url = git://github.com/finnlabs/acts_as_journalized

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -18,21 +18,23 @@
class IssuesController < ApplicationController
menu_item :new_issue, :only => [:new, :create]
default_search_scope :issues
before_filter :find_issue, :only => [:show, :edit, :update]
before_filter :find_issues, :only => [:bulk_edit, :move, :perform_move, :destroy]
before_filter :find_project, :only => [:new, :create]
before_filter :authorize, :except => [:index, :changes]
before_filter :find_optional_project, :only => [:index, :changes]
before_filter :check_for_default_issue_status, :only => [:new, :create]
before_filter :build_new_issue_from_params, :only => [:new, :create]
accept_key_auth :index, :show, :changes
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
include JournalsHelper
helper :projects
include ProjectsHelper
include ProjectsHelper
helper :custom_fields
include CustomFieldsHelper
helper :issue_relations
@ -55,12 +57,12 @@ class IssuesController < ApplicationController
verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
def index
retrieve_query
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
sort_update(@query.sortable_columns)
if @query.valid?
limit = case params[:format]
when 'csv', 'pdf'
@ -70,15 +72,15 @@ class IssuesController < ApplicationController
else
per_page_option
end
@issue_count = @query.issue_count
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
:order => sort_clause,
:offset => @issue_pages.current.offset,
:order => sort_clause,
:offset => @issue_pages.current.offset,
:limit => limit)
@issue_count_by_group = @query.issue_count_by_group
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.xml { render :layout => false }
@ -94,14 +96,14 @@ class IssuesController < ApplicationController
rescue ActiveRecord::RecordNotFound
render_404
end
def changes
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
@ -109,10 +111,9 @@ class IssuesController < ApplicationController
rescue ActiveRecord::RecordNotFound
render_404
end
def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals = @issue.journals.find(:all, :include => [:user], :order => "#{Journal.table_name}.created_at ASC")
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
@ -162,12 +163,13 @@ class IssuesController < ApplicationController
end
end
end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
def edit
return render_reply(@journal) if @journal
update_issue_from_params
@journal = @issue.current_journal
@ -183,7 +185,7 @@ class IssuesController < ApplicationController
if @issue.save_issue_with_child_records(params, @time_entry)
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
respond_to do |format|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
@ -192,7 +194,7 @@ class IssuesController < ApplicationController
end
else
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
@journal = @issue.current_journal
respond_to do |format|
@ -210,7 +212,7 @@ class IssuesController < ApplicationController
attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
unsaved_issue_ids = []
@issues.each do |issue|
issue.reload
@ -229,7 +231,7 @@ class IssuesController < ApplicationController
@available_statuses = Workflow.available_statuses(@project)
@custom_fields = @project.all_issue_custom_fields
end
def destroy
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
if @hours > 0
@ -268,14 +270,14 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
@project = Project.find(project_id)
rescue ActiveRecord::RecordNotFound
render_404
end
# Used by #edit and #update to set some common instance variables
# from the params
# TODO: Refactor, not everything in here is needed by #edit
@ -284,7 +286,7 @@ private
@priorities = IssuePriority.all
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
@time_entry = TimeEntry.new
@notes = params[:notes]
@issue.init_journal(User.current, @notes)
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
@ -294,7 +296,7 @@ private
attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
@issue.safe_attributes = attrs
end
@journal = @issue.current_journal
end
# TODO: Refactor, lots of extra code in here
@ -306,7 +308,7 @@ private
else
@issue = @project.issues.visible.find(params[:id])
end
@issue.project = @project
# Tracker must be set before custom field values
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)

@ -1,73 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit]
before_filter :find_issue, :only => [:new]
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
def edit
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
(render_403; return false) unless @journal.editable_by?(User.current)
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end

@ -73,7 +73,7 @@ class WikiController < ApplicationController
@content.comments = nil
if request.get?
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
@content.lock_version = @page.content.lock_version
else
if !@page.new_record? && @content.text == params[:content][:text]
attachments = Attachment.attach_files(@page, params[:attachments])
@ -84,6 +84,7 @@ class WikiController < ApplicationController
end
#@content.text = params[:content][:text]
#@content.comments = params[:content][:comments]
@content.init_journal(User.current, params[:content][:comments])
@content.attributes = params[:content]
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
@ -120,9 +121,9 @@ class WikiController < ApplicationController
def history
@version_count = @page.content.versions.count
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comments, updated_on, version",
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, user_id, notes, created_at, version",
:order => 'version DESC',
:limit => @version_pages.items_per_page + 1,
:offset => @version_pages.current.offset

@ -102,82 +102,6 @@ module IssuesHelper
@sidebar_queries
end
def show_detail(detail, no_html=false)
case detail.property
when 'attr'
field = detail.prop_key.to_s.gsub(/\_id$/, "")
label = l(("field_" + field).to_sym)
case
when ['due_date', 'start_date'].include?(detail.prop_key)
value = format_date(detail.value.to_date) if detail.value
old_value = format_date(detail.old_value.to_date) if detail.old_value
when ['project_id', 'status_id', 'tracker_id', 'assigned_to_id', 'priority_id', 'category_id', 'fixed_version_id'].include?(detail.prop_key)
value = find_name_by_reflection(field, detail.value)
old_value = find_name_by_reflection(field, detail.old_value)
when detail.prop_key == 'estimated_hours'
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
when detail.prop_key == 'parent_id'
label = l(:field_parent_issue)
value = "##{detail.value}" unless detail.value.blank?
old_value = "##{detail.old_value}" unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
if custom_field
label = custom_field.name
value = format_value(detail.value, custom_field.field_format) if detail.value
old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
end
when 'attachment'
label = l(:label_attachment)
end
call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value })
label ||= detail.prop_key
value ||= detail.value
old_value ||= detail.old_value
unless no_html
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if detail.old_value
old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
# Link to the attachment if it has not been removed
value = link_to_attachment(a)
else
value = content_tag("i", h(value)) if value
end
end
if !detail.value.blank?
case detail.property
when 'attr', 'cf'
if !detail.old_value.blank?
l(:text_journal_changed, :label => label, :old => old_value, :new => value)
else
l(:text_journal_set_to, :label => label, :value => value)
end
when 'attachment'
l(:text_journal_added, :label => label, :value => value)
end
else
l(:text_journal_deleted, :label => label, :old => old_value)
end
end
# Find the name of an associated record stored in the field attribute
def find_name_by_reflection(field, id)
association = Issue.reflect_on_association(field.to_sym)
if association
record = association.class_name.constantize.find_by_id(id)
return record.name if record
end
end
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
decimal_separator = l(:general_csv_decimal_separator)

@ -1,42 +0,0 @@
# redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module JournalsHelper
def render_notes(issue, journal, options={})
content = ''
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
links = []
if !journal.notes.blank?
links << link_to_remote(image_tag('comment.png'),
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} },
:title => l(:button_quote)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
:title => l(:button_edit)) if editable
end
content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
css_classes = "wiki"
css_classes << " editable" if editable
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => css_classes)
end
def link_to_in_place_notes_editor(text, field_id, url, options={})
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
link_to text, '#', options.merge(:onclick => onclick)
end
end

@ -19,32 +19,47 @@ require "digest/md5"
class Attachment < ActiveRecord::Base
belongs_to :container, :polymorphic => true
# FIXME: Remove these once the Versions, Documents and Projects themselves can provide file events
belongs_to :version, :foreign_key => "container_id"
belongs_to :document, :foreign_key => "container_id"
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
validates_presence_of :container, :filename, :author
validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
acts_as_journalized :event_title => :filename,
:event_url => (Proc.new do |o|
{ :controller => 'attachments', :action => 'download',
:id => o.versioned_id, :filename => o.filename }
end),
:activity_type => 'files',
:activity_permission => :view_files,
:activity_find_options => { :include => { :version => :project } }
acts_as_activity_provider :type => 'files',
:permission => :view_files,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity :type => 'documents', :permission => :view_documents,
:find_options => { :include => { :document => :project } }
# This method is called on save by the AttachmentJournal in order to
# decide which kind of activity we are dealing with. When that activity
# is retrieved later, we don't need to check the container_type in
# SQL anymore as that will be just the one we have specified here.
def activity_type
case container_type
when "Document"
"documents"
when "Version"
"files"
else
super
end
end
cattr_accessor :storage_path
@@storage_path = "#{RAILS_ROOT}/files"
def validate
if self.filesize > Setting.attachment_max_size.to_i.kilobytes
errors.add(:base, :too_long, :count => Setting.attachment_max_size.to_i.kilobytes)

@ -23,20 +23,18 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments,
:datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:event_description => :long_comments,
:event_datetime => :committed_on,
:event_url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}},
:activity_timestamp => "#{table_name}.committed_on",
:activity_find_options => {:include => [:user, {:repository => :project}]}
acts_as_searchable :columns => 'comments',
:include => {:repository => :project},
:project_key => "#{Repository.table_name}.project_id",
:date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true

@ -20,15 +20,18 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents
acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:event_url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.versioned_id}},
:event_author => (Proc.new do |o|
o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC").try(:author)
end)
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => :project}
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_documents, project)
end

@ -25,10 +25,9 @@ class Issue < ActiveRecord::Base
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
@ -36,21 +35,23 @@ class Issue < ActiveRecord::Base
acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
acts_as_journalized :event_title => Proc.new {|o| "#{o.tracker.name} ##{o.versioned.id} (#{o.status}): #{o.subject}"},
:event_type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
register_on_journal_formatter(:id, 'parent_id')
register_on_journal_formatter(:named_association, 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
'priority_id', 'category_id', 'fixed_version_id')
register_on_journal_formatter(:fraction, 'estimated_hours', 'done_ratio')
register_on_journal_formatter(:datetime, 'due_date', 'start_date')
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
:include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
:order_column => "#{table_name}.id"
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_reader :current_journal
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
@ -69,7 +70,7 @@ class Issue < ActiveRecord::Base
before_create :default_assign
before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
after_save :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_save :update_nested_set_attributes, :update_parent_attributes
after_destroy :destroy_children
after_destroy :update_parent_attributes
@ -312,18 +313,7 @@ class Issue < ActiveRecord::Base
self.done_ratio = status.default_done_ratio
end
end
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
# Make sure updated_on is updated when adding a note.
updated_on_will_change!
@current_journal
end
# Return true if the issue is closed, otherwise false
def closed?
self.status.is_closed?
@ -498,13 +488,12 @@ class Issue < ActiveRecord::Base
if valid?
attachments = Attachment.attach_files(self, params[:attachments])
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
begin
if save
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
else
raise ActiveRecord::Rollback
end
@ -727,22 +716,12 @@ class Issue < ActiveRecord::Base
).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
end
end
end
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
# Default assignment based on category
def default_assign
if assigned_to.nil? && category && category.assigned_to
@ -767,40 +746,14 @@ class Issue < ActiveRecord::Base
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
if @current_journal
duplicate.init_journal(@current_journal.user, @current_journal.notes)
end
# Implicitely creates a new journal
duplicate.update_attribute :status, self.status
# Same user and notes
duplicate.versions.last.user = current_journal.user
duplicate.versions.last.notes = current_journal.notes
end
end
end
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id description root_id lft rgt lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
# reset current journal
init_journal @current_journal.user, @current_journal.notes
end
end
# Query generator for selecting groups of issue counts for a project
# based on specific criteria
@ -828,6 +781,14 @@ class Issue < ActiveRecord::Base
and i.project_id=#{project.id}
group by s.id, s.is_closed, j.id")
end
IssueJournal.class_eval do
# Shortcut
def new_status
if details.keys.include? 'status_id'
(newval = details['status_id'].last) ? IssueStatus.find_by_id(newval.to_i) : nil
end
end
end
end

@ -1,68 +0,0 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Journal < ActiveRecord::Base
belongs_to :journalized, :polymorphic => true
# added as a quick fix to allow eager loading of the polymorphic association
# since always associated to an issue, for now
belongs_to :issue, :foreign_key => :journalized_id
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
def save(*args)
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
# Returns the new status if the journal contains a status change, otherwise nil
def new_status
c = details.detect {|detail| detail.prop_key == 'status_id'}
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
end
def new_value_for(prop)
c = details.detect {|detail| detail.prop_key == prop}
c ? c.value : nil
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
end

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

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

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -20,25 +20,25 @@ class MailHandler < ActionMailer::Base
class UnauthorizedAction < StandardError; end
class MissingInformation < StandardError; end
attr_reader :email, :user
def self.receive(email, options={})
@@handler_options = options.dup
@@handler_options[:issue] ||= {}
@@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
@@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
# Status overridable by default
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
super email
end
# Processes incoming emails
# Returns the created object (eg. an issue, a message) or false
def receive(email)
@ -77,13 +77,13 @@ class MailHandler < ActionMailer::Base
User.current = @user
dispatch
end
private
MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
def dispatch
headers = [email.in_reply_to, email.references].flatten.compact
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
@ -112,7 +112,7 @@ class MailHandler < ActionMailer::Base
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
false
end
# Creates a new issue
def receive_issue
project = target_project
@ -153,7 +153,7 @@ class MailHandler < ActionMailer::Base
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
issue
end
def target_project
# TODO: other ways to specify project:
# * parse the email To field
@ -162,14 +162,14 @@ class MailHandler < ActionMailer::Base
raise MissingInformation.new('Unable to determine target project') if target.nil?
target
end
# Adds a note to an existing issue
def receive_issue_reply(issue_id)
status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
due_date = get_keyword(:due_date, :override => true)
start_date = get_keyword(:start_date, :override => true)
assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
issue = Issue.find_by_id(issue_id)
return unless issue
# check permission
@ -188,30 +188,30 @@ class MailHandler < ActionMailer::Base
issue.start_date = start_date if start_date
issue.due_date = due_date if due_date
issue.assigned_to = assigned_to if assigned_to
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal
end
# Reply will be added to the issue
def receive_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id)
if journal && journal.journalized_type == 'Issue'
receive_issue_reply(journal.journalized_id)
if journal and journal.versioned.is_a? Issue
receive_issue_reply(journal.versioned_id)
end
end
# Receives a reply to a forum message
def receive_message_reply(message_id)
message = Message.find_by_id(message_id)
if message
message = message.root
unless @@handler_options[:no_permission_check]
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
end
if !message.locked?
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
:content => cleaned_up_text_body)
@ -225,7 +225,7 @@ class MailHandler < ActionMailer::Base
end
end
end
def add_attachments(obj)
if email.has_attachments?
email.attachments.each do |attachment|
@ -236,7 +236,7 @@ class MailHandler < ActionMailer::Base
end
end
end
# Adds To and Cc as watchers of the given object if the sender has the
# appropriate permission
def add_watchers(obj)
@ -248,7 +248,7 @@ class MailHandler < ActionMailer::Base
end
end
end
def get_keyword(attr, options={})
@keywords ||= {}
if @keywords.has_key?(attr)
@ -263,7 +263,7 @@ class MailHandler < ActionMailer::Base
end
end
end
# Returns the text/plain part of the email
# If not found (eg. HTML-only email), returns the body with tags removed
def plain_text_body
@ -284,7 +284,7 @@ class MailHandler < ActionMailer::Base
@plain_text_body.strip!
@plain_text_body
end
def cleaned_up_text_body
cleanup_body(plain_text_body)
end
@ -292,19 +292,19 @@ class MailHandler < ActionMailer::Base
def self.full_sanitizer
@full_sanitizer ||= HTML::FullSanitizer.new
end
# Creates a user account for the +email+ sender
def self.create_user_from_email(email)
addr = email.from_addrs.to_a.first
if addr && !addr.spec.blank?
user = User.new
user.mail = addr.spec
names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
user.firstname = names.shift
user.lastname = names.join(' ')
user.lastname = '-' if user.lastname.blank?
user.login = user.mail
user.password = ActiveSupport::SecureRandom.hex(5)
user.language = Setting.default_language
@ -313,7 +313,7 @@ class MailHandler < ActionMailer::Base
end
private
# Removes the email body of text after the truncation configurations.
def cleanup_body(body)
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}

@ -19,6 +19,7 @@ class Mailer < ActionMailer::Base
layout 'mailer'
helper :application
helper :issues
helper :journals
helper :custom_fields
include ActionController::UrlWriter
@ -55,7 +56,7 @@ class Mailer < ActionMailer::Base
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal)
issue = journal.journalized.reload
issue = journal.versioned.reload
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login
@ -67,7 +68,7 @@ class Mailer < ActionMailer::Base
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
s << "(#{issue.status.name}) " if journal.details['status_id']
s << issue.subject
subject s
body :issue => issue,

@ -21,21 +21,26 @@ class Message < ActiveRecord::Base
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
acts_as_attachable
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_journalized :event_title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:event_description => :content,
:event_type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:event_url => (Proc.new do |o|
if o.parent_id.nil?
{:id => o.id}
else
{:id => o.parent_id, :r => o.versioned.id, :anchor => "message-#{o.versioned.id}"}
end.reverse_merge :controller => 'messages', :action => 'show', :board_id => o.board_id
end),
:activity_find_options => { :include => { :board => :project } }
acts_as_searchable :columns => ['subject', 'content'],
:include => {:board => :project},
:project_key => 'project_id',
:date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
{:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
:author_key => :author_id
acts_as_watchable
attr_protected :locked, :sticky
validates_presence_of :board, :subject, :content
validates_length_of :subject, :maximum => 255

@ -24,11 +24,9 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255
acts_as_journalized :event_url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.versioned_id} }
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => [:project, :author]},
:author_key => :author_id
def visible?(user=User.current)
!user.nil? && user.allowed_to?(:view_news, project)
end

@ -22,18 +22,14 @@ class TimeEntry < ActiveRecord::Base
belongs_to :issue
belongs_to :user
belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
acts_as_customizable
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
:url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
:author => :user,
:description => :comments
acts_as_activity_provider :timestamp => "#{table_name}.created_on",
:author_key => :user_id,
:find_options => {:include => :project}
acts_as_journalized :event_title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).current_journal.event_title})"},
:event_url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project, :issue_id => o.issue}},
:event_author => :user,
:event_description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true, :message => :invalid

@ -18,7 +18,8 @@
class Version < ActiveRecord::Base
after_update :update_issues_from_sharing_change
belongs_to :project
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
acts_as_customizable
acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files

@ -18,93 +18,86 @@
require 'zlib'
class WikiContent < ActiveRecord::Base
set_locking_column :version
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
validates_presence_of :text
validates_length_of :comments, :maximum => 255, :allow_nil => true
acts_as_versioned
acts_as_journalized :event_type => 'wiki-page',
:event_title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:event_url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}},
:activity_type => 'wiki_edits',
:activity_permission => :view_wiki_edits,
:activity_find_options => { :include => { :page => { :wiki => :project } } }
def activity_type
'wiki_edits'
end
def visible?(user=User.current)
page.visible?(user)
end
def project
page.project
end
def attachments
page.nil? ? [] : page.attachments
end
# Returns the mail adresses of users that should be notified
def recipients
notified = project.notified_users
notified.reject! {|user| !visible?(user)}
notified.collect(&:mail)
end
class Version
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
attr_protected :data
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
:description => :comments,
:datetime => :updated_on,
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
# FIXME: This is for backwards compatibility only. Remove once we decide it is not needed anymore
WikiContentJournal.class_eval do
attr_protected :data
after_save :compress_version_text
acts_as_activity_provider :type => 'wiki_edits',
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",
:author_key => "#{WikiContent.versioned_table_name}.author_id",
:permission => :view_wiki_edits,
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
"#{WikiContent.versioned_table_name}.id",
:joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
# Wiki Content might be large and the data should possibly be compressed
def compress_version_text
self.text = changes["text"].last if changes["text"]
self.text ||= self.versioned.text
end
def text=(plain)
case Setting.wiki_compression
when 'gzip'
begin
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
self.compression = 'gzip'
rescue
self.data = plain
self.compression = ''
end
when "gzip"
begin
text_hash :text => Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION), :compression => Setting.wiki_compression
rescue
text_hash :text => plain, :compression => ''
end
else
self.data = plain
self.compression = ''
text_hash :text => plain, :compression => ''
end
plain
end
def text_hash(hash)
changes.delete("text")
changes["data"] = hash[:text]
changes["compression"] = hash[:compression]
update_attribute(:changes, changes.to_yaml)
end
def text
@text ||= case compression
@text ||= case changes[:compression]
when 'gzip'
Zlib::Inflate.inflate(data)
else
# uncompressed data
data
end
end
def project
page.project
changes["data"]
end
end
# Returns the previous version or nil
def previous
@previous ||= WikiContent::Version.find(:first,
:order => 'version DESC',
:include => :author,
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
@previous ||= versioned.journals.at(version - 1)
end
end
end

@ -1,19 +1,5 @@
<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
<div id="change-<%= journal.id %>" class="journal">
<h4><div class="journal-link"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
<%= avatar(journal.user, :size => "24") %>
<%= content_tag('a', '', :name => "note-#{journal.indice}")%>
<%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %></h4>
<% if journal.details.any? %>
<ul class="details">
<% for detail in journal.details %>
<li><%= show_detail(detail) %></li>
<% end %>
</ul>
<% end %>
<%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
<%= render_journal issue, journal, :edit_permission => :edit_issue_notes,
:edit_own_permission => :edit_own_issue_notes %>
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
<% end %>

@ -1,8 +0,0 @@
<% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
<%= text_area_tag :notes, @journal.notes, :class => 'wiki-edit',
:rows => (@journal.notes.blank? ? 10 : [[10, @journal.notes.length / 50].max, 100].min) %>
<%= call_hook(:view_journals_notes_form_after_notes, { :journal => @journal}) %>
<p><%= submit_tag l(:button_save) %>
<%= link_to l(:button_cancel), '#', :onclick => "Element.remove('journal-#{@journal.id}-form'); " +
"Element.show('journal-#{@journal.id}-notes'); return false;" %></p>
<% end %>

@ -1,3 +0,0 @@
page.hide "journal-#{@journal.id}-notes"
page.insert_html :after, "journal-#{@journal.id}-notes",
:partial => 'notes_form'

@ -1,10 +0,0 @@
if @journal.frozen?
# journal was destroyed
page.remove "change-#{@journal.id}"
else
page.replace "journal-#{@journal.id}-notes", render_notes(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit'))
page.show "journal-#{@journal.id}-notes"
page.remove "journal-#{@journal.id}-form"
end
call_hook(:view_journals_update_rjs_bottom, { :page => page, :journal => @journal })

@ -2,7 +2,7 @@
<ul>
<% for detail in @journal.details %>
<li><%= show_detail(detail, true) %></li>
<li><%= @journal.render_detail(detail, true) %></li>
<% end %>
</ul>

@ -1,7 +1,7 @@
<%= l(:text_issue_updated, :id => "##{@issue.id}", :author => @journal.user) %>
<% for detail in @journal.details -%>
<%= show_detail(detail, true) %>
<%= @journal.render_detail(detail, true) %>
<% end -%>
<%= @journal.notes if @journal.notes? %>

@ -1,7 +1,7 @@
<h2><%=h @page.pretty_title %></h2>
<% form_for :content, @content, :url => {:action => 'edit', :page => @page.title}, :html => {:multipart => true, :id => 'wiki_form'} do |f| %>
<%= f.hidden_field :version %>
<%= f.hidden_field :lock_version %>
<%= error_messages_for 'content' %>
<p><%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %></p>

@ -21,9 +21,9 @@
<td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></td>
<td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
<td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
<td align="center"><%= format_time(ver.updated_on) %></td>
<td><%= link_to_user ver.author %></td>
<td><%=h ver.comments %></td>
<td align="center"><%= format_time(ver.created_at) %></td>
<td><%= link_to_user ver.user %></td>
<td><%=h ver.notes %></td>
<td align="center"><%= link_to l(:button_annotate), :action => 'annotate', :page => @page.title, :version => ver.version %></td>
</tr>
<% line_num += 1 %>

@ -36,11 +36,11 @@ Rails::Initializer.run do |config|
# Activate observers that should always be running
# config.active_record.observers = :cacher, :garbage_collector
config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer
config.active_record.observers = :journal_observer, :message_observer, :issue_observer, :news_observer, :document_observer, :wiki_content_observer
# Make Active Record use UTC-base instead of local time
# config.active_record.default_timezone = :utc
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
# config.active_record.schema_format = :ruby

@ -2,7 +2,7 @@ class RenameCommentToComments < ActiveRecord::Migration
def self.up
rename_column(:comments, :comment, :comments) if ActiveRecord::Base.connection.columns(Comment.table_name).detect{|c| c.name == "comment"}
rename_column(:wiki_contents, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.table_name).detect{|c| c.name == "comment"}
rename_column(:wiki_content_versions, :comment, :comments) if ActiveRecord::Base.connection.columns(WikiContent.versioned_table_name).detect{|c| c.name == "comment"}
rename_column(:wiki_content_versions, :comment, :comments) if ActiveRecord::Base.connection.columns("wiki_content_versions").detect{|c| c.name == "comment"}
rename_column(:time_entries, :comment, :comments) if ActiveRecord::Base.connection.columns(TimeEntry.table_name).detect{|c| c.name == "comment"}
rename_column(:changesets, :comment, :comments) if ActiveRecord::Base.connection.columns(Changeset.table_name).detect{|c| c.name == "comment"}
end

@ -1,11 +1,9 @@
class Meeting < ActiveRecord::Base
belongs_to :project
acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
:datetime => :scheduled_on,
:author => nil,
:url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
acts_as_activity_provider :timestamp => 'scheduled_on',
:find_options => { :include => :project }
acts_as_journalized :event_title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
:event_datetime => :scheduled_on,
:event_author => nil,
:event_url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
:activity_timestamp => 'scheduled_on'
end

@ -205,12 +205,12 @@ Redmine::MenuManager.map :project_menu do |menu|
end
Redmine::Activity.map do |activity|
activity.register :issues, :class_name => %w(Issue Journal)
activity.register :issues, :class_name => 'Issue'
activity.register :changesets
activity.register :news
activity.register :documents, :class_name => %w(Document Attachment)
activity.register :files, :class_name => 'Attachment'
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
activity.register :wiki_edits, :class_name => 'WikiContent', :default => false
activity.register :messages, :default => false
activity.register :time_entries, :default => false
end

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -20,33 +20,43 @@ module Redmine
# Class used to retrieve activity events
class Fetcher
attr_reader :user, :project, :scope
# Needs to be unloaded in development mode
@@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
def initialize(user, options={})
options.assert_valid_keys(:project, :with_subprojects, :author)
@user = user
@project = options[:project]
@options = options
@scope = event_types
end
# Returns an array of available event types
def event_types
return @event_types unless @event_types.nil?
@event_types = Redmine::Activity.available_event_types
@event_types = @event_types.select {|o| @project.self_and_descendants.detect {|p| @user.allowed_to?("view_#{o}".to_sym, p)}} if @project
if @project
@event_types = @event_types.select do |o|
@project.self_and_descendants.detect do |p|
permissions = constantized_providers(o).collect do |p|
p.activity_provider_options[o].try(:[], :permission)
end.compact
return @user.allowed_to?("view_#{o}".to_sym, @project) if permissions.blank?
permissions.all? {|p| @user.allowed_to?(p, @project) } if @project
end
end
end
@event_types
end
# Yields to filter the activity scope
def scope_select(&block)
@scope = @scope.select {|t| yield t }
end
# Sets the scope
# Argument can be :all, :default or an array of event types
def scope=(s)
@ -59,34 +69,34 @@ module Redmine
@scope = s & event_types
end
end
# Resets the scope to the default scope
def default_scope!
@scope = Redmine::Activity.default_event_types
end
# Returns an array of events for the given date range
# sorted in reverse chronological order
def events(from = nil, to = nil, options={})
e = []
@options[:limit] = options[:limit]
@scope.each do |event_type|
constantized_providers(event_type).each do |provider|
e += provider.find_events(event_type, @user, from, to, @options)
end
end
e.sort! {|a,b| b.event_datetime <=> a.event_datetime}
if options[:limit]
e = e.slice(0, options[:limit])
end
e
end
private
def constantized_providers(event_type)
@@constantized_providers[event_type]
end

@ -282,7 +282,7 @@ module Redmine
pdf.Ln
for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
pdf.SetFontStyle('B',8)
pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
pdf.Cell(190,5, format_time(journal.created_at) + " - " + journal.user.name)
pdf.Ln
pdf.SetFontStyle('I',8)
for detail in journal.details

@ -95,7 +95,7 @@ module Redmine
page = nil
if args.size > 0
page = Wiki.find_page(args.first.to_s, :project => @project)
elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
elsif obj.is_a?(WikiContent)
page = obj.page
else
raise 'With no argument, this macro can be called from wiki pages only.'

@ -1,22 +0,0 @@
---
journal_details_001:
old_value: "1"
property: attr
id: 1
value: "2"
prop_key: status_id
journal_id: 1
journal_details_002:
old_value: "40"
property: attr
id: 2
value: "30"
prop_key: done_ratio
journal_id: 1
journal_details_003:
old_value: nil
property: attr
id: 3
value: "6"
prop_key: fixed_version_id
journal_id: 4

@ -1,29 +1,207 @@
---
journals_001:
created_on: <%= 2.days.ago.to_date.to_s(:db) %>
notes: "Journal notes"
---
journals_001:
id: 1
journalized_type: Issue
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 1
journalized_id: 1
journals_002:
created_on: <%= 1.days.ago.to_date.to_s(:db) %>
notes: "Some notes with Redmine links: #2, r2."
notes: "Journal notes"
versioned_id: 1
changes: |
---
status_id:
- 1
- 2
done_ratio:
- 40
- 30
journals_002:
id: 2
journalized_type: Issue
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 2
user_id: 2
journalized_id: 1
journals_003:
created_on: <%= 1.days.ago.to_date.to_s(:db) %>
notes: "A comment with inline image: !picture.jpg!"
notes: "Some notes with Redmine links: #2, r2."
versioned_id: 1
changes: "--- {}"
journals_003:
id: 3
journalized_type: Issue
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
journalized_id: 2
journals_004:
created_on: <%= 1.days.ago.to_date.to_s(:db) %>
notes: "A comment with a private version."
notes: "A comment with inline image: !picture.jpg!"
versioned_id: 2
changes: "--- {}"
journals_004:
id: 4
journalized_type: Issue
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A comment with a private version."
versioned_id: 6
changes: |
---
fixed_version_id:
-
- 6
journals_005:
id: 5
type: "IssueJournal"
activity_type: "issues"
created_at: <%= 1.days.ago.to_date.to_s(:db) %>
version: 0
user_id: 1
notes:
versioned_id: 5
changes: "--- {}"
journals_006:
id: 6
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:08:07 +01:00
version: 1
user_id: 2
notes: Page creation
versioned_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some [[documentation]] here...
journals_007:
id: 7
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:08:34 +01:00
version: 2
user_id: 1
journalized_id: 6
notes: Small update
versioned_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
journals_008:
id: 8
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-07 00:10:51 +01:00
version: 3
user_id: 1
notes: ""
versioned_id: 1
changes: |
---
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
journals_009:
id: 9
type: "WikiContentJournal"
activity_type: "wiki_edits"
created_at: 2007-03-08 00:18:07 +01:00
version: 1
user_id: 1
notes:
versioned_id: 2
changes: |
---
data: |-
h1. Another page
This is a link to a ticket: #2
journals_010:
id: 10
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 1
notes:
versioned_id: 5
changes: --- {}
journals_011:
id: 11
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a version"
versioned_id: 9
changes: --- {}
journals_012:
id: 12
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a project"
versioned_id: 8
changes: --- {}
journals_013:
id: 13
type: "AttachmentJournal"
activity_type: "files"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on an issue"
versioned_id: 7
changes: --- {}
journals_014:
id: 14
type: "AttachmentJournal"
activity_type: "documents"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "An attachment on a document"
versioned_id: 2
changes: --- {}
journals_015:
id: 15
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
versioned_id: 1
journals_016:
id: 16
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
versioned_id: 2
journals_017:
id: 17
type: "MessageJournal"
activity_type: "messages"
created_at: <%= 2.days.ago.to_date.to_s(:db) %>
version: 1
user_id: 2
notes: "A message journal"
versioned_id: 3

@ -1,56 +0,0 @@
---
wiki_content_versions_001:
updated_on: 2007-03-07 00:08:07 +01:00
page_id: 1
id: 1
version: 1
author_id: 2
comments: Page creation
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some [[documentation]] here...
wiki_content_versions_002:
updated_on: 2007-03-07 00:08:34 +01:00
page_id: 1
id: 2
version: 2
author_id: 1
comments: Small update
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
wiki_content_versions_003:
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
id: 3
version: 3
author_id: 1
comments: ""
wiki_content_id: 1
compression: ""
data: |-
h1. CookBook documentation
Some updated [[documentation]] here...
wiki_content_versions_004:
data: |-
h1. Another page
This is a link to a ticket: #2
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2
wiki_content_id: 2
id: 4
version: 1
author_id: 1
comments:

@ -9,10 +9,10 @@ wiki_contents_001:
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
id: 1
version: 3
lock_version: 3
author_id: 1
comments: Gzip compression activated
wiki_contents_002:
wiki_contents_002:
text: |-
h1. Another page
@ -22,10 +22,10 @@ wiki_contents_002:
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 2
id: 2
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_003:
comments:
wiki_contents_003:
text: |-
h1. Start page
@ -33,10 +33,10 @@ wiki_contents_003:
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 3
id: 3
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_004:
comments:
wiki_contents_004:
text: |-
h1. Page with an inline image
@ -46,10 +46,10 @@ wiki_contents_004:
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 4
id: 4
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_005:
comments:
wiki_contents_005:
text: |-
h1. Child page 1
@ -57,10 +57,10 @@ wiki_contents_005:
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 5
id: 5
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_006:
comments:
wiki_contents_006:
text: |-
h1. Child page 2
@ -68,31 +68,30 @@ wiki_contents_006:
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 6
id: 6
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_007:
comments:
wiki_contents_007:
text: This is a child page
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 7
id: 7
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_008:
comments:
wiki_contents_008:
text: This is a parent page
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 8
id: 8
version: 1
lock_version: 1
author_id: 1
comments:
wiki_contents_009:
comments:
wiki_contents_009:
text: This is a child page
updated_on: 2007-03-08 00:18:07 +01:00
page_id: 9
id: 9
version: 1
lock_version: 1
author_id: 1
comments:
comments:

File diff suppressed because it is too large Load Diff

@ -43,11 +43,10 @@ class IssuesControllerTransactionTest < ActionController::TestCase
:custom_fields_trackers,
:time_entries,
:journals,
:journal_details,
:queries
self.use_transactional_fixtures = false
def setup
@controller = IssuesController.new
@request = ActionController::TestRequest.new

@ -22,8 +22,8 @@ require 'journals_controller'
class JournalsController; def rescue_action(e) raise e end; end
class JournalsControllerTest < ActionController::TestCase
fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules
fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :enabled_modules
def setup
@controller = JournalsController.new
@request = ActionController::TestRequest.new

@ -22,7 +22,7 @@ require 'projects_controller'
class ProjectsController; def rescue_action(e) raise e end; end
class ProjectsControllerTest < ActionController::TestCase
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
:attachments, :custom_fields, :custom_values, :time_entries

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -22,15 +22,15 @@ require 'wiki_controller'
class WikiController; def rescue_action(e) raise e end; end
class WikiControllerTest < ActionController::TestCase
fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :journals, :attachments
def setup
@controller = WikiController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
User.current = nil
end
def test_show_start_page
get :index, :id => 'ecookbook'
assert_response :success
@ -43,7 +43,7 @@ class WikiControllerTest < ActionController::TestCase
:child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
:content => 'Page with an inline image' } }
end
def test_show_page_with_name
get :index, :id => 1, :page => 'Another_page'
assert_response :success
@ -54,30 +54,30 @@ class WikiControllerTest < ActionController::TestCase
assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
:alt => 'This is a logo' }
end
def test_show_with_sidebar
page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
page.save!
get :index, :id => 1, :page => 'Another_page'
assert_response :success
assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
:content => /Side bar content for test_show_with_sidebar/
end
def test_show_unexistent_page_without_edit_right
get :index, :id => 1, :page => 'Unexistent page'
assert_response 404
end
def test_show_unexistent_page_with_edit_right
@request.session[:user_id] = 2
get :index, :id => 1, :page => 'Unexistent page'
assert_response :success
assert_template 'edit'
end
def test_create_page
@request.session[:user_id] = 2
post :edit, :id => 1,
@ -91,7 +91,7 @@ class WikiControllerTest < ActionController::TestCase
assert_not_nil page.content
assert_equal 'Created the page', page.content.comments
end
def test_create_page_with_attachments
@request.session[:user_id] = 2
assert_difference 'WikiPage.count' do
@ -108,7 +108,7 @@ class WikiControllerTest < ActionController::TestCase
assert_equal 1, page.attachments.count
assert_equal 'testfile.txt', page.attachments.first.filename
end
def test_preview
@request.session[:user_id] = 2
xhr :post, :preview, :id => 1, :page => 'CookBook_documentation',
@ -119,7 +119,7 @@ class WikiControllerTest < ActionController::TestCase
assert_template 'common/_preview'
assert_tag :tag => 'strong', :content => /previewed text/
end
def test_preview_new_page
@request.session[:user_id] = 2
xhr :post, :preview, :id => 1, :page => 'New page',
@ -130,7 +130,7 @@ class WikiControllerTest < ActionController::TestCase
assert_template 'common/_preview'
assert_tag :tag => 'h1', :content => /New page/
end
def test_history
get :history, :id => 1, :page => 'CookBook_documentation'
assert_response :success
@ -148,7 +148,7 @@ class WikiControllerTest < ActionController::TestCase
assert_equal 1, assigns(:versions).size
assert_select "input[type=submit][name=commit]", false
end
def test_diff
get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
assert_response :success
@ -156,7 +156,7 @@ class WikiControllerTest < ActionController::TestCase
assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
:content => /updated/
end
def test_annotate
get :annotate, :id => 1, :page => 'CookBook_documentation', :version => 2
assert_response :success
@ -170,7 +170,7 @@ class WikiControllerTest < ActionController::TestCase
:child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
:child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
end
def test_rename_with_redirect
@request.session[:user_id] = 2
post :rename, :id => 1, :page => 'Another_page',
@ -193,13 +193,13 @@ class WikiControllerTest < ActionController::TestCase
# Check that there's no redirects
assert_nil wiki.find_page('Another page')
end
def test_destroy_child
@request.session[:user_id] = 2
post :destroy, :id => 1, :page => 'Child_1'
assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
end
def test_destroy_parent
@request.session[:user_id] = 2
assert_no_difference('WikiPage.count') do
@ -208,7 +208,7 @@ class WikiControllerTest < ActionController::TestCase
assert_response :success
assert_template 'destroy'
end
def test_destroy_parent_with_nullify
@request.session[:user_id] = 2
assert_difference('WikiPage.count', -1) do
@ -217,7 +217,7 @@ class WikiControllerTest < ActionController::TestCase
assert_redirected_to :action => 'special', :id => 'ecookbook', :page => 'Page_index'
assert_nil WikiPage.find_by_id(2)
end
def test_destroy_parent_with_cascade
@request.session[:user_id] = 2
assert_difference('WikiPage.count', -3) do
@ -227,7 +227,7 @@ class WikiControllerTest < ActionController::TestCase
assert_nil WikiPage.find_by_id(2)
assert_nil WikiPage.find_by_id(5)
end
def test_destroy_parent_with_reassign
@request.session[:user_id] = 2
assert_difference('WikiPage.count', -1) do
@ -237,7 +237,7 @@ class WikiControllerTest < ActionController::TestCase
assert_nil WikiPage.find_by_id(2)
assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
end
def test_page_index
get :special, :id => 'ecookbook', :page => 'Page_index'
assert_response :success
@ -245,7 +245,7 @@ class WikiControllerTest < ActionController::TestCase
pages = assigns(:pages)
assert_not_nil pages
assert_equal Project.find(1).wiki.pages.size, pages.size
assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
:child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
:content => 'CookBook documentation' },
@ -256,12 +256,12 @@ class WikiControllerTest < ActionController::TestCase
:child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
:content => 'Another page' } }
end
def test_not_found
get :index, :id => 999
assert_response 404
end
def test_protect_page
page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
assert !page.protected?
@ -270,7 +270,7 @@ class WikiControllerTest < ActionController::TestCase
assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'Another_page'
assert page.reload.protected?
end
def test_unprotect_page
page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
assert page.protected?
@ -279,7 +279,7 @@ class WikiControllerTest < ActionController::TestCase
assert_redirected_to :action => 'index', :id => 'ecookbook', :page => 'CookBook_documentation'
assert !page.reload.protected?
end
def test_show_page_with_edit_link
@request.session[:user_id] = 2
get :index, :id => 1
@ -287,15 +287,15 @@ class WikiControllerTest < ActionController::TestCase
assert_template 'show'
assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
end
def test_show_page_without_edit_link
@request.session[:user_id] = 4
get :index, :id => 1
assert_response :success
assert_template 'show'
assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
end
end
def test_edit_unprotected_page
# Non members can edit unprotected wiki pages
@request.session[:user_id] = 4
@ -303,21 +303,21 @@ class WikiControllerTest < ActionController::TestCase
assert_response :success
assert_template 'edit'
end
def test_edit_protected_page_by_nonmember
# Non members can't edit protected wiki pages
@request.session[:user_id] = 4
get :edit, :id => 1, :page => 'CookBook_documentation'
assert_response 403
end
def test_edit_protected_page_by_member
@request.session[:user_id] = 2
get :edit, :id => 1, :page => 'CookBook_documentation'
assert_response :success
assert_template 'edit'
assert_template 'edit'
end
def test_history_of_non_existing_page_should_return_404
get :history, :id => 1, :page => 'Unknown_page'
assert_response 404

@ -39,7 +39,6 @@ class IssuesApiTest < ActionController::IntegrationTest
:custom_fields_trackers,
:time_entries,
:journals,
:journal_details,
:queries
def setup

@ -18,7 +18,7 @@
require "#{File.dirname(__FILE__)}/../test_helper"
class ProjectsApiTest < ActionController::IntegrationTest
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
:attachments, :custom_fields, :custom_values, :time_entries

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -47,7 +47,7 @@ class ActiveSupport::TestCase
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
def log_user(login, password)
User.anonymous
get "/login"
@ -57,7 +57,7 @@ class ActiveSupport::TestCase
post "/login", :username => login, :password => password
assert_equal login, User.find(session[:user_id]).login
end
def uploaded_test_file(name, mime)
ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime)
end
@ -82,7 +82,7 @@ class ActiveSupport::TestCase
Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
end
def with_settings(options, &block)
saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
options.each {|k, v| Setting[k] = v}
@ -103,12 +103,12 @@ class ActiveSupport::TestCase
# LDAP is not listening
return nil
end
# Returns the path to the test +vendor+ repository
def self.repository_path(vendor)
File.join(RAILS_ROOT.gsub(%r{config\/\.\.}, ''), "/tmp/test/#{vendor.downcase}_repository")
end
# Returns true if the +vendor+ test repository is configured
def self.repository_configured?(vendor)
File.directory?(repository_path(vendor))
@ -154,21 +154,17 @@ class ActiveSupport::TestCase
end
should "use the new value's name" do
@detail = JournalDetail.generate!(:property => 'attr',
:old_value => @old_value.id,
:value => @new_value.id,
:prop_key => prop_key)
assert_match @new_value.name, show_detail(@detail, true)
@detail = IssueJournal.generate(:version => 1)
@detail.update_attribute(:changes, {prop_key => [@old_value.id, @new_value.id]}.to_yaml)
assert_match @new_value.name, @detail.render_detail(prop_key, true)
end
should "use the old value's name" do
@detail = JournalDetail.generate!(:property => 'attr',
:old_value => @old_value.id,
:value => @new_value.id,
:prop_key => prop_key)
assert_match @old_value.name, show_detail(@detail, true)
@detail = IssueJournal.generate(:version => 1)
@detail.update_attribute(:changes, {prop_key => [@old_value.id, @new_value.id]}.to_yaml)
assert_match @old_value.name, @detail.render_detail(prop_key, true)
end
end
end

@ -18,7 +18,7 @@
require File.dirname(__FILE__) + '/../test_helper'
class ActivityTest < ActiveSupport::TestCase
fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals,
:trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
def setup
@ -28,7 +28,6 @@ class ActivityTest < ActiveSupport::TestCase
def test_activity_without_subprojects
events = find_events(User.anonymous, :project => @project)
assert_not_nil events
assert events.include?(Issue.find(1))
assert !events.include?(Issue.find(4))
# subproject issue
@ -43,40 +42,40 @@ class ActivityTest < ActiveSupport::TestCase
# subproject issue
assert events.include?(Issue.find(5))
end
def test_global_activity_anonymous
events = find_events(User.anonymous)
assert_not_nil events
assert events.include?(Issue.find(1))
assert events.include?(Message.find(5))
# Issue of a private project
assert !events.include?(Issue.find(4))
assert !events.include?(Issue.find(6))
end
def test_global_activity_logged_user
events = find_events(User.find(2)) # manager
assert_not_nil events
assert events.include?(Issue.find(1))
# Issue of a private project the user belongs to
assert events.include?(Issue.find(4))
assert events.include?(Issue.find(6))
end
def test_user_activity
user = User.find(2)
events = Redmine::Activity::Fetcher.new(User.anonymous, :author => user).events(nil, nil, :limit => 10)
assert(events.size > 0)
assert(events.size <= 10)
assert_nil(events.detect {|e| e.event_author != user})
end
def test_files_activity
f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
f.scope = ['files']
events = f.events
assert_kind_of Array, events
assert events.include?(Attachment.find_by_container_type_and_container_id('Project', 1))
assert events.include?(Attachment.find_by_container_type_and_container_id('Version', 1))
@ -85,8 +84,11 @@ class ActivityTest < ActiveSupport::TestCase
end
private
def find_events(user, options={})
Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
events = Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
# Because events are provided by the journals, but we want to test for
# their targets here, transform that
events.group_by(&:versioned).keys
end
end

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -22,7 +22,7 @@ class IssueTest < ActiveSupport::TestCase
:trackers, :projects_trackers,
:enabled_modules,
:versions,
:issue_statuses, :issue_categories, :issue_relations, :workflows,
:issue_statuses, :issue_categories, :issue_relations, :workflows,
:enumerations,
:issues,
:custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
@ -34,17 +34,17 @@ class IssueTest < ActiveSupport::TestCase
issue.reload
assert_equal 1.5, issue.estimated_hours
end
def test_create_minimal
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
assert issue.save
assert issue.description.nil?
end
def test_create_with_required_custom_field
field = IssueCustomField.find_by_name('Database')
field.update_attribute(:is_required, true)
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
assert issue.available_custom_fields.include?(field)
# No value for the custom field
@ -64,7 +64,7 @@ class IssueTest < ActiveSupport::TestCase
issue.reload
assert_equal 'PostgreSQL', issue.custom_value_for(field).value
end
def test_visible_scope_for_anonymous
# Anonymous user should see issues of public projects only
issues = Issue.visible(User.anonymous).all
@ -75,7 +75,7 @@ class IssueTest < ActiveSupport::TestCase
issues = Issue.visible(User.anonymous).all
assert issues.empty?
end
def test_visible_scope_for_user
user = User.find(9)
assert user.projects.empty?
@ -95,7 +95,7 @@ class IssueTest < ActiveSupport::TestCase
assert issues.any?
assert_nil issues.detect {|issue| issue.project_id != 2}
end
def test_visible_scope_for_admin
user = User.find(1)
user.members.each(&:destroy)
@ -105,24 +105,24 @@ class IssueTest < ActiveSupport::TestCase
# Admin should see issues on private projects that he does not belong to
assert issues.detect {|issue| !issue.project.is_public?}
end
def test_errors_full_messages_should_include_custom_fields_errors
field = IssueCustomField.find_by_name('Database')
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
assert issue.available_custom_fields.include?(field)
# Invalid value
issue.custom_field_values = { field.id => 'SQLServer' }
assert !issue.valid?
assert_equal 1, issue.errors.full_messages.size
assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
end
def test_update_issue_with_required_custom_field
field = IssueCustomField.find_by_name('Database')
field.update_attribute(:is_required, true)
issue = Issue.find(1)
assert_nil issue.custom_value_for(field)
assert issue.available_custom_fields.include?(field)
@ -137,23 +137,23 @@ class IssueTest < ActiveSupport::TestCase
issue.reload
assert_equal 'PostgreSQL', issue.custom_value_for(field).value
end
def test_should_not_update_attributes_if_custom_fields_validation_fails
issue = Issue.find(1)
field = IssueCustomField.find_by_name('Database')
assert issue.available_custom_fields.include?(field)
issue.custom_field_values = { field.id => 'Invalid' }
issue.subject = 'Should be not be saved'
assert !issue.save
issue.reload
assert_equal "Can't print recipes", issue.subject
end
def test_should_not_recreate_custom_values_objects_on_update
field = IssueCustomField.find_by_name('Database')
issue = Issue.find(1)
issue.custom_field_values = { field.id => 'PostgreSQL' }
assert issue.save
@ -164,14 +164,14 @@ class IssueTest < ActiveSupport::TestCase
issue.reload
assert_equal custom_value.id, issue.custom_value_for(field).id
end
def test_assigning_tracker_id_should_reload_custom_fields_values
issue = Issue.new(:project => Project.find(1))
assert issue.custom_field_values.empty?
issue.tracker_id = 1
assert issue.custom_field_values.any?
end
def test_assigning_attributes_should_assign_tracker_id_first
attributes = ActiveSupport::OrderedHash.new
attributes['custom_field_values'] = { '1' => 'MySQL' }
@ -181,35 +181,35 @@ class IssueTest < ActiveSupport::TestCase
assert_not_nil issue.custom_value_for(1)
assert_equal 'MySQL', issue.custom_value_for(1).value
end
def test_should_update_issue_with_disabled_tracker
p = Project.find(1)
issue = Issue.find(1)
p.trackers.delete(issue.tracker)
assert !p.trackers.include?(issue.tracker)
issue.reload
issue.subject = 'New subject'
assert issue.save
end
def test_should_not_set_a_disabled_tracker
p = Project.find(1)
p.trackers.delete(Tracker.find(2))
issue = Issue.find(1)
issue.tracker_id = 2
issue.subject = 'New subject'
assert !issue.save
assert_not_nil issue.errors.on(:tracker_id)
end
def test_category_based_assignment
issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
end
def test_copy
issue = Issue.new.copy_from(1)
assert issue.save
@ -223,13 +223,13 @@ class IssueTest < ActiveSupport::TestCase
def test_copy_should_copy_status
orig = Issue.find(8)
assert orig.status != IssueStatus.default
issue = Issue.new.copy_from(orig)
assert issue.save
issue.reload
assert_equal orig.status, issue.status
end
def test_should_close_duplicates
# Create 3 issues
issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
@ -238,37 +238,37 @@ class IssueTest < ActiveSupport::TestCase
assert issue2.save
issue3 = issue1.clone
assert issue3.save
# 2 is a dupe of 1
IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
# And 3 is a dupe of 2
IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
# And 3 is a dupe of 1 (circular duplicates)
IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
assert issue1.reload.duplicates.include?(issue2)
# Closing issue 1
issue1.init_journal(User.find(:first), "Closing issue1")
issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
assert issue1.save
# 2 and 3 should be also closed
assert issue2.reload.closed?
assert issue3.reload.closed?
assert issue3.reload.closed?
end
def test_should_not_close_duplicated_issue
# Create 3 issues
issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
assert issue1.save
issue2 = issue1.clone
assert issue2.save
# 2 is a dupe of 1
IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
# 2 is a dup of 1 but 1 is not a duplicate of 2
assert !issue2.reload.duplicates.include?(issue1)
# Closing issue 2
issue2.init_journal(User.find(:first), "Closing issue2")
issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
@ -276,57 +276,57 @@ class IssueTest < ActiveSupport::TestCase
# 1 should not be also closed
assert !issue1.reload.closed?
end
def test_assignable_versions
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
end
def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
assert !issue.save
assert_not_nil issue.errors.on(:fixed_version_id)
end
def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
assert !issue.save
assert_not_nil issue.errors.on(:fixed_version_id)
end
def test_should_be_able_to_assign_a_new_issue_to_an_open_version
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
assert issue.save
end
def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
issue = Issue.find(11)
assert_equal 'closed', issue.fixed_version.status
issue.subject = 'Subject changed'
assert issue.save
end
def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
issue = Issue.find(11)
issue.status_id = 1
assert !issue.save
assert_not_nil issue.errors.on_base
end
def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
issue = Issue.find(11)
issue.status_id = 1
issue.fixed_version_id = 3
assert issue.save
end
def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
issue = Issue.find(12)
assert_equal 'locked', issue.fixed_version.status
issue.status_id = 1
assert issue.save
end
def test_move_to_another_project_with_same_category
issue = Issue.find(1)
assert issue.move_to_project(Project.find(2))
@ -337,7 +337,7 @@ class IssueTest < ActiveSupport::TestCase
# Make sure time entries were move to the target project
assert_equal 2, issue.time_entries.first.project_id
end
def test_move_to_another_project_without_same_category
issue = Issue.find(2)
assert issue.move_to_project(Project.find(2))
@ -346,7 +346,7 @@ class IssueTest < ActiveSupport::TestCase
# Category cleared
assert_nil issue.category_id
end
def test_move_to_another_project_should_clear_fixed_version_when_not_shared
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 1)
@ -356,7 +356,7 @@ class IssueTest < ActiveSupport::TestCase
# Cleared fixed_version
assert_equal nil, issue.fixed_version
end
def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 4)
@ -366,7 +366,7 @@ class IssueTest < ActiveSupport::TestCase
# Keep fixed_version
assert_equal 4, issue.fixed_version_id
end
def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 1)
@ -376,7 +376,7 @@ class IssueTest < ActiveSupport::TestCase
# Cleared fixed_version
assert_equal nil, issue.fixed_version
end
def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
issue = Issue.find(1)
issue.update_attribute(:fixed_version_id, 7)
@ -386,7 +386,7 @@ class IssueTest < ActiveSupport::TestCase
# Keep fixed_version
assert_equal 7, issue.fixed_version_id
end
def test_move_to_another_project_with_disabled_tracker
issue = Issue.find(1)
target = Project.find(2)
@ -396,7 +396,7 @@ class IssueTest < ActiveSupport::TestCase
issue.reload
assert_equal 1, issue.project_id
end
def test_copy_to_the_same_project
issue = Issue.find(1)
copy = nil
@ -407,7 +407,7 @@ class IssueTest < ActiveSupport::TestCase
assert_equal issue.project, copy.project
assert_equal "125", copy.custom_value_for(2).value
end
def test_copy_to_another_project_and_tracker
issue = Issue.find(1)
copy = nil
@ -453,7 +453,7 @@ class IssueTest < ActiveSupport::TestCase
end
end
end
def test_recipients_should_not_include_users_that_cannot_view_the_issue
issue = Issue.find(12)
assert issue.recipients.include?(issue.author.mail)
@ -470,39 +470,39 @@ class IssueTest < ActiveSupport::TestCase
assert issue.watched_by?(user)
assert !issue.watcher_recipients.include?(user.mail)
end
def test_issue_destroy
Issue.find(1).destroy
assert_nil Issue.find_by_id(1)
assert_nil TimeEntry.find_by_issue_id(1)
end
def test_blocked
blocked_issue = Issue.find(9)
blocking_issue = Issue.find(10)
assert blocked_issue.blocked?
assert !blocking_issue.blocked?
end
def test_blocked_issues_dont_allow_closed_statuses
blocked_issue = Issue.find(9)
allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
assert !allowed_statuses.empty?
closed_statuses = allowed_statuses.select {|st| st.is_closed?}
assert closed_statuses.empty?
end
def test_unblocked_issues_allow_closed_statuses
blocking_issue = Issue.find(10)
allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
assert !allowed_statuses.empty?
closed_statuses = allowed_statuses.select {|st| st.is_closed?}
assert !closed_statuses.empty?
end
def test_overdue
assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
assert !Issue.new(:due_date => Date.today).overdue?
@ -510,11 +510,11 @@ class IssueTest < ActiveSupport::TestCase
assert !Issue.new(:due_date => nil).overdue?
assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
end
def test_assignable_users
assert_kind_of User, Issue.find(1).assignable_users.first
end
def test_create_should_send_email_notification
ActionMailer::Base.deliveries.clear
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
@ -522,18 +522,18 @@ class IssueTest < ActiveSupport::TestCase
assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_stale_issue_should_not_send_email_notification
ActionMailer::Base.deliveries.clear
issue = Issue.find(1)
stale = Issue.find(1)
issue.init_journal(User.find(1))
issue.subject = 'Subjet update'
assert issue.save
assert_equal 1, ActionMailer::Base.deliveries.size
ActionMailer::Base.deliveries.clear
stale.init_journal(User.find(1))
stale.subject = 'Another subjet update'
assert_raise ActiveRecord::StaleObjectError do
@ -541,7 +541,7 @@ class IssueTest < ActiveSupport::TestCase
end
assert ActionMailer::Base.deliveries.empty?
end
def test_saving_twice_should_not_duplicate_journal_details
i = Issue.find(:first)
i.init_journal(User.find(2), 'Some notes')
@ -551,18 +551,19 @@ class IssueTest < ActiveSupport::TestCase
assert_difference 'Journal.count' do
assert i.save
end
assert i.current_journal.changes.has_key? "subject"
assert i.current_journal.changes.has_key? "done_ratio"
# 1 more change
i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
assert_no_difference 'Journal.count' do
assert_difference 'JournalDetail.count', 1 do
i.save
end
assert_difference 'Journal.count' do
i.save
end
assert i.current_journal.changes.has_key? "priority_id"
# no more change
assert_no_difference 'Journal.count' do
assert_no_difference 'JournalDetail.count' do
i.save
end
i.save
end
end
@ -572,12 +573,12 @@ class IssueTest < ActiveSupport::TestCase
@issue_status = IssueStatus.find(1)
@issue_status.update_attribute(:default_done_ratio, 50)
end
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
end
should "read the issue's field" do
assert_equal 0, @issue.done_ratio
end
@ -587,7 +588,7 @@ class IssueTest < ActiveSupport::TestCase
setup do
Setting.issue_done_ratio = 'issue_status'
end
should "read the Issue Status's default done ratio" do
assert_equal 50, @issue.done_ratio
end
@ -600,12 +601,12 @@ class IssueTest < ActiveSupport::TestCase
@issue_status = IssueStatus.find(1)
@issue_status.update_attribute(:default_done_ratio, 50)
end
context "with Setting.issue_done_ratio using the issue_field" do
setup do
Setting.issue_done_ratio = 'issue_field'
end
should "not change the issue" do
@issue.update_done_ratio_from_issue_status
@ -617,7 +618,7 @@ class IssueTest < ActiveSupport::TestCase
setup do
Setting.issue_done_ratio = 'issue_status'
end
should "not change the issue's done ratio" do
@issue.update_done_ratio_from_issue_status
@ -667,19 +668,19 @@ class IssueTest < ActiveSupport::TestCase
assert_equal 2, groups.size
assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
end
context ".allowed_target_projects_on_move" do
should "return all active projects for admin users" do
User.current = User.find(1)
assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
end
should "return allowed projects for non admin users" do
User.current = User.find(2)
Role.non_member.remove_permission! :move_issues
assert_equal 3, Issue.allowed_target_projects_on_move.size
Role.non_member.add_permission! :move_issues
assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
end
@ -693,7 +694,7 @@ class IssueTest < ActiveSupport::TestCase
def test_on_active_projects_scope
assert Project.find(2).archive
before = Issue.on_active_project.length
# test inclusion to results
issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)

@ -18,14 +18,14 @@
require File.dirname(__FILE__) + '/../test_helper'
class JournalTest < ActiveSupport::TestCase
fixtures :issues, :issue_statuses, :journals, :journal_details
fixtures :issues, :issue_statuses, :journals
def setup
@journal = Journal.find 1
@journal = IssueJournal.first
end
def test_journalized_is_an_issue
issue = @journal.issue
issue = @journal.journalized
assert_kind_of Issue, issue
assert_equal 1, issue.id
end
@ -34,16 +34,16 @@ class JournalTest < ActiveSupport::TestCase
status = @journal.new_status
assert_not_nil status
assert_kind_of IssueStatus, status
assert_equal 2, status.id
assert_equal 2, status.id
end
def test_create_should_send_email_notification
ActionMailer::Base.deliveries.clear
issue = Issue.find(:first)
user = User.find(:first)
journal = issue.init_journal(user, issue)
assert journal.save
assert_equal 0, ActionMailer::Base.deliveries.size
issue.update_attribute(:subject, "New subject to trigger automatic journal entry")
assert_equal 1, ActionMailer::Base.deliveries.size
end

@ -259,7 +259,7 @@ class MailHandlerTest < ActiveSupport::TestCase
# This email contains: 'Status: Resolved'
journal = submit_email('ticket_reply_with_status.eml')
assert journal.is_a?(Journal)
issue = Issue.find(journal.issue.id)
issue = Issue.find(journal.journalized.id)
assert_equal User.find_by_login('jsmith'), journal.user
assert_equal Issue.find(2), journal.journalized
assert_match /This is reply/, journal.notes

@ -20,8 +20,8 @@ require File.dirname(__FILE__) + '/../test_helper'
class MailerTest < ActiveSupport::TestCase
include Redmine::I18n
include ActionController::Assertions::SelectorAssertions
fixtures :projects, :enabled_modules, :issues, :users, :members, :member_roles, :roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
fixtures :projects, :enabled_modules, :issues, :users, :members, :member_roles, :roles, :documents, :attachments, :news, :tokens, :journals, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
def setup
ActionMailer::Base.deliveries.clear
Setting.host_name = 'mydomain.foo'
@ -171,7 +171,7 @@ class MailerTest < ActiveSupport::TestCase
mail = ActionMailer::Base.deliveries.last
assert_not_nil mail
assert_equal Mailer.message_id_for(journal), mail.message_id
assert_equal Mailer.message_id_for(journal.issue), mail.references.first.to_s
assert_equal Mailer.message_id_for(journal.versioned), mail.references.first.to_s
end
def test_message_posted_message_id

@ -27,7 +27,6 @@ class SearchTest < ActiveSupport::TestCase
:issues,
:trackers,
:journals,
:journal_details,
:repositories,
:changesets

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -18,19 +18,19 @@
require File.dirname(__FILE__) + '/../test_helper'
class WikiContentTest < ActiveSupport::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :users
fixtures :wikis, :wiki_pages, :wiki_contents, :journals, :users
def setup
@wiki = Wiki.find(1)
@page = @wiki.pages.first
end
def test_create
page = WikiPage.new(:wiki => @wiki, :title => "Page")
page = WikiPage.new(:wiki => @wiki, :title => "Page")
page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment")
assert page.save
page.reload
content = page.content
assert_kind_of WikiContent, content
assert_equal 1, content.version
@ -40,14 +40,14 @@ class WikiContentTest < ActiveSupport::TestCase
assert_equal User.find(1), content.author
assert_equal content.text, content.versions.last.text
end
def test_create_should_send_email_notification
Setting.notified_events = ['wiki_content_added']
ActionMailer::Base.deliveries.clear
page = WikiPage.new(:wiki => @wiki, :title => "A new page")
page = WikiPage.new(:wiki => @wiki, :title => "A new page")
page.content = WikiContent.new(:text => "Content text", :author => User.find(1), :comments => "My comment")
assert page.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
@ -60,26 +60,26 @@ class WikiContentTest < ActiveSupport::TestCase
assert_equal version_count+1, content.version
assert_equal version_count+1, content.versions.length
end
def test_update_should_send_email_notification
Setting.notified_events = ['wiki_content_updated']
ActionMailer::Base.deliveries.clear
content = @page.content
content.text = "My new content"
assert content.save
assert_equal 1, ActionMailer::Base.deliveries.size
end
def test_fetch_history
assert !@page.content.versions.empty?
@page.content.versions.each do |version|
assert_kind_of String, version.text
end
end
def test_large_text_should_not_be_truncated_to_64k
page = WikiPage.new(:wiki => @wiki, :title => "Big page")
page = WikiPage.new(:wiki => @wiki, :title => "Big page")
page.content = WikiContent.new(:text => "a" * 500.kilobyte, :author => User.find(1))
assert page.save
page.reload

@ -5,12 +5,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -18,51 +18,51 @@
require File.dirname(__FILE__) + '/../test_helper'
class WikiPageTest < ActiveSupport::TestCase
fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :journals
def setup
@wiki = Wiki.find(1)
@page = @wiki.pages.first
end
def test_create
page = WikiPage.new(:wiki => @wiki)
assert !page.save
assert_equal 1, page.errors.count
page.title = "Page"
assert page.save
page.reload
assert !page.protected?
@wiki.reload
assert @wiki.pages.include?(page)
end
def test_sidebar_should_be_protected_by_default
page = @wiki.find_or_new_page('sidebar')
assert page.new_record?
assert page.protected?
end
def test_find_or_new_page
page = @wiki.find_or_new_page("CookBook documentation")
assert_kind_of WikiPage, page
assert !page.new_record?
page = @wiki.find_or_new_page("Non existing page")
assert_kind_of WikiPage, page
assert page.new_record?
end
def test_parent_title
page = WikiPage.find_by_title('Another_page')
assert_nil page.parent_title
page = WikiPage.find_by_title('Page_with_an_inline_image')
assert_equal 'CookBook documentation', page.parent_title
end
def test_assign_parent
page = WikiPage.find_by_title('Another_page')
page.parent_title = 'CookBook documentation'
@ -70,7 +70,7 @@ class WikiPageTest < ActiveSupport::TestCase
page.reload
assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
end
def test_unassign_parent
page = WikiPage.find_by_title('Page_with_an_inline_image')
page.parent_title = ''
@ -78,10 +78,10 @@ class WikiPageTest < ActiveSupport::TestCase
page.reload
assert_nil page.parent
end
def test_parent_validation
page = WikiPage.find_by_title('CookBook_documentation')
# A page that doesn't exist
page.parent_title = 'Unknown title'
assert !page.save
@ -98,23 +98,26 @@ class WikiPageTest < ActiveSupport::TestCase
page.parent_title = 'Another_page'
assert page.save
end
def test_destroy
page = WikiPage.find(1)
content_ids = WikiContent.find_all_by_page_id(1).collect(&:id)
page.destroy
assert_nil WikiPage.find_by_id(1)
# make sure that page content and its history are deleted
assert WikiContent.find_all_by_page_id(1).empty?
assert WikiContent.versioned_class.find_all_by_page_id(1).empty?
content_ids.each do |wiki_content_id|
assert WikiContent.journal_class.find_all_by_versioned_id(wiki_content_id).empty?
end
end
def test_destroy_should_not_nullify_children
page = WikiPage.find(2)
child_ids = page.child_ids
assert child_ids.any?
page.destroy
assert_nil WikiPage.find_by_id(2)
children = WikiPage.find_all_by_id(child_ids)
assert_equal child_ids.size, children.size
children.each do |child|

@ -7,12 +7,12 @@
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
@ -20,13 +20,13 @@
require File.dirname(__FILE__) + '/../test_helper'
class WikiTest < ActiveSupport::TestCase
fixtures :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
fixtures :wikis, :wiki_pages, :wiki_contents, :journals
def test_create
wiki = Wiki.new(:project => Project.find(2))
assert !wiki.save
assert_equal 1, wiki.errors.count
wiki.start_page = "Start page"
assert wiki.save
end
@ -38,26 +38,26 @@ class WikiTest < ActiveSupport::TestCase
@wiki.reload
assert_equal "Another start page", @wiki.start_page
end
def test_titleize
assert_equal 'Page_title_with_CAPITALES', Wiki.titleize('page title with CAPITALES')
assert_equal 'テスト', Wiki.titleize('テスト')
end
context "#sidebar" do
setup do
@wiki = Wiki.find(1)
end
should "return nil if undefined" do
assert_nil @wiki.sidebar
end
should "return a WikiPage if defined" do
page = @wiki.pages.new(:title => 'Sidebar')
page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
page.save!
assert_kind_of WikiPage, @wiki.sidebar
assert_equal 'Sidebar', @wiki.sidebar.title
end

@ -54,7 +54,7 @@ module Redmine
def find_events(event_type, user, from, to, options)
provider_options = activity_provider_options[event_type]
raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
scope_options = {}
cond = ARCondition.new
if from && to
@ -68,12 +68,12 @@ module Redmine
scope_options[:conditions] = cond.conditions
if options[:limit]
# id and creation time should be in same order in most cases
scope_options[:order] = "#{table_name}.id DESC"
scope_options[:order] = "#{journal_class.table_name}.id DESC"
scope_options[:limit] = options[:limit]
end
with_scope(:find => scope_options) do
find(:all, provider_options[:find_options].dup)
journal_class.with_scope(:find => scope_options) do
journal_class.find(:all, provider_options[:find_options].dup)
end
end
end

@ -0,0 +1 @@
Subproject commit f7a5e2d2903829f98c8e2e4dcb33e9891b918e49

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

@ -1,20 +0,0 @@
Copyright (c) 2005 Rick Olson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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

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

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

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

@ -1,564 +0,0 @@
# Copyright (c) 2005 Rick Olson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
# column is present as well.
#
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
#
# class Page < ActiveRecord::Base
# # assumes pages_versions table
# acts_as_versioned
# end
#
# Example:
#
# page = Page.create(:title => 'hello world!')
# page.version # => 1
#
# page.title = 'hello world'
# page.save
# page.version # => 2
# page.versions.size # => 2
#
# page.revert_to(1) # using version number
# page.title # => 'hello world!'
#
# page.revert_to(page.versions.last) # using versioned instance
# page.title # => 'hello world'
#
# page.versions.earliest # efficient query to find the first version
# page.versions.latest # efficient query to find the most recently created version
#
#
# Simple Queries to page between versions
#
# page.versions.before(version)
# page.versions.after(version)
#
# Access the previous/next versions from the versioned model itself
#
# version = page.versions.latest
# version.previous # go back one version
# version.next # go forward one version
#
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
module Versioned
CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
# == Configuration options
#
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
# For finer control, pass either a Proc or modify Model#version_condition_met?
#
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
#
# or...
#
# class Auction
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
# !expired?
# end
# end
#
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
# either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
#
# def name=(new_name)
# write_changed_attribute :name, new_name
# end
#
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
# to create an anonymous mixin:
#
# class Auction
# acts_as_versioned do
# def started?
# !started_at.nil?
# end
# end
# end
#
# or...
#
# module AuctionExtension
# def started?
# !started_at.nil?
# end
# end
# class Auction
# acts_as_versioned :extend => AuctionExtension
# end
#
# Example code:
#
# @auction = Auction.find(1)
# @auction.started?
# @auction.versions.first.started?
#
# == Database Schema
#
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
# into a table called #{model}_versions where the model name is singlular. The _versions table should
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
#
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
# then that field is reflected in the versioned model as 'versioned_type' by default.
#
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
#
# class AddVersions < ActiveRecord::Migration
# def self.up
# # create_versioned_table takes the same options hash
# # that create_table does
# Post.create_versioned_table
# end
#
# def self.down
# Post.drop_versioned_table
# end
# end
#
# == Changing What Fields Are Versioned
#
# By default, acts_as_versioned will version all but these fields:
#
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
#
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
#
# class Post < ActiveRecord::Base
# acts_as_versioned
# self.non_versioned_columns << 'comments_count'
# end
#
def acts_as_versioned(options = {}, &extension)
# don't allow multiple calls
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
send :include, ActiveRecord::Acts::Versioned::ActMethods
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
:version_association_options
# legacy
alias_method :non_versioned_fields, :non_versioned_columns
alias_method :non_versioned_fields=, :non_versioned_columns=
class << self
alias_method :non_versioned_fields, :non_versioned_columns
alias_method :non_versioned_fields=, :non_versioned_columns=
end
send :attr_accessor, :altered_attributes
self.versioned_class_name = options[:class_name] || "Version"
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
self.version_column = options[:version_column] || 'version'
self.version_sequence_name = options[:sequence_name]
self.max_version_limit = options[:limit].to_i
self.version_condition = options[:if] || true
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
self.version_association_options = {
:class_name => "#{self.to_s}::#{versioned_class_name}",
:foreign_key => versioned_foreign_key,
:dependent => :delete_all
}.merge(options[:association_options] || {})
if block_given?
extension_module_name = "#{versioned_class_name}Extension"
silence_warnings do
self.const_set(extension_module_name, Module.new(&extension))
end
options[:extend] = self.const_get(extension_module_name)
end
class_eval do
has_many :versions, version_association_options do
# finds earliest version of this record
def earliest
@earliest ||= find(:first, :order => 'version')
end
# find latest version of this record
def latest
@latest ||= find(:first, :order => 'version desc')
end
end
before_save :set_new_version
after_create :save_version_on_create
after_update :save_version
after_save :clear_old_versions
after_save :clear_altered_attributes
unless options[:if_changed].nil?
self.track_altered_attributes = true
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
options[:if_changed].each do |attr_name|
define_method("#{attr_name}=") do |value|
write_changed_attribute attr_name, value
end
end
end
include options[:extend] if options[:extend].is_a?(Module)
end
# create the dynamic versioned model
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
def self.reloadable? ; false ; end
# find first version before the given version
def self.before(version)
find :first, :order => 'version desc',
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
end
# find first version after the given version.
def self.after(version)
find :first, :order => 'version',
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
end
def previous
self.class.before(self)
end
def next
self.class.after(self)
end
def versions_count
page.version
end
end
versioned_class.cattr_accessor :original_class
versioned_class.original_class = self
versioned_class.set_table_name versioned_table_name
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
:class_name => "::#{self.to_s}",
:foreign_key => versioned_foreign_key
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end
end
module ActMethods
def self.included(base) # :nodoc:
base.extend ClassMethods
end
# Finds a specific version of this record
def find_version(version = nil)
self.class.find_version(id, version)
end
# Saves a version of the model if applicable
def save_version
save_version_on_create if save_version?
end
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
def save_version_on_create
rev = self.class.versioned_class.new
self.clone_versioned_model(self, rev)
rev.version = send(self.class.version_column)
rev.send("#{self.class.versioned_foreign_key}=", self.id)
rev.save
end
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
# Override this method to set your own criteria for clearing old versions.
def clear_old_versions
return if self.class.max_version_limit == 0
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
if excess_baggage > 0
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
self.class.versioned_class.connection.execute sql
end
end
def versions_count
version
end
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
def revert_to(version)
if version.is_a?(self.class.versioned_class)
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
else
return false unless version = versions.find_by_version(version)
end
self.clone_versioned_model(version, self)
self.send("#{self.class.version_column}=", version.version)
true
end
# Reverts a model to a given version and saves the model.
# Takes either a version number or an instance of the versioned model
def revert_to!(version)
revert_to(version) ? save_without_revision : false
end
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
def save_without_revision
save_without_revision!
true
rescue
false
end
def save_without_revision!
without_locking do
without_revision do
save!
end
end
end
# Returns an array of attribute keys that are versioned. See non_versioned_columns
def versioned_attributes
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
end
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
# If called with a single parameter, gets whether the parameter has changed.
def changed?(attr_name = nil)
attr_name.nil? ?
(!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
(altered_attributes && altered_attributes.include?(attr_name.to_s))
end
# keep old dirty? method
alias_method :dirty?, :changed?
# Clones a model. Used when saving a new version or reverting a model's version.
def clone_versioned_model(orig_model, new_model)
self.versioned_attributes.each do |key|
new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
end
if orig_model.is_a?(self.class.versioned_class)
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
elsif new_model.is_a?(self.class.versioned_class)
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
end
end
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
def save_version?
version_condition_met? && changed?
end
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
# custom version condition checking.
def version_condition_met?
case
when version_condition.is_a?(Symbol)
send(version_condition)
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
version_condition.call(self)
else
version_condition
end
end
# Executes the block with the versioning callbacks disabled.
#
# @foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
self.class.without_revision(&block)
end
# Turns off optimistic locking for the duration of the block
#
# @foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
self.class.without_locking(&block)
end
def empty_callback() end #:nodoc:
protected
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
def set_new_version
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
end
# Gets the next available version for the current record, or 1 for a new record
def next_version
return 1 if new_record?
(versions.calculate(:max, :version) || 0) + 1
end
# clears current changed attributes. Called after save.
def clear_altered_attributes
self.altered_attributes = []
end
def write_changed_attribute(attr_name, attr_value)
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
(self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
write_attribute(attr_name, attr_value_for_db)
end
module ClassMethods
# Finds a specific version of a specific row of this model
def find_version(id, version = nil)
return find(id) unless version
conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
options = { :conditions => conditions, :limit => 1 }
if result = find_versions(id, options).first
result
else
raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
end
end
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
def find_versions(id, options = {})
versioned_class.find :all, {
:conditions => ["#{versioned_foreign_key} = ?", id],
:order => 'version' }.merge(options)
end
# Returns an array of columns that are versioned. See non_versioned_columns
def versioned_columns
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
end
# Returns an instance of the dynamic versioned model
def versioned_class
const_get versioned_class_name
end
# Rake migration task to create the versioned table using options passed to acts_as_versioned
def create_versioned_table(create_table_options = {})
# create version column in main table if it does not exist
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
self.connection.add_column table_name, :version, :integer
end
self.connection.create_table(versioned_table_name, create_table_options) do |t|
t.column versioned_foreign_key, :integer
t.column :version, :integer
end
updated_col = nil
self.versioned_columns.each do |col|
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
self.connection.add_column versioned_table_name, col.name, col.type,
:limit => col.limit,
:default => col.default,
:scale => col.scale,
:precision => col.precision
end
if type_col = self.columns_hash[inheritance_column]
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
:limit => type_col.limit,
:default => type_col.default,
:scale => type_col.scale,
:precision => type_col.precision
end
if updated_col.nil?
self.connection.add_column versioned_table_name, :updated_at, :timestamp
end
end
# Rake migration task to drop the versioned table
def drop_versioned_table
self.connection.drop_table versioned_table_name
end
# Executes the block with the versioning callbacks disabled.
#
# Foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
class_eval do
CALLBACKS.each do |attr_name|
alias_method "orig_#{attr_name}".to_sym, attr_name
alias_method attr_name, :empty_callback
end
end
block.call
ensure
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, "orig_#{attr_name}".to_sym
end
end
end
# Turns off optimistic locking for the duration of the block
#
# Foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
current = ActiveRecord::Base.lock_optimistically
ActiveRecord::Base.lock_optimistically = false if current
result = block.call
ActiveRecord::Base.lock_optimistically = true if current
result
end
end
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,46 +0,0 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
if ActiveRecord::Base.connection.supports_migrations?
class Thing < ActiveRecord::Base
attr_accessor :version
acts_as_versioned
end
class MigrationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
def teardown
if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
ActiveRecord::Base.connection.initialize_schema_information
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
else
ActiveRecord::Base.connection.initialize_schema_migrations_table
ActiveRecord::Base.connection.assume_migrated_upto_version(0)
end
Thing.connection.drop_table "things" rescue nil
Thing.connection.drop_table "thing_versions" rescue nil
Thing.reset_column_information
end
def test_versioned_migration
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
# take 'er up
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
assert_equal 1, t.versions.size
# check that the price column has remembered its value correctly
assert_equal t.price, t.versions.first.price
assert_equal t.title, t.versions.first.title
assert_equal t[:type], t.versions.first[:type]
# make sure that the precision of the price column has been preserved
assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
# now lets take 'er back down
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
end
end
end

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

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