Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Tasks list #164

Open
wants to merge 1 commit into from

9 participants

Gustavo Chaín Kale Worsley James Gifford Malcolm Locke Bradley Temple Matt Raykowski Marnen Laibow-Koser Katarzyna Szawan Sebastian Oelke
Gustavo Chaín

Adds tasks list for stories

Kale Worsley
Owner

Any specs for this?

Gustavo Chaín

No yet, having troubles building qtwebkit : (
I'm planing to do soon

James Gifford

@gchaincl which OS?

Gustavo Chaín

@jrgifford It's Archlinux, the problem I guess is that I'm using qtwebkit 2.3.beta2-1 instead of 2.2.2.
So I'll build from sources and try

Malcolm Locke
Owner

A few things:

  • The jasmine tests are broken for me. Run rake jasmine then point a browser to localhost:8888
  • Can you remove the file app/assets/javascripts/tasks.js
  • Can you remove the empty spec files so there are no pending specs.
  • I get a translation error, see screenshot.

Screenshot from 2013-04-12 22:45:08

Bradley Temple
DVG commented

+1 would love this

Matt Raykowski

@gchaincl , if you haven't solved your Capybara webkit problem... bundle update capybara-webkit - it updates to 1.0.0 and resolves the build failure. I elicited the same build problem as you on master using Ubuntu 13.04 and updating fixed it.

Gustavo Chaín

@mattraykowski cool, thanks!
I'll try to rebase and fix my commit to get task list working ASAP, too much work here and no time to code :(

Marnen Laibow-Koser

Any further word on this? What still needs to be done in order to be able to merge this pull request? I'll help if I can.

Gustavo Chaín

I've updated this branch to fix some bugs.

Katarzyna Szawan

What is the status of this PR?

Some time ago I merged it for myself and everything seems to be working, the tests are passing too.

Sebastian Oelke

I would love to have this feature, too!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 12, 2014
  1. Gustavo Chaín

    Added tasklist support

    gchaincl authored
This page is out of date. Refresh to see the latest.
Showing with 476 additions and 13 deletions.
  1. +18 −0 app/assets/javascripts/collections/task_collection.js
  2. +1 −1  app/assets/javascripts/i18n/translations.js
  3. +12 −0 app/assets/javascripts/models/story.js
  4. +17 −0 app/assets/javascripts/models/task.js
  5. +2 −0  app/assets/javascripts/tasks.js
  6. +2 −0  app/assets/javascripts/templates/task.jst.ejs
  7. +47 −3 app/assets/javascripts/views/story_view.js
  8. +64 −0 app/assets/javascripts/views/task_form.js
  9. +46 −0 app/assets/javascripts/views/task_view.js
  10. +1 −1  app/assets/stylesheets/screen.css.scss
  11. +39 −0 app/controllers/tasks_controller.rb
  12. +2 −0  app/helpers/tasks_helper.rb
  13. +3 −1 app/models/story.rb
  14. +5 −0 app/models/task.rb
  15. +5 −0 app/models/task_observer.rb
  16. +1 −1  config/application.rb
  17. +2 −0  config/locales/en.yml
  18. +1 −0  config/routes.rb
  19. +12 −0 db/migrate/20130222215035_create_tasks.rb
  20. +14 −4 db/schema.rb
  21. +119 −0 spec/controllers/tasks_controller_spec.rb
  22. +5 −0 spec/factories.rb
  23. +15 −0 spec/helpers/tasks_helper_spec.rb
  24. +3 −1 spec/javascripts/support/jasmine.yml
  25. +6 −0 spec/javascripts/views/story_view_spec.js
  26. +1 −1  spec/models/story_spec.rb
  27. +5 −0 spec/models/task_observer_spec.rb
  28. +28 −0 spec/models/task_spec.rb
18 app/assets/javascripts/collections/task_collection.js
View
@@ -0,0 +1,18 @@
+if (typeof Fulcrum == 'undefined') {
+ Fulcrum = {};
+}
+
+Fulcrum.TaskCollection = Backbone.Collection.extend({
+ model: Fulcrum.Task,
+
+ url: function() {
+ return this.story.url() + '/tasks';
+ },
+
+ saved: function() {
+ return this.reject(function(task) {
+ return task.isNew();
+ });
+ }
+});
+
2  app/assets/javascripts/i18n/translations.js
View
1 addition, 1 deletion not shown
12 app/assets/javascripts/models/story.js
View
@@ -17,6 +17,7 @@ Fulcrum.Story = Backbone.Model.extend({
this.maybeUnwrap(args);
this.initNotes();
+ this.initTasks();
this.setColumn();
},
@@ -249,6 +250,17 @@ Fulcrum.Story = Backbone.Model.extend({
return this.notes.any(function(note) {
return !note.isNew();
});
+ },
+
+ initTasks: function() {
+ this.tasks = new Fulcrum.TaskCollection();
+ this.tasks.story = this;
+ this.populateTasks();
+ },
+
+ populateTasks: function() {
+ var tasks = this.get("tasks") || [];
+ this.tasks.reset(tasks);
}
});
17 app/assets/javascripts/models/task.js
View
@@ -0,0 +1,17 @@
+if (typeof Fulcrum == 'undefined') {
+ Fulcrum = {};
+}
+
+Fulcrum.Task = Backbone.Model.extend({
+
+ name: 'task',
+
+ i18nScope: 'activerecord.attributes.task',
+
+ defaults: {
+ done: false
+ }
+
+});
+
+_.defaults(Fulcrum.Task.prototype, Fulcrum.SharedModelMethods);
2  app/assets/javascripts/tasks.js
View
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
2  app/assets/javascripts/templates/task.jst.ejs
View
@@ -0,0 +1,2 @@
+<%= task.escape("task") %>
+<a href="#" title="<%= I18n.t('delete') %>" class="delete-task"><%= I18n.t('delete') %></a>
50 app/assets/javascripts/views/story_view.js
View
@@ -11,7 +11,8 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
initialize: function() {
_.bindAll(this, "render", "highlight", "moveColumn", "setClassName",
"transition", "estimate", "disableForm", "renderNotes",
- "renderNotesCollection", "addEmptyNote");
+ "renderNotesCollection", "renderTasks", "renderTasksCollection",
+ "addEmptyTask", "addEmptyNote");
// Rerender on any relevant change to the views story
this.model.bind("change", this.render);
@@ -32,6 +33,9 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
this.model.bind("change:notes", this.addEmptyNote);
this.model.bind("change:notes", this.renderNotesCollection);
+ this.model.bind("change:tasks", this.addEmptyNote);
+ this.model.bind("change:tasks", this.renderNotesCollection);
+
this.model.bind("render", this.hoverBox());
// Supply the model with a reference to it's own view object, so it can
// remove itself from the page when destroy() gets called.
@@ -48,6 +52,9 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
// Add an empty note to the collection
this.addEmptyNote();
+
+ this.addEmptyTask();
+
},
events: {
@@ -334,8 +341,6 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
})
);
-
-
this.$el.append(
this.makeFormControl(function(div) {
$(div).append(this.label("description", "Description"));
@@ -363,6 +368,7 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
this.initTags();
this.renderNotes();
+ this.renderTasks();
} else {
this.$el.removeClass('editing');
@@ -407,6 +413,16 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
});
},
+ renderTasks: function() {
+ if (this.model.tasks.length > 0) {
+ var el = this.$el;
+ el.append('<hr />');
+ el.append('<h3>' + I18n.t('tasks') + '</h3>');
+ el.append('<div class="tasklist"/>');
+ this.renderTasksCollection();
+ }
+ },
+
renderNotes: function() {
if (this.model.notes.length > 0) {
var el = this.$el;
@@ -417,6 +433,34 @@ Fulcrum.StoryView = Fulcrum.FormView.extend({
}
},
+ renderTasksCollection: function() {
+ var tasklist = this.$('div.tasklist');
+ tasklist.html('');
+ this.addEmptyTask();
+ this.model.tasks.each(function(task) {
+ var view;
+ if (task.isNew()) {
+ view = new Fulcrum.TaskForm({model:task});
+ } else {
+ view = new Fulcrum.TaskView({model:task});
+ }
+ tasklist.append(view.render().el);
+ });
+ },
+
+ addEmptyTask: function() {
+ if (this.model.isNew()) {
+ return;
+ }
+
+ var task = this.model.tasks.last();
+ if (task && task.isNew()) {
+ return;
+ }
+
+ this.model.tasks.add();
+ },
+
renderNotesCollection: function() {
var notelist = this.$('div.notelist');
notelist.html('');
64 app/assets/javascripts/views/task_form.js
View
@@ -0,0 +1,64 @@
+if (typeof Fulcrum == 'undefined') {
+ Fulcrum = {};
+}
+
+Fulcrum.TaskForm = Fulcrum.FormView.extend({
+
+ tagName: 'div',
+
+ className: 'task_form',
+
+ events: {
+ "click input[type=button]": "saveTask"
+ },
+
+ render: function() {
+ var view = this;
+
+ div = this.make('div');
+ $(div).append(this.textField("task"));
+
+ var submit = this.make('input', {id: 'task_submit', type: 'button', value: 'Add Task'});
+ $(div).append(submit);
+
+ this.$el.html(div);
+
+ return this;
+ },
+
+ saveTask: function() {
+ this.disableForm();
+
+ var view = this;
+
+ this.model.save(null, {
+ success: function(model, response) {
+ },
+
+ error: function(model, response) {
+ var json = $.parseJSON(response.responseText);
+ view.enableForm();
+ model.set({errors: json.task.errors});
+ window.projectView.notice({
+ title: I18n.t("save error", {defaultValue: "Save error"}),
+ text: model.errorMessages()
+ });
+ }
+ });
+ },
+
+ // Makes the note for uneditable during save
+ disableForm: function() {
+ this.$('input,text').attr('disabled', 'disabled');
+ this.$('input[type="button"]').addClass('saving');
+ },
+
+ // Re-enables the note form once save is complete
+ enableForm: function() {
+ this.$('input,text').removeAttr('disabled');
+ this.$('input[type="button"]').removeClass('saving');
+ }
+
+});
+
+
46 app/assets/javascripts/views/task_view.js
View
@@ -0,0 +1,46 @@
+if (typeof Fulcrum == 'undefined') {
+ Fulcrum = {};
+}
+
+Fulcrum.TaskView = Fulcrum.FormView.extend({
+
+ template: JST['templates/task'],
+
+ tagName: 'div',
+
+ className: 'task',
+
+ events: {
+ "change input": "updateTask",
+ "click a.delete-task": "removeTask",
+ },
+
+ render: function() {
+ var view = this;
+
+ div = this.make('div');
+ $(div).append(this.checkBox("done"));
+ $(div).append( this.template({task: this.model}) );
+ this.$el.html(div);
+
+ return this;
+ },
+
+ updateTask: function() {
+ /*
+ * Ignore this.checkBox() element bindng (bindElementToAttribute)
+ * since check/uncheck does not update value
+ */
+ var done = this.$el.find("input").is(":checked");
+ this.model.set('done', done);
+ this.model.save(null);
+ },
+
+ removeTask: function() {
+ this.model.destroy();
+ this.$el.remove();
+ return false;
+ }
+
+});
+
2  app/assets/stylesheets/screen.css.scss
View
@@ -552,7 +552,7 @@ div.note {
}
}
-.note_form {
+.note_form, .task_form {
// The submit button while the server is saving the note.
input.saving {
padding-left: 16px;
39 app/controllers/tasks_controller.rb
View
@@ -0,0 +1,39 @@
+class TasksController < ApplicationController
+ before_filter :find_current_story
+
+ def create
+ @task = @story.tasks.build(allowed_params)
+
+ if @task.save
+ render :json => @task
+ else
+ render :json => @task, :status => :unprocessable_entity
+ end
+ end
+
+ def update
+ @task = @story.tasks.find(params[:id])
+ @task.update_attributes(params[:task])
+
+ head :ok
+ end
+
+ def destroy
+ @task = @story.tasks.find(params[:id])
+ @task.destroy
+
+ head :ok
+ end
+
+ private
+
+ def find_current_story
+ @project = current_user.projects.find(params[:project_id])
+ @story = @project.stories.find(params[:story_id])
+ end
+
+ def allowed_params
+ params.require(:task).permit(:done, :task)
+ end
+
+end
2  app/helpers/tasks_helper.rb
View
@@ -0,0 +1,2 @@
+module TasksHelper
+end
4 app/models/story.rb
View
@@ -6,7 +6,7 @@ class Story < ActiveRecord::Base
"state", "position", "id", "labels"
]
JSON_METHODS = [
- "errors", "notes"
+ "errors", "notes", "tasks"
]
CSV_HEADERS = [
"Id", "Story","Labels","Iteration","Iteration Start","Iteration End",
@@ -62,6 +62,8 @@ def from_csv_row(row)
end
+ has_many :tasks
+
# This attribute is used to store the user who is acting on a story, for
# example delivering or modifying it. Usually set by the controller.
attr_accessor :acting_user
5 app/models/task.rb
View
@@ -0,0 +1,5 @@
+class Task < ActiveRecord::Base
+ belongs_to :story
+
+ validates :task, :presence => true
+end
5 app/models/task_observer.rb
View
@@ -0,0 +1,5 @@
+class TaskObserver < ActiveRecord::Observer
+ def after_create(task)
+ task.story.changesets.create!
+ end
+end
2  config/application.rb
View
@@ -29,7 +29,7 @@ class Application < Rails::Application
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
- config.active_record.observers = :story_observer
+ config.active_record.observers = :story_observer, :task_observer
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
2  config/locales/en.yml
View
@@ -20,6 +20,7 @@ en:
points: "Points"
saving: "Saving ..."
expand: "Expand"
+ tasks: "Tasks"
author unknown: "Author Unknown"
add story: "Add story"
@@ -89,6 +90,7 @@ en:
labels: 'Labels'
requested_by: 'Requested by'
owned_by: 'Owned by'
+ tasks: 'Tasks'
projects:
1  config/routes.rb
View
@@ -7,6 +7,7 @@
resources :changesets, :only => [:index]
resources :stories, :only => [:index, :create, :update, :destroy, :show] do
resources :notes, :only => [:index, :create, :show, :destroy]
+ resources :tasks, :only => [:create, :update, :destroy]
collection do
get :done
get :in_progress
12 db/migrate/20130222215035_create_tasks.rb
View
@@ -0,0 +1,12 @@
+class CreateTasks < ActiveRecord::Migration
+ def change
+ create_table :tasks do |t|
+ t.references :story
+ t.string :task
+ t.boolean :done, :default => false
+
+ t.timestamps
+ end
+ add_index :tasks, :story_id
+ end
+end
18 db/schema.rb
View
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20120504152649) do
+ActiveRecord::Schema.define(version: 20130222215035) do
create_table "changesets", force: true do |t|
t.integer "story_id"
@@ -60,9 +60,19 @@
t.string "labels"
end
- create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", limit: 128, default: "", null: false
+ create_table "tasks", :force => true do |t|
+ t.integer "story_id"
+ t.string "task"
+ t.boolean "done", :default => false
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
+ add_index "tasks", ["story_id"], :name => "index_tasks_on_story_id"
+
+ create_table "users", :force => true do |t|
+ t.string "email", :default => "", :null => false
+ t.string "encrypted_password", :limit => 128, :default => "", :null => false
t.string "reset_password_token"
t.string "remember_token"
t.datetime "remember_created_at"
119 spec/controllers/tasks_controller_spec.rb
View
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe TasksController do
+
+ let(:user) { FactoryGirl.create :user }
+ let(:project) { mock_model(Project, :id => 42) }
+ let(:story) { mock_model(Story, :id => 99) }
+ let(:projects) { double("projects") }
+ let(:stories) { double("stories") }
+ let(:tasks) { double("tasks", :to_json => '{foo:bar}') }
+ let(:task) { mock_model(Task, :id => 66, :to_json => '{foo:bar}') }
+ let(:request_params) { {:project_id => project.id, :story_id => story.id } }
+
+ context "when not logged in" do
+
+ describe "collection actions" do
+
+ specify "#create" do
+ xhr :post, :create, request_params
+ response.status.should == 401
+ end
+
+ end
+
+ describe "member actions" do
+
+ before do
+ request_params[:id] = task.id
+ end
+
+ specify "#destroy" do
+ xhr :delete, :destroy, request_params
+ response.status.should == 401
+ end
+
+ end
+
+ end
+
+
+ context "when logged in" do
+
+
+ before do
+ user.stub(:projects => projects)
+ projects.stub(:find).with(project.id.to_s).and_return(project)
+ project.stub(:stories => stories)
+ stories.stub(:find).with(story.id.to_s).and_return(story)
+ story.stub(:tasks => tasks)
+ tasks.stub(:find).with(task.id.to_s).and_return(task)
+ subject.stub(:current_user => user)
+
+ sign_in user
+ end
+
+ describe "collection actions" do
+
+ describe "#create" do
+
+ before do
+ request_params[:task] = {'task' => 'foo'}
+ tasks.should_receive(:build).with(request_params[:task]).and_return(task)
+ task.stub(:save => true)
+ end
+
+ specify do
+ xhr :post, :create, request_params
+ response.should be_success
+ assigns[:project].should == project
+ assigns[:story].should == story
+ assigns[:task].should == task
+ response.content_type.should == 'application/json'
+ response.body.should == task.to_json
+ end
+
+ context "when save fails" do
+
+ before do
+ task.stub(:save => false)
+ end
+
+ specify do
+ xhr :post, :create, request_params
+ response.status.should == 422
+ end
+
+ end
+
+ end
+
+ end
+
+ describe "member actions" do
+
+ let(:request_params) {
+ {:id => task.id, :project_id => project.id, :story_id => story.id}
+ }
+
+ describe "#destroy" do
+
+ before do
+ task.should_receive(:destroy)
+ end
+
+ specify do
+ xhr :delete, :destroy, request_params
+ response.should be_success
+ assigns[:project].should == project
+ assigns[:story].should == story
+ assigns[:task].should == task
+ response.body.should be_blank
+ end
+ end
+
+ end
+
+ end
+
+end
5 spec/factories.rb
View
@@ -36,4 +36,9 @@
n.association :user
end
+ factory :task do |t|
+ t.task 'Test task'
+ t.association :story
+ end
+
end
15 spec/helpers/tasks_helper_spec.rb
View
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+# Specs in this file have access to a helper object that includes
+# the TasksHelper. For example:
+#
+# describe TasksHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# helper.concat_strings("this","that").should == "this that"
+# end
+# end
+# end
+describe TasksHelper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
4 spec/javascripts/support/jasmine.yml
View
@@ -30,8 +30,10 @@ src_files:
- app/assets/javascripts/views/form_view.js
- app/assets/javascripts/views/iteration_view.js
- app/assets/javascripts/views/keycut_view.js
- - app/assets/javascripts/views/note_form.js
- app/assets/javascripts/views/note_view.js
+ - app/assets/javascripts/views/note_form.js
+ - app/assets/javascripts/views/task_view.js
+ - app/assets/javascripts/views/task_form.js
- app/assets/javascripts/views/project_velocity_override_view.js
- app/assets/javascripts/views/project_velocity_view.js
- app/assets/javascripts/views/project_view.js
6 spec/javascripts/views/story_view_spec.js
View
@@ -9,7 +9,12 @@ describe('Fulcrum.StoryView', function() {
name: 'note',
humanAttributeName: sinon.stub()
});
+ var Task = Backbone.Model.extend({
+ name: 'task',
+ humanAttributeName: sinon.stub()
+ });
var NotesCollection = Backbone.Collection.extend({model: Note});
+ var TasksCollection = Backbone.Collection.extend({model: Task});
var Story = Backbone.Model.extend({
name: 'story', defaults: {story_type: 'feature'},
estimable: function() { return true; },
@@ -28,6 +33,7 @@ describe('Fulcrum.StoryView', function() {
this.story = new Story({id: 999, title: 'Story'});
this.new_story = new Story({title: 'New Story'});
this.story.notes = this.new_story.notes = new NotesCollection();
+ this.story.tasks = this.new_story.tasks = new TasksCollection();
this.view = new Fulcrum.StoryView({
model: this.story
});
2  spec/models/story_spec.rb
View
@@ -122,7 +122,7 @@
subject.as_json['story'].keys.sort.should == [
"title", "accepted_at", "created_at", "updated_at", "description",
"project_id", "story_type", "owned_by_id", "requested_by_id", "estimate",
- "state", "position", "id", "errors", "labels", "notes"
+ "state", "position", "id", "errors", "labels", "notes", "tasks"
].sort
end
end
5 spec/models/task_observer_spec.rb
View
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe TaskObserver do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
28 spec/models/task_spec.rb
View
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Task do
+
+ let(:project) { mock_model(Project, :suppress_notifications => true) }
+ let(:story) { mock_model(Story, :project => project) }
+
+ subject { FactoryGirl.build :task, :story => story }
+
+ describe "validations" do
+
+ describe "#name" do
+ before { subject.task = '' }
+ it { should have(1).error_on(:task) }
+ end
+
+ end
+
+ describe "#as_json" do
+
+ it "returns the right keys" do
+ subject.as_json["task"].keys.sort.should == %w[
+ created_at done id story_id task updated_at
+ ]
+ end
+
+ end
+end
Something went wrong with that request. Please try again.