Index: app/controllers/my_controller.rb =================================================================== --- app/controllers/my_controller.rb (revision 1524) +++ app/controllers/my_controller.rb (working copy) @@ -5,19 +5,19 @@ # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class MyController < ApplicationController helper :issues - + layout 'base' before_filter :require_login @@ -27,11 +27,12 @@ 'news' => :label_news_latest, 'calendar' => :label_calendar, 'documents' => :label_document_plural, - 'timelog' => :label_spent_time + 'timelog' => :label_spent_time, + 'projects' => :label_project_plural }.freeze - DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], - 'right' => ['issuesreportedbyme'] + DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], + 'right' => ['issuesreportedbyme'] }.freeze verify :xhr => true, @@ -72,7 +73,7 @@ # Only users that belong to more than 1 project can select projects for which they are notified # Note that @user.membership.size would fail since AR ignores :include association option when doing a count @notification_options.insert 1, [l(:label_user_mail_option_selected), 'selected'] if @user.memberships.length > 1 - @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') + @notification_option = @user.mail_notification? ? 'all' : (@user.notified_projects_ids.empty? ? 'none' : 'selected') end # Manage user's password @@ -91,7 +92,7 @@ end end end - + # Create a new feeds key def reset_rss_key if request.post? && User.current.rss_token @@ -110,7 +111,7 @@ @block_options = [] BLOCKS.each {|k, v| @block_options << [l(v), k]} end - + # Add a block to user's page # The block is added on top of the page # params[:block] : id of the block to add @@ -124,7 +125,7 @@ session[:page_layout]['top'].unshift block render :partial => "block", :locals => {:user => @user, :block_name => block} end - + # Remove a block to user's page # params[:block] : id of the block to remove def remove_block @@ -145,12 +146,12 @@ %w(top left right).each {|f| session[:page_layout][f] = (session[:page_layout][f] || []) - group_items } - session[:page_layout][group] = group_items + session[:page_layout][group] = group_items end render :nothing => true end - - # Save user's page layout + + # Save user's page layout def page_layout_save @user = User.current @user.pref[:my_page_layout] = session[:page_layout] if session[:page_layout] Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 1524) +++ app/models/project.rb (working copy) @@ -5,12 +5,12 @@ # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. @@ -19,7 +19,7 @@ # Project statuses STATUS_ACTIVE = 1 STATUS_ARCHIVED = 9 - + has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :users, :through => :members has_many :custom_values, :dependent => :delete_all, :as => :customized @@ -38,12 +38,12 @@ has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy # Custom field for the project issues - has_and_belongs_to_many :custom_fields, + has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :order => "#{CustomField.table_name}.position", :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id' - + acts_as_tree :order => "name", :counter_cache => true acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil @@ -51,7 +51,7 @@ :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}} attr_protected :status, :enabled_module_names - + validates_presence_of :name, :identifier validates_uniqueness_of :name, :identifier validates_associated :custom_values, :on => :update @@ -60,17 +60,17 @@ validates_length_of :homepage, :maximum => 255 validates_length_of :identifier, :in => 3..20 validates_format_of :identifier, :with => /^[a-z0-9\-]*$/ - + before_destroy :delete_all_members - + def identifier=(identifier) super unless identifier_frozen? end - + def identifier_frozen? errors[:identifier].nil? && !(new_record? || identifier.blank?) end - + def issues_with_subprojects(include_subprojects=false) conditions = nil if include_subprojects @@ -79,18 +79,18 @@ end conditions ||= ["#{Project.table_name}.id = ?", id] # Quick and dirty fix for Rails 2 compatibility - Issue.send(:with_scope, :find => { :conditions => conditions }) do + Issue.send(:with_scope, :find => { :conditions => conditions }) do Version.send(:with_scope, :find => { :conditions => conditions }) do yield end - end + end end # returns latest created projects # non public projects will be returned only if user is a member of those def self.latest(user=nil, count=5) - find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") - end + find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC") + end def self.visible_by(user=nil) user ||= User.current @@ -102,7 +102,15 @@ return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}" end end - + + def self.members(user=nil) + user ||= User.current + if user && user.memberships.any? + return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')})" + end + end + + def self.allowed_to_condition(user, permission, options={}) statements = [] base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" @@ -119,20 +127,20 @@ statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any? elsif Role.anonymous.allowed_to?(permission) # anonymous user allowed on public project - statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" + statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" else # anonymous user is not authorized statements << "1=0" end statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))" end - + def project_condition(with_subprojects) cond = "#{Project.table_name}.id = #{id}" cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects cond end - + def self.find(*args) if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) project = find_by_identifier(*args) @@ -142,16 +150,16 @@ super end end - + def to_param # id is used for projects with a numeric identifier (compatibility) @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier) end - + def active? self.status == STATUS_ACTIVE end - + def archive # Archive subprojects if any children.each do |subproject| @@ -159,16 +167,16 @@ end update_attribute :status, STATUS_ARCHIVED end - + def unarchive return false if parent && !parent.active? update_attribute :status, STATUS_ACTIVE end - + def active_children children.select {|child| child.active?} end - + # Returns an array of the trackers used by the project and its sub projects def rolled_up_trackers @rolled_up_trackers ||= @@ -177,49 +185,49 @@ :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id], :order => "#{Tracker.table_name}.position") end - + # Deletes all project's members def delete_all_members Member.delete_all(['project_id = ?', id]) end - + # Users issues can be assigned to def assignable_users members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort end - + # Returns the mail adresses of users that should be always notified on project events def recipients members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail} end - + # Returns an array of all custom fields enabled for project issues # (explictly associated custom fields and custom fields enabled for all projects) def custom_fields_for_issues(tracker) all_custom_fields.select {|c| tracker.custom_fields.include? c } end - + def all_custom_fields @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq end - + def project self end - + def <=>(project) name.downcase <=> project.name.downcase end - + def to_s name end - + # Returns a short description of the projects (first lines) def short_description(length = 255) description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description end - + def allows_to?(action) if action.is_a? Hash allowed_actions.include? "#{action[:controller]}/#{action[:action]}" @@ -227,12 +235,12 @@ allowed_permissions.include? action end end - + def module_enabled?(module_name) module_name = module_name.to_s enabled_modules.detect {|m| m.name == module_name} end - + def enabled_module_names=(module_names) enabled_modules.clear module_names = [] unless module_names && module_names.is_a?(Array) @@ -247,7 +255,7 @@ errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) end - + private def allowed_permissions @allowed_permissions ||= begin Index: app/views/my/blocks/_projects.rhtml =================================================================== --- app/views/my/blocks/_projects.rhtml (revision 0) +++ app/views/my/blocks/_projects.rhtml (revision 0) @@ -0,0 +1,56 @@ +

<%=l(:label_project_plural)%>

+ +<% myprojects = Project.find(:all, + :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]) %> +<% form_tag({}) do %> + + + + + + + + + <% for project in myprojects %> + <% if User.current.member_of?(project) %> + + + <% end %> + <% end %> + +
<%=l(:field_project)%><%=l(:label_assigned_issues)%><%=l(:label_reported_issues)%><%=l(:label_all_time)%>
+ <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> + + <% num_assigned = 0 %> + <% num_reported = 0 %> + <% spent_time = 0 %> + <% for issue in project.issues %> + <% if issue.assigned_to == User.current && !issue.status.is_closed %> + <% num_assigned = num_assigned + 1 %> + <% end %> + <% if issue.author == User.current && !issue.status.is_closed %> + <% num_reported = num_reported + 1 %> + <% end %> + <% for timeentry in issue.time_entries %> + <% if timeentry.user_id == User.current.id %> + <% spent_time = spent_time + timeentry.hours %> + <% end %> + <% end %> + <% end %> + <% if num_assigned > 0 %> + <%= link_to num_assigned, :controller => 'projects/' << project.identifier, :action => 'issues', :set_filter => 1, :assigned_to_id => 'me' %> + <% end %> + + <% if num_reported > 0 %> + <%= link_to num_reported, :controller => 'projects/' << project.identifier, :action => 'issues', :set_filter => 1, :author_id => 'me' %> + <% end %> + + <% if spent_time > 0 %> + <%= link_to spent_time, :controller => 'projects/' << project.identifier << '/timelog', :action => 'details' %> + <% end %> +
+<% end %> + +<% if myprojects.length > 0 %> +

<%= link_to l(:label_project_all), :controller => 'projects', :action => 'index' %>

+<% end %> Index: lang/en.yml =================================================================== --- lang/en.yml (revision 1524) +++ lang/en.yml (working copy) @@ -278,7 +278,8 @@ label_login: Sign in label_logout: Sign out label_help: Help -label_reported_issues: Reported issues +label_reported_issues: Reported +label_assigned_issues: Assigned label_assigned_to_me_issues: Issues assigned to me label_last_login: Last connection label_last_updates: Last updated @@ -373,7 +374,7 @@ label_in_more_than: in more than label_in: in label_today: today -label_all_time: all time +label_all_time: Time label_yesterday: yesterday label_this_week: this week label_last_week: last week @@ -409,7 +410,7 @@ label_sort_lower: Move down label_sort_lowest: Move to bottom label_roadmap: Roadmap -label_roadmap_due_in: Due in +label_roadmap_due_in: Due in label_roadmap_overdue: %s late label_roadmap_no_issues: No issues for this version label_search: Search Index: public/stylesheets/application.css =================================================================== --- public/stylesheets/application.css (revision 1524) +++ public/stylesheets/application.css (working copy) @@ -57,7 +57,7 @@ #content { width: 80%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; height:600px; min-height: 600px;} * html #content{ width: 80%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} -html>body #content { height: auto; min-height: 600px; overflow: auto; } +html>body #content { height: auto; min-height: 600px; overflow: auto; } #main.nosidebar #sidebar{ display: none; } #main.nosidebar #content{ width: auto; border-right: 0; } @@ -78,13 +78,14 @@ a.issue.closed, .issue.closed a { text-decoration: line-through; } /***** Tables *****/ -table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } +table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px;} table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } table.list td { vertical-align: top; } table.list td.id { width: 2%; text-align: center;} table.list td.checkbox { width: 15px; padding: 0px;} +table.list.issues { margin-top: 10px; } +table.projects { margin-top: 10px; } -table.list.issues { margin-top: 10px; } tr.issue { text-align: center; white-space: nowrap; } tr.issue td.subject, tr.issue td.category { white-space: normal; } tr.issue td.subject { text-align: left; } @@ -159,7 +160,7 @@ li p {margin-top: 0;} div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} -p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } +p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } fieldset#filters { padding: 0.7em; } fieldset#filters p { margin: 1.2em 0 0.8em 2px; } @@ -236,7 +237,7 @@ float: left; text-align: right; margin-left: -180px; /*width of left column*/ -width: 175px; /*width of labels. Should be smaller than left column to create some right +width: 175px; /*width of labels. Should be smaller than left column to create some right margin*/ } @@ -272,31 +273,31 @@ #errorExplanation, div.flash, .nodata, .warning { padding: 4px 4px 4px 30px; margin-bottom: 12px; - font-size: 1.1em; - border: 2px solid; + font-size: 1.1em; + border: 2px solid; } div.flash {margin-top: 8px;} div.flash.error, #errorExplanation { background: url(../images/false.png) 8px 5px no-repeat; - background-color: #ffe3e3; - border-color: #dd0000; - color: #550000; + background-color: #ffe3e3; + border-color: #dd0000; + color: #550000; } div.flash.notice { background: url(../images/true.png) 8px 5px no-repeat; - background-color: #dfffdf; - border-color: #9fcf9f; - color: #005f00; + background-color: #dfffdf; + border-color: #9fcf9f; + color: #005f00; } .nodata, .warning { text-align: center; - background-color: #FFEBC1; - border-color: #FDBF3B; - color: #A6750C; + background-color: #FFEBC1; + border-color: #FDBF3B; + color: #A6750C; } #errorExplanation ul { font-size: 0.9em;} @@ -528,7 +529,7 @@ } .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } -.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } +.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; } .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } .milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }