diff --git a/app/assets/javascripts/issues.js b/app/assets/javascripts/issues.js index ded66b1c..19de614f 100644 --- a/app/assets/javascripts/issues.js +++ b/app/assets/javascripts/issues.js @@ -2,6 +2,7 @@ function switchToNewIssue(form){ $(".issues_content").hide("fade", { direction: "left" }, 150, function(){ $(".issues_content").after(form); $('select#issue_assignee_id').chosen(); + $('select#issue_milestone_id').chosen(); $("#new_issue_dialog").show("fade", { direction: "right" }, 150); $('.top-tabs .add_new').hide(); }); @@ -11,6 +12,7 @@ function switchToEditIssue(form){ $(".issues_content").hide("fade", { direction: "left" }, 150, function(){ $(".issues_content").after(form); $('select#issue_assignee_id').chosen(); + $('select#issue_milestone_id').chosen(); $("#edit_issue_dialog").show("fade", { direction: "right" }, 150); $('.add_new').hide(); }); diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index ee466348..89729e5c 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -45,6 +45,13 @@ a { &:hover { } + + &.primary { + background:$link_color; + &:hover { + background:$blue_link; + } + } } a:focus { diff --git a/app/assets/stylesheets/jquery_ui.scss b/app/assets/stylesheets/jquery_ui.scss new file mode 100644 index 00000000..70401539 --- /dev/null +++ b/app/assets/stylesheets/jquery_ui.scss @@ -0,0 +1,31 @@ +/** + * JQUERY UI datepicker + * + */ +.ui-datepicker { + border-color:#eee; + padding:20px; + + .ui-state-default { + background:#f1f1f1; + padding:5px; + } + .ui-state-active { + background:#fff; + } +} + +/** + * JQUERY UI progressbar + * + */ +.ui-progressbar { + border:1px solid #ddd; + height:6px; + + .ui-progressbar-value { + background-color: #62C462;//$blue_link; + margin:0; + } +} + diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 10388bcf..4fec0633 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -116,3 +116,9 @@ $hover: #FDF5D9; * */ @import "highlight.black.scss"; + +/** + * JQUERY UI ext + * + */ +@import "jquery_ui.scss"; diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index 87364278..64be20ff 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -15,7 +15,7 @@ class IssuesController < ApplicationController before_filter :authorize_write_issue!, :only => [:new, :create] # Allow modify issue - before_filter :authorize_modify_issue!, :only => [:close, :edit, :update, :sort] + before_filter :authorize_modify_issue!, :only => [:close, :edit, :update] # Allow destroy issue before_filter :authorize_admin_issue!, :only => [:destroy] @@ -28,8 +28,10 @@ class IssuesController < ApplicationController when 2 then @project.issues.closed when 3 then @project.issues.opened.assigned(current_user) else @project.issues.opened - end.page(params[:page]).per(20) + end + @issues = @issues.where(:milestone_id => params[:milestone_id]) if params[:milestone_id].present? + @issues = @issues.page(params[:page]).per(20) @issues = @issues.includes(:author, :project).order("critical, updated_at") respond_to do |format| @@ -51,13 +53,6 @@ class IssuesController < ApplicationController def show @note = @project.notes.new(:noteable => @issue) - @commits = if @issue.branch_name && @project.repo.heads.map(&:name).include?(@issue.branch_name) - @project.repo.commits_between("master", @issue.branch_name) - else - [] - end - - respond_to do |format| format.html format.js @@ -102,6 +97,8 @@ class IssuesController < ApplicationController end def sort + return render_404 unless can?(current_user, :admin_issue, @project) + @issues = @project.issues.where(:id => params['issue']) @issues.each do |issue| issue.position = params['issue'].index(issue.id.to_s) + 1 diff --git a/app/controllers/milestones_controller.rb b/app/controllers/milestones_controller.rb new file mode 100644 index 00000000..5e23426f --- /dev/null +++ b/app/controllers/milestones_controller.rb @@ -0,0 +1,94 @@ +class MilestonesController < ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :module_enabled + before_filter :milestone, :only => [:edit, :update, :destroy, :show] + layout "project" + + # Authorize + before_filter :add_project_abilities + + # Allow read any milestone + before_filter :authorize_read_milestone! + + # Allow admin milestone + before_filter :authorize_admin_milestone!, :except => [:index, :show] + + respond_to :html + + def index + @milestones = case params[:f].to_i + when 1; @project.milestones + else @project.milestones.active + end + + @milestones = @milestones.includes(:project).order("due_date") + @milestones = @milestones.page(params[:page]).per(20) + end + + def new + @milestone = @project.milestones.new + respond_with(@milestone) + end + + def edit + respond_with(@milestone) + end + + def show + respond_to do |format| + format.html + format.js + end + end + + def create + @milestone = @project.milestones.new(params[:milestone]) + + if @milestone.save + redirect_to project_milestone_path(@project, @milestone) + else + render "new" + end + end + + def update + @milestone.update_attributes(params[:milestone]) + + respond_to do |format| + format.js + format.html do + if @milestone.valid? + redirect_to [@project, @milestone] + else + render :edit + end + end + end + end + + def destroy + return access_denied! unless can?(current_user, :admin_milestone, @milestone) + + @milestone.destroy + + respond_to do |format| + format.html { redirect_to project_milestones_path } + format.js { render :nothing => true } + end + end + + protected + + def milestone + @milestone ||= @project.milestones.find(params[:id]) + end + + def authorize_admin_milestone! + return render_404 unless can?(current_user, :admin_milestone, @project) + end + + def module_enabled + return render_404 unless @project.issues_enabled + end +end diff --git a/app/decorators/milestone_decorator.rb b/app/decorators/milestone_decorator.rb new file mode 100644 index 00000000..c881da0e --- /dev/null +++ b/app/decorators/milestone_decorator.rb @@ -0,0 +1,4 @@ +class MilestoneDecorator < ApplicationDecorator + decorates :milestone + +end diff --git a/app/models/ability.rb b/app/models/ability.rb index e97b662b..5792948f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -17,6 +17,7 @@ class Ability :read_project, :read_wiki, :read_issue, + :read_milestone, :read_snippet, :read_team_member, :read_merge_request, @@ -42,6 +43,7 @@ class Ability :modify_merge_request, :admin_project, :admin_issue, + :admin_milestone, :admin_snippet, :admin_team_member, :admin_merge_request, diff --git a/app/models/issue.rb b/app/models/issue.rb index ce4944a6..5ca25796 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,5 +1,6 @@ class Issue < ActiveRecord::Base belongs_to :project + belongs_to :milestone belongs_to :author, :class_name => "User" belongs_to :assignee, :class_name => "User" has_many :notes, :as => :noteable, :dependent => :destroy diff --git a/app/models/milestone.rb b/app/models/milestone.rb new file mode 100644 index 00000000..20bc2486 --- /dev/null +++ b/app/models/milestone.rb @@ -0,0 +1,29 @@ +class Milestone < ActiveRecord::Base + belongs_to :project + has_many :issues + + validates_presence_of :project_id + validates_presence_of :title + + def self.active + where("due_date > ? ", Date.today) + end + + def percent_complete + @percent_complete ||= begin + total_i = self.issues.count + closed_i = self.issues.closed.count + if total_i > 0 + (closed_i * 100) / total_i + else + 100 + end + rescue => ex + 0 + end + end + + def expires_at + "expires at #{due_date.stamp("Aug 21, 2011")}" if due_date + end +end diff --git a/app/models/project.rb b/app/models/project.rb index e2646ead..72cc833c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ActiveRecord::Base has_many :events, :dependent => :destroy has_many :merge_requests, :dependent => :destroy has_many :issues, :dependent => :destroy, :order => "position" + has_many :milestones, :dependent => :destroy has_many :users_projects, :dependent => :destroy has_many :notes, :dependent => :destroy has_many :snippets, :dependent => :destroy diff --git a/app/views/issues/_form.html.haml b/app/views/issues/_form.html.haml index 543bab1f..4698bed2 100644 --- a/app/views/issues/_form.html.haml +++ b/app/views/issues/_form.html.haml @@ -9,18 +9,25 @@ %li= msg .clearfix - = f.label :title, "Issue Subject" - .input= f.text_field :title, :maxlength => 255, :class => "xxlarge" - + = f.label :title, "Issue Subject *" + .input + = f.text_field :title, :maxlength => 255, :class => "xxlarge" + + .clearfix + = f.label :assignee_id, "Assign to *" + .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Assign to user" }) + .clearfix = f.label :description, "Issue Details" .input = f.text_area :description, :maxlength => 2000, :class => "xxlarge", :rows => 10 %p.hint Markdown is enabled. + .clearfix - = f.label :assignee_id - .input= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Assign to user" }) + = f.label :milestone_id + .input= f.select(:milestone_id, @project.milestones.active.all.collect {|p| [ p.title, p.id ] }, { :include_blank => "Select milestone" }) + .clearfix = f.label :critical, "Critical" diff --git a/app/views/issues/_head.html.haml b/app/views/issues/_head.html.haml index d5390252..9fede71d 100644 --- a/app/views/issues/_head.html.haml +++ b/app/views/issues/_head.html.haml @@ -1,4 +1,7 @@ .tabs %li{:class => "#{'active' if current_page?(project_issues_path(@project))}"} = link_to project_issues_path(@project), :class => "tab" do - Issues + Browse Issues + %li{:class => "#{'active' if current_page?(project_milestones_path(@project))}"} + = link_to project_milestones_path(@project), :class => "tab" do + Milestones diff --git a/app/views/issues/_issues.html.haml b/app/views/issues/_issues.html.haml index f82aee94..ee5dc795 100644 --- a/app/views/issues/_issues.html.haml +++ b/app/views/issues/_issues.html.haml @@ -10,3 +10,6 @@ .span10= paginate @issues, :remote => true, :theme => "gitlab" .span4.right %span.cgray.right #{@issues.total_count} issues for this filter +- else + %li + %p.padded Nothing to show here diff --git a/app/views/issues/edit.html.haml b/app/views/issues/edit.html.haml index bada7245..aa0c931b 100644 --- a/app/views/issues/edit.html.haml +++ b/app/views/issues/edit.html.haml @@ -3,5 +3,6 @@ :javascript $(function(){ $('select#issue_assignee_id').chosen(); + $('select#issue_milestone_id').chosen(); }); diff --git a/app/views/issues/index.html.haml b/app/views/issues/index.html.haml index 7146027b..e0afa71f 100644 --- a/app/views/issues/index.html.haml +++ b/app/views/issues/index.html.haml @@ -1,3 +1,4 @@ += render "issues/head" .issues_content %h3 Issues @@ -5,14 +6,22 @@ = link_to project_issues_path(@project, :atom, { :private_token => current_user.private_token }) do = image_tag "Rss-UI.PNG", :width => 16, :title => "feed" - - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project), :class => "right btn small", :title => "New Issue", :remote => true do - New Issue + .right + .span4.left + = form_tag search_project_issues_path(@project), :method => :get, :remote => true, :id => "issue_search_form", :class => :left do + = hidden_field_tag :project_id, @project.id, { :id => 'project_id' } + = hidden_field_tag :status, params[:f] + = search_field_tag :issue_search, nil, { :placeholder => 'Search', :class => 'issue_search' } + + - if can? current_user, :write_issue, @project + .span2.left + = link_to new_project_issue_path(@project), :class => "right btn small", :title => "New Issue", :remote => true do + New Issue %br %div#issues-table-holder.ui-box .title .row - .span8 + .span6 %ul.pills.left %li{:class => ("active" if (params[:f] == "0" || !params[:f]))} = link_to project_issues_path(@project, :f => 0) do @@ -27,17 +36,13 @@ = link_to project_issues_path(@project, :f => 1) do All - .span3.right - = form_tag search_project_issues_path(@project), :method => :get, :remote => true, :id => "issue_search_form", :class => :right do - = hidden_field_tag :project_id, @project.id, { :id => 'project_id' } - = hidden_field_tag :status, params[:f] - = search_field_tag :issue_search, nil, { :placeholder => 'Search', :class => 'issue_search' } + .span6.right + = form_tag project_issues_path(@project), :method => :get, :class => :right do + = select_tag(:milestone_id, options_from_collection_for_select(@project.milestones.order("id desc").all, "id", "title", params[:milestone_id]), :prompt => "Select milestone") %ul#issues-table.unstyled = render "issues" - - if @issues.blank? - %li - %p.padded Nothing to show here + :javascript var href = $('.issue_search').parent().attr('action'); var last_terms = ''; @@ -65,9 +70,8 @@ $('#issues-table').sortable({ axis: 'y', dropOnEmpty: false, - handle: '.handle', - cursor: 'crosshair', - items: 'tr', + handle: '.avatar', + items: 'li', opacity: 0.4, scroll: true, update: function(){ @@ -85,4 +89,8 @@ $(function(){ setSortable(); + $("#milestone_id").chosen(); + $("#milestone_id").live("change", function(){ + $(this).closest("form").submit(); + }); }); diff --git a/app/views/issues/new.html.haml b/app/views/issues/new.html.haml index bada7245..aa0c931b 100644 --- a/app/views/issues/new.html.haml +++ b/app/views/issues/new.html.haml @@ -3,5 +3,6 @@ :javascript $(function(){ $('select#issue_assignee_id').chosen(); + $('select#issue_milestone_id').chosen(); }); diff --git a/app/views/milestones/_form.html.haml b/app/views/milestones/_form.html.haml new file mode 100644 index 00000000..6c3e37a7 --- /dev/null +++ b/app/views/milestones/_form.html.haml @@ -0,0 +1,54 @@ +%h3= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.id}" +.back_link + = link_to project_milestones_path(@project) do + ← To milestones + +%hr + += form_for [@project, @milestone] do |f| + -if @milestone.errors.any? + .alert-message.block-message.error + %ul + - @milestone.errors.full_messages.each do |msg| + %li= msg + .row + .span7 + .clearfix + = f.label :title, "Title" + .input + = f.text_field :title, :maxlength => 255, :class => "xlarge" + %p.hint Required + .clearfix + = f.label :description, "Description" + .input + = f.text_area :description, :maxlength => 2000, :class => "xlarge", :rows => 10 + %p.hint Markdown is enabled. + .span8 + .clearfix + = f.label :due_date, "Due Date" + .input= f.hidden_field :due_date + .input + .datepicker + + + .actions + - if @milestone.new_record? + = f.submit 'Create milestone', :class => "primary btn" + -else + = f.submit 'Save changes', :class => "primary btn" + + - if request.xhr? + = link_to "Cancel", "#back", :onclick => "backToIssues();", :class => "btn" + - else + - if @milestone.new_record? + = link_to "Cancel", project_milestones_path(@project), :class => "btn" + - else + = link_to "Cancel", project_milestone_path(@project, @milestone), :class => "btn" + +:javascript + $(function() { + $( ".datepicker" ).datepicker({ + dateFormat: "yy-mm-dd", + onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } + }); + }); diff --git a/app/views/milestones/_milestone.html.haml b/app/views/milestones/_milestone.html.haml new file mode 100644 index 00000000..a21df7e2 --- /dev/null +++ b/app/views/milestones/_milestone.html.haml @@ -0,0 +1,21 @@ +%li{:class => "wll", :id => dom_id(milestone) } + .right + - if milestone.issues.count > 0 + = link_to 'Browse Issues', project_issues_path(milestone.project, :milestone_id => milestone.id), :class => "btn small" + - if milestone.issues.any? + %span.btn.small.disabled.padded= pluralize milestone.issues.count, 'issues' + - if can? current_user, :admin_milestone, milestone.project + = link_to 'Edit', edit_project_milestone_path(milestone.project, milestone), :class => "btn small edit-milestone-link" + = link_to project_milestone_path(milestone.project, milestone) do + %h4.row_title + = truncate(milestone.title, :length => 100) + %small= milestone.expires_at + + .progress.span4 + + :javascript + $(function() { + $( "##{dom_id(milestone)} .progress" ).progressbar({ + value: #{milestone.percent_complete} + }); + }); diff --git a/app/views/milestones/edit.html.haml b/app/views/milestones/edit.html.haml new file mode 100644 index 00000000..bada7245 --- /dev/null +++ b/app/views/milestones/edit.html.haml @@ -0,0 +1,7 @@ += render "form" + +:javascript + $(function(){ + $('select#issue_assignee_id').chosen(); + }); + diff --git a/app/views/milestones/index.html.haml b/app/views/milestones/index.html.haml new file mode 100644 index 00000000..e55e58b2 --- /dev/null +++ b/app/views/milestones/index.html.haml @@ -0,0 +1,25 @@ += render "issues/head" +.milestones_content + %h3 + Milestones + - if can? current_user, :admin_milestone, @project + = link_to "New Milestone", new_project_milestone_path(@project), :class => "right btn small", :title => "New Milestone" + %br + %div.ui-box + .title + %ul.pills + %li{:class => ("active" if (params[:f] == "0" || !params[:f]))} + = link_to project_milestones_path(@project, :f => 0) do + Active + %li{:class => ("active" if params[:f] == "1")} + = link_to project_milestones_path(@project, :f => 1) do + All + + %ul.unstyled + = render @milestones + + - if @milestones.present? + %li.bottom= paginate @milestones, :remote => true, :theme => "gitlab" + - else + %li + %p.padded Nothing to show here diff --git a/app/views/milestones/new.html.haml b/app/views/milestones/new.html.haml new file mode 100644 index 00000000..b1bc3ba0 --- /dev/null +++ b/app/views/milestones/new.html.haml @@ -0,0 +1 @@ += render "form" diff --git a/app/views/milestones/show.html.haml b/app/views/milestones/show.html.haml new file mode 100644 index 00000000..bac48aa5 --- /dev/null +++ b/app/views/milestones/show.html.haml @@ -0,0 +1,50 @@ +%h3 + Milestone ##{@milestone.id} + %small + = @milestone.expires_at + + %span.right + - if can?(current_user, :admin_milestone, @project) + = link_to edit_project_milestone_path(@project, @milestone), :class => "btn" do + Edit + +.back_link + = link_to project_milestones_path(@project) do + ← To milestones list + +.main_box + .top_box_content + %h5 + - if @milestone.closed + .alert-message.error.status_info Closed + - else + .alert-message.success.status_info Open + = @milestone.title + + .middle_box_content + .row + .span2 + = link_to 'Browse Issues', project_issues_path(@milestone.project, :milestone_id => @milestone.id), :class => "btn small edit-milestone-link" + .span4 + %span + = @milestone.expires_at + + .span4.right + .progress + %br + %span + #{@milestone.issues.opened.count} open + – + #{@milestone.issues.closed.count} closed + + - if @milestone.description.present? + .bottom_box_content + = markdown @milestone.description + + +:javascript + $(function() { + $( ".progress" ).progressbar({ + value: #{@milestone.percent_complete} + }); + }); diff --git a/config/routes.rb b/config/routes.rb index eb999f26..be146f17 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -122,6 +122,7 @@ Gitlab::Application.routes.draw do end end resources :team_members + resources :milestones resources :issues do collection do post :sort diff --git a/db/migrate/20120408180246_create_milestones.rb b/db/migrate/20120408180246_create_milestones.rb new file mode 100644 index 00000000..ed3a510d --- /dev/null +++ b/db/migrate/20120408180246_create_milestones.rb @@ -0,0 +1,12 @@ +class CreateMilestones < ActiveRecord::Migration + def change + create_table :milestones do |t| + t.string :title, :null => false + t.integer :project_id, :null => false + t.text :description + t.date :due_date + t.boolean :closed, :default => false, :null => false + t.timestamps + end + end +end diff --git a/db/migrate/20120408181910_add_milestone_id_to_issue.rb b/db/migrate/20120408181910_add_milestone_id_to_issue.rb new file mode 100644 index 00000000..a6b44090 --- /dev/null +++ b/db/migrate/20120408181910_add_milestone_id_to_issue.rb @@ -0,0 +1,5 @@ +class AddMilestoneIdToIssue < ActiveRecord::Migration + def change + add_column :issues, :milestone_id, :integer, :null => true + end +end diff --git a/db/schema.rb b/db/schema.rb index 40482275..51e78b76 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20120405211750) do +ActiveRecord::Schema.define(:version => 20120408181910) do create_table "events", :force => true do |t| t.string "target_type" @@ -30,13 +30,14 @@ ActiveRecord::Schema.define(:version => 20120405211750) do t.integer "assignee_id" t.integer "author_id" t.integer "project_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.boolean "closed", :default => false, :null => false - t.integer "position", :default => 0 - t.boolean "critical", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "closed", :default => false, :null => false + t.integer "position", :default => 0 + t.boolean "critical", :default => false, :null => false t.string "branch_name" t.text "description" + t.integer "milestone_id" end add_index "issues", ["project_id"], :name => "index_issues_on_project_id" @@ -69,6 +70,15 @@ ActiveRecord::Schema.define(:version => 20120405211750) do add_index "merge_requests", ["project_id"], :name => "index_merge_requests_on_project_id" + create_table "milestones", :force => true do |t| + t.string "title", :null => false + t.text "description" + t.date "due_date", :null => false + t.integer "project_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "notes", :force => true do |t| t.text "note" t.string "noteable_id" diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb new file mode 100644 index 00000000..326d9087 --- /dev/null +++ b/spec/models/milestone_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Milestone do + pending "add some examples to (or delete) #{__FILE__}" +end