diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 2daee50de..debe02162 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -15,6 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +require 'uri' + class ApplicationController < ActionController::Base before_filter :user_setup, :check_if_login_required, :set_localization filter_parameter_logging :password @@ -77,8 +79,7 @@ class ApplicationController < ActionController::Base def require_login if !User.current.logged? - store_location - redirect_to :controller => "account", :action => "login" + redirect_to :controller => "account", :action => "login", :back_url => request.request_uri return false end true @@ -115,20 +116,16 @@ class ApplicationController < ActionController::Base end end - # store current uri in session. - # return to this location by calling redirect_back_or_default - def store_location - session[:return_to_params] = params - end - - # move to the last store_location call or to the passed default one def redirect_back_or_default(default) - if session[:return_to_params].nil? - redirect_to default - else - redirect_to session[:return_to_params] - session[:return_to_params] = nil + back_url = params[:back_url] + if !back_url.blank? + uri = URI.parse(back_url) + # do not redirect user to another host + if uri.relative? || (uri.host == request.host) + redirect_to(back_url) and return + end end + redirect_to default end def render_403 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3af77d1ac..ce4b24b8b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -226,94 +226,22 @@ class ProjectsController < ApplicationController @date_to ||= Date.today + 1 @date_from = @date_to - @days + @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - @event_types = %w(issues news files documents changesets wiki_pages messages) - if @project - @event_types.delete('wiki_pages') unless @project.wiki - @event_types.delete('changesets') unless @project.repository - @event_types.delete('messages') unless @project.boards.any? - # only show what the user is allowed to view - @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)} - @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1') - end - @scope = @event_types.select {|t| params["show_#{t}"]} - # default events if none is specified in parameters - @scope = (@event_types - %w(wiki_pages messages))if @scope.empty? - - @events = [] - - if @scope.include?('issues') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions) - - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - cond.add("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''") - @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions) - end - - if @scope.include?('news') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions) - end - - if @scope.include?('files') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Attachment.find(:all, :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", - :conditions => cond.conditions) - end - - if @scope.include?('documents') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Document.find(:all, :include => :project, :conditions => cond.conditions) - - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Attachment.find(:all, :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", - :conditions => cond.conditions) - end - - if @scope.include?('wiki_pages') - 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" + @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects) + @activity.scope_select {|t| !params["show_#{t}"].nil?} + @activity.default_scope! if @activity.scope.empty? - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions) - end - - if @scope.include?('changesets') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions) - end - - if @scope.include?('messages') - cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects)) - cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) - @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions) - end - - @events_by_day = @events.group_by(&:event_date) + events = @activity.events(@date_from, @date_to) respond_to do |format| - format.html { render :layout => false if request.xhr? } + format.html { + @events_by_day = events.group_by(&:event_date) + render :layout => false if request.xhr? + } format.atom { title = (@scope.size == 1) ? l("label_#{@scope.first.singularize}_plural") : l(:label_activity) - render_feed(@events, :title => "#{@project || Setting.app_title}: #{title}") + render_feed(events, :title => "#{@project || Setting.app_title}: #{title}") } end end diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index 8655cfed7..5a5f3949f 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -147,6 +147,7 @@ class WikiController < ApplicationController :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id", :order => 'title' @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} + @pages_by_parent_id = @pages.group_by(&:parent_id) # export wiki to a single html file when 'export' @pages = @wiki.pages.find :all, :order => 'title' @@ -164,7 +165,10 @@ class WikiController < ApplicationController page = @wiki.find_page(params[:page]) # page is nil when previewing a new page return render_403 unless page.nil? || editable?(page) - @attachements = page.attachments if page + if page + @attachements = page.attachments + @previewed = page.content + end @text = params[:content][:text] render :partial => 'common/preview' end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6e39d093f..a4102c84a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -177,7 +177,8 @@ module ApplicationHelper end def breadcrumb(*args) - content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') + elements = args.flatten + elements.any? ? content_tag('p', args.join(' » ') + ' » ', :class => 'breadcrumb') : nil end def html_title(*args) @@ -205,7 +206,7 @@ module ApplicationHelper options = args.last.is_a?(Hash) ? args.pop : {} case args.size when 1 - obj = nil + obj = options[:object] text = args.shift when 2 obj = args.shift @@ -245,12 +246,12 @@ module ApplicationHelper case options[:wiki_links] when :local # used for local links to html files - format_wiki_link = Proc.new {|project, title| "#{title}.html" } + format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" } when :anchor # used for single-file wiki export - format_wiki_link = Proc.new {|project, title| "##{title}" } + format_wiki_link = Proc.new {|project, title, anchor| "##{title}" } else - format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) } + format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) } end project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) @@ -276,9 +277,14 @@ module ApplicationHelper end if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /^(.+?)\#(.+)$/ + page, anchor = $1, $2 + end # check if page exists wiki_page = link_project.wiki.find_page(page) - link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)), + link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor), :class => ('wiki-page' + (wiki_page ? '' : ' new'))) else # project or wiki doesn't exist @@ -451,7 +457,8 @@ module ApplicationHelper end def back_url_hidden_field_tag - hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER']) + back_url = params[:back_url] || request.env['HTTP_REFERER'] + hidden_field_tag('back_url', back_url) unless back_url.blank? end def check_all_links(form_name) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 92f2da8a5..cd96dbd3f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -18,7 +18,8 @@ module SearchHelper def highlight_tokens(text, tokens) return text unless text && tokens && !tokens.empty? - regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE + re_tokens = tokens.collect {|t| Regexp.escape(t)} + regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE result = '' text.split(regexp).each_with_index do |words, i| if result.length > 1200 diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 980035bd4..0a6b810de 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -17,6 +17,22 @@ module WikiHelper + def render_page_hierarchy(pages, node=nil) + content = '' + if pages[node] + content << "\n" + end + content + end + def html_diff(wdiff) words = wdiff.words.collect{|word| h(word)} words_add = 0 diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 8f3f530c7..95ba8491f 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -28,6 +28,18 @@ class Attachment < ActiveRecord::Base acts_as_event :title => :filename, :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} + acts_as_activity_provider :type => 'files', + :permission => :view_files, + :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"} + + acts_as_activity_provider :type => 'documents', + :permission => :view_documents, + :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"} + cattr_accessor :storage_path @@storage_path = "#{RAILS_ROOT}/files" diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 41f5ed86a..0663af34e 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -30,6 +30,9 @@ class Changeset < ActiveRecord::Base :include => {:repository => :project}, :project_key => "#{Repository.table_name}.project_id", :date_column => 'committed_on' + + acts_as_activity_provider :timestamp => "#{table_name}.committed_on", + :find_options => {:include => {:repository => :project}} validates_presence_of :repository_id, :revision, :committed_on, :commit_date validates_uniqueness_of :revision, :scope => :repository_id diff --git a/app/models/document.rb b/app/models/document.rb index 9e2818fc7..627a2418f 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -24,7 +24,8 @@ class Document < ActiveRecord::Base 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 end diff --git a/app/models/issue.rb b/app/models/issue.rb index cae603dd8..7ebe9db99 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -42,6 +42,8 @@ class Issue < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} + acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]} + validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status validates_length_of :subject, :maximum => 255 validates_inclusion_of :done_ratio, :in => 0..100 @@ -77,7 +79,9 @@ class Issue < ActiveRecord::Base self.relations_to.clear end # issue is moved to another project - self.category = nil + # reassign to the category with same name if any + new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name) + self.category = new_category self.fixed_version = nil self.project = new_project end diff --git a/app/models/journal.rb b/app/models/journal.rb index a427f84e3..71a51290b 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -31,6 +31,12 @@ class Journal < ActiveRecord::Base :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, + :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 # Do not save an empty journal (details.empty? && notes.blank?) ? false : super diff --git a/app/models/message.rb b/app/models/message.rb index 888bffcc3..80df7a33a 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -31,7 +31,9 @@ class Message < ActiveRecord::Base :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, :anchor => "message-#{o.id}"})} - + + acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]} + attr_protected :locked, :sticky validates_presence_of :subject, :content validates_length_of :subject, :maximum => 255 diff --git a/app/models/news.rb b/app/models/news.rb index 71e2a2d5e..4c4943b78 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -26,7 +26,8 @@ class News < ActiveRecord::Base acts_as_searchable :columns => ['title', "#{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]} + # returns latest news for projects visible by user def self.latest(user=nil, count=5) find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") diff --git a/app/models/query.rb b/app/models/query.rb index 27ab882c6..0ce9a6a21 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -88,7 +88,7 @@ class Query < ActiveRecord::Base :date_past => [ ">t-", " [ "=", "~", "!", "!~" ], :text => [ "~", "!~" ], - :integer => [ "=", ">=", "<=" ] } + :integer => [ "=", ">=", "<=", "!*", "*" ] } cattr_reader :operators_by_filter_type @@ -152,7 +152,8 @@ class Query < ActiveRecord::Base "updated_on" => { :type => :date_past, :order => 10 }, "start_date" => { :type => :date, :order => 11 }, "due_date" => { :type => :date, :order => 12 }, - "done_ratio" => { :type => :integer, :order => 13 }} + "estimated_hours" => { :type => :integer, :order => 13 }, + "done_ratio" => { :type => :integer, :order => 14 }} user_values = [] user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 724354ad6..f2ee39c4d 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -35,6 +35,17 @@ class WikiContent < ActiveRecord::Base :type => 'wiki-page', :url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}} + acts_as_activity_provider :type => 'wiki_pages', + :timestamp => "#{WikiContent.versioned_table_name}.updated_on", + :permission => :view_wiki_pages, + :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"} + def text=(plain) case Setting.wiki_compression when 'gzip' diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 65fc1f68c..2416fab74 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -22,7 +22,8 @@ class WikiPage < ActiveRecord::Base belongs_to :wiki has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy has_many :attachments, :as => :container, :dependent => :destroy - + acts_as_tree :order => 'title' + acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"}, :description => :text, :datetime => :created_on, @@ -110,6 +111,24 @@ class WikiPage < ActiveRecord::Base def editable_by?(usr) !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project) end + + def parent_title + @parent_title || (self.parent && self.parent.pretty_title) + end + + def parent_title=(t) + @parent_title = t + parent_page = t.blank? ? nil : self.wiki.find_page(t) + self.parent = parent_page + end + + protected + + def validate + errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil? + errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self)) + errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id) + end end class WikiDiff diff --git a/app/views/account/login.rhtml b/app/views/account/login.rhtml index ea1a1cd44..d8c1f313f 100644 --- a/app/views/account/login.rhtml +++ b/app/views/account/login.rhtml @@ -1,5 +1,6 @@
<% form_tag({:action=> "login"}) do %> +<%= back_url_hidden_field_tag %> diff --git a/app/views/common/_preview.rhtml b/app/views/common/_preview.rhtml index e3bfc3a25..fd95f1188 100644 --- a/app/views/common/_preview.rhtml +++ b/app/views/common/_preview.rhtml @@ -1,3 +1,3 @@
<%= l(:label_preview) %> -<%= textilizable @text, :attachments => @attachements %> +<%= textilizable @text, :attachments => @attachements, :object => @previewed %>
diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml index af56782e8..0cdcd4733 100644 --- a/app/views/issues/context_menu.rhtml +++ b/app/views/issues/context_menu.rhtml @@ -44,6 +44,19 @@ :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %> + <% unless @project.issue_categories.empty? -%> +
  • + <%= l(:field_category) %> +
      + <% @project.issue_categories.each do |u| -%> +
    • <%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[category_id]' => u, :back_to => @back}, :method => :post, + :selected => (u == @issue.category), :disabled => !@can[:update] %>
    • + <% end -%> +
    • <%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[category_id]' => '', :back_to => @back}, :method => :post, + :selected => @issue.category.nil?, :disabled => !@can[:update] %>
    • +
    +
  • + <% end -%>
  • <%= l(:field_done_ratio) %>
      diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index c08cd06f9..fa25812ac 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -44,8 +44,8 @@ <% content_for :sidebar do %> <% form_tag({}, :method => :get) do %>

      <%= l(:label_activity) %>

      -

      <% @event_types.each do |t| %> -
      +

      <% @activity.event_types.each do |t| %> +
      <% end %>

      <% if @project && @project.active_children.any? %>

      diff --git a/app/views/wiki/rename.rhtml b/app/views/wiki/rename.rhtml index 0c069f43d..260f9af8b 100644 --- a/app/views/wiki/rename.rhtml +++ b/app/views/wiki/rename.rhtml @@ -4,8 +4,9 @@ <% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %>
      -

      <%= f.text_field :title, :required => true, :size => 255 %>

      +

      <%= f.text_field :title, :required => true, :size => 100 %>

      <%= f.check_box :redirect_existing_links %>

      +

      <%= f.text_field :parent_title, :size => 100 %>

      <%= submit_tag l(:button_rename) %> <% end %> diff --git a/app/views/wiki/show.rhtml b/app/views/wiki/show.rhtml index 8092525bd..255b904f5 100644 --- a/app/views/wiki/show.rhtml +++ b/app/views/wiki/show.rhtml @@ -10,6 +10,8 @@ <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %> +<%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %> + <% if @content.version != @page.content.version %>

      <%= link_to(('« ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %> diff --git a/app/views/wiki/special_page_index.rhtml b/app/views/wiki/special_page_index.rhtml index f21cc3423..72b395ef7 100644 --- a/app/views/wiki/special_page_index.rhtml +++ b/app/views/wiki/special_page_index.rhtml @@ -4,11 +4,7 @@

      <%= l(:label_no_data) %>

      <% end %> -
        <% @pages.each do |page| %> -
      • <%= link_to page.pretty_title, {:action => 'index', :page => page.title}, - :title => l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) %> -
      • -<% end %>
      +<%= render_page_hierarchy(@pages_by_parent_id) %> <% content_for :sidebar do %> <%= render :partial => 'sidebar' %> diff --git a/db/migrate/095_add_wiki_pages_parent_id.rb b/db/migrate/095_add_wiki_pages_parent_id.rb new file mode 100644 index 000000000..36b922ec1 --- /dev/null +++ b/db/migrate/095_add_wiki_pages_parent_id.rb @@ -0,0 +1,9 @@ +class AddWikiPagesParentId < ActiveRecord::Migration + def self.up + add_column :wiki_pages, :parent_id, :integer, :default => nil + end + + def self.down + remove_column :wiki_pages, :parent_id + end +end diff --git a/extra/sample_plugin/app/models/meeting.rb b/extra/sample_plugin/app/models/meeting.rb new file mode 100644 index 000000000..c1bb64a93 --- /dev/null +++ b/extra/sample_plugin/app/models/meeting.rb @@ -0,0 +1,11 @@ +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 } +end diff --git a/extra/sample_plugin/db/migrate/001_create_meetings.rb b/extra/sample_plugin/db/migrate/001_create_meetings.rb new file mode 100644 index 000000000..fec9c8bd1 --- /dev/null +++ b/extra/sample_plugin/db/migrate/001_create_meetings.rb @@ -0,0 +1,15 @@ +# Sample plugin migration +# Use rake db:migrate_plugins to migrate installed plugins +class CreateMeetings < ActiveRecord::Migration + def self.up + create_table :meetings do |t| + t.column :project_id, :integer, :null => false + t.column :description, :string + t.column :scheduled_on, :datetime + end + end + + def self.down + drop_table :meetings + end +end diff --git a/extra/sample_plugin/db/migrate/001_create_some_models.rb b/extra/sample_plugin/db/migrate/001_create_some_models.rb deleted file mode 100644 index 39d58a649..000000000 --- a/extra/sample_plugin/db/migrate/001_create_some_models.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Sample plugin migration -# Use rake db:migrate_plugins to migrate installed plugins -class CreateSomeModels < ActiveRecord::Migration - def self.up - create_table :example_plugin_model, :force => true do |t| - t.column "example_attribute", :integer - end - end - - def self.down - drop_table :example_plugin_model - end -end diff --git a/extra/sample_plugin/init.rb b/extra/sample_plugin/init.rb index 7389aaa6f..5c543338c 100644 --- a/extra/sample_plugin/init.rb +++ b/extra/sample_plugin/init.rb @@ -18,8 +18,13 @@ Redmine::Plugin.register :sample_plugin do # This permission has to be explicitly given # It will be listed on the permissions screen permission :example_say_goodbye, {:example => [:say_goodbye]} + # This permission can be given to project members only + permission :view_meetings, {:meetings => [:index, :show]}, :require => :member end # A new item is added to the project menu menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample' + + # Meetings are added to the activity view + activity_provider :meetings end diff --git a/extra/sample_plugin/lang/en.yml b/extra/sample_plugin/lang/en.yml index bf62bc344..c4005a764 100644 --- a/extra/sample_plugin/lang/en.yml +++ b/extra/sample_plugin/lang/en.yml @@ -1,4 +1,5 @@ # Sample plugin label_plugin_example: Sample Plugin +label_meeting_plural: Meetings text_say_hello: Plugin say 'Hello' text_say_goodbye: Plugin say 'Good bye' diff --git a/extra/sample_plugin/lang/fr.yml b/extra/sample_plugin/lang/fr.yml index 2c0829c32..135050a5a 100644 --- a/extra/sample_plugin/lang/fr.yml +++ b/extra/sample_plugin/lang/fr.yml @@ -1,4 +1,5 @@ # Sample plugin label_plugin_example: Plugin exemple +label_meeting_plural: Meetings text_say_hello: Plugin dit 'Bonjour' text_say_goodbye: Plugin dit 'Au revoir' diff --git a/lang/bg.yml b/lang/bg.yml index c87e3bebc..e8191dc20 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/cs.yml b/lang/cs.yml index 6669c6cf3..68273191a 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -637,3 +637,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/da.yml b/lang/da.yml index cf40e1763..1b5aba69c 100644 --- a/lang/da.yml +++ b/lang/da.yml @@ -634,3 +634,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/de.yml b/lang/de.yml index adc3dd4e9..dc2960c9c 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/en.yml b/lang/en.yml index 791094674..3cc788b8c 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -182,6 +182,7 @@ field_time_zone: Time zone field_searchable: Searchable field_default_value: Default value field_comments_sorting: Display comments +field_parent_title: Parent page setting_app_title: Application title setting_app_subtitle: Application subtitle diff --git a/lang/es.yml b/lang/es.yml index 4205402e4..c753f367f 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -635,3 +635,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/fi.yml b/lang/fi.yml index b69b07557..b48d8a319 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/fr.yml b/lang/fr.yml index 5b0f04fb0..b9d542da6 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -183,6 +183,7 @@ field_time_zone: Fuseau horaire field_searchable: Utilisé pour les recherches field_default_value: Valeur par défaut field_comments_sorting: Afficher les commentaires +field_parent_title: Page parent setting_app_title: Titre de l'application setting_app_subtitle: Sous-titre de l'application diff --git a/lang/he.yml b/lang/he.yml index 030e09dcc..63aa2bb64 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/hu.yml b/lang/hu.yml index 630294c9d..9becbb349 100644 --- a/lang/hu.yml +++ b/lang/hu.yml @@ -633,3 +633,4 @@ label_generate_key: Kulcs generálása setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez setting_mail_handler_api_key: API kulcs text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÁllítsd be az SMTP szervert a config/email.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen." +field_parent_title: Parent page diff --git a/lang/it.yml b/lang/it.yml index 471ef49ed..e7a8772cc 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -632,3 +632,4 @@ label_generate_key: Genera una chiave setting_mail_handler_api_enabled: Abilita WS per le e-mail in arrivo setting_mail_handler_api_key: chiave API text_email_delivery_not_configured: "La consegna via e-mail non è configurata e le notifiche sono disabilitate.\nConfigura il tuo server SMTP in config/email.yml e riavvia l'applicazione per abilitarle." +field_parent_title: Parent page diff --git a/lang/ja.yml b/lang/ja.yml index cba410f6d..f78fcdba4 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/ko.yml b/lang/ko.yml index 2b5b0eb0a..77672c601 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/lt.yml b/lang/lt.yml index 52b6b6fc8..706133bfe 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -635,3 +635,4 @@ setting_mail_handler_api_enabled: Įgalinti WS įeinantiems laiškams setting_mail_handler_api_key: API raktas text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/nl.yml b/lang/nl.yml index 09cdc3c08..3cfb0aa19 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/no.yml b/lang/no.yml index eb1ff59ca..0836bd48f 100644 --- a/lang/no.yml +++ b/lang/no.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/pl.yml b/lang/pl.yml index 9c5a382b0..278d56a88 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/pt-br.yml b/lang/pt-br.yml index 1582e568f..9276d768e 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/pt.yml b/lang/pt.yml index ece12cd97..a7647e31f 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/ro.yml b/lang/ro.yml index 50bbf29af..c3640d7e1 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -632,3 +632,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/ru.yml b/lang/ru.yml index c0cac30a0..e8bda05b7 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -636,3 +636,4 @@ label_generate_key: Сгенерировать ключ setting_mail_handler_api_enabled: Включить веб-сервис для входящих сообщений setting_mail_handler_api_key: API ключ text_email_delivery_not_configured: "Параметры работы с почтовым сервером не настроены и функция уведомления по email не активна.\nНастроить параметры для вашего SMTP сервера вы можете в файле config/email.yml. Для применения изменений перезапустите приложение." +field_parent_title: Parent page diff --git a/lang/sr.yml b/lang/sr.yml index fa1b4d847..688fa8ede 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/sv.yml b/lang/sv.yml index 9015099e7..58507a0b7 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -633,3 +633,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/th.yml b/lang/th.yml index 27f7b14cf..a7f949c96 100644 --- a/lang/th.yml +++ b/lang/th.yml @@ -635,3 +635,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/uk.yml b/lang/uk.yml index 0286779be..766ddaaab 100644 --- a/lang/uk.yml +++ b/lang/uk.yml @@ -634,3 +634,4 @@ label_generate_key: Generate a key setting_mail_handler_api_enabled: Enable WS for incoming emails setting_mail_handler_api_key: API key text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lang/zh-tw.yml b/lang/zh-tw.yml index bad6fcca5..82f26ab86 100644 --- a/lang/zh-tw.yml +++ b/lang/zh-tw.yml @@ -633,3 +633,4 @@ default_activity_development: 開發 enumeration_issue_priorities: 項目優先權 enumeration_doc_categories: 文件分類 enumeration_activities: 活動 (時間追蹤) +field_parent_title: Parent page diff --git a/lang/zh.yml b/lang/zh.yml index 1cde9f502..b4ae89ba7 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -633,3 +633,4 @@ enumeration_issue_priorities: 问题优先级 enumeration_doc_categories: 文档类别 enumeration_activities: 活动(时间跟踪) text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them." +field_parent_title: Parent page diff --git a/lib/redmine.rb b/lib/redmine.rb index 22b60e949..bd413c96c 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -1,5 +1,6 @@ require 'redmine/access_control' require 'redmine/menu_manager' +require 'redmine/activity' require 'redmine/mime_type' require 'redmine/core_ext' require 'redmine/themes' @@ -132,3 +133,13 @@ Redmine::MenuManager.map :project_menu do |menu| :if => Proc.new { |p| p.repository && !p.repository.new_record? } menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true end + +Redmine::Activity.map do |activity| + activity.register :issues, :class_name => %w(Issue Journal) + activity.register :changesets + activity.register :news + activity.register :documents, :class_name => %w(Document Attachment) + activity.register :files, :class_name => 'Attachment' + activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false + activity.register :messages, :default => false +end diff --git a/lib/redmine/activity.rb b/lib/redmine/activity.rb new file mode 100644 index 000000000..565a53f36 --- /dev/null +++ b/lib/redmine/activity.rb @@ -0,0 +1,46 @@ +# 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 Redmine + module Activity + + mattr_accessor :available_event_types, :default_event_types, :providers + + @@available_event_types = [] + @@default_event_types = [] + @@providers = Hash.new {|h,k| h[k]=[] } + + class << self + def map(&block) + yield self + end + + # Registers an activity provider + def register(event_type, options={}) + options.assert_valid_keys(:class_name, :default) + + event_type = event_type.to_s + providers = options[:class_name] || event_type.classify + providers = ([] << providers) unless providers.is_a?(Array) + + @@available_event_types << event_type unless @@available_event_types.include?(event_type) + @@default_event_types << event_type unless options[:default] == false + @@providers[event_type] += providers + end + end + end +end diff --git a/lib/redmine/activity/fetcher.rb b/lib/redmine/activity/fetcher.rb new file mode 100644 index 000000000..adaead564 --- /dev/null +++ b/lib/redmine/activity/fetcher.rb @@ -0,0 +1,79 @@ +# 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 Redmine + module Activity + # 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) + @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| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project + @event_types + end + + # Yields to filter the activity scope + def scope_select(&block) + @scope = @scope.select {|t| yield t } + end + + # Sets the scope + def scope=(s) + @scope = s & event_types + 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 + def events(from, to) + e = [] + + @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 + end + + private + + def constantized_providers(event_type) + @@constantized_providers[event_type] + end + end + end +end diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 69c7ec076..f34f16c62 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -128,6 +128,32 @@ module Redmine #:nodoc: def add_hook(hook_name, method) Redmine::Plugin::Hook::Manager.add_listener(hook_name, method) end + + # Registers an activity provider. + # + # Options: + # * :class_name - one or more model(s) that provide these events (inferred from event_type by default) + # * :default - setting this option to false will make the events not displayed by default + # + # A model can provide several activity event types. + # + # Examples: + # register :news + # register :scrums, :class_name => 'Meeting' + # register :issues, :class_name => ['Issue', 'Journal'] + # + # Retrieving events: + # Associated model(s) must implement the find_events class method. + # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method. + # + # The following call should return all the scrum events visible by current user that occured in the 5 last days: + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today) + # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only + # + # Note that :view_scrums permission is required to view these events in the activity view. + def activity_provider(*args) + Redmine::Activity.register(*args) + end # Returns +true+ if the plugin can be configured. def configurable? diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index 11ec6803a..8c18d547f 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -65,10 +65,22 @@ module Redmine # Patch to add 'table of content' support to RedCloth def textile_p_withtoc(tag, atts, cite, content) - if tag =~ /^h(\d)$/ - @toc << [$1.to_i, content] + # removes wiki links from the item + toc_item = content.gsub(/(\[\[|\]\])/, '') + # removes styles + # eg. %{color:red}Triggers% => Triggers + toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' + + # replaces non word caracters by dashes + anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + + unless anchor.blank? + if tag =~ /^h(\d)$/ + @toc << [$1.to_i, anchor, toc_item] + end + atts << " id=\"#{anchor}\"" + content = content + "" end - content = "" + content textile_p(tag, atts, cite, content) end @@ -82,13 +94,9 @@ module Redmine div_class << ' right' if $1 == '>' div_class << ' left' if $1 == '<' out = "
        " - @toc.each_with_index do |heading, index| - # remove wiki links from the item - toc_item = heading.last.gsub(/(\[\[|\]\])/, '') - # remove styles - # eg. %{color:red}Triggers% => Triggers - toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1' - out << "
      • #{toc_item}
      • \n" + @toc.each do |heading| + level, anchor, toc_item = heading + out << "
      • #{toc_item}
      • \n" end out << '
      ' out diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index 0848aee4e..adfc590e4 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -77,6 +77,12 @@ module Redmine content_tag('dl', out) end + desc "Displays a list of child pages." + macro :child_pages do |obj, args| + raise 'This macro applies to wiki pages only.' unless obj.is_a?(WikiContent) + render_page_hierarchy(obj.page.descendants.group_by(&:parent_id), obj.page.id) + end + desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" macro :include do |obj, args| project = @project diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 9ae4d1595..c7e9284d2 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -487,6 +487,10 @@ div.wiki ul.toc a { } div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} +a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } +a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } + /***** My page layout *****/ .block-receiver { border:1px dashed #c0c0c0; diff --git a/public/stylesheets/context_menu.css b/public/stylesheets/context_menu.css index a88b6c6c8..b3aa1aca0 100644 --- a/public/stylesheets/context_menu.css +++ b/public/stylesheets/context_menu.css @@ -1,4 +1,4 @@ -#context-menu { position: absolute; z-index: 40;} +#context-menu { position: absolute; z-index: 40; font-size: 0.9em;} #context-menu ul, #context-menu li, #context-menu a { display:block; @@ -31,10 +31,10 @@ #context-menu a { border:1px solid white; - text-decoration:none; + text-decoration:none !important; background-repeat: no-repeat; background-position: 1px 50%; - padding: 2px 0px 2px 20px; + padding: 1px 0px 1px 20px; width:100%; /* IE */ } #context-menu li>a { width:auto; } /* others */ diff --git a/test/fixtures/issue_categories.yml b/test/fixtures/issue_categories.yml index 2b74b5977..aa2f70351 100644 --- a/test/fixtures/issue_categories.yml +++ b/test/fixtures/issue_categories.yml @@ -14,4 +14,9 @@ issue_categories_003: project_id: 2 assigned_to_id: id: 3 +issue_categories_004: + name: Printing + project_id: 2 + assigned_to_id: + id: 4 \ No newline at end of file diff --git a/test/fixtures/wiki_contents.yml b/test/fixtures/wiki_contents.yml index 5d6d3f1de..8c53d4d97 100644 --- a/test/fixtures/wiki_contents.yml +++ b/test/fixtures/wiki_contents.yml @@ -2,7 +2,7 @@ wiki_contents_001: text: |- h1. CookBook documentation - + {{child_pages}} Some updated [[documentation]] here with gzipped history updated_on: 2007-03-07 00:10:51 +01:00 page_id: 1 diff --git a/test/fixtures/wiki_pages.yml b/test/fixtures/wiki_pages.yml index ef7242430..e285441ff 100644 --- a/test/fixtures/wiki_pages.yml +++ b/test/fixtures/wiki_pages.yml @@ -4,23 +4,27 @@ wiki_pages_001: title: CookBook_documentation id: 1 wiki_id: 1 - protected: true + protected: true + parent_id: wiki_pages_002: created_on: 2007-03-08 00:18:07 +01:00 title: Another_page id: 2 wiki_id: 1 protected: false + parent_id: wiki_pages_003: created_on: 2007-03-08 00:18:07 +01:00 title: Start_page id: 3 wiki_id: 2 protected: false + parent_id: wiki_pages_004: created_on: 2007-03-08 00:18:07 +01:00 title: Page_with_an_inline_image id: 4 wiki_id: 1 protected: false + parent_id: 1 \ No newline at end of file diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb index 666acf0dd..26218d177 100644 --- a/test/functional/account_controller_test.rb +++ b/test/functional/account_controller_test.rb @@ -44,6 +44,17 @@ class AccountControllerTest < Test::Unit::TestCase assert_nil assigns(:user) end + def test_login_should_redirect_to_back_url_param + # request.uri is "test.host" in test environment + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1' + assert_redirected_to '/issues/show/1' + end + + def test_login_should_not_redirect_to_another_host + post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake' + assert_redirected_to '/my/page' + end + def test_login_with_wrong_password post :login, :username => 'admin', :password => 'bad' assert_response :success diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 4f0a2f179..935d5ba7c 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -153,10 +153,6 @@ class ProjectsControllerTest < Test::Unit::TestCase assert_response :success assert_template 'activity' assert_not_nil assigns(:events_by_day) - assert_not_nil assigns(:events) - - # subproject issue not included by default - assert !assigns(:events).include?(Issue.find(5)) assert_tag :tag => "h3", :content => /#{2.days.ago.to_date.day}/, @@ -168,7 +164,9 @@ class ProjectsControllerTest < Test::Unit::TestCase } } } - + end + + def test_previous_project_activity get :activity, :id => 1, :from => 3.days.ago.to_date assert_response :success assert_template 'activity' @@ -186,53 +184,24 @@ class ProjectsControllerTest < Test::Unit::TestCase } end - def test_activity_with_subprojects - get :activity, :id => 1, :with_subprojects => 1 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - assert !assigns(:events).include?(Issue.find(4)) - # subproject issue - assert assigns(:events).include?(Issue.find(5)) - end - - def test_global_activity_anonymous + def test_global_activity get :activity assert_response :success assert_template 'activity' - assert_not_nil assigns(:events) + assert_not_nil assigns(:events_by_day) - assert assigns(:events).include?(Issue.find(1)) - # Issue of a private project - assert !assigns(:events).include?(Issue.find(4)) + assert_tag :tag => "h3", + :content => /#{5.day.ago.to_date.day}/, + :sibling => { :tag => "dl", + :child => { :tag => "dt", + :attributes => { :class => /issue/ }, + :child => { :tag => "a", + :content => /#{Issue.find(5).subject}/, + } + } + } end - def test_global_activity_logged_user - @request.session[:user_id] = 2 # manager - get :activity - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - # Issue of a private project the user belongs to - assert assigns(:events).include?(Issue.find(4)) - end - - - def test_global_activity_with_all_types - get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1 - assert_response :success - assert_template 'activity' - assert_not_nil assigns(:events) - - assert assigns(:events).include?(Issue.find(1)) - assert !assigns(:events).include?(Issue.find(4)) - assert assigns(:events).include?(Message.find(5)) - end - def test_calendar get :calendar, :id => 1 assert_response :success diff --git a/test/functional/wiki_controller_test.rb b/test/functional/wiki_controller_test.rb index f1ae7a9c2..bbfdc8e7f 100644 --- a/test/functional/wiki_controller_test.rb +++ b/test/functional/wiki_controller_test.rb @@ -32,10 +32,16 @@ class WikiControllerTest < Test::Unit::TestCase end def test_show_start_page - get :index, :id => 1 + get :index, :id => 'ecookbook' assert_response :success assert_template 'show' assert_tag :tag => 'h1', :content => /CookBook documentation/ + + # child_pages macro + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } end def test_show_page_with_name @@ -163,8 +169,16 @@ class WikiControllerTest < Test::Unit::TestCase pages = assigns(:pages) assert_not_nil pages assert_equal Project.find(1).wiki.pages.size, pages.size - assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, - :content => /CookBook documentation/ + + assert_tag :ul, :attributes => { :class => 'pages-hierarchy' }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' }, + :content => 'CookBook documentation' }, + :child => { :tag => 'ul', + :child => { :tag => 'li', + :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' }, + :content => 'Page with an inline image' } } } }, + :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' }, + :content => 'Another page' } } end def test_not_found diff --git a/test/unit/activity_test.rb b/test/unit/activity_test.rb new file mode 100644 index 000000000..ccda9f119 --- /dev/null +++ b/test/unit/activity_test.rb @@ -0,0 +1,71 @@ +# 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. + +require File.dirname(__FILE__) + '/../test_helper' + +class ActivityTest < Test::Unit::TestCase + fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + + def setup + @project = Project.find(1) + end + + 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 + assert !events.include?(Issue.find(5)) + end + + def test_activity_with_subprojects + events = find_events(User.anonymous, :project => @project, :with_subprojects => 1) + assert_not_nil events + + assert events.include?(Issue.find(1)) + # 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)) + 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)) + end + + private + + def find_events(user, options={}) + Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1) + end +end diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 9559b76cc..979321bd3 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -133,6 +133,9 @@ class ApplicationHelperTest < HelperTestCase to_test = { '[[CookBook documentation]]' => 'CookBook documentation', '[[Another page|Page]]' => 'Page', + # link with anchor + '[[CookBook documentation#One-section]]' => 'CookBook documentation', + '[[Another page#anchor|Page]]' => 'Page', # page that doesn't exist '[[Unknown page]]' => 'Unknown page', '[[Unknown page|404]]' => '404', @@ -214,14 +217,14 @@ h1. Another title RAW - expected = '
      ' + - 'Title' + - 'Subtitle' + - 'Subtitle with red text' + - 'Another title' + - '
      ' + expected = '' - assert textilizable(raw).include?(expected) + assert textilizable(raw).gsub("\n", "").include?(expected) end def test_blockquote diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 0d98f89d2..12b4da336 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -165,17 +165,26 @@ class IssueTest < Test::Unit::TestCase assert !issue1.reload.closed? end - def test_move_to_another_project + def test_move_to_another_project_with_same_category issue = Issue.find(1) assert issue.move_to(Project.find(2)) issue.reload assert_equal 2, issue.project_id - # Category removed - assert_nil issue.category + # Category changes + assert_equal 4, issue.category_id # 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.find(2)) + issue.reload + assert_equal 2, issue.project_id + # Category cleared + assert_nil issue.category_id + end + def test_issue_destroy Issue.find(1).destroy assert_nil Issue.find_by_id(1) diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 3f77cd835..c243dfbad 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -26,11 +26,17 @@ class QueryTest < Test::Unit::TestCase assert !query.available_filters.has_key?('cf_3') end + def find_issues_with_query(query) + Issue.find :all, + :include => [ :assigned_to, :status, :tracker, :project, :priority ], + :conditions => query.statement + end + def test_query_with_multiple_custom_fields query = Query.find(1) assert query.valid? assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + issues = find_issues_with_query(query) assert_equal 1, issues.length assert_equal Issue.find(3), issues.first end @@ -41,72 +47,80 @@ class QueryTest < Test::Unit::TestCase query.add_filter('cf_1', '!*', ['']) assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL") assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end + def test_operator_none_for_integer + query = Query.new(:project => Project.find(1), :name => '_') + query.add_filter('estimated_hours', '!*', ['']) + issues = find_issues_with_query(query) + assert !issues.empty? + assert issues.all? {|i| !i.estimated_hours} + end + def test_operator_all query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('fixed_version_id', '*', ['']) query.add_filter('cf_1', '*', ['']) assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL") assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_greater_than query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('done_ratio', '>=', ['40']) assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_in_more_than query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', '>t+', ['15']) assert query.statement.include?("#{Issue.table_name}.due_date >=") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_in_less_than query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', ' [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_today query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', 't', ['']) assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_this_week_on_date query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('due_date', 'w', ['']) assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_this_week_on_datetime query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('created_on', 'w', ['']) assert query.statement.include?("#{Issue.table_name}.created_on BETWEEN") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_contains query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('subject', '~', ['string']) assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_operator_does_not_contains query = Query.new(:project => Project.find(1), :name => '_') query.add_filter('subject', '!~', ['string']) assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'") - issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement + find_issues_with_query(query) end def test_default_columns diff --git a/test/unit/wiki_page_test.rb b/test/unit/wiki_page_test.rb index bb8111176..e5ebeeea6 100644 --- a/test/unit/wiki_page_test.rb +++ b/test/unit/wiki_page_test.rb @@ -48,6 +48,50 @@ class WikiPageTest < Test::Unit::TestCase 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' + assert page.save + 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 = '' + assert page.save + 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 + assert_equal :activerecord_error_invalid, page.errors.on(:parent_title) + # A child page + page.parent_title = 'Page_with_an_inline_image' + assert !page.save + assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title) + # The page itself + page.parent_title = 'CookBook_documentation' + assert !page.save + assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title) + + page.parent_title = 'Another_page' + assert page.save + end + def test_destroy page = WikiPage.find(1) page.destroy diff --git a/vendor/plugins/acts_as_activity_provider/init.rb b/vendor/plugins/acts_as_activity_provider/init.rb new file mode 100644 index 000000000..5bf05da2c --- /dev/null +++ b/vendor/plugins/acts_as_activity_provider/init.rb @@ -0,0 +1,2 @@ +require File.dirname(__FILE__) + '/lib/acts_as_activity_provider' +ActiveRecord::Base.send(:include, Redmine::Acts::ActivityProvider) diff --git a/vendor/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb b/vendor/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb new file mode 100644 index 000000000..7c4fac8b1 --- /dev/null +++ b/vendor/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb @@ -0,0 +1,68 @@ +# 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 Redmine + module Acts + module ActivityProvider + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_activity_provider(options = {}) + unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods) + cattr_accessor :activity_provider_options + send :include, Redmine::Acts::ActivityProvider::InstanceMethods + end + + options.assert_valid_keys(:type, :permission, :timestamp, :find_options) + self.activity_provider_options ||= {} + + # One model can provide different event types + # We store these options in activity_provider_options hash + event_type = options.delete(:type) || self.name.underscore.pluralize + + options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission) + options[:timestamp] ||= "#{table_name}.created_on" + options[:find_options] ||= {} + self.activity_provider_options[event_type] = options + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + # Returns events of type event_type visible by user that occured between from and to + 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? + + cond = ARCondition.new(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to]) + cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission] + + with_scope(:find => { :conditions => cond.conditions }) do + find(:all, provider_options[:find_options]) + end + end + end + end + end + end +end diff --git a/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb b/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb index 1f00e90a9..6a6827ee6 100644 --- a/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb +++ b/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb @@ -70,6 +70,13 @@ module ActiveRecord nodes end + # Returns list of descendants. + # + # root.descendants # => [child1, subchild1, subchild2] + def descendants + children + children.collect(&:children).flatten + end + # Returns the root node of the tree. def root node = self