From 0e30724d660146d133318d9b3288a8a1eba4d8c4 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 8 Dec 2012 08:24:01 +0000 Subject: [PATCH] Group events in the activity view (#12542). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@10951 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/helpers/activities_helper.rb | 33 ++++++ app/models/journal.rb | 1 + app/models/message.rb | 1 + app/models/time_entry.rb | 1 + app/models/wiki_content.rb | 1 + app/views/activities/index.html.erb | 9 +- .../acts_as_event/lib/acts_as_event.rb | 5 + public/stylesheets/application.css | 2 + test/unit/activity_test.rb | 36 ++++++- test/unit/helpers/activities_helper_test.rb | 101 ++++++++++++++++++ 10 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 app/helpers/activities_helper.rb create mode 100644 test/unit/helpers/activities_helper_test.rb diff --git a/app/helpers/activities_helper.rb b/app/helpers/activities_helper.rb new file mode 100644 index 000000000..54556a0a7 --- /dev/null +++ b/app/helpers/activities_helper.rb @@ -0,0 +1,33 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2012 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 ActivitiesHelper + def sort_activity_events(events) + events_by_group = events.group_by(&:event_group) + sorted_events = [] + events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each do |event| + if group_events = events_by_group.delete(event.event_group) + group_events.sort {|x, y| y.event_datetime <=> x.event_datetime}.each_with_index do |e, i| + sorted_events << [e, i > 0] + end + end + end + sorted_events + end +end diff --git a/app/models/journal.rb b/app/models/journal.rb index 560ccf3b8..df52d89b2 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -28,6 +28,7 @@ class Journal < ActiveRecord::Base 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, + :group => :issue, :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}"}} diff --git a/app/models/message.rb b/app/models/message.rb index f41d48f2f..53b461501 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -29,6 +29,7 @@ class Message < ActiveRecord::Base :date_column => "#{table_name}.created_on" acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"}, :description => :content, + :group => :parent, :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}"})} diff --git a/app/models/time_entry.rb b/app/models/time_entry.rb index 3abe114c0..17c2389ec 100644 --- a/app/models/time_entry.rb +++ b/app/models/time_entry.rb @@ -30,6 +30,7 @@ class TimeEntry < ActiveRecord::Base acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"}, :url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}}, :author => :user, + :group => :issue, :description => :comments acts_as_activity_provider :timestamp => "#{table_name}.created_on", diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb index 7c5704902..7e1430ee8 100644 --- a/app/models/wiki_content.rb +++ b/app/models/wiki_content.rb @@ -59,6 +59,7 @@ class WikiContent < ActiveRecord::Base :description => :comments, :datetime => :updated_on, :type => 'wiki-page', + :group => :page, :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}} acts_as_activity_provider :type => 'wiki_edits', diff --git a/app/views/activities/index.html.erb b/app/views/activities/index.html.erb index bb129477b..7522e4ba0 100644 --- a/app/views/activities/index.html.erb +++ b/app/views/activities/index.html.erb @@ -5,13 +5,14 @@ <% @events_by_day.keys.sort.reverse.each do |day| %>

<%= format_activity_day(day) %>

-<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> -
+<% sort_activity_events(@events_by_day[day]).each do |e, in_group| -%> +
<%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>"> <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %> <%= format_time(e.event_datetime, false) %> <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> - <%= link_to format_activity_title(e.event_title), e.event_url %>
-
<%= format_activity_description(e.event_description) %> + <%= link_to format_activity_title(e.event_title), e.event_url %> + +
"><%= format_activity_description(e.event_description) %> <%= link_to_user(e.event_author) if e.respond_to?(:event_author) %>
<% end -%>
diff --git a/lib/plugins/acts_as_event/lib/acts_as_event.rb b/lib/plugins/acts_as_event/lib/acts_as_event.rb index b4d86a9b8..76b7fb82b 100644 --- a/lib/plugins/acts_as_event/lib/acts_as_event.rb +++ b/lib/plugins/acts_as_event/lib/acts_as_event.rb @@ -63,6 +63,11 @@ module Redmine event_datetime.to_date end + def event_group + group = event_options[:group] ? send(event_options[:group]) : self + group || self + end + def event_url(options = {}) option = event_options[:url] if option.is_a?(Proc) diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 20e49f9c3..fe5f28365 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -372,6 +372,8 @@ div#activity dt .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } div#activity span.project:after, #search-results span.project:after { content: " -"; } div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } +div#activity dt.grouped {margin-left:5em;} +div#activity dd.grouped {margin-left:9em;} #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } diff --git a/test/unit/activity_test.rb b/test/unit/activity_test.rb index d92e9c31b..dab575d74 100644 --- a/test/unit/activity_test.rb +++ b/test/unit/activity_test.rb @@ -19,7 +19,8 @@ require File.expand_path('../../test_helper', __FILE__) class ActivityTest < ActiveSupport::TestCase fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details, - :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :time_entries, + :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions def setup @project = Project.find(1) @@ -87,6 +88,39 @@ class ActivityTest < ActiveSupport::TestCase assert_equal %w(Project Version), events.collect(&:container_type).uniq.sort end + def test_event_group_for_issue + issue = Issue.find(1) + assert_equal issue, issue.event_group + end + + def test_event_group_for_journal + issue = Issue.find(1) + journal = issue.journals.first + assert_equal issue, journal.event_group + end + + def test_event_group_for_issue_time_entry + time = TimeEntry.where(:issue_id => 1).first + assert_equal time.issue, time.event_group + end + + def test_event_group_for_project_time_entry + time = TimeEntry.where(:issue_id => nil).first + assert_equal time, time.event_group + end + + def test_event_group_for_message + message = Message.find(1) + reply = message.children.first + assert_equal message, message.event_group + assert_equal message, reply.event_group + end + + def test_event_group_for_wiki_content_version + content = WikiContent::Version.find(1) + assert_equal content.page, content.event_group + end + private def find_events(user, options={}) diff --git a/test/unit/helpers/activities_helper_test.rb b/test/unit/helpers/activities_helper_test.rb new file mode 100644 index 000000000..48bcd9eb2 --- /dev/null +++ b/test/unit/helpers/activities_helper_test.rb @@ -0,0 +1,101 @@ +# Redmine - project management software +# Copyright (C) 2006-2012 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.expand_path('../../../test_helper', __FILE__) + +class ActivitiesHelperTest < ActionView::TestCase + include ActivitiesHelper + + class MockEvent + attr_reader :event_datetime, :event_group, :name + + def initialize(group=nil) + @@count ||= 0 + @name = "e#{@@count}" + @event_datetime = Time.now + @@count.hours + @event_group = group || self + @@count += 1 + end + + def self.clear + @@count = 0 + end + end + + def setup + MockEvent.clear + end + + def test_sort_activity_events_should_sort_by_datetime + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + + assert_equal [ + ['e2', false], + ['e1', false], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_group_events + events = [] + events << MockEvent.new + events << MockEvent.new(events[0]) + events << MockEvent.new(events[0]) + + assert_equal [ + ['e2', false], + ['e1', true], + ['e0', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_with_group_not_in_set_should_group_events + e = MockEvent.new + events = [] + events << MockEvent.new(e) + events << MockEvent.new(e) + + assert_equal [ + ['e2', false], + ['e1', true] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end + + def test_sort_activity_events_should_sort_by_datetime_and_group + events = [] + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new + events << MockEvent.new(events[1]) + events << MockEvent.new(events[2]) + events << MockEvent.new + events << MockEvent.new(events[2]) + + assert_equal [ + ['e6', false], + ['e4', true], + ['e2', true], + ['e5', false], + ['e3', false], + ['e1', true], + ['e0', false] + ], sort_activity_events(events).map {|event, grouped| [event.name, grouped]} + end +end -- 2.39.5