From 8b57ffc3c750b253e316fbfd8871d5c2da045557 Mon Sep 17 00:00:00 2001 From: Marius Balteanu Date: Fri, 3 May 2024 13:14:18 +0000 Subject: [PATCH] Adds the date of the last activity to the list of available columns for Projects (#23954). MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Patch by Frederico Camara (@fredsdc) and Marius BĂLTEANU (@marius.balteanu). git-svn-id: https://svn.redmine.org/redmine/trunk@22811 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/admin_controller.rb | 5 ++--- app/controllers/projects_controller.rb | 15 +++++++------- app/models/project.rb | 20 +++++++++++++++++++ app/models/project_query.rb | 17 ++++++++++++++-- config/locales/en.yml | 1 + config/locales/pt-BR.yml | 1 + .../lib/acts_as_activity_provider.rb | 9 ++++++--- lib/redmine/activity/fetcher.rb | 11 +++++++--- test/functional/projects_controller_test.rb | 12 +++++++++++ test/unit/project_test.rb | 7 +++++++ 10 files changed, 79 insertions(+), 19 deletions(-) diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 7fcb6ac5d..7e36d5293 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -38,11 +38,10 @@ class AdminController < ApplicationController def projects retrieve_query(ProjectQuery, false, :defaults => @default_columns_names) @query.admin_projects = 1 - scope = @query.results_scope - @entry_count = scope.count + @entry_count = @query.result_count @entry_pages = Paginator.new @entry_count, per_page_option, params['page'] - @projects = scope.limit(@entry_pages.per_page).offset(@entry_pages.offset).to_a + @projects = @query.results_scope(:limit => @entry_pages.per_page, :offset => @entry_pages.offset).to_a render :action => "projects", :layout => false if request.xhr? end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 66d54db38..91616d619 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -53,31 +53,30 @@ class ProjectsController < ApplicationController retrieve_default_query retrieve_project_query - scope = project_scope respond_to do |format| format.html do # TODO: see what to do with the board view and pagination if @query.display_type == 'board' - @entries = scope.to_a + @entries = project_scope.to_a else - @entry_count = scope.count + @entry_count = @query.result_count @entry_pages = Paginator.new @entry_count, per_page_option, params['page'] - @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a + @entries = project_scope(:offset => @entry_pages.offset, :limit => @entry_pages.per_page).to_a end end format.api do @offset, @limit = api_offset_and_limit - @project_count = scope.count - @projects = scope.offset(@offset).limit(@limit).to_a + @project_count = @query.result_count + @projects = project_scope(:offset => @offset, :limit => @limit) end format.atom do - projects = scope.reorder(:created_on => :desc).limit(Setting.feeds_limit.to_i).to_a + projects = project_scope(:order => {:created_on => :desc}, :limit => Setting.feeds_limit.to_i).to_a render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") end format.csv do # Export all entries - entries = scope.to_a + entries = project_scope.to_a send_data(query_to_csv(entries, @query, params), :type => 'text/csv; header=present', :filename => 'projects.csv') end end diff --git a/app/models/project.rb b/app/models/project.rb index f9000829b..abac2a1f0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -379,6 +379,7 @@ class Project < ApplicationRecord @due_date = nil @override_members = nil @assignable_users = nil + @last_activity_date = nil base_reload(*args) end @@ -1005,6 +1006,20 @@ class Project < ApplicationRecord end end + def last_activity_date + @last_activity_date || fetch_last_activity_date + end + + # Preloads last activity date for a collection of projects + def self.load_last_activity_date(projects, user=User.current) + if projects.any? + last_activities = Redmine::Activity::Fetcher.new(User.current).events(nil, nil, :last_by_project => true).to_h + projects.each do |project| + project.instance_variable_set(:@last_activity_date, last_activities[project.id]) + end + end + end + private def update_inherited_members @@ -1312,4 +1327,9 @@ class Project < ApplicationRecord end update_attribute :status, STATUS_ARCHIVED end + + def fetch_last_activity_date + latest_activities = Redmine::Activity::Fetcher.new(User.current, :project => self).events(nil, nil, :last_by_project => true) + latest_activities.empty? ? nil : latest_activities.to_h[self.id] + end end diff --git a/app/models/project_query.rb b/app/models/project_query.rb index 113287ed8..dde3e9cc1 100644 --- a/app/models/project_query.rb +++ b/app/models/project_query.rb @@ -36,7 +36,8 @@ class ProjectQuery < Query QueryColumn.new(:identifier, :sortable => "#{Project.table_name}.identifier"), QueryColumn.new(:parent_id, :sortable => "#{Project.table_name}.lft ASC", :default_order => 'desc', :caption => :field_parent), QueryColumn.new(:is_public, :sortable => "#{Project.table_name}.is_public", :groupable => true), - QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc') + QueryColumn.new(:created_on, :sortable => "#{Project.table_name}.created_on", :default_order => 'desc'), + QueryColumn.new(:last_activity_date) ] def self.default(project: nil, user: User.current) @@ -140,6 +141,13 @@ class ProjectQuery < Query end end + # Returns the project count + def result_count + base_scope.count + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid, e.message + end + def results_scope(options={}) order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?) @@ -156,6 +164,11 @@ class ProjectQuery < Query scope = scope.preload(:parent) end - scope + projects = scope.to_a + if has_column?(:last_activity_date) + Project.load_last_activity_date(scope) + end + + projects end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3ae06f745..c842e4d7b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -419,6 +419,7 @@ en: field_default_time_entry_activity: Default spent time activity field_any_searchable: Any searchable text field_estimated_remaining_hours: Estimated remaining time + field_last_activity_date: Last activity setting_app_title: Application title setting_welcome_text: Welcome text diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 93d5fad01..0a422ce86 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -299,6 +299,7 @@ pt-BR: field_default_value: Padrão field_comments_sorting: Visualizar comentários field_parent_title: Página pai + field_last_activity_date: Última atividade setting_app_title: Título da aplicação setting_welcome_text: Texto de boas-vindas diff --git a/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb b/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb index 5954d501d..ac334c19a 100644 --- a/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb +++ b/lib/plugins/acts_as_activity_provider/lib/acts_as_activity_provider.rb @@ -64,9 +64,8 @@ module Redmine ActiveSupport::Deprecation.warn "acts_as_activity_provider with implicit :scope option is deprecated. Please pass a scope on the #{self.name} as a proc." end - if from && to - scope = scope.where("#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to) - end + scope = scope.where("#{provider_options[:timestamp]} >= ?", from) if from + scope = scope.where("#{provider_options[:timestamp]} <= ?", to) if to if options[:author] return [] if provider_options[:author_key].nil? @@ -87,6 +86,10 @@ module Redmine scope = scope.where(Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym, options)) end + if options[:last_by_project] + scope = scope.group("#{Project.table_name}.id").maximum(provider_options[:timestamp]) + end + scope.to_a end end diff --git a/lib/redmine/activity/fetcher.rb b/lib/redmine/activity/fetcher.rb index cbd20425d..4264c2e5d 100644 --- a/lib/redmine/activity/fetcher.rb +++ b/lib/redmine/activity/fetcher.rb @@ -87,6 +87,7 @@ module Redmine def events(from = nil, to = nil, options={}) e = [] @options[:limit] = options[:limit] + @options[:last_by_project] = options[:last_by_project] if options[:last_by_project] @scope.each do |event_type| constantized_providers(event_type).each do |provider| @@ -94,10 +95,14 @@ module Redmine end end - e.sort! {|a, b| b.event_datetime <=> a.event_datetime} + if options[:last_by_project] + e.sort! + else + e.sort! {|a, b| b.event_datetime <=> a.event_datetime} - if options[:limit] - e = e.slice(0, options[:limit]) + if options[:limit] + e = e.slice(0, options[:limit]) + end end e end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index a2356185b..d1de3b631 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -252,6 +252,18 @@ class ProjectsControllerTest < Redmine::ControllerTest assert_select ".total-for-cf-#{field.id} span.value", :text => '9' end + def test_index_with_last_activity_date_column + with_settings :project_list_defaults => {'column_names' => %w(name short_description last_activity_date)} do + get :index, :params => { + :display_type => 'list' + } + assert_response :success + end + assert_equal ['Name', 'Description', 'Last activity'], columns_in_list + assert_select 'tr#project-1 td.last_activity_date', :text => format_time(Journal.find(3).created_on) + assert_select 'tr#project-4 td.last_activity_date', :text => '' + end + def test_index_should_retrieve_default_query query = ProjectQuery.find(11) ProjectQuery.stubs(:default).returns query diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index eacbf0c92..a7047d71c 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -1161,4 +1161,11 @@ class ProjectTest < ActiveSupport::TestCase r = Project.like('eco_k') assert_include project, r end + + def test_last_activity_date + # Note with id 3 is the last activity on Project 1 + assert_equal Journal.find(3).created_on, Project.find(1).last_activity_date + # Project without activity should return nil + assert_nil Project.find(4).last_activity_date + end end -- 2.39.5